Files
Jonathan Sykes bee4a1f5ab
Some checks failed
tests / PHP 8.2 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.3 (swoole-5.1.6) (push) Has been cancelled
tests / PHP 8.4 (swoole-6.0) (push) Has been cancelled
feat: complete all plan phases — reports, cleanup market fragments
- Add Reports page with population/household/document/blotter/budget/project views
- Add ReportsController with year-filtered queries for all report types
- Add /reports module to config/modules.php
- Register /barangay/reports in VueRouteMap and web.php
- Remove unused market Home fragments (HomeCoopMember, HomeStoreOwner, etc.)
- Remove leftover market Components/Market/ directory
- Add Reports card to Home.vue admin quick access
2026-06-07 03:15:04 +08:00

359 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use Hypervel\Support\Facades\Route;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserTypes;
use App\Support\ModuleHelper;
/**
* VueRouteMap handles automated registration of Vue SPA pages mapped via the Custom Inertia setup.
* It provides a modular way to handle ViewMap functionality without cluttering web.php.
*/
class VueRouteMap
{
/**
* Define the route mappings here.
* key: The URI route
* value: An array with:
* - 'component' (string): The Vue component path (e.g. 'ListProductsMarket')
* - 'middlewares' (array|string): Optional middlewares to apply to this route
* - 'name' (string): Optional route name
* - 'loginRequired' (bool): Whether authentication is required to access this page
* - 'allowedUserTypes' (array): List of allowed user types who can view this page
*/
protected static array $routes = [
// ── Public / Auth
'/' => ['component' => 'Home', 'loginRequired' => false],
'/app' => ['component' => 'Home', 'loginRequired' => false],
'/barangaysystem' => ['component' => 'Home', 'loginRequired' => false],
// ── Dashboard / Home
'/home' => ['component' => 'Home', 'loginRequired' => true],
'/dashboard' => ['component' => 'Home', 'loginRequired' => true],
// ── Auth
'/accountsettings' => ['component' => 'AccountSettings', 'loginRequired' => true],
// ── Announcements
'/manageannouncements' => ['component' => 'ManageAnnouncements', 'loginRequired' => true, 'module' => 'announcements'],
// ── System Settings / Admin
'/systemsettings' => ['component' => 'SystemSettings', 'loginRequired' => true],
'/landingpageeditor' => ['component' => 'LandingPageEditor', 'loginRequired' => true],
'/adminconsole' => ['component' => 'AdminConsole', 'loginRequired' => true],
// ── User Management
'/userlist' => ['component' => 'UserList', 'loginRequired' => true],
'/createuser' => ['component' => 'CreateUser', 'loginRequired' => true],
'/edituser' => ['component' => 'EditUser', 'loginRequired' => true],
'/manageuser' => ['component' => 'ManageUser', 'loginRequired' => true],
'/userregistration' => ['component' => 'UserRegistration', 'loginRequired' => true],
// ── Chapter Hierarchy
'/createchapter' => ['component' => 'CreateChapter', 'loginRequired' => true, 'module' => 'chapters'],
'/registerchapter' => ['component' => 'RegisterChapter', 'loginRequired' => true, 'module' => 'chapters'],
'/chapterorgchart' => ['component' => 'ChapterOrgChart', 'loginRequired' => true, 'module' => 'chapters'],
'/assignchapterofficer' => ['component' => 'AssignChapterOfficer', 'loginRequired' => true, 'module' => 'chapters'],
// ── Barangay Residents
'/barangay/manageresidents' => ['component' => 'Barangay.ManageResidents', 'loginRequired' => true, 'module' => 'residents'],
'/barangay/residentprofile' => ['component' => 'Barangay.ResidentProfile', 'loginRequired' => true, 'module' => 'residents'],
// ── Barangay Households
'/barangay/managehouseholds' => ['component' => 'Barangay.ManageHouseholds', 'loginRequired' => true, 'module' => 'households'],
// ── Blotters
'/barangay/manageblotters' => ['component' => 'Barangay.ManageBlotters', 'loginRequired' => true, 'module' => 'blotters'],
'/barangay/blotterdetail' => ['component' => 'Barangay.BlotterDetail', 'loginRequired' => true, 'module' => 'blotters'],
// ── Document Requests
'/barangay/requestdocument' => ['component' => 'Barangay.RequestDocument', 'loginRequired' => true, 'module' => 'certificates'],
'/barangay/managedocumentrequests' => ['component' => 'Barangay.ManageDocumentRequests','loginRequired' => true, 'module' => 'documents'],
'/barangay/documentrequestdetail' => ['component' => 'Barangay.DocumentRequestDetail','loginRequired' => true, 'module' => 'documents'],
'/barangay/managerequesttypes' => ['component' => 'Barangay.ManageRequestTypes', 'loginRequired' => true, 'module' => 'documents'],
// ── Projects
'/barangay/manageprojects' => ['component' => 'Barangay.ManageProjects', 'loginRequired' => true, 'module' => 'projects'],
// ── Budget
'/barangay/budgetledger' => ['component' => 'Barangay.BudgetLedger', 'loginRequired' => true, 'module' => 'budget'],
// ── Reports
'/barangay/reports' => ['component' => 'Barangay.Reports', 'loginRequired' => true, 'module' => 'reports'],
];
/**
* Helper method to check if user is allowed based on their type.
* Returns true if the user can access the page, false otherwise.
*/
private static function isUserAllowed(string $currentUserType, array $allowedUserTypes): bool
{
// If no restrictions specified, allow all authenticated users
if (empty($allowedUserTypes)) {
return true;
}
// Check if user type matches any allowed types
foreach ($allowedUserTypes as $type) {
if (is_string($type) && $currentUserType === $type) {
return true;
}
}
return false;
}
/**
* Registers the defined Vue routes into the application.
* This should be called in `routes/web.php`.
*/
public static function registerRoutes(): void
{
foreach (self::$routes as $uri => $settings) {
$component = $settings['component'] ?? '';
$middlewares = $settings['middlewares'] ?? [];
$name = $settings['name'] ?? null;
$loginRequired = $settings['loginRequired'] ?? false;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$options = [];
if (!empty($middlewares)) {
$options['middleware'] = $middlewares;
}
if ($name) {
$options['as'] = $name;
}
// Add login requirement middleware if needed
if ($loginRequired) {
$options['middleware'] = array_unique(array_merge($options['middleware'] ?? [], ['auth']));
}
// Add module requirement middleware if specified
$moduleKey = $settings['module'] ?? null;
if ($moduleKey) {
$options['middleware'] = array_unique(array_merge($options['middleware'] ?? [], ["module:{$moduleKey}"]));
}
Route::get($uri, function (...$routeParams) use ($component, $loginRequired, $allowedUserTypes, $moduleKey) {
// Check if user is authenticated
if (empty(Auth::user())) {
// If login is required but not available, redirect to login page
if ($loginRequired) {
return redirect('/login');
}
}
// Check if module is enabled
if ($moduleKey && ModuleHelper::isDisabled($moduleKey)) {
return redirect('/');
}
// Global Page Disabled Check
/** @var \App\Models\User $user */
$user = Auth::user();
$disabledPages = \App\Models\SystemSetting::getValue('disabled_pages', []);
if (is_array($disabledPages) && in_array(strtolower((string)$component), array_map('strtolower', $disabledPages))) {
// Ultimate accounts can still access to allow fixing settings
if (!$user || $user->acct_type !== UserTypes::SUPER_ADMIN) {
return redirect('/');
}
}
// Check allowed user types if specified
if (!empty($allowedUserTypes)) {
$currentUserType = $user->acct_type?->value ?? $user->acct_type ?? UserTypes::PUBLIC->value;
$isAllowed = self::isUserAllowed($currentUserType, $allowedUserTypes);
if (!$isAllowed) {
// Redirect to a forbidden page or login
return redirect('/login');
}
}
// Return Inertia page properly set up with current user data
// Pass parameterized URL segments to the Vue component as properties
$page = Inertia::render($component, [
'user' => Auth::user(),
'routeParams' => empty($routeParams) ? null : $routeParams
]);
return view('layouts/application-layout', compact('page'));
}, $options);
}
}
/**
* Handles a generic SPA request by converting the path into a component name.
* This acts as a catch-all universal router.
*/
public static function handleSpa(string $path = '/')
{
$path = trim($path, '/');
// Get user type for access control
/** @var \App\Models\User $user */
$user = Auth::user();
$currentUserType = null;
if ($user) {
$currentUserType = $user->acct_type?->value ?? $user->acct_type ?? UserTypes::PUBLIC->value;
}
// Strip hashkey/payload suffix from path (e.g., "edituser--h:HASHKEY" -> "edituser")
// Use RouteArgumentParser to properly extract the base component name
$component = $path;
try {
$parser = new \App\Support\RouteArgumentParser();
$parsedData = $parser->parseArgument($path);
// If we have a hash or payload format, use the slug as the component
if (isset($parsedData['slug']) && ($parsedData['type'] === 'hash' || $parsedData['type'] === 'payload')) {
$component = $parsedData['slug'];
}
} catch (\Throwable $th) {
// If parsing fails or no hash/payload format, use original path
}
// fallback to Home if empty
$component = empty($component) ? 'Home' : $component;
// Convert lowercase component names to camelCase for Vue component matching
// e.g., "edituser" -> "EditUser"
$vueComponent = self::toCamelCase($component);
// Find base component from routes map using the route key
$componentLower = strtolower($component);
$routeKey = '/' . $componentLower;
$loginRequired = true; // Default: require login
$pathWithSlash = '/' . ltrim($path, '/');
$pathLower = strtolower($pathWithSlash);
$routeSettings = self::$routes[$pathWithSlash] ?? self::$routes[$pathLower] ?? null;
$allowedUserTypes = [];
$moduleKey = null;
$foundInMap = false;
if ($routeSettings) {
$settings = $routeSettings;
$vueComponent = $settings['component'] ?? $vueComponent;
$loginRequired = $settings['loginRequired'] ?? true;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$moduleKey = $settings['module'] ?? null;
$foundInMap = true;
} else {
// Try hyphen-insensitive match (e.g. "viewstoremarket" vs "view-store-market")
$cleanSlug = str_replace(['-', '_'], '', $componentLower);
foreach (self::$routes as $uri => $settings) {
$cleanUri = str_replace(['-', '_'], '', strtolower(trim($uri, '/')));
if ($cleanUri === $cleanSlug) {
$vueComponent = $settings['component'] ?? $vueComponent;
$loginRequired = $settings['loginRequired'] ?? true;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$moduleKey = $settings['module'] ?? null;
$foundInMap = true;
break;
}
}
if (!$foundInMap) {
// Fallback: search by Vue component name (case-insensitive) in case they passed the component direct name
foreach (self::$routes as $uri => $settings) {
$mappedComponent = $settings['component'] ?? '';
if (strcasecmp($mappedComponent, $vueComponent) === 0) {
$vueComponent = $mappedComponent; // Use the correctly cased name from map
$loginRequired = $settings['loginRequired'] ?? true;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$moduleKey = $settings['module'] ?? null;
$foundInMap = true;
break;
}
}
}
}
// Enforce login requirement
if ($loginRequired && empty($user)) {
return redirect('/login');
}
// Enforce module requirement
if ($moduleKey && ModuleHelper::isDisabled($moduleKey)) {
return redirect('/');
}
// For store-level users, check the store-specific module toggle
$storeModuleKey = $routeSettings['store_module'] ?? null;
if ($storeModuleKey && in_array($currentUserType, ['store owner', 'store manager'])) {
if (ModuleHelper::isDisabled($storeModuleKey)) {
return redirect('/');
}
}
// Global Page Disabled Check
$disabledPages = \App\Models\SystemSetting::getValue('disabled_pages', []);
if (is_array($disabledPages) && in_array(strtolower((string)$vueComponent), array_map('strtolower', $disabledPages))) {
// Ultimate accounts can still access to allow fixing settings
if (!$user || $user->acct_type !== UserTypes::SUPER_ADMIN) {
return redirect('/');
}
}
// Check allowed user types if specified
if (!empty($allowedUserTypes) && $currentUserType !== null) {
$isAllowed = self::isUserAllowed($currentUserType, $allowedUserTypes);
if (!$isAllowed) {
// Redirect to a forbidden page or login
return redirect('/login');
}
}
// Pass standard Inertia props with hashkey if present
$props = [
'user' => Auth::user(),
];
// Add hashkey to props if it was in the URL for special pages
if (isset($parsedData['type']) && $parsedData['type'] === 'hash') {
// Provide the hash value under multiple common prop names for better compatibility
$props['hashkey'] = $parsedData['value'];
$props['target'] = $parsedData['value'];
$props['id'] = $parsedData['value'];
} elseif (isset($parsedData['type']) && $parsedData['type'] === 'payload') {
$props['payload'] = $parsedData['value'];
}
$page = Inertia::render($vueComponent, $props);
return view('layouts/application-layout', compact('page'));
}
/**
* Convert snake_case or lowercase to camelCase for Vue component matching.
*/
private static function toCamelCase(string $name): string
{
// Replace slashes with dots first to handle directory structures
$name = str_replace('/', '.', $name);
// Split by dot (hierarchy)
$parts = explode('.', $name);
$pascalParts = array_map(function($part) {
// Split by hyphen and capitalize each part for PascalCase
$subParts = explode('-', $part);
return implode('', array_map('ucfirst', $subParts));
}, $parts);
// Return with dots if original had them, otherwise just one string
return implode('.', $pascalParts);
}
}