Add invitation frontend: invite modal, pending invitations, no-company view

Members Index:
- "Invite Member" button opens InviteMemberModal (email + role dropdown)
- Pending invitations section shows below members table with cancel buttons
- Members store gains inviteMember, fetchPendingInvitations, cancelInvitation

CompanySwitcher:
- Shows pending invitations greyed out below active companies
- Each with Accept/Decline mini-buttons
- Accepting refreshes bootstrap and switches to new company

NoCompanyView:
- Standalone page for users with zero accepted companies
- Shows pending invitations with Accept/Decline or "no companies" message
- Route: /admin/no-company

Invitation Pinia store:
- Manages user's own pending invitations (fetchPending, accept, decline)
- Bootstrap populates invitations from API response

Global store:
- Bootstrap action stores pending_invitations from response
This commit is contained in:
Darko Gjorgjijoski
2026-04-03 23:20:41 +02:00
parent 8a6c085288
commit 6343b4a17f
9 changed files with 491 additions and 2 deletions

View File

@@ -27,7 +27,22 @@
<BaseButton
v-if="userStore.currentUser.is_owner"
@click="$router.push('users/create')"
variant="primary-outline"
@click="showInviteModal = true"
>
<template #left="slotProps">
<BaseIcon
name="EnvelopeIcon"
:class="slotProps.class"
aria-hidden="true"
/>
</template>
{{ $t('members.invite_member') }}
</BaseButton>
<BaseButton
v-if="userStore.currentUser.is_owner"
@click="$router.push('members/create')"
>
<template #left="slotProps">
<BaseIcon
@@ -36,7 +51,7 @@
aria-hidden="true"
/>
</template>
{{ $t('members.add_user') }}
{{ $t('members.add_member') }}
</BaseButton>
</div>
</template>
@@ -181,6 +196,47 @@
</template>
</BaseTable>
</div>
<!-- Pending Invitations Section -->
<div
v-if="userStore.currentUser.is_owner && usersStore.pendingInvitations.length > 0"
class="mt-8"
>
<h3 class="text-lg font-medium text-gray-900 mb-4">
{{ $t('members.pending_invitations') }}
</h3>
<BaseCard>
<div class="divide-y divide-gray-200">
<div
v-for="invitation in usersStore.pendingInvitations"
:key="invitation.id"
class="flex items-center justify-between px-6 py-4"
>
<div>
<p class="text-sm font-medium text-gray-900">
{{ invitation.email }}
</p>
<p class="text-sm text-gray-500">
{{ invitation.role?.title }} &middot;
{{ $t('members.invited_by') }}: {{ invitation.invited_by?.name }}
</p>
</div>
<BaseButton
variant="danger"
size="sm"
@click="cancelInvitation(invitation.id)"
>
{{ $t('members.cancel_invitation') }}
</BaseButton>
</div>
</div>
</BaseCard>
</div>
<InviteMemberModal
:show="showInviteModal"
@close="showInviteModal = false"
/>
</BasePage>
</template>
@@ -194,6 +250,7 @@ import { useDialogStore } from '@/scripts/stores/dialog'
import { useUserStore } from '@/scripts/admin/stores/user'
import AstronautIcon from '@/scripts/components/icons/empty/AstronautIcon.vue'
import UserDropdown from '@/scripts/admin/components/dropdowns/MemberIndexDropdown.vue'
import InviteMemberModal from '@/scripts/admin/components/modal-components/InviteMemberModal.vue'
import abilities from '@/scripts/admin/stub/abilities'
const notificationStore = useNotificationStore()
@@ -204,6 +261,7 @@ const userStore = useUserStore()
const router = useRouter()
let showFilters = ref(false)
let showInviteModal = ref(false)
let isFetchingInitialData = ref(true)
let id = ref(null)
let sortedBy = ref('created_at')
@@ -277,8 +335,13 @@ watch(
onMounted(() => {
usersStore.fetchUsers()
usersStore.fetchRoles()
usersStore.fetchPendingInvitations()
})
function cancelInvitation(id) {
usersStore.cancelInvitation(id)
}
onUnmounted(() => {
if (usersStore.selectAllField) {
usersStore.selectAllUsers()