@nuxt-customer-portal/invoices 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +4 -0
- package/app/assets/invoices.css +332 -0
- package/app/components/InvoicesAdminEmptyState.vue +28 -0
- package/app/components/InvoicesAdminInvoices.vue +950 -0
- package/app/components/InvoicesAdminListToolbar.vue +19 -0
- package/app/components/InvoicesAdminPaginatedList.vue +95 -0
- package/app/components/InvoicesClientAccessPanel.vue +63 -0
- package/app/components/InvoicesClientSettingsPanel.vue +150 -0
- package/app/components/InvoicesDashboardCard.vue +29 -0
- package/app/components/InvoicesDashboardReceivedInvoices.vue +1 -0
- package/app/components/InvoicesDashboardSalesInvoices.vue +1 -0
- package/app/components/InvoicesInvoiceDetail.vue +1109 -0
- package/app/components/InvoicesInvoiceEmailModal.vue +223 -0
- package/app/components/InvoicesPageShell.vue +7 -0
- package/app/components/InvoicesSenderDetailsForm.vue +229 -0
- package/app/composables/useInvoiceSources.ts +9 -0
- package/app/composables/useInvoices.ts +68 -0
- package/app/composables/useInvoicesAdminList.ts +113 -0
- package/app/pages/admin/invoices/[id].vue +36 -0
- package/app/pages/admin/invoices/index.vue +61 -0
- package/app/pages/admin/invoices/new.vue +18 -0
- package/app/pages/admin/invoices/settings.vue +86 -0
- package/app/pages/invoices/[id].vue +37 -0
- package/app/pages/invoices/index.vue +131 -0
- package/app/pages/invoices/viewers.vue +115 -0
- package/app/plugins/invoices-feature.ts +60 -0
- package/i18n/locales/en.json +306 -0
- package/i18n/locales/nl.json +306 -0
- package/migrations/0000_baseline.sql +235 -0
- package/migrations/meta/0000_snapshot.json +1318 -0
- package/migrations/meta/_journal.json +13 -0
- package/nuxt.config.ts +15 -0
- package/package.json +60 -0
- package/portal.manifest.mjs +9 -0
- package/server/api/invoices/admin/bootstrap.get.ts +17 -0
- package/server/api/invoices/admin/clients/[clientId]/access.get.ts +11 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/[id].delete.ts +8 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/[id].patch.ts +13 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/index.get.ts +8 -0
- package/server/api/invoices/admin/clients/[clientId]/contacts/index.post.ts +12 -0
- package/server/api/invoices/admin/email-domain.get.ts +17 -0
- package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].delete.ts +23 -0
- package/server/api/invoices/admin/invoices/[id]/attachments/[attachmentId].get.ts +30 -0
- package/server/api/invoices/admin/invoices/[id]/attachments.post.ts +27 -0
- package/server/api/invoices/admin/invoices/[id]/email-preview.get.ts +22 -0
- package/server/api/invoices/admin/invoices/[id]/email-status.post.ts +18 -0
- package/server/api/invoices/admin/invoices/[id]/email.post.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/index.get.ts +17 -0
- package/server/api/invoices/admin/invoices/[id]/issue.post.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/payments.post.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/pdf.get.ts +29 -0
- package/server/api/invoices/admin/invoices/[id]/reminder-preview.get.ts +24 -0
- package/server/api/invoices/admin/invoices/[id]/reminder.post.ts +25 -0
- package/server/api/invoices/admin/invoices/[id].patch.ts +24 -0
- package/server/api/invoices/admin/invoices/index.get.ts +20 -0
- package/server/api/invoices/admin/invoices/index.post.ts +19 -0
- package/server/api/invoices/admin/invoices/next-number.get.ts +16 -0
- package/server/api/invoices/admin/settings.put.ts +9 -0
- package/server/api/invoices/capabilities.get.ts +25 -0
- package/server/api/invoices/client/[accessId]/viewers.get.ts +10 -0
- package/server/api/invoices/client/[accessId]/viewers.put.ts +17 -0
- package/server/api/invoices/client/invoices/[id]/attachments/[attachmentId].get.ts +20 -0
- package/server/api/invoices/client/invoices/[id]/index.get.ts +12 -0
- package/server/api/invoices/client/invoices/[id]/pdf.get.ts +23 -0
- package/server/api/invoices/client/invoices/index.get.ts +14 -0
- package/server/api/invoices/client/suppliers.get.ts +8 -0
- package/server/db/schema/invoices.ts +251 -0
- package/server/plugins/openapi-contracts.ts +55 -0
- package/server/utils/invoice-email-status.ts +64 -0
- package/server/utils/invoice-email.ts +240 -0
- package/server/utils/invoice-listing.ts +71 -0
- package/server/utils/invoice-pdf.ts +358 -0
- package/server/utils/invoice-repository.ts +821 -0
- package/server/utils/invoice-validation.ts +112 -0
- package/shared/email-delivery-status.ts +23 -0
- package/shared/feature.ts +148 -0
- package/shared/invoice-introduction.ts +21 -0
- package/shared/invoice-number.ts +24 -0
- package/shared/types/invoice.ts +251 -0
- package/tokens.css +25 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineOptions({ inheritAttrs: false })
|
|
3
|
+
const props = defineProps<{
|
|
4
|
+
filters: Array<{ key: string; placeholder: string; items: Array<{ label: string; value: string | undefined }> }>
|
|
5
|
+
filterValues: Record<string, string | undefined>
|
|
6
|
+
sortOptions: Array<{ label: string; value: string }>
|
|
7
|
+
sortBy: string
|
|
8
|
+
sortDir: 'asc' | 'desc'
|
|
9
|
+
}>()
|
|
10
|
+
const search = defineModel<string>('search', { required: true })
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<PortalListToolbar
|
|
15
|
+
v-bind="{ ...props, ...$attrs }"
|
|
16
|
+
v-model:search="search"
|
|
17
|
+
:search-placeholder="$t('features.invoices.admin.list.search')"
|
|
18
|
+
/>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { InvoicesListPagination } from '@nuxt-customer-portal/invoices/shared/types/invoice'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
pagination: InvoicesListPagination
|
|
6
|
+
pending: boolean
|
|
7
|
+
loadingNext: boolean
|
|
8
|
+
loadingPrevious: boolean
|
|
9
|
+
hasNext: boolean
|
|
10
|
+
hasPrevious: boolean
|
|
11
|
+
initialScrollTop?: number
|
|
12
|
+
}>()
|
|
13
|
+
const emit = defineEmits<{ next: []; previous: []; page: [page: number]; scroll: [scrollTop: number] }>()
|
|
14
|
+
const scrollContainer = ref<HTMLElement | null>(null)
|
|
15
|
+
const previousSentinel = ref<HTMLElement | null>(null)
|
|
16
|
+
const nextSentinel = ref<HTMLElement | null>(null)
|
|
17
|
+
let previousScrollHeight = 0
|
|
18
|
+
const loadPrevious = () => {
|
|
19
|
+
previousScrollHeight = scrollContainer.value?.scrollHeight ?? 0
|
|
20
|
+
emit('previous')
|
|
21
|
+
}
|
|
22
|
+
watch(
|
|
23
|
+
() => props.loadingPrevious,
|
|
24
|
+
async (loading, wasLoading) => {
|
|
25
|
+
if (loading || !wasLoading || !scrollContainer.value) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
await nextTick()
|
|
29
|
+
scrollContainer.value.scrollTop += scrollContainer.value.scrollHeight - previousScrollHeight
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
useAutoPagination({
|
|
33
|
+
sentinel: previousSentinel,
|
|
34
|
+
scrollContainer,
|
|
35
|
+
canLoadMore: () => props.hasPrevious,
|
|
36
|
+
loading: () => props.pending,
|
|
37
|
+
loadMore: loadPrevious,
|
|
38
|
+
rootMargin: '240px 0px 0px 0px'
|
|
39
|
+
})
|
|
40
|
+
useAutoPagination({
|
|
41
|
+
sentinel: nextSentinel,
|
|
42
|
+
scrollContainer,
|
|
43
|
+
canLoadMore: () => props.hasNext,
|
|
44
|
+
loading: () => props.pending,
|
|
45
|
+
loadMore: () => emit('next')
|
|
46
|
+
})
|
|
47
|
+
onMounted(async () => {
|
|
48
|
+
if (!props.initialScrollTop) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
await nextTick()
|
|
52
|
+
if (scrollContainer.value) {
|
|
53
|
+
scrollContainer.value.scrollTop = props.initialScrollTop
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div class="flex h-full min-h-0 flex-col">
|
|
60
|
+
<div
|
|
61
|
+
ref="scrollContainer"
|
|
62
|
+
class="invoices-list-scroll min-h-0 flex-1 overflow-y-auto overflow-x-hidden"
|
|
63
|
+
@scroll="emit('scroll', scrollContainer?.scrollTop ?? 0)"
|
|
64
|
+
>
|
|
65
|
+
<div v-if="hasPrevious" ref="previousSentinel" class="h-px" aria-hidden="true" />
|
|
66
|
+
<div v-if="loadingPrevious" class="space-y-2 pb-3">
|
|
67
|
+
<USkeleton v-for="index in 2" :key="index" class="h-24 w-full" />
|
|
68
|
+
</div>
|
|
69
|
+
<slot />
|
|
70
|
+
<div v-if="hasNext" ref="nextSentinel" class="h-px" aria-hidden="true" />
|
|
71
|
+
<div v-if="loadingNext" class="space-y-2 pt-3">
|
|
72
|
+
<USkeleton v-for="index in 2" :key="index" class="h-24 w-full" />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<footer
|
|
76
|
+
class="flex min-h-12 shrink-0 flex-wrap items-center justify-between gap-3 border-t border-default px-1 pt-3 text-sm text-muted"
|
|
77
|
+
>
|
|
78
|
+
<span>{{ $t('features.invoices.admin.list.totalRecords', pagination.total) }}</span>
|
|
79
|
+
<UPagination
|
|
80
|
+
v-if="pagination.pageCount > 1"
|
|
81
|
+
:page="pagination.page"
|
|
82
|
+
:total="pagination.total"
|
|
83
|
+
:items-per-page="pagination.pageSize"
|
|
84
|
+
:disabled="pending"
|
|
85
|
+
@update:page="emit('page', $event)"
|
|
86
|
+
/>
|
|
87
|
+
</footer>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style scoped>
|
|
92
|
+
.invoices-list-scroll {
|
|
93
|
+
padding: 2px 2px 16px;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { GenericClientDto } from '@nuxt-customer-portal/clients/types'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ client: GenericClientDto }>()
|
|
5
|
+
const { t } = useI18n()
|
|
6
|
+
const invoices = useInvoices()
|
|
7
|
+
const { data: access, error } = await useAsyncData(
|
|
8
|
+
`invoice-client-access-${props.client.organizationId}`,
|
|
9
|
+
() => invoices.clientAccessOverview(props.client.organizationId),
|
|
10
|
+
{ watch: [() => props.client] }
|
|
11
|
+
)
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<UCard>
|
|
16
|
+
<template #header>
|
|
17
|
+
<h2 class="font-semibold">{{ t('features.invoices.clientAccess.title') }}</h2>
|
|
18
|
+
<p class="mt-1 text-sm text-muted">{{ t('features.invoices.clientAccess.description') }}</p>
|
|
19
|
+
</template>
|
|
20
|
+
<UAlert v-if="error" color="error" :title="t('features.invoices.clientAccess.error')" />
|
|
21
|
+
<div v-else-if="access" class="space-y-4">
|
|
22
|
+
<UAlert
|
|
23
|
+
v-if="!access.moduleEnabled || !access.configured || !access.enabled"
|
|
24
|
+
color="info"
|
|
25
|
+
variant="subtle"
|
|
26
|
+
icon="i-lucide-info"
|
|
27
|
+
:title="
|
|
28
|
+
t(
|
|
29
|
+
`features.invoices.clientAccess.${!access.moduleEnabled ? 'moduleDisabled' : !access.configured ? 'notConfigured' : 'disabled'}`
|
|
30
|
+
)
|
|
31
|
+
"
|
|
32
|
+
/>
|
|
33
|
+
<p v-if="!access.members.length" class="text-sm text-muted">
|
|
34
|
+
{{ t('features.invoices.clientAccess.noMembers') }}
|
|
35
|
+
</p>
|
|
36
|
+
<div
|
|
37
|
+
v-for="member in access.members"
|
|
38
|
+
:key="member.id"
|
|
39
|
+
class="flex flex-wrap items-center justify-between gap-3 rounded-md border border-default p-3"
|
|
40
|
+
>
|
|
41
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
42
|
+
<UAvatar :src="member.image || undefined" :alt="member.name" size="sm" />
|
|
43
|
+
<div class="min-w-0">
|
|
44
|
+
<p class="font-medium">{{ member.name }}</p>
|
|
45
|
+
<p class="break-all text-sm text-muted">{{ member.email }}</p>
|
|
46
|
+
<p class="text-sm text-muted">
|
|
47
|
+
{{ t(`features.invoices.roles.${member.role}`) }} ·
|
|
48
|
+
{{
|
|
49
|
+
t(
|
|
50
|
+
`features.invoices.clientAccess.${member.fixedAccess ? 'automatic' : member.assigned ? 'assigned' : 'notAssigned'}`
|
|
51
|
+
)
|
|
52
|
+
}}
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<UBadge :color="member.canView ? 'success' : 'neutral'" variant="subtle">{{
|
|
57
|
+
t(`features.invoices.clientAccess.${member.canView ? 'granted' : 'denied'}`)
|
|
58
|
+
}}</UBadge>
|
|
59
|
+
</div>
|
|
60
|
+
<p class="text-sm text-muted">{{ t('features.invoices.clientAccess.contactsExplanation') }}</p>
|
|
61
|
+
</div>
|
|
62
|
+
</UCard>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { GenericClientDto } from '@nuxt-customer-portal/clients/types'
|
|
4
|
+
import type { InvoiceContactDto } from '@nuxt-customer-portal/invoices/shared/types/invoice'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{ client: GenericClientDto }>()
|
|
7
|
+
const { t } = useI18n()
|
|
8
|
+
const toast = useToast()
|
|
9
|
+
const busy = ref(false)
|
|
10
|
+
const editingId = ref('')
|
|
11
|
+
const formOpen = ref(false)
|
|
12
|
+
const state = reactive({ name: '', email: '', phone: '', jobTitle: '' })
|
|
13
|
+
const schema = computed(() =>
|
|
14
|
+
z.object({
|
|
15
|
+
name: z
|
|
16
|
+
.string()
|
|
17
|
+
.trim()
|
|
18
|
+
.min(1, t('features.invoices.validation.required'))
|
|
19
|
+
.max(200, t('features.invoices.validation.maxLength', { count: 200 })),
|
|
20
|
+
email: z
|
|
21
|
+
.string()
|
|
22
|
+
.trim()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.min(1, t('features.invoices.validation.required'))
|
|
25
|
+
.email(t('features.invoices.validation.validEmail'))
|
|
26
|
+
.max(320, t('features.invoices.validation.maxLength', { count: 320 })),
|
|
27
|
+
phone: z
|
|
28
|
+
.string()
|
|
29
|
+
.trim()
|
|
30
|
+
.max(80, t('features.invoices.validation.maxLength', { count: 80 })),
|
|
31
|
+
jobTitle: z
|
|
32
|
+
.string()
|
|
33
|
+
.trim()
|
|
34
|
+
.max(160, t('features.invoices.validation.maxLength', { count: 160 }))
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
const endpoint = `/api/invoices/admin/clients/${props.client.organizationId}/contacts`
|
|
38
|
+
const { data: contacts, refresh } = await useFetch<InvoiceContactDto[]>(endpoint, { default: () => [] })
|
|
39
|
+
const reset = () => {
|
|
40
|
+
editingId.value = ''
|
|
41
|
+
formOpen.value = false
|
|
42
|
+
Object.assign(state, { name: '', email: '', phone: '', jobTitle: '' })
|
|
43
|
+
}
|
|
44
|
+
const edit = (contact: InvoiceContactDto) => {
|
|
45
|
+
editingId.value = contact.id
|
|
46
|
+
formOpen.value = true
|
|
47
|
+
Object.assign(state, {
|
|
48
|
+
name: contact.name,
|
|
49
|
+
email: contact.email,
|
|
50
|
+
phone: contact.phone ?? '',
|
|
51
|
+
jobTitle: contact.jobTitle ?? ''
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
const save = async () => {
|
|
55
|
+
busy.value = true
|
|
56
|
+
try {
|
|
57
|
+
const validated = schema.value.parse(state)
|
|
58
|
+
const body = { ...validated, phone: validated.phone || null, jobTitle: validated.jobTitle || null }
|
|
59
|
+
if (editingId.value) {
|
|
60
|
+
await $fetch(`${endpoint}/${editingId.value}`, { method: 'PATCH', body })
|
|
61
|
+
} else {
|
|
62
|
+
await $fetch(endpoint, { method: 'POST', body })
|
|
63
|
+
}
|
|
64
|
+
await refresh()
|
|
65
|
+
reset()
|
|
66
|
+
toast.add({ title: t('features.invoices.messages.saved'), color: 'success' })
|
|
67
|
+
} catch (error) {
|
|
68
|
+
toast.add({ title: t('features.invoices.messages.saveError'), description: String(error), color: 'error' })
|
|
69
|
+
} finally {
|
|
70
|
+
busy.value = false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const remove = async (contact: InvoiceContactDto) => {
|
|
74
|
+
await $fetch(`${endpoint}/${contact.id}`, { method: 'DELETE' })
|
|
75
|
+
await refresh()
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<div class="space-y-6">
|
|
81
|
+
<InvoicesClientAccessPanel :client="client" />
|
|
82
|
+
<UCard>
|
|
83
|
+
<template #header>
|
|
84
|
+
<div class="flex items-center justify-between gap-3">
|
|
85
|
+
<div>
|
|
86
|
+
<h2 class="font-semibold">{{ t('features.invoices.admin.contactPersons') }}</h2>
|
|
87
|
+
<p class="text-sm text-muted">{{ t('features.invoices.admin.noContactsDescription') }}</p>
|
|
88
|
+
</div>
|
|
89
|
+
<UButton
|
|
90
|
+
v-if="!formOpen"
|
|
91
|
+
type="button"
|
|
92
|
+
size="sm"
|
|
93
|
+
variant="outline"
|
|
94
|
+
icon="i-lucide-plus"
|
|
95
|
+
@click="formOpen = true"
|
|
96
|
+
>
|
|
97
|
+
{{ t('features.invoices.admin.addContact') }}
|
|
98
|
+
</UButton>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
<div class="grid gap-2">
|
|
102
|
+
<div
|
|
103
|
+
v-for="contact in contacts"
|
|
104
|
+
:key="contact.id"
|
|
105
|
+
class="flex items-center justify-between gap-3 rounded-md border border-default p-3"
|
|
106
|
+
>
|
|
107
|
+
<div>
|
|
108
|
+
<p class="font-medium">{{ contact.name }}</p>
|
|
109
|
+
<p class="text-sm text-muted">{{ contact.email }}</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="flex gap-1">
|
|
112
|
+
<UButton type="button" variant="ghost" icon="i-lucide-pencil" @click="edit(contact)" /><UButton
|
|
113
|
+
type="button"
|
|
114
|
+
color="error"
|
|
115
|
+
variant="ghost"
|
|
116
|
+
icon="i-lucide-trash-2"
|
|
117
|
+
@click="remove(contact)"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<p v-if="!contacts.length && !formOpen" class="text-sm text-muted">
|
|
122
|
+
{{ t('features.invoices.admin.noContactsTitle') }}
|
|
123
|
+
</p>
|
|
124
|
+
<UForm
|
|
125
|
+
v-if="formOpen"
|
|
126
|
+
:state="state"
|
|
127
|
+
:schema="schema"
|
|
128
|
+
novalidate
|
|
129
|
+
class="grid gap-3 md:grid-cols-2"
|
|
130
|
+
@submit="save"
|
|
131
|
+
>
|
|
132
|
+
<UFormField name="name" :label="t('features.invoices.admin.name')" required>
|
|
133
|
+
<UInput v-model="state.name" class="w-full" /> </UFormField
|
|
134
|
+
><UFormField name="email" :label="t('features.invoices.admin.email')" required>
|
|
135
|
+
<UInput v-model="state.email" type="email" class="w-full" /> </UFormField
|
|
136
|
+
><UFormField name="phone" :label="t('features.invoices.admin.phone')">
|
|
137
|
+
<UInput v-model="state.phone" class="w-full" /> </UFormField
|
|
138
|
+
><UFormField name="jobTitle" :label="t('features.invoices.admin.jobTitle')">
|
|
139
|
+
<UInput v-model="state.jobTitle" class="w-full" />
|
|
140
|
+
</UFormField>
|
|
141
|
+
<div class="flex justify-end gap-2 md:col-span-2">
|
|
142
|
+
<UButton type="button" color="neutral" variant="outline" @click="reset">
|
|
143
|
+
{{ t('features.invoices.cancel') }} </UButton
|
|
144
|
+
><UButton type="submit" :loading="busy">{{ t('features.invoices.save') }}</UButton>
|
|
145
|
+
</div>
|
|
146
|
+
</UForm>
|
|
147
|
+
</div>
|
|
148
|
+
</UCard>
|
|
149
|
+
</div>
|
|
150
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { InvoicesListResponse } from '@nuxt-customer-portal/invoices/shared/types/invoice'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{ section: 'salesInvoices' | 'receivedInvoices' }>()
|
|
5
|
+
const { t } = useI18n()
|
|
6
|
+
const sales = props.section === 'salesInvoices'
|
|
7
|
+
const endpoint = sales ? '/api/invoices/admin/invoices' : '/api/invoices/client/invoices'
|
|
8
|
+
const { data, pending } = await useFetch<InvoicesListResponse<unknown>>(endpoint, { query: { pageSize: 1 } })
|
|
9
|
+
const total = computed(() => data.value?.pagination.total ?? 0)
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<UCard class="h-full">
|
|
14
|
+
<template #header>
|
|
15
|
+
<div class="flex items-center justify-between gap-3">
|
|
16
|
+
<h2 class="font-semibold">
|
|
17
|
+
{{ t(sales ? 'features.invoices.salesInvoices' : 'features.invoices.receivedInvoices') }}
|
|
18
|
+
</h2>
|
|
19
|
+
<UIcon :name="sales ? 'i-lucide-receipt-text' : 'i-lucide-inbox'" class="size-5 text-primary" />
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
<div class="flex items-end justify-between gap-4">
|
|
23
|
+
<p class="text-3xl font-semibold tabular-nums">{{ pending ? '…' : total }}</p>
|
|
24
|
+
<UButton :to="sales ? '/admin/invoices' : '/invoices'" variant="outline" icon="i-lucide-arrow-right">
|
|
25
|
+
{{ t('features.invoices.dashboard.open') }}
|
|
26
|
+
</UButton>
|
|
27
|
+
</div>
|
|
28
|
+
</UCard>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template><InvoicesDashboardCard section="receivedInvoices" /></template>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template><InvoicesDashboardCard section="salesInvoices" /></template>
|