diff --git a/app/Http/Controllers/Admin/AdminDashboardController.php b/app/Http/Controllers/Admin/AdminDashboardController.php new file mode 100644 index 00000000..4e37a505 --- /dev/null +++ b/app/Http/Controllers/Admin/AdminDashboardController.php @@ -0,0 +1,48 @@ +getDatabaseVersion($dbDriver); + + return response()->json([ + 'app_version' => $version, + 'php_version' => phpversion(), + 'database' => [ + 'driver' => $dbDriver, + 'version' => $dbVersion, + ], + 'counts' => [ + 'companies' => Company::count(), + 'users' => User::count(), + ], + ]); + } + + private function getDatabaseVersion(string $driver): ?string + { + try { + return match ($driver) { + 'mysql' => DB::selectOne('SELECT VERSION() as version')?->version, + 'pgsql' => DB::selectOne('SHOW server_version')?->server_version, + 'sqlite' => DB::selectOne('SELECT sqlite_version() as version')?->version, + default => null, + }; + } catch (\Throwable) { + return null; + } + } +} diff --git a/app/Http/Controllers/Company/General/BootstrapController.php b/app/Http/Controllers/Company/General/BootstrapController.php index 22651804..484ca1d5 100644 --- a/app/Http/Controllers/Company/General/BootstrapController.php +++ b/app/Http/Controllers/Company/General/BootstrapController.php @@ -49,16 +49,28 @@ class BootstrapController extends Controller 'save_pdf_to_disk', ]); + // Super admin mode — return admin-only menu with all companies listed + if ($current_user->isSuperAdmin() && $request->has('admin_mode')) { + return response()->json([ + 'current_user' => new UserResource($current_user), + 'current_user_settings' => $current_user_settings, + 'current_user_abilities' => [], + 'companies' => CompanyResource::collection($companies), + 'current_company' => null, + 'current_company_settings' => [], + 'current_company_currency' => Currency::first(), + 'config' => config('invoiceshelf'), + 'global_settings' => $global_settings, + 'main_menu' => $this->generateMenu('admin_menu', $current_user), + 'setting_menu' => [], + 'modules' => [], + 'admin_mode' => true, + 'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations), + ]); + } + // User has no companies — return minimal bootstrap if ($companies->isEmpty()) { - $main_menu = $current_user->isSuperAdmin() - ? $this->generateMenu('main_menu', $current_user) - : []; - - $setting_menu = $current_user->isSuperAdmin() - ? $this->generateMenu('setting_menu', $current_user) - : []; - return response()->json([ 'current_user' => new UserResource($current_user), 'current_user_settings' => $current_user_settings, @@ -69,8 +81,8 @@ class BootstrapController extends Controller 'current_company_currency' => Currency::first(), 'config' => config('invoiceshelf'), 'global_settings' => $global_settings, - 'main_menu' => $main_menu, - 'setting_menu' => $setting_menu, + 'main_menu' => [], + 'setting_menu' => [], 'modules' => [], 'pending_invitations' => CompanyInvitationResource::collection($pendingInvitations), ]); diff --git a/app/Http/Middleware/CompanyMiddleware.php b/app/Http/Middleware/CompanyMiddleware.php index 6612ac02..0137d71d 100644 --- a/app/Http/Middleware/CompanyMiddleware.php +++ b/app/Http/Middleware/CompanyMiddleware.php @@ -21,11 +21,15 @@ class CompanyMiddleware $firstCompany = $user->companies()->first(); // User has no companies — allow request through without company header - // (BootstrapController handles this gracefully) if (! $firstCompany) { return $next($request); } + // Super admin without company header — allow pass-through (admin mode) + if ($user->isSuperAdmin() && ! $request->header('company')) { + return $next($request); + } + if (! $request->header('company') || ! $user->hasCompany($request->header('company'))) { $request->headers->set('company', $firstCompany->id); } diff --git a/app/Policies/SettingsPolicy.php b/app/Policies/SettingsPolicy.php index cf5fea3e..4115a8ae 100644 --- a/app/Policies/SettingsPolicy.php +++ b/app/Policies/SettingsPolicy.php @@ -21,7 +21,7 @@ class SettingsPolicy public function manageBackups(User $user) { - if ($user->isOwner()) { + if ($user->isSuperAdmin()) { return true; } @@ -30,7 +30,7 @@ class SettingsPolicy public function manageFileDisk(User $user) { - if ($user->isOwner()) { + if ($user->isSuperAdmin()) { return true; } @@ -39,7 +39,7 @@ class SettingsPolicy public function manageEmailConfig(User $user) { - if ($user->isOwner()) { + if ($user->isSuperAdmin()) { return true; } @@ -48,7 +48,7 @@ class SettingsPolicy public function managePDFConfig(User $user) { - if ($user->isOwner()) { + if ($user->isSuperAdmin()) { return true; } @@ -57,7 +57,7 @@ class SettingsPolicy public function manageSettings(User $user) { - if ($user->isOwner()) { + if ($user->isSuperAdmin()) { return true; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7cdd0a2d..acf9eda6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -90,6 +90,13 @@ class AppServiceProvider extends ServiceProvider } }); + // admin menu (super admin mode) + \Menu::make('admin_menu', function ($menu) { + foreach (config('invoiceshelf.admin_menu') as $data) { + $this->generateMenu($menu, $data); + } + }); + // setting menu \Menu::make('setting_menu', function ($menu) { foreach (config('invoiceshelf.setting_menu') as $data) { diff --git a/config/invoiceshelf.php b/config/invoiceshelf.php index 267d12e9..d8515cfb 100644 --- a/config/invoiceshelf.php +++ b/config/invoiceshelf.php @@ -381,10 +381,26 @@ return [ 'ability' => '', 'model' => '', ], + ], + + /* + * List of admin mode menu (super admin only) + */ + 'admin_menu' => [ + [ + 'title' => 'navigation.dashboard', + 'group' => 1, + 'link' => '/admin/administration/dashboard', + 'icon' => 'ServerIcon', + 'name' => 'AdminDashboard', + 'owner_only' => false, + 'super_admin_only' => true, + 'ability' => '', + 'model' => '', + ], [ 'title' => 'navigation.companies', - 'group' => 4, - 'group_label' => 'navigation.administration', + 'group' => 1, 'link' => '/admin/administration/companies', 'icon' => 'BuildingOfficeIcon', 'name' => 'AdminCompanies', @@ -395,7 +411,7 @@ return [ ], [ 'title' => 'navigation.all_users', - 'group' => 4, + 'group' => 1, 'link' => '/admin/administration/users', 'icon' => 'UsersIcon', 'name' => 'AdminUsers', @@ -406,7 +422,7 @@ return [ ], [ 'title' => 'navigation.settings', - 'group' => 4, + 'group' => 1, 'link' => '/admin/administration/settings/mail-configuration', 'icon' => 'CogIcon', 'name' => 'AdminSettings', diff --git a/lang/en.json b/lang/en.json index 58e4ead2..f6c0ca25 100644 --- a/lang/en.json +++ b/lang/en.json @@ -30,6 +30,8 @@ "decline": "Decline", "welcome": "Welcome", "no_company_description": "You are not a member of any company yet. Accept an invitation below or contact your administrator.", + "app_version": "Application Version", + "database": "Database", "update": "Update", "deselect": "Deselect", "download": "Download", diff --git a/resources/scripts/admin/admin-router.js b/resources/scripts/admin/admin-router.js index f68b6536..76a9473a 100644 --- a/resources/scripts/admin/admin-router.js +++ b/resources/scripts/admin/admin-router.js @@ -114,6 +114,8 @@ const InvoicePublicPage = () => import('@/scripts/components/InvoicePublicPage.vue') // Administration (Super Admin) +const AdminDashboard = () => + import('@/scripts/admin/views/administration/AdminDashboard.vue') const AdminCompaniesIndex = () => import('@/scripts/admin/views/administration/companies/Index.vue') const AdminCompaniesEdit = () => @@ -515,6 +517,12 @@ export default [ }, // Administration (Super Admin) + { + path: 'administration/dashboard', + name: 'admin.dashboard', + meta: { isSuperAdmin: true }, + component: AdminDashboard, + }, { path: 'administration/companies', name: 'admin.companies.index', diff --git a/resources/scripts/admin/layouts/LayoutBasic.vue b/resources/scripts/admin/layouts/LayoutBasic.vue index 1f98078a..de505a9c 100644 --- a/resources/scripts/admin/layouts/LayoutBasic.vue +++ b/resources/scripts/admin/layouts/LayoutBasic.vue @@ -55,12 +55,16 @@ const isAppLoaded = computed(() => { }) const hasCompany = computed(() => { - return !!companyStore.selectedCompany || !!userStore.currentUser?.is_super_admin + return !!companyStore.selectedCompany || companyStore.isAdminMode }) onMounted(() => { globalStore.bootstrap().then((res) => { - if (!res.data.current_company && !res.data.current_user.is_super_admin) { + if (companyStore.isAdminMode) { + return + } + + if (!res.data.current_company) { if (route.name !== 'no.company') { router.push({ name: 'no.company' }) } diff --git a/resources/scripts/admin/layouts/partials/TheSiteHeader.vue b/resources/scripts/admin/layouts/partials/TheSiteHeader.vue index ce2d849d..a06d1bcb 100644 --- a/resources/scripts/admin/layouts/partials/TheSiteHeader.vue +++ b/resources/scripts/admin/layouts/partials/TheSiteHeader.vue @@ -18,7 +18,7 @@ " > -
  • +
  • + + + + + +
    + +
    + +
    + + + +

    + {{ data.app_version }} +

    +
    + + + + +

    + {{ data.php_version }} +

    +
    + + + + +

    + {{ data.database?.driver?.toUpperCase() }} +

    +

    + {{ data.database?.version }} +

    +
    + + + + +

    + {{ data.counts?.companies }} +

    +
    + + + + +

    + {{ data.counts?.users }} +

    +
    +
    + + + + diff --git a/resources/scripts/components/CompanySwitcher.vue b/resources/scripts/components/CompanySwitcher.vue index 4c1fa036..adfc8c61 100644 --- a/resources/scripts/components/CompanySwitcher.vue +++ b/resources/scripts/components/CompanySwitcher.vue @@ -19,7 +19,13 @@ @click="isShow = !isShow" > + {{ $t('navigation.administration') }} + + {{ companyStore.selectedCompany.name }} @@ -49,6 +55,29 @@ pb-4 " > + +
    +
    +
    + + + +
    + {{ $t('navigation.administration') }} +
    +
    +
    +
    +
    +