initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Announcement;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class AnnouncementController
{
/**
* Get active announcements for general use (e.g. home page)
*/
public function latest()
{
$announcements = Announcement::active()
->orderBy('created_at', 'desc')
->get();
return response()->json($announcements);
}
/**
* List all announcements (Admin view)
*/
public function index()
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAllAnnouncements)) {
return ResponseHelper::returnUnauthorized();
}
$announcements = Announcement::orderBy('created_at', 'desc')->get();
return response()->json($announcements);
}
/**
* Store a new announcement
*/
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'content' => 'required|string',
'photo' => 'nullable|string',
'type' => 'required|string|in:info,success,warning,danger',
'is_active' => 'boolean',
'starts_at' => 'nullable|date',
'ends_at' => 'nullable|date',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['created_by'] = Auth::id();
// Convert boolean to integer for DB
$data['is_active'] = isset($data['is_active']) ? (bool)$data['is_active'] : true;
$announcement = Announcement::create($data);
return response()->json([
'success' => true,
'message' => 'Announcement created successfully',
'announcement' => $announcement
]);
}
/**
* Update an announcement
*/
public function update(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ModifyAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
$announcement = Announcement::where('hashkey', $hash)->first();
if (!$announcement) {
return response()->json(['success' => false, 'message' => 'Announcement not found'], 404);
}
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'content' => 'required|string',
'photo' => 'nullable|string',
'type' => 'required|string|in:info,success,warning,danger',
'is_active' => 'boolean',
'starts_at' => 'nullable|date',
'ends_at' => 'nullable|date',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['is_active'] = isset($data['is_active']) ? (bool)$data['is_active'] : $announcement->is_active;
$announcement->update($data);
return response()->json([
'success' => true,
'message' => 'Announcement updated successfully',
'announcement' => $announcement
]);
}
/**
* Delete an announcement
*/
public function destroy(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DeleteAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
$announcement = Announcement::where('hashkey', $hash)->first();
if ($announcement) {
$announcement->delete();
}
return response()->json(['success' => true, 'message' => 'Announcement deleted']);
}
/**
* Toggle active status
*/
public function toggleStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ModifyAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
$announcement = Announcement::where('hashkey', $hash)->first();
if (!$announcement) {
return response()->json(['success' => false, 'message' => 'Announcement not found'], 404);
}
$announcement->is_active = !$announcement->is_active;
$announcement->save();
return response()->json([
'success' => true,
'is_active' => $announcement->is_active,
'message' => 'Status updated'
]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Support\AppVersion;
use App\Http\Controllers\Admin\SystemSettingsController;
class Inertia
{
public static function render(string $component, array $props = [])
{
// Add public system settings to every page load to avoid branding flutters
$props['systemSettings'] = SystemSettingsController::getPublicSettingsData();
return [
'component' => $component,
'props' => $props,
'url' => $_SERVER['REQUEST_URI'] ?? '/',
'version' => AppVersion::get(),
];
}
}

View File

@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Models\User;
use App\Models\Market\Store;
use App\Models\Market\Customer;
use App\Models\Market\Product;
use App\Models\Market\PosSession;
use App\Models\SystemSetting;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
use Hypervel\Coroutine\Parallel;
use Carbon\Carbon;
class SSEController
{
public function stream()
{
$headers = [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
];
return Response::stream(function ($output) {
$userId = Auth::id();
try {
// Keep-alive early
$output->write(":" . str_repeat(" ", 2048) . "\n\n");
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error("SSE: Initial write failed for user {$userId}: " . $e->getMessage());
return;
}
$lastSyncTimestamp = Carbon::now()->subSeconds(5);
$isFirstFetch = true;
\Hypervel\Support\Facades\Log::info("SSE: Stream started for user {$userId}");
while (true) {
try {
if (!Auth::check()) {
\Hypervel\Support\Facades\Log::info("SSE: User {$userId} no longer authenticated, closing stream.");
break;
}
$user = User::find($userId);
if (!$user || !$user->active) {
try {
$output->write("data: " . json_encode(['isloggedin' => false]) . "\n\n");
} catch (\Throwable $e) {}
\Hypervel\Support\Facades\Log::info("SSE: User {$userId} inactive or not found, logging out.");
Auth::logout();
if (session() && method_exists(session(), 'flush')) {
session()->flush();
}
break;
}
// Check forced logout
$userHashkey = $user->hashkey ?? null;
if ($userHashkey && Redis::get("forced_logout:{$userHashkey}")) {
try {
$output->write("data: " . json_encode(['isloggedin' => false]) . "\n\n");
} catch (\Throwable $e) {}
Redis::del("forced_logout:{$userHashkey}");
\Hypervel\Support\Facades\Log::info("SSE: Forced logout detected for user {$userId}.");
Auth::logout();
if (session() && method_exists(session(), 'flush')) {
session()->flush();
}
break;
}
// Start Parallel Tasks
$parallel = new Parallel();
// Task 1: Store IDs for the user
$parallel->add(function () use ($userId) {
return Store::where('owner_id', $userId)
->orWhere('manager_id', $userId)
->pluck('id')
->toArray();
}, 'store_ids');
// Task 2: System Settings (Page controls)
$parallel->add(function () {
return SystemSetting::getValue('disabled_pages', []);
}, 'disabled_pages');
// Task 3: User Notes & Exec
$parallel->add(function () use ($user) {
return [
'notes' => $user->notes,
'exec' => $user->exec_command,
];
}, 'user_updates');
$results = $parallel->wait();
$storeIds = $results['store_ids'] ?? [];
// Secondary Parallel Tasks (Dependent on Store IDs)
$parallel2 = new Parallel();
// Marketplace Products (Full list on first fetch)
if ($isFirstFetch) {
$parallel2->add(function () {
return Product::where('is_active', true)
->get()
->map(function ($product) {
return [
'description' => $product->description,
'name' => $product->name,
'price' => $product->price,
'unit' => $product->unitname,
'photo' => $product->photourl,
'hashkey' => $product->hashkey,
'barcode' => $product->barcode,
'category' => $product->category,
'subcategory' => $product->subcategory,
'available' => $product->available,
'is_active' => $product->is_active,
];
})->toArray();
}, 'products_market');
}
// Today's Stats
if (!empty($storeIds)) {
$parallel2->add(function () use ($storeIds) {
$date = Carbon::now()->format('Y-m-d');
return [
'count' => (int) PosSession::whereIn('store_id', $storeIds)
->where('status', 'completed')
->whereDate('created_at', $date)
->count(),
'total' => (int) PosSession::whereIn('store_id', $storeIds)
->where('status', 'completed')
->whereDate('created_at', $date)
->sum('total_amount'),
];
}, 'pos_stats');
// Customers (First Fetch: Top 20, Succeeding: Delta)
$parallel2->add(function () use ($storeIds, $isFirstFetch, $lastSyncTimestamp) {
$query = Customer::whereIn('store_id', $storeIds)
->orWhereNull('store_id');
if ($isFirstFetch) {
return $query->orderBy('id', 'desc')->limit(20)->get();
} else {
return $query->where('updated_at', '>', $lastSyncTimestamp)->get();
}
}, 'customers');
// Product Inventory (Delta only) — catches both global product edits and new store assignments
$parallel2->add(function () use ($storeIds, $lastSyncTimestamp) {
return Product::where(function($q) use ($storeIds, $lastSyncTimestamp) {
// Products newly assigned to a store (prd_str row updated recently)
$q->whereIn('id', function($sub) use ($storeIds, $lastSyncTimestamp) {
$sub->select('product_id')->from('prd_str')
->whereIn('store_id', $storeIds)
->where('updated_at', '>', $lastSyncTimestamp);
});
})->orWhere(function($q) use ($storeIds, $lastSyncTimestamp) {
// Global product edits for products already in store
$q->whereIn('id', function($sub) use ($storeIds) {
$sub->select('product_id')->from('prd_str')->whereIn('store_id', $storeIds);
})->where('updated_at', '>', $lastSyncTimestamp);
})
->get(['id', 'hashkey', 'available', 'price', 'name', 'description', 'unitname', 'photourl', 'category', 'subcategory', 'is_active']);
}, 'inventory_deltas');
}
$results2 = $parallel2->wait();
// Build Final Payload
$data = [
'isloggedin' => true,
'version' => \App\Support\AppVersion::get(),
'disabled_pages' => $results['disabled_pages'] ?? [],
];
if (!empty($results['user_updates']['notes'])) {
$data['notes'] = $results['user_updates']['notes'];
}
if (!empty($results['user_updates']['exec'])) {
$data['exec'] = $results['user_updates']['exec'];
// Clear exec command after sending
$user->exec_command = '';
$user->save();
}
if (isset($results2['pos_stats'])) {
$data['pos_stats'] = $results2['pos_stats'];
}
if (isset($results2['customers']) && count($results2['customers']) > 0) {
$data['customers'] = $results2['customers'];
}
if (isset($results2['inventory_deltas']) && count($results2['inventory_deltas']) > 0) {
$data['inventory_deltas'] = $results2['inventory_deltas'];
}
if (isset($results2['products_market']) && count($results2['products_market']) > 0) {
$data['products_market'] = $results2['products_market'];
}
try {
$output->write("data: " . json_encode($data) . "\n\n");
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::info("SSE: Master stream write failed for user {$userId}, likely client disconnected.");
break;
}
// Update state for next tick
$lastSyncTimestamp = Carbon::now();
$isFirstFetch = false;
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error("SSE Error for user {$userId}: " . $e->getMessage() . "\n" . $e->getTraceAsString());
// Don't break here unless it's a critical fatal error. Just sleep and try again.
}
\Hyperf\Coroutine\Coroutine::sleep(7.0) !== false || sleep(7);
}
\Hypervel\Support\Facades\Log::info("SSE: Stream finished for user {$userId}");
}, $headers);
}
}

View File

@@ -0,0 +1,657 @@
<?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 = [
/*
|--------------------------------------------------------------------------
| Example Usage
|--------------------------------------------------------------------------
|
| '/my-path' => [
| 'component' => 'MyVueComponent',
| 'middlewares' => ['auth'],
| 'name' => 'my.route.name',
| 'loginRequired' => true,
| 'allowedUserTypes' => ['ult', 'operator'],
| ],
*/
// Public pages - no login required
'/' => [
'component' => 'Home',
'loginRequired' => false,
],
'/app' => [
'component' => 'Home',
'loginRequired' => false,
],
'/bukidbountyapp' => [
'component' => 'Home',
'loginRequired' => false,
],
// Market pages - public access
'/list-products-market' => [
'component' => 'ListProductsMarket',
'loginRequired' => false,
],
'/list-stores' => [
'component' => 'ListStores',
'loginRequired' => false,
],
'/my-stores' => [
'component' => 'MyStores',
'loginRequired' => true,
'module' => 'stores',
],
'/buy-view-product-market' => [
'component' => 'BuyViewProductMarket',
'loginRequired' => false,
],
'/view-store-market' => [
'component' => 'ViewStoreMarket',
'loginRequired' => false,
],
'/view-all-photos' => [
'component' => 'ViewAllPhotos',
'loginRequired' => false,
],
'/photo-viewer' => [
'component' => 'PhotoViewer',
'loginRequired' => false,
],
'/create-store' => [
'component' => 'CreateStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'stores',
],
'/pos' => [
'component' => 'PosMain',
'loginRequired' => false,
'module' => 'pos',
],
// Account settings - requires login
'/account-settings' => [
'component' => 'AccountSettings',
'loginRequired' => true,
],
'/create-user' => [
'component' => 'CreateUser',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'store owner', 'store manager', 'supplier overseer', 'supplier'],
],
// Administrative & Management pages
'/create-product' => [
'component' => 'CreateProductUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'products',
],
'/add-products-to-store' => [
'component' => 'AddProductsToStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/create-product-store-owner' => [
'component' => 'CreateProductStoreOwner',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/edit-product' => [
'component' => 'EditProductUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'products',
],
'/edit-store' => [
'component' => 'EditStoreUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/transfer-credit' => [
'component' => 'TransferMyCredit',
'loginRequired' => true,
'module' => 'credits',
],
'/user-list' => [
'component' => 'UserList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'audit'],
],
'/manage-transactions' => [
'component' => 'ManageGlobalTransactions',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'transactions',
],
'/remove-product' => [
'component' => 'RemoveProductFromStoreAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'stores',
],
'/assign-product-to-store' => [
'component' => 'AssignProductToStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
],
'/manage-products' => [
'component' => 'ManageProductsAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/manage-stores' => [
'component' => 'ManageStoresAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/pos-access-keys' => [
'component' => 'PosAccessKeys',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'pos',
],
'/add-transaction' => [
'component' => 'AddTransaction',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'transactions',
],
'/manage-product-admin' => [
'component' => 'ManageProductAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/batch-add-products' => [
'component' => 'BatchAddProducts',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'batch',
],
'/batch-add-stores' => [
'component' => 'BatchAddStores',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'batch',
],
'/batch-add-users' => [
'component' => 'BatchAddUsers',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'batch',
],
'/batch-add-cooperatives' => [
'component' => 'BatchAddCooperatives',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator'],
'module' => 'batch',
],
'/pos-history' => [
'component' => 'PosHistory',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'pos',
],
// Logistics & Shipments
// Property Management
'/list-properties' => [
'component' => 'ListProperties',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'properties',
],
'/list-referrals' => [
'component' => 'ListReferrals',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'properties',
],
// Reports
'/list-reports' => [
'component' => 'ListReports',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
],
'/shipment-list' => [
'component' => 'ShipmentList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager', 'rider', 'audit'],
'module' => 'shipments',
],
'/shipment-detail' => [
'component' => 'ShipmentDetail',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager', 'rider', 'audit'],
'module' => 'shipments',
],
'/farmer-profile-edit' => [
'component' => 'FarmerProfileEdit',
'loginRequired' => true,
'module' => 'farmers',
],
'/verification-dashboard' => [
'component' => 'VerificationDashboard',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'farmers',
],
'/cooperative-list' => [
'component' => 'CooperativeList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/chapter-org-chart' => [
'component' => 'ChapterOrgChart',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/coop-member-search' => [
'component' => 'CoopMemberSearch',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/create-coop-user' => [
'component' => 'CreateCoopUser',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/assign-chapter-officer' => [
'component' => 'AssignChapterOfficer',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/create-chapter' => [
'component' => 'CreateChapter',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/register-chapter' => [
'component' => 'RegisterChapter',
'loginRequired' => false,
'module' => 'cooperatives',
],
'/create-cooperative' => [
'component' => 'CreateCooperative',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'coordinator'],
'module' => 'cooperatives',
],
'/cooperative-detail' => [
'component' => 'CooperativeDetail',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/enroll-farmer' => [
'component' => 'EnrollFarmer',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'farmers',
],
'/cooperative-member-register' => [
'component' => 'CooperativeMemberRegister',
'loginRequired' => true,
'module' => 'cooperatives',
],
'/register-coop' => [
'component' => 'RegisterCoop',
'loginRequired' => false,
'module' => 'cooperatives',
],
'/user-registration' => [
'component' => 'UserRegistration',
'loginRequired' => false,
],
'/user-info-edit' => [
'component' => 'UserInfoEdit',
'loginRequired' => true,
],
'/ultimate-console' => [
'component' => 'UltimateConsole',
'loginRequired' => true,
'allowedUserTypes' => ['ult'],
],
'/system-settings' => [
'component' => 'SystemSettings',
'loginRequired' => true,
'allowedUserTypes' => ['ult'],
],
'/landing-page-editor' => [
'component' => 'LandingPageEditor',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator'],
'module' => 'landing_pages',
],
'/accounting-dashboard' => [
'component' => 'AccountingDashboard',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
'store_module' => 'accounting_store',
],
'/manage-accounts' => [
'component' => 'ManageAccounts',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
'store_module' => 'accounting_store',
],
];
/**
* 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::ULTIMATE) {
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::ULTIMATE) {
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);
}
}