Skip to content

Show admin-only modal for non-admin billing access#1479

Merged
riderx merged 2 commits into
mainfrom
riderx/non-admin-billing-modal
Jan 21, 2026
Merged

Show admin-only modal for non-admin billing access#1479
riderx merged 2 commits into
mainfrom
riderx/non-admin-billing-modal

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Jan 21, 2026

Summary

Non-admin users can now see all billing and subscription tabs instead of having them hidden. When clicked, a modal appears showing which admins they need to contact. This improves UX by making features discoverable while preventing unauthorized access with clear guidance.

Test plan

  1. Login as a non-admin user in an organization
  2. Go to Settings > Organization
  3. Verify all tabs (Usage, Plans, Credits, Audit Logs, Security, Billing) are visible
  4. Click on any of these tabs to see the admin-only modal with blur effect
  5. Verify admin contact information is displayed in the modal

Checklist

  • My code follows the code style of this project and passes bun run lint
  • My change requires a change to the documentation
  • I have tested my code manually

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Admin-only access UI: non-admins see a restriction modal when accessing admin routes or clicking billing; billing opens for admins as before.
    • Modal shows admins' avatars and contact emails to help request access.
  • Localization

    • Added admin-access and contact-admin messaging across multiple languages for the new flow.

✏️ Tip: You can customize this high-level summary in your review settings.

…nagement

Instead of hiding the billing and admin tabs for non-admin users, all tabs are now visible. When non-admins click on admin-only tabs (billing, usage, plans, credits, audit-logs, security), the content is blurred and a modal appears showing which admins they need to contact.

This provides better UX by allowing non-admins to discover these features while preventing access with a clear explanation.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 21, 2026 17:25
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds admin-only access UI: new AdminOnlyModal component, settings layout checks to show the modal for non-admins (including billing click handling and content blur), TypeScript component declarations, and localized strings in many message files.

Changes

Cohort / File(s) Summary
Internationalization
messages/*.json
messages/en.json, messages/de.json, messages/es.json, messages/fr.json, messages/hi.json, messages/id.json, messages/it.json, messages/ja.json, messages/ko.json, messages/pl.json, messages/pt-br.json, messages/ru.json, messages/tr.json, messages/vi.json, messages/zh-cn.json
Added three translation keys to each locale: admin-only-access, admin-only-billing-description, and contact-your-admin.
Type Declarations
src/components.d.ts
Added AdminOnlyModal entry to GlobalComponents and a global TSX/JSX declaration for AdminOnlyModal.
New Modal Component
src/components/AdminOnlyModal.vue
New Vue component (script setup, TS) that fetches org members on mount, filters admins, keeps loading state, and renders a modal with admin avatars/emails and localized copy.
Settings Layout
src/layouts/settings.vue
Adds computed admin checks and route filtering, billing-tab click handling to show modal for non-admins, conditional blur of content and insertion of AdminOnlyModal instances for admin-only routes and billing access.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Settings as SettingsLayout
    participant Store as OrganizationStore
    participant Modal as AdminOnlyModal

    User->>Settings: Navigate to settings / click Billing tab
    Settings->>Store: Read user role / org info
    Settings->>Settings: Evaluate isSuperAdmin / isOnAdminOnlyRoute
    alt non-admin accessing admin-only route or billing
        Settings->>Modal: open AdminOnlyModal
        Modal->>Store: fetch organization members
        Store-->>Modal: return members list
        Modal->>Modal: filter admins (super_admin/admin), map emails & avatars
        Modal-->>User: render modal with admin contacts (or loading spinner)
    else admin allowed
        Settings-->>User: proceed to normal settings flow (open portal or show content)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐇
I fetched the list, with twitching nose and ear,
A modal blooms where only admins appear,
Avatars and emails in tidy little rows,
If you're not one, a friendly gate now shows.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: displaying an admin-only modal when non-admin users attempt billing access.
Description check ✅ Passed The description is mostly complete with a clear summary, comprehensive test plan, and populated checklist. All key sections are addressed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Added translations for:
- admin-only-access
- admin-only-billing-description
- contact-your-admin

Languages: de, es, fr, hi, id, it, ja, ko, pl, pt-br, ru, tr, vi, zh-cn

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/layouts/settings.vue`:
- Around line 27-34: The adminOnlyRoutes array currently contains two incorrect
audit-log entries ('/settings/organization/audit-logs' and
'/settings/organization/auditlogs') that don't match the registered route;
update the adminOnlyRoutes array (symbol: adminOnlyRoutes in settings.vue) to
replace both entries with the correct route string
'/settings/organization/AuditLogs' so the admin-only access check will match the
actual route.
🧹 Nitpick comments (3)
src/components/AdminOnlyModal.vue (2)

13-26: Consider handling the empty admins state.

If fetching fails or no admins are found, the user sees no contact information. Consider adding an error state or fallback message.

💡 Suggested improvement
 const admins = ref<{ email: string, image_url: string }[]>([])
 const isLoading = ref(true)
+const hasError = ref(false)
 
 onMounted(async () => {
   try {
     const members = await organizationStore.getMembers()
     admins.value = members
       .filter(m => m.role === 'super_admin' || m.role === 'admin')
       .map(m => ({ email: m.email, image_url: m.image_url }))
   }
   catch (e) {
     console.error('Failed to fetch admins:', e)
+    hasError.value = true
   }
   finally {
     isLoading.value = false
   }
 })

Then in the template, add a fallback when admins.length === 0 && !isLoading:

<div v-else-if="admins.length === 0" class="mt-4 text-sm text-gray-500 dark:text-gray-400">
  {{ t('contact-your-admin') }}
</div>

29-42: Consider adding accessibility attributes to the modal.

The modal overlay could benefit from semantic attributes for screen readers.

♿ Accessibility improvement
-  <div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60">
-    <div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md">
+  <div
+    class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60"
+    role="dialog"
+    aria-modal="true"
+    aria-labelledby="admin-only-title"
+  >
+    <div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md">

And add an id to the heading:

-      <h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
+      <h2 id="admin-only-title" class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
src/layouts/settings.vue (1)

150-153: Consider improving the billing modal dismissal UX.

The billing modal (line 153) dismisses when clicking anywhere on it, which may not be intuitive. Users might accidentally dismiss it before reading the admin contacts. Consider adding an explicit close button inside the AdminOnlyModal component or passing a prop to enable a close button.

Alternatively, you could add @click.stop on the inner modal content in AdminOnlyModal.vue and only dismiss when clicking the backdrop:

<!-- In settings.vue -->
<AdminOnlyModal v-if="showBillingModal" `@click.self`="showBillingModal = false" />

This would allow clicks on the modal content (admin list) without dismissing it.

Comment thread src/layouts/settings.vue
Comment on lines +27 to +34
const adminOnlyRoutes = [
'/settings/organization/usage',
'/settings/organization/plans',
'/settings/organization/credits',
'/settings/organization/audit-logs',
'/settings/organization/auditlogs',
'/settings/organization/security',
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*router*" -o -name "*routes*" | grep -v node_modules | head -20

Repository: Cap-go/capgo

Length of output: 79


🏁 Script executed:

rg -n "audit" --type=ts --type=js -g '!node_modules' -g '!*.d.ts' | head -30

Repository: Cap-go/capgo

Length of output: 2930


🏁 Script executed:

rg -n "path.*audit|audit.*path" --type=ts --type=js --type=vue -g '!node_modules' | head -30

Repository: Cap-go/capgo

Length of output: 83


🏁 Script executed:

cat -n ./src/typed-router.d.ts | grep -i "audit" -A 2 -B 2

Repository: Cap-go/capgo

Length of output: 539


🏁 Script executed:

fd -e ts -e js "router\|routes" src/ | grep -v node_modules | grep -v dist

Repository: Cap-go/capgo

Length of output: 38


🏁 Script executed:

rg "audit" --type ts -g '!node_modules' -g '!*.d.ts' -C 3 | grep -i "path\|route" -A 3 -B 3

Repository: Cap-go/capgo

Length of output: 38


🏁 Script executed:

cat -n ./src/pages/settings/organization/AuditLogs.vue | head -20

Repository: Cap-go/capgo

Length of output: 911


🏁 Script executed:

rg "/settings/organization.*[Aa]udit" --type ts --type vue -g '!node_modules'

Repository: Cap-go/capgo

Length of output: 83


🏁 Script executed:

rg "/settings/organization" --type ts -g '!node_modules' -g '!*.d.ts' | grep -i audit

Repository: Cap-go/capgo

Length of output: 174


🏁 Script executed:

rg "audit" -i --type ts -g '!node_modules' -g '!*.d.ts' | grep -E "path|route|/settings"

Repository: Cap-go/capgo

Length of output: 174


🏁 Script executed:

cat -n ./src/constants/organizationTabs.ts

Repository: Cap-go/capgo

Length of output: 1453


🏁 Script executed:

cat -n ./src/typed-router.d.ts | grep -i "audit" -C 5

Repository: Cap-go/capgo

Length of output: 940


🏁 Script executed:

cat -n ./src/typed-router.d.ts | grep "/settings/organization/" | head -20

Repository: Cap-go/capgo

Length of output: 1137


🏁 Script executed:

cat -n ./src/layouts/settings.vue | head -50

Repository: Cap-go/capgo

Length of output: 2288


🏁 Script executed:

rg "adminOnlyRoutes" --type ts --type vue -g '!node_modules'

Repository: Cap-go/capgo

Length of output: 83


Replace both audit log route entries with the correct path.

The adminOnlyRoutes array contains /settings/organization/audit-logs and /settings/organization/auditlogs, but the actual registered route is /settings/organization/AuditLogs (CamelCase). The current entries will not match the actual route, breaking the admin-only access check. Replace both with /settings/organization/AuditLogs.

🤖 Prompt for AI Agents
In `@src/layouts/settings.vue` around lines 27 - 34, The adminOnlyRoutes array
currently contains two incorrect audit-log entries
('/settings/organization/audit-logs' and '/settings/organization/auditlogs')
that don't match the registered route; update the adminOnlyRoutes array (symbol:
adminOnlyRoutes in settings.vue) to replace both entries with the correct route
string '/settings/organization/AuditLogs' so the admin-only access check will
match the actual route.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves UX by making admin-only features discoverable to non-admin users. Instead of hiding billing and subscription-related tabs, they are now always visible. When non-admin users click on these tabs or navigate to admin-only routes, a modal appears showing which admins they need to contact.

Changes:

  • Removed complex conditional logic that showed/hid tabs based on permissions
  • Added AdminOnlyModal component that displays admin contact information
  • Tabs are now always visible with permission checks happening at interaction time
  • Added blur effect and modal overlay for admin-only routes

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 8 comments.

File Description
src/layouts/settings.vue Removed 65+ lines of conditional tab visibility logic; added admin-only route detection and modal triggering; billing tab now always shown with click-time permission check
src/components/AdminOnlyModal.vue New modal component that fetches and displays organization admins when non-admin users attempt to access restricted features
messages/en.json Added three translation keys for the admin-only modal UI
src/components.d.ts Auto-generated TypeScript definitions for the new AdminOnlyModal component

Comment thread messages/en.json
"admin-dashboard-construction": "Admin dashboard is under construction. Components will be added in the next phase.",
"admin-dashboard-description": "Platform-wide statistics and analytics",
"admin-only-access": "Admin Access Required",
"admin-only-billing-description": "Only organization admins can manage billing and subscriptions. Please contact an admin to make changes.",
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation key 'admin-only-billing-description' is used for all admin-only features (usage, plans, credits, audit logs, security), not just billing. This message specifically mentions "billing and subscriptions" which is incorrect when shown for other admin-only routes. Consider renaming to 'admin-only-description' and updating the message to be more generic, such as "Only organization admins can access this feature. Please contact an admin for assistance."

Suggested change
"admin-only-billing-description": "Only organization admins can manage billing and subscriptions. Please contact an admin to make changes.",
"admin-only-billing-description": "Only organization admins can access this feature. Please contact an admin for assistance.",

Copilot uses AI. Check for mistakes.
Comment thread src/layouts/settings.vue
<main class="flex relative flex-1 w-full min-h-0 mt-0 overflow-hidden bg-blue-50 dark:bg-slate-800/40">
<div
class="flex-1 w-full min-h-0 px-0 pt-0 mx-auto mb-8 overflow-y-auto sm:px-6 md:pt-16 lg:px-8 max-w-9xl"
:class="{ 'blur-sm pointer-events-none select-none': showAdminOnlyModal }"
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blur effect is only applied when showAdminOnlyModal is true, but the billing modal (showBillingModal) at line 153 also overlays the content without applying blur. For consistency in UX, consider applying the same blur effect when showBillingModal is true, or update the blur condition to include both modal states.

Suggested change
:class="{ 'blur-sm pointer-events-none select-none': showAdminOnlyModal }"
:class="{ 'blur-sm pointer-events-none select-none': showAdminOnlyModal || showBillingModal }"

Copilot uses AI. Check for mistakes.
Comment thread src/layouts/settings.vue
Comment on lines 68 to 85
watchEffect(() => {
// ensure usage/plans tabs based on permissions (keeps icons from base)
const needsUsage = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])
const hasUsage = organizationTabs.value.find(tab => tab.key === '/settings/organization/usage')
if (needsUsage && !hasUsage) {
const base = baseOrgTabs.find(t => t.key === '/settings/organization/usage')
if (base)
organizationTabs.value.push({ ...base })
}
if (!needsUsage && hasUsage)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/usage')

const needsCredits = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])
const hasCredits = organizationTabs.value.find(tab => tab.key === '/settings/organization/credits')

if (needsCredits && !hasCredits) {
const base = baseOrgTabs.find(t => t.key === '/settings/organization/credits')
if (base)
organizationTabs.value.push({ ...base })
}

if (!needsCredits && hasCredits)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/credits')

const needsPlans = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])
const hasPlans = organizationTabs.value.find(tab => tab.key === '/settings/organization/plans')
if (needsPlans && !hasPlans) {
const base = baseOrgTabs.find(t => t.key === '/settings/organization/plans')
if (base)
organizationTabs.value.push({ ...base })
}
if (!needsPlans && hasPlans)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/plans')

// Audit logs - visible only to super_admins
const needsAuditLogs = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])
const hasAuditLogs = organizationTabs.value.find(tab => tab.key === '/settings/organization/audit-logs')
if (needsAuditLogs && !hasAuditLogs) {
const base = baseOrgTabs.find(t => t.key === '/settings/organization/audit-logs')
if (base)
organizationTabs.value.push({ ...base })
}
if (!needsAuditLogs && hasAuditLogs)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/audit-logs')

// Security - visible only to super_admins
const needsSecurity = organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])
const hasSecurity = organizationTabs.value.find(tab => tab.key === '/settings/organization/security')
if (needsSecurity && !hasSecurity) {
const base = baseOrgTabs.find(t => t.key === '/settings/organization/security')
if (base)
organizationTabs.value.push({ ...base })
}
if (!needsSecurity && hasSecurity)
organizationTabs.value = organizationTabs.value.filter(tab => tab.key !== '/settings/organization/security')

if (!Capacitor.isNativePlatform()
&& organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])
&& !organizationTabs.value.find(tab => tab.key === '/billing')) {
// Billing tab - always show on web, with different behavior for non-admins
if (!Capacitor.isNativePlatform() && !organizationTabs.value.find(tab => tab.key === '/billing')) {
organizationTabs.value.push({
label: 'billing',
icon: IconBilling,
key: '/billing',
onClick: () => openPortal(organizationStore.currentOrganization?.gid ?? '', t),
onClick: () => {
// Check permissions at click time to handle role changes
if (organizationStore.hasPermissionsInRole(organizationStore.currentRole, ['super_admin'])) {
openPortal(organizationStore.currentOrganization?.gid ?? '', t)
}
else {
showBillingModal.value = true
}
},
})
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The watchEffect adds the billing tab every time it runs if the tab doesn't exist. However, if organizationTabs.value is reset or modified elsewhere, this could lead to duplicate billing tabs being added. While the check '!organizationTabs.value.find(tab => tab.key === '/billing')' prevents immediate duplicates, consider tracking whether the tab has been added or using a computed property for tabs instead of watchEffect to avoid potential edge cases.

Copilot uses AI. Check for mistakes.
try {
const members = await organizationStore.getMembers()
admins.value = members
.filter(m => m.role === 'super_admin' || m.role === 'admin')
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering includes both 'super_admin' and 'admin' roles, but all permission checks throughout the codebase (including in settings.vue) only check for 'super_admin'. This inconsistency means users with the 'admin' role will be shown in the modal as contacts, but they also cannot access these features. Either remove 'admin' from this filter or update the permission checks to allow 'admin' role as well.

Suggested change
.filter(m => m.role === 'super_admin' || m.role === 'admin')
.filter(m => m.role === 'super_admin')

Copilot uses AI. Check for mistakes.
Comment thread src/layouts/settings.vue
<RouterView v-else class="w-full" />
</div>
<!-- Admin-only modal for admin-only routes -->
<AdminOnlyModal v-if="showAdminOnlyModal" />
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal for admin-only routes (line 151) has no click handler, meaning users cannot dismiss it and are stuck on the page. While the billing modal (line 153) has a click handler, route-based modals should either redirect users back to an accessible route or provide a way to dismiss the modal. Consider adding navigation back to '/settings/organization' or another accessible route when the modal is clicked.

Copilot uses AI. Check for mistakes.
Comment thread src/layouts/settings.vue
'/settings/organization/plans',
'/settings/organization/credits',
'/settings/organization/audit-logs',
'/settings/organization/auditlogs',
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array contains both '/settings/organization/audit-logs' and '/settings/organization/auditlogs' as separate entries. This duplication is unnecessary - only one route should be needed. Check which route format is actually used in the router configuration and remove the duplicate.

Suggested change
'/settings/organization/auditlogs',

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +74
<div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60">
<div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md">
<div class="flex justify-center mb-4">
<div class="flex justify-center items-center w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900/30">
<svg class="w-8 h-8 text-blue-500" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
{{ t('admin-only-access') }}
</h2>
<p class="mb-4 text-gray-600 dark:text-gray-400">
{{ t('admin-only-billing-description') }}
</p>
<div v-if="isLoading" class="flex justify-center py-2">
<div class="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
</div>
<div v-else-if="admins.length > 0" class="mt-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('contact-your-admin') }}:
</p>
<div class="flex flex-wrap gap-2 justify-center">
<div
v-for="admin in admins"
:key="admin.email"
class="flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-600"
>
<img
v-if="admin.image_url"
:src="admin.image_url"
:alt="admin.email"
class="w-5 h-5 rounded-full"
>
<IconUserCircle v-else class="w-5 h-5 text-gray-400" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{ admin.email }}</span>
</div>
</div>
</div>
</div>
</div>
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modal lacks critical accessibility features: no focus trap to prevent tabbing out of the modal, no keyboard handler for ESC key to dismiss, no ARIA role="dialog", no aria-modal="true", and no aria-labelledby pointing to the heading. These features are essential for screen reader users and keyboard navigation. Consider wrapping the modal in a focus trap and adding appropriate ARIA attributes.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +75
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconUserCircle from '~icons/heroicons/user-circle'
import { useOrganizationStore } from '~/stores/organization'

const { t } = useI18n()
const organizationStore = useOrganizationStore()

const admins = ref<{ email: string, image_url: string }[]>([])
const isLoading = ref(true)

onMounted(async () => {
try {
const members = await organizationStore.getMembers()
admins.value = members
.filter(m => m.role === 'super_admin' || m.role === 'admin')
.map(m => ({ email: m.email, image_url: m.image_url }))
}
catch (e) {
console.error('Failed to fetch admins:', e)
}
finally {
isLoading.value = false
}
})
</script>

<template>
<div class="flex absolute inset-0 z-10 flex-col justify-center items-center bg-white/60 dark:bg-gray-900/60">
<div class="p-8 text-center bg-white rounded-xl border shadow-xl dark:bg-gray-800 border-blue-200 dark:border-blue-700 max-w-md">
<div class="flex justify-center mb-4">
<div class="flex justify-center items-center w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900/30">
<svg class="w-8 h-8 text-blue-500" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<h2 class="mb-2 text-2xl font-bold text-gray-900 dark:text-white">
{{ t('admin-only-access') }}
</h2>
<p class="mb-4 text-gray-600 dark:text-gray-400">
{{ t('admin-only-billing-description') }}
</p>
<div v-if="isLoading" class="flex justify-center py-2">
<div class="w-6 h-6 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
</div>
<div v-else-if="admins.length > 0" class="mt-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<p class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
{{ t('contact-your-admin') }}:
</p>
<div class="flex flex-wrap gap-2 justify-center">
<div
v-for="admin in admins"
:key="admin.email"
class="flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800 rounded-full border border-gray-200 dark:border-gray-600"
>
<img
v-if="admin.image_url"
:src="admin.image_url"
:alt="admin.email"
class="w-5 h-5 rounded-full"
>
<IconUserCircle v-else class="w-5 h-5 text-gray-400" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{ admin.email }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new AdminOnlyModal component and the modal triggering behavior in settings.vue lack test coverage. Since the repository has Playwright E2E tests for frontend functionality, consider adding tests that verify: 1) Non-admin users can see all tabs, 2) Clicking admin-only tabs shows the modal with admin contact information, 3) The modal can be dismissed for billing tabs, 4) Non-admin users navigating directly to admin-only routes see the modal.

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21e53e9b20

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +13 to +16
onMounted(async () => {
try {
const members = await organizationStore.getMembers()
admins.value = members
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-fetch admins after org is loaded

The modal only calls organizationStore.getMembers() once in onMounted. On a hard refresh into an admin-only settings route, currentOrganization is often still unset when this runs, and getMembers() returns an empty list in that case. Because there’s no watch or retry when the org becomes available, the modal can permanently show no admin contacts even after the org finishes loading. Consider watching currentOrganization (or a fetch completion flag) and reloading admins when it becomes defined.

Useful? React with 👍 / 👎.

@riderx riderx merged commit aa32229 into main Jan 21, 2026
11 checks passed
@riderx riderx deleted the riderx/non-admin-billing-modal branch January 21, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants