Finalize Typescript restructure

This commit is contained in:
Darko Gjorgjijoski
2026-04-06 17:59:15 +02:00
parent cab785172e
commit 74b4b2df4e
209 changed files with 12419 additions and 1745 deletions

View File

@@ -1,5 +1,5 @@
<template>
<div class="bg-surface/70 backdrop-blur-lg rounded-xl shadow-sm border border-white/15">
<div class="bg-surface rounded-xl shadow-sm border border-line-default">
<div
v-if="hasHeaderSlot"
class="px-5 py-4 text-heading border-b border-line-light border-solid"

View File

@@ -227,7 +227,7 @@ getFields()
:close-on-select="true"
max-height="220"
position="top-end"
width-class="w-92"
width-class="w-auto min-w-[40rem]"
class="mb-2"
>
<template #activator>

View File

@@ -4,10 +4,14 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDebounceFn } from '@vueuse/core'
import { useRoute } from 'vue-router'
import { usePermissions } from '@v2/composables/use-permissions'
import { useModal } from '@v2/composables/use-modal'
import { useUserStore } from '@v2/stores/user.store'
import { useModalStore } from '@v2/stores/modal.store'
import { ABILITIES } from '@v2/config/abilities'
import type { Customer, Address } from '@v2/types/domain'
import { useCustomerStore } from '@v2/features/company/customers/store'
import { useInvoiceStore } from '@v2/features/company/invoices/store'
import { useEstimateStore } from '@v2/features/company/estimates/store'
import { useRecurringInvoiceStore } from '@v2/features/company/recurring-invoices/store'
import CustomerModal from '@v2/features/company/customers/components/CustomerModal.vue'
type DocumentType = 'estimate' | 'invoice' | 'recurring-invoice'
@@ -20,28 +24,11 @@ interface Validation {
$errors: ValidationError[]
}
interface SelectedCustomerData {
id: number
name: string
billing?: Pick<Address, 'name' | 'city' | 'state' | 'zip'> | null
shipping?: Pick<Address, 'name' | 'city' | 'state' | 'zip'> | null
}
interface Props {
valid?: Validation
customerId?: number | null
type?: DocumentType | null
contentLoading?: boolean
selectedCustomer?: SelectedCustomerData | null
customers?: Customer[]
}
interface Emits {
(e: 'select', customerId: number): void
(e: 'deselect'): void
(e: 'edit', customerId: number): void
(e: 'search', query: string): void
(e: 'create'): void
}
const props = withDefaults(defineProps<Props>(), {
@@ -49,44 +36,112 @@ const props = withDefaults(defineProps<Props>(), {
customerId: null,
type: null,
contentLoading: false,
selectedCustomer: null,
customers: () => [],
})
const emit = defineEmits<Emits>()
const { hasAbility } = usePermissions()
const { openModal } = useModal()
const userStore = useUserStore()
const modalStore = useModalStore()
const { t } = useI18n()
const route = useRoute()
const customerStore = useCustomerStore()
const invoiceStore = useInvoiceStore()
const estimateStore = useEstimateStore()
const recurringInvoiceStore = useRecurringInvoiceStore()
const search = ref<string | null>(null)
const isSearchingCustomer = ref<boolean>(false)
const selectedCustomer = computed(() => {
switch (props.type) {
case 'invoice':
return invoiceStore.newInvoice.customer
case 'estimate':
return estimateStore.newEstimate.customer
case 'recurring-invoice':
return recurringInvoiceStore.newRecurringInvoice.customer
default:
return null
}
})
// Fetch initial customers on setup
async function fetchInitialCustomers(): Promise<void> {
await customerStore.fetchCustomers({
orderByField: '',
orderBy: '',
})
}
// Select customer on setup if customerId is provided
if (props.customerId) {
if (props.type === 'invoice') {
invoiceStore.selectCustomer(props.customerId)
} else if (props.type === 'estimate') {
estimateStore.selectCustomer(props.customerId)
} else if (props.type === 'recurring-invoice') {
recurringInvoiceStore.selectCustomer(props.customerId)
}
}
fetchInitialCustomers()
const debounceSearchCustomer = useDebounceFn(() => {
isSearchingCustomer.value = true
searchCustomer()
}, 500)
function searchCustomer(): void {
if (search.value !== null) {
emit('search', search.value)
}
async function searchCustomer(): Promise<void> {
await customerStore.fetchCustomers({
display_name: search.value ?? '',
page: 1,
})
isSearchingCustomer.value = false
}
function editCustomer(): void {
if (props.selectedCustomer) {
emit('edit', props.selectedCustomer.id)
function selectNewCustomer(id: number, close: () => void): void {
const params: Record<string, unknown> = { userId: id }
if (route.params.id) params.model_id = route.params.id
if (props.type === 'invoice') {
invoiceStore.getNextNumber(params, true)
invoiceStore.selectCustomer(id)
} else if (props.type === 'estimate') {
estimateStore.getNextNumber(params, true)
estimateStore.selectCustomer(id)
} else if (props.type === 'recurring-invoice') {
recurringInvoiceStore.selectCustomer(id)
}
close()
search.value = null
}
function resetSelectedCustomer(): void {
emit('deselect')
if (props.type === 'invoice') {
invoiceStore.resetSelectedCustomer()
} else if (props.type === 'estimate') {
estimateStore.resetSelectedCustomer()
} else if (props.type === 'recurring-invoice') {
recurringInvoiceStore.resetSelectedCustomer()
}
}
async function editCustomer(): Promise<void> {
if (!selectedCustomer.value) return
await customerStore.fetchCustomer(selectedCustomer.value.id)
modalStore.openModal({
title: t('customers.edit_customer'),
componentName: 'CustomerModal',
})
}
function openCustomerModal(): void {
emit('create')
customerStore.resetCurrentCustomer()
modalStore.openModal({
title: t('customers.add_customer'),
componentName: 'CustomerModal',
variant: 'md',
})
}
function initGenerator(name: string): string {
@@ -96,16 +151,13 @@ function initGenerator(name: string): string {
}
return ''
}
function selectNewCustomer(id: number, close: () => void): void {
emit('select', id)
close()
search.value = null
}
</script>
<template>
<BaseContentPlaceholders v-if="contentLoading">
<div>
<CustomerModal />
<BaseContentPlaceholders v-if="contentLoading">
<BaseContentPlaceholdersBox
:rounded="true"
class="w-full"
@@ -359,7 +411,7 @@ function selectNewCustomer(id: number, close: () => void): void {
:placeholder="$t('general.search')"
type="text"
icon="search"
@update:modelValue="(val: string | null) => debounceSearchCustomer()"
@update:modelValue="() => debounceSearchCustomer()"
/>
<ul
@@ -372,7 +424,7 @@ function selectNewCustomer(id: number, close: () => void): void {
"
>
<li
v-for="(customer, index) in customers"
v-for="(customer, index) in customerStore.customers"
:key="index"
href="#"
class="
@@ -434,7 +486,7 @@ function selectNewCustomer(id: number, close: () => void): void {
</div>
</li>
<div
v-if="customers.length === 0"
v-if="customerStore.customers.length === 0"
class="flex justify-center p-5 text-subtle"
>
<label class="text-base text-muted cursor-pointer">
@@ -445,7 +497,7 @@ function selectNewCustomer(id: number, close: () => void): void {
</div>
<button
v-if="hasAbility(ABILITIES.CREATE_CUSTOMER)"
v-if="userStore.hasAbilities(ABILITIES.CREATE_CUSTOMER)"
type="button"
class="
h-10
@@ -483,4 +535,5 @@ function selectNewCustomer(id: number, close: () => void): void {
</transition>
</Popover>
</div>
</div>
</template>

View File

@@ -30,7 +30,7 @@
leave-to="opacity-0"
>
<DialogOverlay
class="fixed inset-0 transition-opacity bg-surface-secondary0/75"
class="fixed inset-0 transition-opacity bg-black/50"
/>
</TransitionChild>
@@ -59,9 +59,9 @@
text-left
align-bottom
transition-all
bg-surface/80 backdrop-blur-2xl
rounded-xl border border-white/15
shadow-xl
bg-surface/95 backdrop-blur-xl backdrop-saturate-150
rounded-xl border border-line-default
shadow-2xl
sm:my-8 sm:align-middle sm:w-full sm:p-6
relative
"
@@ -86,12 +86,12 @@
>
<BaseIcon
v-if="dialogStore.variant === 'primary'"
name="CheckIcon"
name="CheckCircleIcon"
class="w-6 h-6 text-alert-success-text"
/>
<BaseIcon
v-else
name="ExclamationIcon"
name="ExclamationTriangleIcon"
class="w-6 h-6 text-alert-error-text"
aria-hidden="true"
/>

View File

@@ -11,40 +11,49 @@
/>
</BaseContentPlaceholders>
<Menu v-else>
<MenuButton ref="trigger" class="focus:outline-hidden" @click="onClick">
<slot name="activator" />
</MenuButton>
<span ref="trigger" class="inline-flex">
<MenuButton class="focus:outline-hidden" @click="onClick">
<slot name="activator" />
</MenuButton>
</span>
<div ref="container" class="z-10" :class="widthClass">
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="scale-95 opacity-0"
enter-to-class="scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="scale-100 opacity-100"
leave-to-class="scale-95 opacity-0"
<Teleport to="body">
<div
ref="container"
class="fixed top-0 left-0 z-10"
:class="[widthClass, !contentLoading ? 'pointer-events-none' : '']"
>
<MenuItems :class="containerClasses">
<div class="py-1">
<slot />
</div>
</MenuItems>
</transition>
</div>
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="scale-95 opacity-0"
enter-to-class="scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="scale-100 opacity-100"
leave-to-class="scale-95 opacity-0"
>
<MenuItems :class="containerClasses">
<div class="py-1">
<slot />
</div>
</MenuItems>
</transition>
</div>
</Teleport>
</Menu>
</div>
</template>
<script setup lang="ts">
import { Menu, MenuButton, MenuItems } from '@headlessui/vue'
import { computed, ref } from 'vue'
import { computed, nextTick } from 'vue'
import { usePopper } from '@v2/composables/use-popper'
import type { Placement } from '@popperjs/core'
interface Props {
containerClass?: string
widthClass?: string
positionClass?: string
position?: string
position?: Placement
wrapperClass?: string
contentLoading?: boolean
}
@@ -60,16 +69,19 @@ const props = withDefaults(defineProps<Props>(), {
const containerClasses = computed<string>(() => {
const baseClass = `origin-top-right rounded-xl shadow-xl bg-surface/80 backdrop-blur-xl border border-white/15 divide-y divide-line-light focus:outline-hidden`
return `${baseClass} ${props.containerClass}`
return `${baseClass} pointer-events-auto ${props.containerClass}`
})
const [trigger, container, popper] = usePopper({
placement: 'bottom-end',
placement: props.position,
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [0, 10] } }],
})
function onClick(): void {
popper.value.update()
async function onClick(): Promise<void> {
await nextTick()
requestAnimationFrame(() => {
popper.value?.update()
})
}
</script>

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { computed, reactive, ref, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { usePermissions } from '@v2/composables/use-permissions'
import { useModal } from '@v2/composables/use-modal'
import { useUserStore } from '@v2/stores/user.store'
import { useModalStore } from '@v2/stores/modal.store'
import { useItemStore } from '@v2/features/company/items/store'
import { ABILITIES } from '@v2/config/abilities'
import ItemModal from '@v2/features/company/items/components/ItemModal.vue'
import type { Item } from '@v2/types/domain'
import type { Tax } from '@v2/types/domain'
@@ -23,6 +25,8 @@ interface Props {
invalidDescription?: boolean
taxPerItem?: string
taxes?: Tax[] | null
store?: { deselectItem: (index: number) => void } | null
storeProp?: string
}
interface Emits {
@@ -40,18 +44,27 @@ const props = withDefaults(defineProps<Props>(), {
invalidDescription: false,
taxPerItem: '',
taxes: null,
store: null,
storeProp: '',
})
const emit = defineEmits<Emits>()
const { hasAbility } = usePermissions()
const { openModal } = useModal()
const userStore = useUserStore()
const modalStore = useModalStore()
const itemStore = useItemStore()
const { t } = useI18n()
const itemSelect = ref<Item | null>(null)
const multiselectRef = ref<{ close?: () => void } | null>(null)
const loading = ref<boolean>(false)
const itemData = reactive<LineItem>({ ...props.item })
async function searchItems(search: string): Promise<Item[]> {
const res = await itemStore.fetchItems({ search })
return res.data as unknown as Item[]
}
const description = computed<string | null>({
get: () => props.item.description,
set: (value: string | null) => {
@@ -60,25 +73,35 @@ const description = computed<string | null>({
})
function openItemModal(): void {
openModal({
title: t('items.add_item'),
componentName: 'ItemModal',
refreshData: () => {},
data: {
taxPerItem: props.taxPerItem,
taxes: props.taxes,
itemIndex: props.index,
},
// Close the multiselect dropdown before opening the modal
;(document.activeElement as HTMLElement)?.blur()
nextTick(() => {
modalStore.openModal({
title: t('items.add_item'),
componentName: 'ItemModal',
refreshData: (val: Item) => emit('select', val),
data: {
taxPerItem: props.taxPerItem,
taxes: props.taxes,
itemIndex: props.index,
},
})
})
}
function deselectItem(index: number): void {
if (props.store) {
props.store.deselectItem(index)
}
emit('deselect', index)
}
</script>
<template>
<div class="flex-1 text-sm">
<ItemModal />
<!-- Selected Item Field -->
<div
v-if="item.item_id"
@@ -106,6 +129,7 @@ function deselectItem(index: number): void {
<!-- Select Item Field -->
<BaseMultiselect
v-else
ref="multiselectRef"
v-model="itemSelect"
:content-loading="contentLoading"
value-prop="id"
@@ -118,6 +142,7 @@ function deselectItem(index: number): void {
resolve-on-load
:delay="500"
searchable
:options="searchItems"
object
@update:modelValue="(val: Item) => $emit('select', val)"
@searchChange="(val: string) => $emit('search', val)"
@@ -125,7 +150,7 @@ function deselectItem(index: number): void {
<!-- Add Item Action -->
<template #action>
<BaseSelectAction
v-if="hasAbility(ABILITIES.CREATE_ITEM)"
v-if="userStore.hasAbilities(ABILITIES.CREATE_ITEM)"
@click="openItemModal"
>
<BaseIcon

View File

@@ -29,7 +29,7 @@
leave-to="opacity-0"
>
<DialogOverlay
class="fixed inset-0 transition-opacity bg-gray-700/25"
class="fixed inset-0 transition-opacity bg-black/50"
/>
</TransitionChild>
@@ -51,17 +51,17 @@
<div
:class="`inline-block
align-middle
bg-surface/80 backdrop-blur-2xl
rounded-xl border border-white/15
bg-surface/95 backdrop-blur-xl backdrop-saturate-150
rounded-xl border border-line-default
text-left
overflow-hidden
relative
shadow-xl
shadow-2xl
transition-all
my-4
${modalSize}
sm:w-full
border-t-8 border-solid rounded shadow-xl border-primary-500`"
border-t-8 border-solid rounded border-primary-500`"
>
<div
v-if="hasHeaderSlot"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
interface Props {
src: string | false
}
const props = defineProps<Props>()
const status = ref<'idle' | 'loading' | 'ready' | 'error'>('idle')
async function checkPdf(url: string): Promise<void> {
status.value = 'loading'
try {
const response = await fetch(url, { method: 'GET', credentials: 'same-origin' })
if (response.ok) {
status.value = 'ready'
} else {
status.value = 'error'
}
} catch {
status.value = 'error'
}
}
function retry(): void {
if (props.src) {
checkPdf(props.src)
}
}
watch(
() => props.src,
(url) => {
if (url) {
checkPdf(url)
} else {
status.value = 'idle'
}
},
{ immediate: true },
)
</script>
<template>
<div
class="flex flex-col min-h-0 mt-8 overflow-hidden"
style="height: 75vh"
>
<!-- Loading -->
<div
v-if="status === 'loading' || status === 'idle'"
class="flex-1 flex items-center justify-center border border-line-default rounded-md bg-surface"
>
<BaseSpinner class="w-8 h-8 text-primary-400" />
</div>
<!-- Error -->
<div
v-else-if="status === 'error'"
class="flex-1 flex flex-col items-center justify-center gap-4 border border-line-default rounded-md bg-surface"
>
<BaseIcon name="ExclamationCircleIcon" class="w-12 h-12 text-muted" />
<p class="text-sm text-muted">
{{ $t('general.unable_to_load_pdf') }}
</p>
<BaseButton variant="primary-outline" size="sm" @click="retry">
{{ $t('general.retry') }}
</BaseButton>
</div>
<!-- PDF iframe -->
<iframe
v-else
:src="src || undefined"
class="flex-1 border border-line-default border-solid rounded-md bg-surface"
/>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<template>
<TabPanel :class="[tabPanelContainer, 'focus:outline-hidden']">
<slot />
</TabPanel>
</template>
<script setup lang="ts">
import { TabPanel } from '@headlessui/vue'
withDefaults(
defineProps<{
title?: string | number
count?: string | number
countVariant?: string | number
tabPanelContainer?: string
}>(),
{
title: 'Tab',
count: '',
countVariant: '',
tabPanelContainer: 'py-4 mt-px',
},
)
</script>

View File

@@ -40,7 +40,7 @@ function onChange(d: number): void {
</script>
<template>
<div>
<div class="w-full">
<TabGroup :default-index="defaultIndex" @change="onChange">
<TabList
:class="[
@@ -57,9 +57,9 @@ function onChange(d: number): void {
>
<button
:class="[
'px-8 py-2 text-sm leading-5 font-medium flex items-center relative border-b-2 mt-4 focus:outline-hidden whitespace-nowrap',
'px-5 py-2.5 text-sm leading-5 font-medium flex items-center relative -mb-px border-b-2 focus:outline-hidden whitespace-nowrap transition-colors',
selected
? ' border-primary-400 text-heading font-medium'
? 'border-primary-400 text-heading'
: 'border-transparent text-muted hover:text-body hover:border-line-strong',
]"
>

View File

@@ -10,28 +10,30 @@ const props = withDefaults(defineProps<Props>(), {
status: '',
})
const baseClasses = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium'
const badgeColorClasses = computed<string>(() => {
switch (props.status) {
case EstimateStatus.DRAFT:
case 'DRAFT':
return 'bg-yellow-300/25 px-2 py-1 text-sm text-status-yellow uppercase font-normal text-center'
return `${baseClasses} bg-gray-100 text-gray-700 ring-1 ring-inset ring-gray-300/50`
case EstimateStatus.SENT:
case 'SENT':
return 'bg-green-500/25 px-2 py-1 text-sm text-status-green uppercase font-normal text-center'
return `${baseClasses} bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-300/50`
case EstimateStatus.VIEWED:
case 'VIEWED':
return 'bg-blue-400/25 px-2 py-1 text-sm text-status-blue uppercase font-normal text-center'
return `${baseClasses} bg-indigo-50 text-indigo-700 ring-1 ring-inset ring-indigo-300/50`
case EstimateStatus.EXPIRED:
case 'EXPIRED':
return 'bg-red-300/25 px-2 py-1 text-sm text-status-red uppercase font-normal text-center'
return `${baseClasses} bg-red-50 text-red-700 ring-1 ring-inset ring-red-300/50`
case EstimateStatus.ACCEPTED:
case 'ACCEPTED':
return 'bg-green-400/25 px-2 py-1 text-sm text-status-green uppercase font-normal text-center'
return `${baseClasses} bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-300/50`
case EstimateStatus.REJECTED:
case 'REJECTED':
return 'bg-purple-300/25 px-2 py-1 text-sm text-status-purple uppercase font-normal text-center'
return `${baseClasses} bg-rose-50 text-rose-700 ring-1 ring-inset ring-rose-300/50`
default:
return 'bg-surface-secondary0/25 px-2 py-1 text-sm text-heading uppercase font-normal text-center'
return `${baseClasses} bg-surface-secondary text-muted ring-1 ring-inset ring-line-default`
}
})
</script>

View File

@@ -2,6 +2,8 @@
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { client } from '@v2/api/client'
import InvoiceInformationCard from './InvoiceInformationCard.vue'
import type { Currency } from '@v2/types/domain'
import type { Company } from '@v2/types/domain'
import type { Customer } from '@v2/types/domain'
@@ -23,12 +25,6 @@ interface InvoicePublicData {
customer?: Pick<Customer, 'name'>
}
interface Props {
fetchInvoice: (hash: string) => Promise<InvoicePublicData>
}
const props = defineProps<Props>()
const invoiceData = ref<InvoicePublicData | null>(null) as Ref<InvoicePublicData | null>
const route = useRoute()
const router = useRouter()
@@ -37,7 +33,8 @@ loadInvoice()
async function loadInvoice(): Promise<void> {
const hash = route.params.hash as string
invoiceData.value = await props.fetchInvoice(hash)
const { data } = await client.get(`/customer/invoices/${hash}`)
invoiceData.value = data.data
}
const shareableLink = computed<string>(() => {
@@ -60,13 +57,16 @@ const customerLogo = computed<string | false>(() => {
const pageTitle: ComputedRef<string> = computed(() => invoiceData.value?.invoice_number ?? '')
function payInvoice(): void {
router.push({
name: 'invoice.pay',
params: {
hash: route.params.hash as string,
company: invoiceData.value?.company?.slug ?? '',
},
})
const resolved = router.resolve({ name: 'invoice.pay' })
if (resolved.matched.length) {
router.push({
name: 'invoice.pay',
params: {
hash: route.params.hash as string,
company: invoiceData.value?.company?.slug ?? '',
},
})
}
}
</script>

View File

@@ -16,35 +16,37 @@ const props = withDefaults(defineProps<Props>(), {
status: '',
})
const baseClasses = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium'
const badgeColorClasses = computed<string>(() => {
switch (props.status) {
case InvoiceStatus.DRAFT:
case 'DRAFT':
return 'bg-yellow-300/25 px-2 py-1 text-sm text-status-yellow uppercase font-normal text-center'
return `${baseClasses} bg-gray-100 text-gray-700 ring-1 ring-inset ring-gray-300/50`
case InvoiceStatus.SENT:
case 'SENT':
return 'bg-green-500/25 px-2 py-1 text-sm text-status-green uppercase font-normal text-center'
return `${baseClasses} bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-300/50`
case InvoiceStatus.VIEWED:
case 'VIEWED':
return 'bg-blue-400/25 px-2 py-1 text-sm text-status-blue uppercase font-normal text-center'
return `${baseClasses} bg-indigo-50 text-indigo-700 ring-1 ring-inset ring-indigo-300/50`
case InvoiceStatus.COMPLETED:
case 'COMPLETED':
return 'bg-green-500/25 px-2 py-1 text-sm text-status-green uppercase font-normal text-center'
return `${baseClasses} bg-green-50 text-green-700 ring-1 ring-inset ring-green-300/50`
case 'DUE':
return 'bg-yellow-500/25 px-2 py-1 text-sm text-status-yellow uppercase font-normal text-center'
return `${baseClasses} bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-300/50`
case 'OVERDUE':
return 'bg-red-300/50 px-2 py-1 text-sm text-status-red uppercase font-normal text-center'
return `${baseClasses} bg-red-50 text-red-700 ring-1 ring-inset ring-red-300/50`
case InvoicePaidStatus.UNPAID:
case 'UNPAID':
return 'bg-yellow-500/25 px-2 py-1 text-sm text-status-yellow uppercase font-normal text-center'
return `${baseClasses} bg-orange-50 text-orange-700 ring-1 ring-inset ring-orange-300/50`
case InvoicePaidStatus.PARTIALLY_PAID:
case 'PARTIALLY_PAID':
return 'bg-blue-400/25 px-2 py-1 text-sm text-status-blue uppercase font-normal text-center'
return `${baseClasses} bg-cyan-50 text-cyan-700 ring-1 ring-inset ring-cyan-300/50`
case InvoicePaidStatus.PAID:
case 'PAID':
return 'bg-green-500/40 px-2 py-1 text-sm text-status-green uppercase font-semibold text-center'
return `${baseClasses} bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-300/50`
default:
return 'bg-surface-secondary0/25 px-2 py-1 text-sm text-heading uppercase font-normal text-center'
return `${baseClasses} bg-surface-secondary text-muted ring-1 ring-inset ring-line-default`
}
})
</script>

View File

@@ -6,35 +6,35 @@ type PaidBadgeStatus = InvoicePaidStatus | 'OVERDUE' | string
interface Props {
status?: PaidBadgeStatus
defaultClass?: string
}
const props = withDefaults(defineProps<Props>(), {
status: '',
defaultClass: 'px-1 py-0.5 text-xs',
})
const baseClasses = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium'
const badgeColorClasses = computed<string>(() => {
switch (props.status) {
case InvoicePaidStatus.PAID:
case 'PAID':
return 'bg-green-500/40 text-status-green uppercase font-semibold text-center'
return `${baseClasses} bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-300/50`
case InvoicePaidStatus.UNPAID:
case 'UNPAID':
return 'bg-yellow-500/25 text-status-yellow uppercase font-normal text-center'
return `${baseClasses} bg-orange-50 text-orange-700 ring-1 ring-inset ring-orange-300/50`
case InvoicePaidStatus.PARTIALLY_PAID:
case 'PARTIALLY_PAID':
return 'bg-blue-400/25 text-status-blue uppercase font-normal text-center'
return `${baseClasses} bg-cyan-50 text-cyan-700 ring-1 ring-inset ring-cyan-300/50`
case 'OVERDUE':
return 'bg-red-300/50 px-2 py-1 text-sm text-status-red uppercase font-normal text-center'
return `${baseClasses} bg-red-50 text-red-700 ring-1 ring-inset ring-red-300/50`
default:
return 'bg-surface-secondary0/25 text-heading uppercase font-normal text-center'
return `${baseClasses} bg-surface-secondary text-muted ring-1 ring-inset ring-line-default`
}
})
</script>
<template>
<span :class="[badgeColorClasses, defaultClass]">
<span :class="badgeColorClasses">
<slot />
</span>
</template>

View File

@@ -10,19 +10,21 @@ const props = withDefaults(defineProps<Props>(), {
status: '',
})
const baseClasses = 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium'
const badgeColorClasses = computed<string>(() => {
switch (props.status) {
case RecurringInvoiceStatus.COMPLETED:
case 'COMPLETED':
return 'bg-green-500/25 px-2 py-1 text-sm text-status-green uppercase font-normal text-center'
case RecurringInvoiceStatus.ON_HOLD:
case 'ON_HOLD':
return 'bg-yellow-500/25 px-2 py-1 text-sm text-status-yellow uppercase font-normal text-center'
case RecurringInvoiceStatus.ACTIVE:
case 'ACTIVE':
return 'bg-blue-400/25 px-2 py-1 text-sm text-status-blue uppercase font-normal text-center'
return `${baseClasses} bg-blue-50 text-blue-700 ring-1 ring-inset ring-blue-300/50`
case RecurringInvoiceStatus.ON_HOLD:
case 'ON_HOLD':
return `${baseClasses} bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-300/50`
case RecurringInvoiceStatus.COMPLETED:
case 'COMPLETED':
return `${baseClasses} bg-green-50 text-green-700 ring-1 ring-inset ring-green-300/50`
default:
return 'bg-surface-secondary0/25 px-2 py-1 text-sm text-heading uppercase font-normal text-center'
return `${baseClasses} bg-surface-secondary text-muted ring-1 ring-inset ring-line-default`
}
})
</script>

View File

@@ -30,6 +30,7 @@ export { default as BaseListItem } from './BaseListItem.vue'
export { default as BaseMoney } from './BaseMoney.vue'
export { default as BaseModal } from './BaseModal.vue'
export { default as BaseMultiselect } from './BaseMultiselect.vue'
export { default as BasePdfPreview } from './BasePdfPreview.vue'
export { default as BaseRadio } from './BaseRadio.vue'
export { default as BaseRating } from './BaseRating.vue'
export { default as BaseRecurringInvoiceStatusLabel } from './BaseRecurringInvoiceStatusLabel.vue'
@@ -39,6 +40,7 @@ export { default as BaseSelectInput } from './BaseSelectInput.vue'
export { default as BaseSettingCard } from './BaseSettingCard.vue'
export { default as BaseSpinner } from './BaseSpinner.vue'
export { default as BaseSwitch } from './BaseSwitch.vue'
export { default as BaseTab } from './BaseTab.vue'
export { default as BaseTabGroup } from './BaseTabGroup.vue'
export { default as BaseText } from './BaseText.vue'
export { default as BaseTextarea } from './BaseTextarea.vue'