All files / src/views InvitationsPage.vue

94.28% Statements 33/35
100% Branches 25/25
81.81% Functions 9/11
94.11% Lines 32/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126  20x   1x   1x     20x     1x               1x   10x   1x     10x             1x         1x                 1x 2x             1x                       10x                                         20x 20x 20x 20x 20x   20x 20x       3x 3x 2x   1x 1x         3x   2x 2x   1x 1x                    
<template>
  <PageLayout :title="t('navigation.invitations')" class="invitations-page">
    <div v-if="loading" class="row">
      <div class="col-12 text-center py-4">
        <div class="spinner-border text-primary" role="status">
          <span class="visually-hidden">{{ t('common.loading') }}</span>
        </div>
      </div>
    </div>
 
    <div v-else-if="!loading && invitations.length === 0" class="text-center py-4">
      <p class="text-muted">{{ t('invitation.noPending') }}</p>
    </div>
 
    <VirtualCardGrid
      v-else
      :items="invitations"
      :has-more="rolesStore.invitationsHasMore"
      :is-loading="loading"
      @load-more="rolesStore.loadMoreInvitations()"
    >
      <template #card="{ item: invitation }">
        <UnifiedContentCard>
          <h5 class="card-title mb-3">
            <font-awesome-icon icon="fa-solid fa-envelope" class="me-2 text-primary" />
            {{ invitation.flowName || invitation.flowId }}
          </h5>
 
          <div v-if="invitation.flowSlug" class="mb-3">
            <div class="text-muted small mb-1">Slug</div>
            <div>{{ invitation.flowSlug }}</div>
          </div>
 
          <div class="mb-3">
            <div class="text-muted small mb-1">Invited by</div>
            <div>{{ invitation.invitedBy }}</div>
          </div>
 
          <div class="mb-3">
            <div class="text-muted small mb-1">Date</div>
            <div>
              {{ invitation.joinedAt ? new Date(invitation.joinedAt).toLocaleDateString() : '' }}
            </div>
          </div>
 
          <div v-if="invitation.roleIds?.length > 0" class="mb-3">
            <div class="text-muted small mb-1">Roles</div>
            <div class="d-flex flex-wrap gap-1">
              <span v-for="roleId in invitation.roleIds" :key="roleId" class="badge bg-primary">
                {{ roleId }}
              </span>
            </div>
          </div>
 
          <template #footer>
            <div class="d-flex gap-2">
              <button class="btn btn-sm btn-success flex-fill" @click="accept(invitation.flowId)">
                <font-awesome-icon icon="fa-solid fa-check" class="me-1" />
                Accept
              </button>
              <button
                class="btn btn-sm btn-outline-danger flex-fill"
                @click="decline(invitation.flowId)"
              >
                <font-awesome-icon icon="fa-solid fa-times" class="me-1" />
                Decline
              </button>
            </div>
          </template>
        </UnifiedContentCard>
      </template>
    </VirtualCardGrid>
  </PageLayout>
</template>
 
<script setup lang="ts">
// @implements UC-INVITE-002.1
// @implements UC-INVITE-003.1
import PageLayout from '@/components/PageLayout.vue'
import VirtualCardGrid from '@/components/VirtualCardGrid.vue'
import UnifiedContentCard from '@/components/UnifiedContentCard.vue'
import { onMounted } from 'vue'
import { useRolesStore } from '@/stores/roles'
import { useFlowsStore } from '@/stores/flows'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { useUILanguage } from '@/composables/useUILanguage'
 
const rolesStore = useRolesStore()
const flowsStore = useFlowsStore()
const router = useRouter()
const { t } = useUILanguage()
const { invitations, invitationsLoading: loading } = storeToRefs(rolesStore)
 
onMounted(async () => {
  await rolesStore.fetchInvitations()
})
 
async function accept(flowId: string) {
  try {
    await flowsStore.acceptInvitation(flowId)
    router.push('/')
  } catch (error) {
    console.error('Failed to accept invitation:', error)
    alert('Failed to accept invitation')
  }
}
 
async function decline(flowId: string) {
  if (!confirm('Are you sure you want to decline this invitation?')) return
 
  try {
    await rolesStore.declineInvitation(flowId)
  } catch (error) {
    console.error('Failed to decline invitation:', error)
    alert('Failed to decline invitation')
  }
}
</script>
 
<style scoped>
.invitations-page :deep(.page-content) {
  padding: 0 !important;
}
</style>