mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-15 17:24:10 +00:00
customers, items, invoices, estimates, shared document form 77 files, 14451 lines. Typed layouts (CompanyLayout, AuthLayout, header, sidebar, company switcher), auth views (login, register, forgot/reset password), admin feature (dashboard, companies, users, settings with typed store), company features (dashboard with chart/ stats, customers CRUD, items CRUD, invoices CRUD with full store, estimates CRUD with full store), and shared document form components (items table, item row, totals, notes, tax popup, template select, exchange rate converter, calculation composable). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
173 lines
6.0 KiB
Vue
173 lines
6.0 KiB
Vue
<template>
|
|
<div class="w-full mt-4 tax-select">
|
|
<Popover v-slot="{ open: isOpen }" class="relative">
|
|
<PopoverButton
|
|
class="flex items-center text-sm font-medium text-primary-400 focus:outline-hidden focus:border-none"
|
|
>
|
|
<BaseIcon name="PlusIcon" class="w-4 h-4 font-medium text-primary-400" />
|
|
{{ $t('settings.tax_types.add_tax') }}
|
|
</PopoverButton>
|
|
|
|
<div class="relative w-full max-w-md px-4">
|
|
<transition
|
|
enter-active-class="transition duration-200 ease-out"
|
|
enter-from-class="translate-y-1 opacity-0"
|
|
enter-to-class="translate-y-0 opacity-100"
|
|
leave-active-class="transition duration-150 ease-in"
|
|
leave-from-class="translate-y-0 opacity-100"
|
|
leave-to-class="translate-y-1 opacity-0"
|
|
>
|
|
<PopoverPanel
|
|
v-slot="{ close }"
|
|
style="min-width: 350px; margin-left: 62px; top: -28px"
|
|
class="absolute z-10 px-4 py-2 -translate-x-full sm:px-0"
|
|
>
|
|
<div class="overflow-hidden rounded-xl shadow ring-1 ring-black/5">
|
|
<!-- Search Input -->
|
|
<div class="relative bg-surface">
|
|
<div class="relative p-4">
|
|
<BaseInput
|
|
v-model="textSearch"
|
|
:placeholder="$t('general.search')"
|
|
type="text"
|
|
class="text-heading"
|
|
/>
|
|
</div>
|
|
|
|
<!-- List of Taxes -->
|
|
<div
|
|
v-if="filteredTaxType.length > 0"
|
|
class="relative flex flex-col overflow-auto list max-h-36 border-t border-line-light"
|
|
>
|
|
<div
|
|
v-for="(taxType, idx) in filteredTaxType"
|
|
:key="idx"
|
|
:class="{
|
|
'bg-surface-tertiary cursor-not-allowed opacity-50 pointer-events-none':
|
|
existingTaxIds.has(taxType.id),
|
|
}"
|
|
tabindex="2"
|
|
class="px-6 py-4 border-b border-line-light border-solid cursor-pointer hover:bg-surface-tertiary hover:cursor-pointer last:border-b-0"
|
|
@click="selectTaxType(taxType, close)"
|
|
>
|
|
<div class="flex justify-between px-2">
|
|
<label
|
|
class="m-0 text-base font-semibold leading-tight text-body cursor-pointer"
|
|
>
|
|
{{ taxType.name }}
|
|
</label>
|
|
<label
|
|
class="m-0 text-base font-semibold text-body cursor-pointer"
|
|
>
|
|
<template v-if="taxType.calculation_type === 'fixed'">
|
|
<BaseFormatMoney :amount="taxType.fixed_amount" :currency="companyCurrency" />
|
|
</template>
|
|
<template v-else>
|
|
{{ taxType.percent }} %
|
|
</template>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="flex justify-center p-5 text-subtle">
|
|
<label class="text-base text-muted cursor-pointer">
|
|
{{ $t('general.no_tax_found') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add new Tax action -->
|
|
<button
|
|
v-if="canCreateTaxType"
|
|
type="button"
|
|
class="flex items-center justify-center w-full h-10 px-2 py-3 bg-surface-muted border-none outline-hidden"
|
|
@click="openTaxTypeModal"
|
|
>
|
|
<BaseIcon name="CheckCircleIcon" class="text-primary-400" />
|
|
<label
|
|
class="m-0 ml-3 text-sm leading-none cursor-pointer font-base text-primary-400"
|
|
>
|
|
{{ $t('estimates.add_new_tax') }}
|
|
</label>
|
|
</button>
|
|
</div>
|
|
</PopoverPanel>
|
|
</transition>
|
|
</div>
|
|
</Popover>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
import { computed, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { TaxType } from '../../../types/domain/tax'
|
|
import type { Currency } from '../../../types/domain/currency'
|
|
import type { DocumentFormData, DocumentTax } from './use-document-calculations'
|
|
|
|
interface Props {
|
|
type?: string | null
|
|
store: Record<string, unknown>
|
|
storeProp: string
|
|
taxTypes?: TaxType[]
|
|
companyCurrency?: Currency | Record<string, unknown> | null
|
|
canCreateTaxType?: boolean
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'select:taxType', taxType: TaxType): void
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
type: null,
|
|
taxTypes: () => [],
|
|
companyCurrency: null,
|
|
canCreateTaxType: false,
|
|
})
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
const { t } = useI18n()
|
|
const textSearch = ref<string | null>(null)
|
|
|
|
const formData = computed<DocumentFormData>(() => {
|
|
return props.store[props.storeProp] as DocumentFormData
|
|
})
|
|
|
|
const filteredTaxType = computed<TaxType[]>(() => {
|
|
if (textSearch.value) {
|
|
return props.taxTypes.filter((el) =>
|
|
el.name.toLowerCase().includes(textSearch.value!.toLowerCase()),
|
|
)
|
|
}
|
|
return props.taxTypes
|
|
})
|
|
|
|
const taxes = computed<DocumentTax[]>(() => {
|
|
return formData.value.taxes
|
|
})
|
|
|
|
const existingTaxIds = computed<Set<number>>(() => {
|
|
return new Set(taxes.value.map((t) => t.tax_type_id))
|
|
})
|
|
|
|
function selectTaxType(data: TaxType, close: () => void): void {
|
|
emit('select:taxType', { ...data })
|
|
close()
|
|
}
|
|
|
|
function openTaxTypeModal(): void {
|
|
const modalStore = (window as Record<string, unknown>).__modalStore as
|
|
| { openModal: (opts: Record<string, unknown>) => void }
|
|
| undefined
|
|
modalStore?.openModal({
|
|
title: t('settings.tax_types.add_tax'),
|
|
componentName: 'TaxTypeModal',
|
|
size: 'sm',
|
|
refreshData: (data: TaxType) => emit('select:taxType', data),
|
|
})
|
|
}
|
|
</script>
|