mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-02 21:31:01 +00:00
124 lines
4.7 KiB
Vue
124 lines
4.7 KiB
Vue
<template>
|
|
<BasePage>
|
|
<BasePageHeader :title="$t('modules.title')">
|
|
<BaseBreadcrumb>
|
|
<BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" />
|
|
<BaseBreadcrumbItem :title="$t('modules.module', 2)" to="#" active />
|
|
</BaseBreadcrumb>
|
|
</BasePageHeader>
|
|
|
|
<BaseCard class="mt-6">
|
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
<div>
|
|
<h6 class="text-heading text-lg font-medium">Marketplace access</h6>
|
|
<p class="mt-1 text-sm text-muted">
|
|
Pair this InvoiceShelf instance with your marketplace account. The device credential stays encrypted on this server.
|
|
</p>
|
|
<div v-if="pairingCode" class="mt-4 space-y-1 text-sm text-body">
|
|
<p>Enter code <strong>{{ pairingCode.user_code }}</strong> at the marketplace verification page.</p>
|
|
<a v-if="pairingCode.verification_uri_complete || pairingCode.verification_uri" class="text-primary-600 underline" :href="pairingCode.verification_uri_complete || pairingCode.verification_uri || undefined" target="_blank" rel="noopener">Open verification page</a>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap gap-3">
|
|
<BaseButton v-if="!moduleStore.marketplacePairing?.paired" :loading="isPairing" @click="startPairing">
|
|
Pair marketplace
|
|
</BaseButton>
|
|
<BaseButton v-if="pairingCode" variant="primary-outline" :loading="isPolling" @click="pollPairing">
|
|
I have approved this device
|
|
</BaseButton>
|
|
<BaseButton v-if="moduleStore.marketplacePairing?.paired" variant="primary-outline" @click="disconnect">
|
|
Disconnect
|
|
</BaseButton>
|
|
</div>
|
|
</div>
|
|
</BaseCard>
|
|
|
|
<div class="mt-6">
|
|
<BaseTabGroup @change="setStatusFilter">
|
|
<BaseTab :title="$t('general.all')" filter="" />
|
|
<BaseTab :title="$t('modules.installed')" filter="INSTALLED" />
|
|
</BaseTabGroup>
|
|
<div v-if="isFetchingModule" class="grid mt-6 w-full grid-cols-1 items-start gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
|
<div v-for="n in 3" :key="n" class="h-80 bg-surface-tertiary rounded-lg animate-pulse" />
|
|
</div>
|
|
<div v-else-if="filteredModules.length" class="grid mt-6 w-full grid-cols-1 items-start gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
|
<ModuleCard v-for="mod in filteredModules" :key="mod.slug" :data="mod" />
|
|
</div>
|
|
<div v-else class="mt-24">
|
|
<label class="flex items-center justify-center text-muted">
|
|
{{ activeTab === 'INSTALLED' ? $t('modules.no_modules_installed') : 'No marketplace modules are available yet.' }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</BasePage>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useModuleStore } from '../store'
|
|
import ModuleCard from '../components/ModuleCard.vue'
|
|
import type { MarketplacePairingCode } from '@/scripts/api/services/module.service'
|
|
import type { Module } from '@/scripts/types/domain/module'
|
|
import { useNotificationStore } from '@/scripts/stores/notification.store'
|
|
|
|
const moduleStore = useModuleStore()
|
|
const notificationStore = useNotificationStore()
|
|
const activeTab = ref('')
|
|
const isFetchingModule = ref(false)
|
|
const isPairing = ref(false)
|
|
const isPolling = ref(false)
|
|
const pairingCode = ref<MarketplacePairingCode | null>(null)
|
|
|
|
const filteredModules = computed<Module[]>(() => activeTab.value === 'INSTALLED'
|
|
? moduleStore.installedModules
|
|
: moduleStore.modules)
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([moduleStore.fetchMarketplacePairing(), fetchModulesData()])
|
|
})
|
|
|
|
async function fetchModulesData(): Promise<void> {
|
|
isFetchingModule.value = true
|
|
try {
|
|
await moduleStore.fetchModules()
|
|
} finally {
|
|
isFetchingModule.value = false
|
|
}
|
|
}
|
|
|
|
async function startPairing(): Promise<void> {
|
|
isPairing.value = true
|
|
try {
|
|
pairingCode.value = await moduleStore.startMarketplacePairing()
|
|
} finally {
|
|
isPairing.value = false
|
|
}
|
|
}
|
|
|
|
async function pollPairing(): Promise<void> {
|
|
isPolling.value = true
|
|
try {
|
|
const result = await moduleStore.pollMarketplacePairing()
|
|
if (result.status === 'paired') {
|
|
pairingCode.value = null
|
|
notificationStore.showNotification({ type: 'success', message: 'Marketplace paired' })
|
|
await fetchModulesData()
|
|
} else {
|
|
notificationStore.showNotification({ type: 'info', message: 'Waiting for marketplace approval' })
|
|
}
|
|
} finally {
|
|
isPolling.value = false
|
|
}
|
|
}
|
|
|
|
async function disconnect(): Promise<void> {
|
|
await moduleStore.disconnectMarketplace()
|
|
notificationStore.showNotification({ type: 'success', message: 'Marketplace disconnected' })
|
|
await fetchModulesData()
|
|
}
|
|
|
|
function setStatusFilter(data: { filter: string }): void {
|
|
activeTab.value = data.filter
|
|
}
|
|
</script>
|