mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-17 18:24:10 +00:00
Finalize Typescript restructure
This commit is contained in:
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, maxLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { expenseService } from '@v2/api/services/expense.service'
|
||||
|
||||
interface CategoryForm {
|
||||
@@ -13,6 +14,7 @@ interface CategoryForm {
|
||||
}
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
@@ -77,11 +79,19 @@ async function submitCategoryData(): Promise<void> {
|
||||
name: currentCategory.value.name,
|
||||
description: currentCategory.value.description || null,
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.expense_category.updated_message',
|
||||
})
|
||||
} else {
|
||||
await expenseService.createCategory({
|
||||
name: currentCategory.value.name,
|
||||
description: currentCategory.value.description || null,
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.expense_category.created_message',
|
||||
})
|
||||
}
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useGlobalStore } from '@v2/stores/global.store'
|
||||
|
||||
const router = useRouter()
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
const isFetchingInitialData = ref<boolean>(false)
|
||||
const previewLogo = ref<string | null>(null)
|
||||
const companyLogoFileBlob = ref<string | null>(null)
|
||||
const companyLogoName = ref<string | null>(null)
|
||||
|
||||
const newCompanyForm = reactive<{
|
||||
name: string
|
||||
currency: string | number
|
||||
address: { country_id: number | null }
|
||||
}>({
|
||||
name: '',
|
||||
currency: '',
|
||||
address: {
|
||||
country_id: null,
|
||||
},
|
||||
})
|
||||
|
||||
const modalActive = computed<boolean>(
|
||||
() => modalStore.active && modalStore.componentName === 'CompanyModal',
|
||||
)
|
||||
|
||||
const rules = computed(() => ({
|
||||
newCompanyForm: {
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
minLength: helpers.withMessage(
|
||||
t('validation.name_min_length', { count: 3 }),
|
||||
minLength(3),
|
||||
),
|
||||
},
|
||||
address: {
|
||||
country_id: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
currency: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const v$ = useVuelidate(rules, { newCompanyForm })
|
||||
|
||||
async function getInitials(): Promise<void> {
|
||||
isFetchingInitialData.value = true
|
||||
await globalStore.fetchCurrencies()
|
||||
await globalStore.fetchCountries()
|
||||
|
||||
newCompanyForm.currency = companyStore.selectedCompanyCurrency?.id ?? ''
|
||||
newCompanyForm.address.country_id =
|
||||
(companyStore.selectedCompany as Record<string, unknown>)?.address
|
||||
? ((companyStore.selectedCompany as Record<string, unknown>).address as Record<string, unknown>)?.country_id as number | null
|
||||
: null
|
||||
|
||||
isFetchingInitialData.value = false
|
||||
}
|
||||
|
||||
function onFileInputChange(fileName: string, file: string): void {
|
||||
companyLogoName.value = fileName
|
||||
companyLogoFileBlob.value = file
|
||||
}
|
||||
|
||||
function onFileInputRemove(): void {
|
||||
companyLogoName.value = null
|
||||
companyLogoFileBlob.value = null
|
||||
}
|
||||
|
||||
async function submitCompanyData(): Promise<void> {
|
||||
v$.value.newCompanyForm.$touch()
|
||||
if (v$.value.$invalid) {
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
const res = await companyStore.addNewCompany(newCompanyForm as never)
|
||||
if (res.data) {
|
||||
companyStore.setSelectedCompany(res.data)
|
||||
|
||||
if (companyLogoFileBlob.value) {
|
||||
const logoData = new FormData()
|
||||
logoData.append(
|
||||
'company_logo',
|
||||
JSON.stringify({
|
||||
name: companyLogoName.value,
|
||||
data: companyLogoFileBlob.value,
|
||||
}),
|
||||
)
|
||||
await companyStore.updateCompanyLogo(logoData)
|
||||
}
|
||||
|
||||
globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
closeCompanyModal()
|
||||
router.push('/admin/dashboard')
|
||||
}
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNewCompanyForm(): void {
|
||||
newCompanyForm.name = ''
|
||||
newCompanyForm.currency = ''
|
||||
newCompanyForm.address.country_id = null
|
||||
v$.value.$reset()
|
||||
}
|
||||
|
||||
function closeCompanyModal(): void {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
resetNewCompanyForm()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeCompanyModal" @open="getInitials">
|
||||
<template #header>
|
||||
<div class="flex justify-between w-full">
|
||||
{{ modalStore.title }}
|
||||
<BaseIcon
|
||||
name="XMarkIcon"
|
||||
class="w-6 h-6 text-muted cursor-pointer"
|
||||
@click="closeCompanyModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<form action="" @submit.prevent="submitCompanyData">
|
||||
<div class="p-4 mb-16 sm:p-6 space-y-4">
|
||||
<BaseInputGrid layout="one-column">
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('settings.company_info.company_logo')"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isFetchingInitialData">
|
||||
<BaseContentPlaceholdersBox :rounded="true" class="w-full h-24" />
|
||||
</BaseContentPlaceholders>
|
||||
<div v-else class="flex flex-col items-center">
|
||||
<BaseFileUploader
|
||||
:preview-image="previewLogo"
|
||||
base64
|
||||
@remove="onFileInputRemove"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.company_info.company_name')"
|
||||
:error="
|
||||
v$.newCompanyForm.name.$error &&
|
||||
v$.newCompanyForm.name.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="newCompanyForm.name"
|
||||
:invalid="v$.newCompanyForm.name.$error"
|
||||
:content-loading="isFetchingInitialData"
|
||||
@input="v$.newCompanyForm.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:content-loading="isFetchingInitialData"
|
||||
:label="$t('settings.company_info.country')"
|
||||
:error="
|
||||
v$.newCompanyForm.address.country_id.$error &&
|
||||
v$.newCompanyForm.address.country_id.$errors[0].$message
|
||||
"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.address.country_id"
|
||||
:content-loading="isFetchingInitialData"
|
||||
label="name"
|
||||
:invalid="v$.newCompanyForm.address.country_id.$error"
|
||||
:options="globalStore.countries"
|
||||
value-prop="id"
|
||||
:can-deselect="true"
|
||||
:can-clear="false"
|
||||
searchable
|
||||
track-by="name"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.currency')"
|
||||
:error="
|
||||
v$.newCompanyForm.currency.$error &&
|
||||
v$.newCompanyForm.currency.$errors[0].$message
|
||||
"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:help-text="$t('wizard.currency_set_alert')"
|
||||
required
|
||||
>
|
||||
<BaseMultiselect
|
||||
v-model="newCompanyForm.currency"
|
||||
:content-loading="isFetchingInitialData"
|
||||
:options="globalStore.currencies"
|
||||
label="name"
|
||||
value-prop="id"
|
||||
:searchable="true"
|
||||
track-by="name"
|
||||
:placeholder="$t('settings.currencies.select_currency')"
|
||||
:invalid="v$.newCompanyForm.currency.$error"
|
||||
class="w-full"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseInputGrid>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-surface-secondary border-t border-line-default">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeCompanyModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
name="ArrowDownOnSquareIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.save') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, numeric, helpers } from '@vuelidate/validators'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { customFieldService } from '@v2/api/services/custom-field.service'
|
||||
import type { CreateCustomFieldPayload } from '@v2/api/services/custom-field.service'
|
||||
|
||||
@@ -32,6 +33,7 @@ interface CustomFieldForm {
|
||||
}
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
@@ -178,6 +180,13 @@ async function submitCustomFieldData(): Promise<void> {
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
let defaultAnswer = currentCustomField.value.default_answer
|
||||
// Handle Time type — convert object {HH, mm, ss} to 'HH:mm' string
|
||||
if (currentCustomField.value.type === 'Time' && typeof defaultAnswer === 'object' && defaultAnswer !== null) {
|
||||
const timeObj = defaultAnswer as Record<string, string>
|
||||
defaultAnswer = `${timeObj.HH ?? '00'}:${timeObj.mm ?? '00'}`
|
||||
}
|
||||
|
||||
const payload: CreateCustomFieldPayload = {
|
||||
name: currentCustomField.value.name,
|
||||
label: currentCustomField.value.label,
|
||||
@@ -189,13 +198,22 @@ async function submitCustomFieldData(): Promise<void> {
|
||||
? currentCustomField.value.options.map((o) => o.name)
|
||||
: null,
|
||||
order: currentCustomField.value.order,
|
||||
default_answer: defaultAnswer as string ?? null,
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEdit.value && currentCustomField.value.id) {
|
||||
await customFieldService.update(currentCustomField.value.id, payload)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.custom_fields.updated_message',
|
||||
})
|
||||
} else {
|
||||
await customFieldService.create(payload)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.custom_fields.created_message',
|
||||
})
|
||||
}
|
||||
|
||||
isSaving.value = false
|
||||
@@ -208,6 +226,17 @@ async function submitCustomFieldData(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const newOptionValue = ref<string>('')
|
||||
|
||||
function onAddOption(): void {
|
||||
if (!newOptionValue.value?.trim()) return
|
||||
currentCustomField.value.options = [
|
||||
{ name: newOptionValue.value.trim() },
|
||||
...currentCustomField.value.options,
|
||||
]
|
||||
newOptionValue.value = ''
|
||||
}
|
||||
|
||||
function addNewOption(option: string): void {
|
||||
currentCustomField.value.options = [
|
||||
{ name: option },
|
||||
@@ -357,6 +386,22 @@ function closeCustomFieldModal(): void {
|
||||
v-if="isDropdownSelected"
|
||||
:label="$t('settings.custom_fields.options')"
|
||||
>
|
||||
<!-- Add Option Input -->
|
||||
<div class="flex items-center mt-1">
|
||||
<BaseInput
|
||||
v-model="newOptionValue"
|
||||
type="text"
|
||||
class="w-full md:w-96"
|
||||
:placeholder="$t('settings.custom_fields.press_enter_to_add')"
|
||||
@keydown.enter.prevent.stop="onAddOption"
|
||||
/>
|
||||
<BaseIcon
|
||||
name="PlusCircleIcon"
|
||||
class="ml-1 text-primary-500 cursor-pointer"
|
||||
@click="onAddOption"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(option, index) in currentCustomField.options"
|
||||
:key="index"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { required, helpers, sameAs } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useGlobalStore } from '@v2/stores/global.store'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const modalStore = useModalStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isDeleting = ref<boolean>(false)
|
||||
|
||||
const formData = reactive<{ id: number | null; name: string }>({
|
||||
id: null,
|
||||
name: '',
|
||||
})
|
||||
|
||||
const modalActive = computed<boolean>(
|
||||
() => modalStore.active && modalStore.componentName === 'DeleteCompanyModal',
|
||||
)
|
||||
|
||||
const companyName = computed<string>(
|
||||
() => companyStore.selectedCompany?.name ?? '',
|
||||
)
|
||||
|
||||
const rules = computed(() => ({
|
||||
name: {
|
||||
required: helpers.withMessage(t('validation.required'), required),
|
||||
sameAsName: helpers.withMessage(
|
||||
t('validation.company_name_not_same'),
|
||||
sameAs(companyName.value),
|
||||
),
|
||||
},
|
||||
}))
|
||||
|
||||
const v$ = useVuelidate(rules, formData)
|
||||
|
||||
function setInitialData(): void {
|
||||
formData.id = companyStore.selectedCompany?.id ?? null
|
||||
formData.name = ''
|
||||
}
|
||||
|
||||
async function submitDelete(): Promise<void> {
|
||||
v$.value.$touch()
|
||||
if (v$.value.$invalid) return
|
||||
|
||||
isDeleting.value = true
|
||||
try {
|
||||
await companyStore.deleteCompany(formData)
|
||||
|
||||
closeModal()
|
||||
|
||||
const remaining = companyStore.companies.filter(
|
||||
(c) => c.id !== formData.id,
|
||||
)
|
||||
if (remaining.length) {
|
||||
companyStore.setSelectedCompany(remaining[0])
|
||||
}
|
||||
|
||||
router.push('/admin/dashboard')
|
||||
globalStore.setIsAppLoaded(false)
|
||||
await globalStore.bootstrap()
|
||||
} catch {
|
||||
// Error handled
|
||||
} finally {
|
||||
isDeleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(): void {
|
||||
modalStore.closeModal()
|
||||
setTimeout(() => {
|
||||
formData.id = null
|
||||
formData.name = ''
|
||||
v$.value.$reset()
|
||||
}, 300)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseModal :show="modalActive" @close="closeModal" @open="setInitialData">
|
||||
<div class="px-6 pt-6">
|
||||
<h6 class="font-medium text-lg text-heading">
|
||||
{{ $t('settings.company_info.are_you_absolutely_sure') }}
|
||||
</h6>
|
||||
<p class="mt-2 text-sm text-muted" style="max-width: 680px">
|
||||
{{ $t('settings.company_info.delete_company_modal_desc', { company: companyName }) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitDelete">
|
||||
<div class="p-4 sm:p-6 space-y-4">
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.company_info.delete_company_modal_label', { company: companyName })"
|
||||
:error="v$.name.$error ? String(v$.name.$errors[0]?.$message) : undefined"
|
||||
required
|
||||
>
|
||||
<BaseInput
|
||||
v-model="formData.name"
|
||||
:invalid="v$.name.$error"
|
||||
@input="v$.name.$touch()"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</div>
|
||||
|
||||
<div class="z-0 flex justify-end p-4 bg-surface-secondary border-t border-line-default">
|
||||
<BaseButton
|
||||
class="mr-3 text-sm"
|
||||
variant="primary-outline"
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('general.cancel') }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:loading="isDeleting"
|
||||
:disabled="isDeleting"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isDeleting"
|
||||
name="TrashIcon"
|
||||
:class="slotProps.class"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('general.delete') }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseModal>
|
||||
</template>
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useEstimateStore } from '@v2/features/company/estimates/store'
|
||||
import NumberCustomizer from './NumberCustomizer.vue'
|
||||
import EstimatesTabExpiryDate from './EstimatesTabExpiryDate.vue'
|
||||
import EstimatesTabConvertEstimate from './EstimatesTabConvertEstimate.vue'
|
||||
import EstimatesTabDefaultFormats from './EstimatesTabDefaultFormats.vue'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
@@ -9,6 +13,7 @@ interface Utils {
|
||||
|
||||
const utils = inject<Utils>('utils')!
|
||||
const companyStore = useCompanyStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
|
||||
const estimateSettings = reactive<{ estimate_email_attachment: string | null }>({
|
||||
estimate_email_attachment: null,
|
||||
@@ -41,8 +46,14 @@ const sendAsAttachmentField = computed<boolean>({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberCustomizer type="estimate" :type-store="companyStore" />
|
||||
<NumberCustomizer type="estimate" :type-store="estimateStore" />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
<EstimatesTabExpiryDate />
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
<EstimatesTabConvertEstimate />
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
<EstimatesTabDefaultFormats />
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul class="divide-y divide-line-default">
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useGlobalStore } from '@v2/stores/global.store'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
const utils = inject<Utils>('utils')!
|
||||
|
||||
const settingsForm = reactive<{ estimate_convert_action: string | null }>({
|
||||
estimate_convert_action: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(
|
||||
settingsForm as unknown as Record<string, unknown>,
|
||||
{ ...companyStore.selectedCompanySettings }
|
||||
)
|
||||
|
||||
const convertEstimateOptions = [
|
||||
{ key: 'settings.customization.estimates.no_action', value: 'no_action' },
|
||||
{ key: 'settings.customization.estimates.delete_estimate', value: 'delete_estimate' },
|
||||
{ key: 'settings.customization.estimates.mark_estimate_as_accepted', value: 'mark_estimate_as_accepted' },
|
||||
]
|
||||
|
||||
async function submitForm() {
|
||||
const data = {
|
||||
settings: {
|
||||
...settingsForm,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.estimates.estimate_settings_updated',
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h6 class="text-heading text-lg font-medium">
|
||||
{{ $t('settings.customization.estimates.convert_estimate_setting') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
{{ $t('settings.customization.estimates.convert_estimate_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup required>
|
||||
<BaseRadio
|
||||
v-for="option in convertEstimateOptions"
|
||||
:id="option.value"
|
||||
:key="option.value"
|
||||
v-model="settingsForm.estimate_convert_action"
|
||||
:label="$t(option.key)"
|
||||
size="sm"
|
||||
name="estimate_convert_action"
|
||||
:value="option.value"
|
||||
class="mt-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const utils = inject<Utils>('utils')!
|
||||
|
||||
const estimateMailFields = ref(['customer', 'company', 'estimate'])
|
||||
const companyFields = ref(['company'])
|
||||
const shippingFields = ref(['shipping', 'customer'])
|
||||
const billingFields = ref(['billing', 'customer'])
|
||||
|
||||
const isSaving = ref(false)
|
||||
|
||||
const formatSettings = reactive<{
|
||||
estimate_mail_body: string | null
|
||||
estimate_company_address_format: string | null
|
||||
estimate_shipping_address_format: string | null
|
||||
estimate_billing_address_format: string | null
|
||||
}>({
|
||||
estimate_mail_body: null,
|
||||
estimate_company_address_format: null,
|
||||
estimate_shipping_address_format: null,
|
||||
estimate_billing_address_format: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(
|
||||
formatSettings as unknown as Record<string, unknown>,
|
||||
{ ...companyStore.selectedCompanySettings }
|
||||
)
|
||||
|
||||
async function submitForm() {
|
||||
isSaving.value = true
|
||||
|
||||
const data = {
|
||||
settings: {
|
||||
...formatSettings,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.estimates.estimate_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-heading text-lg font-medium">
|
||||
{{ $t('settings.customization.estimates.default_formats') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-muted mb-2">
|
||||
{{ $t('settings.customization.estimates.default_formats_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.estimates.default_estimate_email_body')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_mail_body"
|
||||
:fields="estimateMailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.estimates.company_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_company_address_format"
|
||||
:fields="companyFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.estimates.shipping_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_shipping_address_format"
|
||||
:fields="shippingFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.estimates.billing_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.estimate_billing_address_format"
|
||||
:fields="billingFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="ArrowDownOnSquareIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { numeric, helpers, requiredIf } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const utils = inject<Utils>('utils')!
|
||||
|
||||
const isSaving = ref(false)
|
||||
|
||||
const expiryDateSettings = reactive<{
|
||||
estimate_set_expiry_date_automatically: string | null
|
||||
estimate_expiry_date_days: string | null
|
||||
}>({
|
||||
estimate_set_expiry_date_automatically: null,
|
||||
estimate_expiry_date_days: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(
|
||||
expiryDateSettings as unknown as Record<string, unknown>,
|
||||
{ ...companyStore.selectedCompanySettings }
|
||||
)
|
||||
|
||||
const expiryDateAutoField = computed<boolean>({
|
||||
get: () => expiryDateSettings.estimate_set_expiry_date_automatically === 'YES',
|
||||
set: (newValue: boolean) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
expiryDateSettings.estimate_set_expiry_date_automatically = value
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
expiryDateSettings: {
|
||||
estimate_expiry_date_days: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(expiryDateAutoField.value)
|
||||
),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const v$ = useVuelidate(rules, { expiryDateSettings })
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.expiryDateSettings.$touch()
|
||||
|
||||
if (v$.value.expiryDateSettings.$invalid) {
|
||||
return false
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
const data = {
|
||||
settings: {
|
||||
...expiryDateSettings,
|
||||
},
|
||||
}
|
||||
|
||||
// Don't pass expiry_date_days if setting is not enabled
|
||||
if (!expiryDateAutoField.value) {
|
||||
delete data.settings.estimate_expiry_date_days
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.estimates.estimate_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-heading text-lg font-medium">
|
||||
{{ $t('settings.customization.estimates.expiry_date_setting') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-muted mb-2">
|
||||
{{ $t('settings.customization.estimates.expiry_date_description') }}
|
||||
</p>
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="expiryDateAutoField"
|
||||
:title="
|
||||
$t('settings.customization.estimates.set_expiry_date_automatically')
|
||||
"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.estimates.set_expiry_date_automatically_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="expiryDateAutoField"
|
||||
:label="$t('settings.customization.estimates.expiry_date_days')"
|
||||
:error="
|
||||
v$.expiryDateSettings.estimate_expiry_date_days.$error &&
|
||||
v$.expiryDateSettings.estimate_expiry_date_days.$errors[0].$message
|
||||
"
|
||||
class="mt-2 mb-4"
|
||||
>
|
||||
<div class="w-full sm:w-1/2 md:w-1/4 lg:w-1/5">
|
||||
<BaseInput
|
||||
v-model="expiryDateSettings.estimate_expiry_date_days"
|
||||
:invalid="v$.expiryDateSettings.estimate_expiry_date_days.$error"
|
||||
type="number"
|
||||
@input="v$.expiryDateSettings.estimate_expiry_date_days.$touch()"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="ArrowDownOnSquareIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, helpers, requiredIf, url } from '@vuelidate/validators'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { exchangeRateService } from '@v2/api/services/exchange-rate.service'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
|
||||
@@ -31,6 +32,7 @@ interface CurrencyConverterForm {
|
||||
|
||||
const { t } = useI18n()
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
const isFetchingInitialData = ref<boolean>(false)
|
||||
@@ -243,10 +245,18 @@ async function submitExchangeRate(): Promise<void> {
|
||||
currentExchangeRate.value.id,
|
||||
data as Parameters<typeof exchangeRateService.updateProvider>[1]
|
||||
)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.exchange_rate.updated_message',
|
||||
})
|
||||
} else {
|
||||
await exchangeRateService.createProvider(
|
||||
data as Parameters<typeof exchangeRateService.createProvider>[0]
|
||||
)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.exchange_rate.created_message',
|
||||
})
|
||||
}
|
||||
|
||||
if (modalStore.refreshData) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
|
||||
import { useDialogStore } from '@v2/stores/dialog.store'
|
||||
import { useUserStore } from '@v2/stores/user.store'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { expenseService } from '@v2/api/services/expense.service'
|
||||
|
||||
const ABILITIES = {
|
||||
@@ -24,6 +25,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
@@ -54,6 +56,10 @@ function removeExpenseCategory(id: number): void {
|
||||
if (res) {
|
||||
const response = await expenseService.deleteCategory(id)
|
||||
if (response.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.expense_category.deleted_message',
|
||||
})
|
||||
props.loadData?.()
|
||||
}
|
||||
}
|
||||
@@ -65,7 +71,7 @@ function removeExpenseCategory(id: number): void {
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton
|
||||
v-if="route.name === 'expenseCategorys.view'"
|
||||
v-if="route.name === 'settings.expense-categories'"
|
||||
variant="primary"
|
||||
>
|
||||
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useInvoiceStore } from '@v2/features/company/invoices/store'
|
||||
import NumberCustomizer from './NumberCustomizer.vue'
|
||||
import InvoicesTabDueDate from './InvoicesTabDueDate.vue'
|
||||
import InvoicesTabRetrospective from './InvoicesTabRetrospective.vue'
|
||||
import InvoicesTabDefaultFormats from './InvoicesTabDefaultFormats.vue'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
@@ -9,6 +13,7 @@ interface Utils {
|
||||
|
||||
const utils = inject<Utils>('utils')!
|
||||
const companyStore = useCompanyStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
|
||||
const invoiceSettings = reactive<{ invoice_email_attachment: string | null }>({
|
||||
invoice_email_attachment: null,
|
||||
@@ -41,7 +46,19 @@ const sendAsAttachmentField = computed<boolean>({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberCustomizer type="invoice" :type-store="companyStore" />
|
||||
<NumberCustomizer type="invoice" :type-store="invoiceStore" />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<InvoicesTabDueDate />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<InvoicesTabRetrospective />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<InvoicesTabDefaultFormats />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
|
||||
const settingsForm = reactive<{
|
||||
invoice_mail_body: string
|
||||
invoice_company_address_format: string
|
||||
invoice_shipping_address_format: string
|
||||
invoice_billing_address_format: string
|
||||
}>({
|
||||
invoice_mail_body:
|
||||
companyStore.selectedCompanySettings.invoice_mail_body ?? '',
|
||||
invoice_company_address_format:
|
||||
companyStore.selectedCompanySettings.invoice_company_address_format ?? '',
|
||||
invoice_shipping_address_format:
|
||||
companyStore.selectedCompanySettings.invoice_shipping_address_format ?? '',
|
||||
invoice_billing_address_format:
|
||||
companyStore.selectedCompanySettings.invoice_billing_address_format ?? '',
|
||||
})
|
||||
|
||||
async function submitForm(): Promise<void> {
|
||||
isSaving.value = true
|
||||
|
||||
const data = {
|
||||
settings: {
|
||||
...settingsForm,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.invoices.invoice_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.customization.invoices.default_formats')"
|
||||
:description="
|
||||
$t('settings.customization.invoices.default_formats_description')
|
||||
"
|
||||
>
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.invoices.default_invoice_email_body')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="settingsForm.invoice_mail_body"
|
||||
:fields="['customer', 'company', 'invoice']"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.invoices.company_address_format')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="settingsForm.invoice_company_address_format"
|
||||
:fields="['company']"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.invoices.shipping_address_format')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="settingsForm.invoice_shipping_address_format"
|
||||
:fields="['shipping', 'customer']"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.invoices.billing_address_format')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="settingsForm.invoice_billing_address_format"
|
||||
:fields="['billing', 'customer']"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
:class="slotProps.class"
|
||||
name="ArrowDownOnSquareIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</BaseSettingCard>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { numeric, helpers, requiredIf } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
|
||||
const dueDateSettings = reactive<{
|
||||
invoice_set_due_date_automatically: string
|
||||
invoice_due_date_days: string
|
||||
}>({
|
||||
invoice_set_due_date_automatically:
|
||||
companyStore.selectedCompanySettings.invoice_set_due_date_automatically ??
|
||||
'NO',
|
||||
invoice_due_date_days:
|
||||
companyStore.selectedCompanySettings.invoice_due_date_days ?? '',
|
||||
})
|
||||
|
||||
const dueDateAutoField = computed<boolean>({
|
||||
get: () => dueDateSettings.invoice_set_due_date_automatically === 'YES',
|
||||
set: (newValue: boolean) => {
|
||||
dueDateSettings.invoice_set_due_date_automatically = newValue
|
||||
? 'YES'
|
||||
: 'NO'
|
||||
},
|
||||
})
|
||||
|
||||
const rules = computed(() => ({
|
||||
dueDateSettings: {
|
||||
invoice_due_date_days: {
|
||||
required: helpers.withMessage(
|
||||
t('validation.required'),
|
||||
requiredIf(dueDateAutoField.value)
|
||||
),
|
||||
numeric: helpers.withMessage(t('validation.numbers_only'), numeric),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const v$ = useVuelidate(rules, { dueDateSettings })
|
||||
|
||||
async function submitForm(): Promise<void> {
|
||||
v$.value.dueDateSettings.$touch()
|
||||
|
||||
if (v$.value.dueDateSettings.$invalid) {
|
||||
return
|
||||
}
|
||||
|
||||
isSaving.value = true
|
||||
|
||||
const data: { settings: Record<string, string> } = {
|
||||
settings: {
|
||||
invoice_set_due_date_automatically:
|
||||
dueDateSettings.invoice_set_due_date_automatically,
|
||||
},
|
||||
}
|
||||
|
||||
if (dueDateAutoField.value) {
|
||||
data.settings.invoice_due_date_days =
|
||||
dueDateSettings.invoice_due_date_days
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.invoices.invoice_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.customization.invoices.due_date')"
|
||||
:description="
|
||||
$t('settings.customization.invoices.due_date_description')
|
||||
"
|
||||
>
|
||||
<BaseSwitchSection
|
||||
v-model="dueDateAutoField"
|
||||
:title="
|
||||
$t('settings.customization.invoices.set_due_date_automatically')
|
||||
"
|
||||
:description="
|
||||
$t(
|
||||
'settings.customization.invoices.set_due_date_automatically_description'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<BaseInputGroup
|
||||
v-if="dueDateAutoField"
|
||||
:label="$t('settings.customization.invoices.due_date_days')"
|
||||
:error="
|
||||
v$.dueDateSettings.invoice_due_date_days.$error &&
|
||||
v$.dueDateSettings.invoice_due_date_days.$errors[0].$message
|
||||
"
|
||||
class="mt-2 mb-4"
|
||||
>
|
||||
<div class="w-full sm:w-1/2 md:w-1/4 lg:w-1/5">
|
||||
<BaseInput
|
||||
v-model="dueDateSettings.invoice_due_date_days"
|
||||
:invalid="v$.dueDateSettings.invoice_due_date_days.$error"
|
||||
type="number"
|
||||
@input="v$.dueDateSettings.invoice_due_date_days.$touch()"
|
||||
/>
|
||||
</div>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon
|
||||
v-if="!isSaving"
|
||||
:class="slotProps.class"
|
||||
name="ArrowDownOnSquareIcon"
|
||||
/>
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</BaseSettingCard>
|
||||
</form>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useGlobalStore } from '@v2/stores/global.store'
|
||||
|
||||
const { t } = useI18n()
|
||||
const companyStore = useCompanyStore()
|
||||
const globalStore = useGlobalStore()
|
||||
|
||||
const settingsForm = reactive<{ retrospective_edits: string | null }>({
|
||||
retrospective_edits:
|
||||
companyStore.selectedCompanySettings.retrospective_edits ?? null,
|
||||
})
|
||||
|
||||
const retrospectiveEditOptions = [
|
||||
{ key: 'settings.customization.invoices.allow', value: 'allow' },
|
||||
{ key: 'settings.customization.invoices.disable_on_invoice_partial_paid', value: 'disable_on_invoice_partial_paid' },
|
||||
{ key: 'settings.customization.invoices.disable_on_invoice_paid', value: 'disable_on_invoice_paid' },
|
||||
{ key: 'settings.customization.invoices.disable_on_invoice_sent', value: 'disable_on_invoice_sent' },
|
||||
]
|
||||
|
||||
async function submitForm(): Promise<void> {
|
||||
const data = {
|
||||
settings: {
|
||||
...settingsForm,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.invoices.invoice_settings_updated',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseSettingCard
|
||||
:title="$t('settings.customization.invoices.retrospective_edits')"
|
||||
:description="
|
||||
$t('settings.customization.invoices.retrospective_edits_description')
|
||||
"
|
||||
>
|
||||
<BaseInputGroup required>
|
||||
<BaseRadio
|
||||
v-for="option in retrospectiveEditOptions"
|
||||
:id="option.value"
|
||||
:key="option.value"
|
||||
v-model="settingsForm.retrospective_edits"
|
||||
:label="$t(option.key)"
|
||||
size="sm"
|
||||
name="retrospective_edits"
|
||||
:value="option.value"
|
||||
class="mt-2"
|
||||
@update:modelValue="submitForm"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { itemService } from '@v2/api/services/item.service'
|
||||
import { useItemStore } from '@v2/features/company/items/store'
|
||||
|
||||
interface ItemUnitForm {
|
||||
id: number | null
|
||||
@@ -12,6 +12,7 @@ interface ItemUnitForm {
|
||||
}
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const itemStore = useItemStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
@@ -41,12 +42,10 @@ const v$ = useVuelidate(rules, currentItemUnit)
|
||||
async function setInitialData(): Promise<void> {
|
||||
if (modalStore.data && typeof modalStore.data === 'number') {
|
||||
isEdit.value = true
|
||||
const response = await itemService.getUnit(modalStore.data)
|
||||
if (response.data) {
|
||||
currentItemUnit.value = {
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
}
|
||||
await itemStore.fetchItemUnit(modalStore.data)
|
||||
currentItemUnit.value = {
|
||||
id: itemStore.currentItemUnit.id ?? null,
|
||||
name: itemStore.currentItemUnit.name,
|
||||
}
|
||||
} else {
|
||||
isEdit.value = false
|
||||
@@ -63,18 +62,20 @@ async function submitItemUnit(): Promise<void> {
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
let res
|
||||
if (isEdit.value && currentItemUnit.value.id) {
|
||||
await itemService.updateUnit(currentItemUnit.value.id, {
|
||||
res = await itemStore.updateItemUnit({
|
||||
id: currentItemUnit.value.id,
|
||||
name: currentItemUnit.value.name,
|
||||
})
|
||||
} else {
|
||||
await itemService.createUnit({
|
||||
res = await itemStore.addItemUnit({
|
||||
name: currentItemUnit.value.name,
|
||||
})
|
||||
}
|
||||
|
||||
if (modalStore.refreshData) {
|
||||
modalStore.refreshData()
|
||||
if (modalStore.refreshData && res?.data) {
|
||||
modalStore.refreshData(res.data)
|
||||
}
|
||||
closeItemUnitModal()
|
||||
} catch {
|
||||
|
||||
@@ -15,7 +15,7 @@ interface MailTestForm {
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
storeType?: string
|
||||
storeType?: 'company' | 'global'
|
||||
}>(),
|
||||
{
|
||||
storeType: 'global',
|
||||
@@ -74,7 +74,18 @@ async function onTestMailSend(): Promise<void> {
|
||||
|
||||
isSaving.value = true
|
||||
try {
|
||||
await mailService.testEmail({ to: formData.to })
|
||||
const payload = {
|
||||
to: formData.to,
|
||||
subject: formData.subject,
|
||||
message: formData.message,
|
||||
}
|
||||
|
||||
if (props.storeType === 'company') {
|
||||
await companyService.testMailConfig(payload)
|
||||
} else {
|
||||
await mailService.testEmail(payload)
|
||||
}
|
||||
|
||||
closeTestModal()
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
|
||||
@@ -6,10 +6,7 @@ import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { useUserStore } from '@v2/stores/user.store'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { noteService } from '@v2/api/services/note.service'
|
||||
|
||||
const ABILITIES = {
|
||||
MANAGE_NOTE: 'manage-note',
|
||||
} as const
|
||||
import { ABILITIES } from '@v2/config/abilities'
|
||||
|
||||
interface NoteRow {
|
||||
id: number
|
||||
@@ -51,7 +48,11 @@ function removeNote(id: number): void {
|
||||
hideNoButton: false,
|
||||
size: 'lg',
|
||||
})
|
||||
.then(async () => {
|
||||
.then(async (confirmed: boolean) => {
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await noteService.delete(id)
|
||||
if (response.success) {
|
||||
notificationStore.showNotification({
|
||||
@@ -72,7 +73,7 @@ function removeNote(id: number): void {
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'notes.view'" variant="primary">
|
||||
<BaseButton v-if="route.name === 'settings.notes'" variant="primary">
|
||||
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
|
||||
|
||||
@@ -157,12 +157,12 @@ async function setInitialFields(): Promise<void> {
|
||||
const res = await globalStore.fetchPlaceholders(data as { key: string })
|
||||
|
||||
res.placeholders.forEach((placeholder) => {
|
||||
const found = allFields.value.find((field) => field.name === placeholder.value)
|
||||
const found = allFields.value.find((field) => field.name === placeholder.name)
|
||||
if (!found) return
|
||||
|
||||
selectedFields.value.push({
|
||||
...found,
|
||||
value: placeholder.value ?? '',
|
||||
value: placeholder.value ?? found.value,
|
||||
id: Guid.raw(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ function removePaymentMode(id: number): void {
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'paymentModes.view'" variant="primary">
|
||||
<BaseButton v-if="route.name === 'settings.payment-modes'" variant="primary">
|
||||
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { paymentService } from '@v2/api/services/payment.service'
|
||||
|
||||
interface PaymentModeForm {
|
||||
@@ -12,6 +13,7 @@ interface PaymentModeForm {
|
||||
}
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
@@ -63,10 +65,18 @@ async function submitPaymentMode(): Promise<void> {
|
||||
await paymentService.updateMethod(currentPaymentMode.value.id, {
|
||||
name: currentPaymentMode.value.name,
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.payment_modes.updated_message',
|
||||
})
|
||||
} else {
|
||||
await paymentService.createMethod({
|
||||
name: currentPaymentMode.value.name,
|
||||
})
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.payment_modes.created_message',
|
||||
})
|
||||
}
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { usePaymentStore } from '@v2/features/company/payments/store'
|
||||
import NumberCustomizer from './NumberCustomizer.vue'
|
||||
import PaymentsTabDefaultFormats from './PaymentsTabDefaultFormats.vue'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
@@ -9,6 +11,7 @@ interface Utils {
|
||||
|
||||
const utils = inject<Utils>('utils')!
|
||||
const companyStore = useCompanyStore()
|
||||
const paymentStore = usePaymentStore()
|
||||
|
||||
const paymentSettings = reactive<{ payment_email_attachment: string | null }>({
|
||||
payment_email_attachment: null,
|
||||
@@ -41,8 +44,10 @@ const sendAsAttachmentField = computed<boolean>({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NumberCustomizer type="payment" :type-store="companyStore" />
|
||||
<NumberCustomizer type="payment" :type-store="paymentStore" />
|
||||
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
<PaymentsTabDefaultFormats />
|
||||
<BaseDivider class="mt-6 mb-2" />
|
||||
|
||||
<ul class="divide-y divide-line-default">
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, inject } from 'vue'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
|
||||
interface Utils {
|
||||
mergeSettings: (target: Record<string, unknown>, source: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
const companyStore = useCompanyStore()
|
||||
const utils = inject<Utils>('utils')!
|
||||
|
||||
const mailFields = ref(['customer', 'company', 'payment'])
|
||||
const companyFields = ref(['company'])
|
||||
const customerAddressFields = ref(['billing', 'customer'])
|
||||
|
||||
const isSaving = ref(false)
|
||||
|
||||
const formatSettings = reactive<{
|
||||
payment_mail_body: string | null
|
||||
payment_company_address_format: string | null
|
||||
payment_from_customer_address_format: string | null
|
||||
}>({
|
||||
payment_mail_body: null,
|
||||
payment_company_address_format: null,
|
||||
payment_from_customer_address_format: null,
|
||||
})
|
||||
|
||||
utils.mergeSettings(
|
||||
formatSettings as unknown as Record<string, unknown>,
|
||||
{ ...companyStore.selectedCompanySettings }
|
||||
)
|
||||
|
||||
async function submitForm() {
|
||||
isSaving.value = true
|
||||
|
||||
const data = {
|
||||
settings: {
|
||||
...formatSettings,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'settings.customization.payments.payment_settings_updated',
|
||||
})
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
return true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<h6 class="text-heading text-lg font-medium">
|
||||
{{ $t('settings.customization.payments.default_formats') }}
|
||||
</h6>
|
||||
<p class="mt-1 text-sm text-muted mb-2">
|
||||
{{ $t('settings.customization.payments.default_formats_description') }}
|
||||
</p>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.payments.default_payment_email_body')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.payment_mail_body"
|
||||
:fields="mailFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="$t('settings.customization.payments.company_address_format')"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.payment_company_address_format"
|
||||
:fields="companyFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseInputGroup
|
||||
:label="
|
||||
$t('settings.customization.payments.from_customer_address_format')
|
||||
"
|
||||
class="mt-6 mb-4"
|
||||
>
|
||||
<BaseCustomInput
|
||||
v-model="formatSettings.payment_from_customer_address_format"
|
||||
:fields="customerAddressFields"
|
||||
/>
|
||||
</BaseInputGroup>
|
||||
|
||||
<BaseButton
|
||||
:loading="isSaving"
|
||||
:disabled="isSaving"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #left="slotProps">
|
||||
<BaseIcon v-if="!isSaving" :class="slotProps.class" name="ArrowDownOnSquareIcon" />
|
||||
</template>
|
||||
{{ $t('settings.customization.save') }}
|
||||
</BaseButton>
|
||||
</form>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
|
||||
import { useDialogStore } from '@v2/stores/dialog.store'
|
||||
import { useUserStore } from '@v2/stores/user.store'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { roleService } from '@v2/api/services/role.service'
|
||||
|
||||
interface RoleRow {
|
||||
@@ -19,12 +20,16 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
|
||||
const PROTECTED_ROLES = ['owner', 'super admin']
|
||||
|
||||
async function editRole(id: number): Promise<void> {
|
||||
if (PROTECTED_ROLES.includes(props.row.name)) return
|
||||
modalStore.openModal({
|
||||
title: t('settings.roles.edit_role'),
|
||||
componentName: 'RolesModal',
|
||||
@@ -35,6 +40,7 @@ async function editRole(id: number): Promise<void> {
|
||||
}
|
||||
|
||||
async function removeRole(id: number): Promise<void> {
|
||||
if (PROTECTED_ROLES.includes(props.row.name)) return
|
||||
dialogStore
|
||||
.openDialog({
|
||||
title: t('general.are_you_sure'),
|
||||
@@ -48,6 +54,10 @@ async function removeRole(id: number): Promise<void> {
|
||||
.then(async (res: boolean) => {
|
||||
if (res) {
|
||||
await roleService.delete(id)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.roles.deleted_message',
|
||||
})
|
||||
props.loadData?.()
|
||||
}
|
||||
})
|
||||
@@ -57,7 +67,7 @@ async function removeRole(id: number): Promise<void> {
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'roles.view'" variant="primary">
|
||||
<BaseButton v-if="route.name === 'settings.roles'" variant="primary">
|
||||
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
|
||||
|
||||
@@ -4,14 +4,17 @@ import { useI18n } from 'vue-i18n'
|
||||
import { required, minLength, helpers } from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { roleService } from '@v2/api/services/role.service'
|
||||
import type { CreateRolePayload } from '@v2/api/services/role.service'
|
||||
import type { Ability } from '@v2/types/domain/role'
|
||||
|
||||
interface AbilityItem extends Ability {
|
||||
interface AbilityItem {
|
||||
name: string
|
||||
ability: string
|
||||
disabled: boolean
|
||||
depends_on?: string[]
|
||||
model?: string
|
||||
}
|
||||
|
||||
interface AbilitiesList {
|
||||
@@ -25,6 +28,7 @@ interface RoleForm {
|
||||
}
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
@@ -67,15 +71,20 @@ async function setInitialData(): Promise<void> {
|
||||
const abilitiesRes = await roleService.getAbilities()
|
||||
if (abilitiesRes.abilities) {
|
||||
const grouped: AbilitiesList = {}
|
||||
abilitiesRes.abilities.forEach((a) => {
|
||||
const group = a.title || 'General'
|
||||
if (!grouped[group]) grouped[group] = []
|
||||
grouped[group].push({
|
||||
...a,
|
||||
ability: a.name,
|
||||
abilitiesRes.abilities.forEach((a: Record<string, unknown>) => {
|
||||
// Extract model name from PHP class path (e.g., "App\Models\Customer" → "Customer")
|
||||
const modelPath = (a.model as string) ?? ''
|
||||
const modelName = modelPath
|
||||
? modelPath.substring(modelPath.lastIndexOf('\\') + 1)
|
||||
: 'Common'
|
||||
|
||||
if (!grouped[modelName]) grouped[modelName] = []
|
||||
grouped[modelName].push({
|
||||
name: a.name as string,
|
||||
ability: a.ability as string,
|
||||
disabled: false,
|
||||
depends_on: [],
|
||||
})
|
||||
depends_on: (a.depends_on as string[]) ?? [],
|
||||
} as AbilityItem)
|
||||
})
|
||||
abilitiesList.value = grouped
|
||||
}
|
||||
@@ -87,13 +96,33 @@ async function setInitialData(): Promise<void> {
|
||||
currentRole.value = {
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
abilities: response.data.abilities.map((a) => ({
|
||||
...a,
|
||||
ability: a.name,
|
||||
disabled: false,
|
||||
depends_on: [],
|
||||
})),
|
||||
abilities: [],
|
||||
}
|
||||
|
||||
// Match role's abilities with the full ability objects from abilitiesList
|
||||
const roleAbilities = (response.data.abilities ?? []) as Array<Record<string, unknown>>
|
||||
roleAbilities.forEach((ra) => {
|
||||
Object.keys(abilitiesList.value).forEach((group) => {
|
||||
abilitiesList.value[group].forEach((_p) => {
|
||||
if (_p.ability === ra.name) {
|
||||
currentRole.value.abilities.push(_p)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Set disabled state for dependent abilities
|
||||
currentRole.value.abilities.forEach((ab) => {
|
||||
ab.depends_on?.forEach((_d) => {
|
||||
Object.keys(abilitiesList.value).forEach((group) => {
|
||||
abilitiesList.value[group].forEach((_a) => {
|
||||
if (_d === _a.ability) {
|
||||
_a.disabled = true
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
isEdit.value = false
|
||||
@@ -121,8 +150,16 @@ async function submitRoleData(): Promise<void> {
|
||||
|
||||
if (isEdit.value && currentRole.value.id) {
|
||||
await roleService.update(currentRole.value.id, payload)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.roles.updated_message',
|
||||
})
|
||||
} else {
|
||||
await roleService.create(payload)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.roles.created_message',
|
||||
})
|
||||
}
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRoute } from 'vue-router'
|
||||
import { useDialogStore } from '@v2/stores/dialog.store'
|
||||
import { useUserStore } from '@v2/stores/user.store'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { taxTypeService } from '@v2/api/services/tax-type.service'
|
||||
|
||||
const ABILITIES = {
|
||||
@@ -24,6 +25,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
@@ -54,6 +56,10 @@ function removeTaxType(id: number): void {
|
||||
if (res) {
|
||||
const response = await taxTypeService.delete(id)
|
||||
if (response.success) {
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.tax_types.deleted_message',
|
||||
})
|
||||
props.loadData?.()
|
||||
}
|
||||
}
|
||||
@@ -64,7 +70,7 @@ function removeTaxType(id: number): void {
|
||||
<template>
|
||||
<BaseDropdown>
|
||||
<template #activator>
|
||||
<BaseButton v-if="route.name === 'tax-types.view'" variant="primary">
|
||||
<BaseButton v-if="route.name === 'settings.tax-types'" variant="primary">
|
||||
<BaseIcon name="EllipsisHorizontalIcon" class="h-5 text-white" />
|
||||
</BaseButton>
|
||||
<BaseIcon v-else name="EllipsisHorizontalIcon" class="h-5 text-muted" />
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useModalStore } from '@v2/stores/modal.store'
|
||||
import { useCompanyStore } from '@v2/stores/company.store'
|
||||
import { useNotificationStore } from '@v2/stores/notification.store'
|
||||
import { taxTypeService } from '@v2/api/services/tax-type.service'
|
||||
import type { CreateTaxTypePayload } from '@v2/api/services/tax-type.service'
|
||||
|
||||
@@ -25,6 +26,7 @@ interface TaxTypeForm {
|
||||
|
||||
const modalStore = useModalStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isSaving = ref<boolean>(false)
|
||||
@@ -122,8 +124,16 @@ async function submitTaxTypeData(): Promise<void> {
|
||||
|
||||
if (isEdit.value && currentTaxType.value.id) {
|
||||
await taxTypeService.update(currentTaxType.value.id, payload)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.tax_types.updated_message',
|
||||
})
|
||||
} else {
|
||||
await taxTypeService.create(payload)
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
message: 'settings.tax_types.created_message',
|
||||
})
|
||||
}
|
||||
|
||||
isSaving.value = false
|
||||
|
||||
Reference in New Issue
Block a user