mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-15 17:24:10 +00:00
244 lines
6.8 KiB
Vue
244 lines
6.8 KiB
Vue
<template>
|
|
<BasePage class="relative estimate-create-page">
|
|
<form @submit.prevent="submitForm">
|
|
<BasePageHeader :title="pageTitle">
|
|
<BaseBreadcrumb>
|
|
<BaseBreadcrumbItem :title="$t('general.home')" to="/admin/dashboard" />
|
|
<BaseBreadcrumbItem
|
|
:title="$t('estimates.estimate', 2)"
|
|
to="/admin/estimates"
|
|
/>
|
|
<BaseBreadcrumbItem
|
|
v-if="isEdit"
|
|
:title="$t('estimates.edit_estimate')"
|
|
to="#"
|
|
active
|
|
/>
|
|
<BaseBreadcrumbItem v-else :title="$t('estimates.new_estimate')" to="#" active />
|
|
</BaseBreadcrumb>
|
|
|
|
<template #actions>
|
|
<router-link
|
|
v-if="isEdit"
|
|
:to="`/estimates/pdf/${estimateStore.newEstimate.unique_hash}`"
|
|
target="_blank"
|
|
>
|
|
<BaseButton class="mr-3" variant="primary-outline" type="button">
|
|
<span class="flex">
|
|
{{ $t('general.view_pdf') }}
|
|
</span>
|
|
</BaseButton>
|
|
</router-link>
|
|
|
|
<BaseButton
|
|
:loading="isSaving"
|
|
:disabled="isSaving"
|
|
:content-loading="isLoadingContent"
|
|
variant="primary"
|
|
type="submit"
|
|
>
|
|
<template #left="slotProps">
|
|
<BaseIcon
|
|
v-if="!isSaving"
|
|
:class="slotProps.class"
|
|
name="ArrowDownOnSquareIcon"
|
|
/>
|
|
</template>
|
|
{{ $t('estimates.save_estimate') }}
|
|
</BaseButton>
|
|
</template>
|
|
</BasePageHeader>
|
|
|
|
<!-- Select Customer & Basic Fields -->
|
|
<EstimateBasicFields
|
|
:v="v$"
|
|
:is-loading="isLoadingContent"
|
|
:is-edit="isEdit"
|
|
/>
|
|
|
|
<BaseScrollPane>
|
|
<!-- Estimate Items -->
|
|
<DocumentItemsTable
|
|
:currency="estimateStore.newEstimate.selectedCurrency"
|
|
:is-loading="isLoadingContent"
|
|
:item-validation-scope="estimateValidationScope"
|
|
:store="estimateStore"
|
|
store-prop="newEstimate"
|
|
/>
|
|
|
|
<!-- Estimate Footer Section -->
|
|
<div
|
|
class="block mt-10 estimate-foot lg:flex lg:justify-between lg:items-start"
|
|
>
|
|
<div class="relative w-full lg:w-1/2">
|
|
<!-- Estimate Custom Notes -->
|
|
<DocumentNotes
|
|
:store="estimateStore"
|
|
store-prop="newEstimate"
|
|
:fields="estimateNoteFieldList"
|
|
type="Estimate"
|
|
/>
|
|
|
|
<!-- Estimate Template Button -->
|
|
<TemplateSelectButton
|
|
:store="estimateStore"
|
|
store-prop="newEstimate"
|
|
:is-mark-as-default="isMarkAsDefault"
|
|
/>
|
|
<SelectTemplateModal />
|
|
</div>
|
|
|
|
<DocumentTotals
|
|
:currency="estimateStore.newEstimate.selectedCurrency"
|
|
:is-loading="isLoadingContent"
|
|
:store="estimateStore"
|
|
store-prop="newEstimate"
|
|
tax-popup-type="estimate"
|
|
/>
|
|
</div>
|
|
</BaseScrollPane>
|
|
</form>
|
|
</BasePage>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import cloneDeep from 'lodash/cloneDeep'
|
|
import {
|
|
required,
|
|
maxLength,
|
|
helpers,
|
|
requiredIf,
|
|
decimal,
|
|
} from '@vuelidate/validators'
|
|
import useVuelidate from '@vuelidate/core'
|
|
import { useEstimateStore } from '../store'
|
|
import EstimateBasicFields from '../components/EstimateBasicFields.vue'
|
|
import {
|
|
DocumentItemsTable,
|
|
DocumentTotals,
|
|
DocumentNotes,
|
|
TemplateSelectButton,
|
|
SelectTemplateModal,
|
|
} from '../../../shared/document-form'
|
|
|
|
const estimateStore = useEstimateStore()
|
|
const { t } = useI18n()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const estimateValidationScope = 'newEstimate'
|
|
const isSaving = ref<boolean>(false)
|
|
const isMarkAsDefault = ref<boolean>(false)
|
|
|
|
const estimateNoteFieldList = ref<string[]>(['customer', 'company', 'estimate'])
|
|
|
|
const isLoadingContent = computed<boolean>(
|
|
() => estimateStore.isFetchingInitialSettings,
|
|
)
|
|
|
|
const pageTitle = computed<string>(() =>
|
|
isEdit.value ? t('estimates.edit_estimate') : t('estimates.new_estimate'),
|
|
)
|
|
|
|
const isEdit = computed<boolean>(() => route.name === 'estimates.edit')
|
|
|
|
const rules = {
|
|
estimate_date: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
},
|
|
estimate_number: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
},
|
|
reference_number: {
|
|
maxLength: helpers.withMessage(t('validation.price_maxlength'), maxLength(255)),
|
|
},
|
|
customer_id: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
},
|
|
exchange_rate: {
|
|
required: requiredIf(() => estimateStore.showExchangeRate),
|
|
},
|
|
}
|
|
|
|
const v$ = useVuelidate(
|
|
rules,
|
|
computed(() => estimateStore.newEstimate),
|
|
{ $scope: estimateValidationScope },
|
|
)
|
|
|
|
// Initialization
|
|
estimateStore.resetCurrentEstimate()
|
|
v$.value.$reset
|
|
estimateStore.fetchEstimateInitialSettings(
|
|
isEdit.value,
|
|
{ id: route.params.id as string, query: route.query as Record<string, string> },
|
|
)
|
|
|
|
watch(
|
|
() => estimateStore.newEstimate.customer,
|
|
(newVal) => {
|
|
if (newVal && (newVal as Record<string, unknown>).currency) {
|
|
estimateStore.newEstimate.selectedCurrency = (
|
|
newVal as Record<string, unknown>
|
|
).currency as Record<string, unknown>
|
|
}
|
|
},
|
|
)
|
|
|
|
async function submitForm(): Promise<void> {
|
|
v$.value.$touch()
|
|
|
|
if (v$.value.$invalid) {
|
|
console.log('Estimate form invalid. Errors:', JSON.stringify(
|
|
v$.value.$errors.map((e: { $property: string; $message: string }) => `${e.$property}: ${e.$message}`)
|
|
))
|
|
return
|
|
}
|
|
|
|
isSaving.value = true
|
|
|
|
const data: Record<string, unknown> = {
|
|
...cloneDeep(estimateStore.newEstimate),
|
|
sub_total: estimateStore.getSubTotal,
|
|
total: estimateStore.getTotal,
|
|
tax: estimateStore.getTotalTax,
|
|
}
|
|
|
|
const items = data.items as Array<Record<string, unknown>>
|
|
if (data.discount_per_item === 'YES') {
|
|
items.forEach((item, index) => {
|
|
if (item.discount_type === 'fixed') {
|
|
items[index].discount = Math.round((item.discount as number) * 100)
|
|
}
|
|
})
|
|
} else {
|
|
if (data.discount_type === 'fixed') {
|
|
data.discount = Math.round((data.discount as number) * 100)
|
|
}
|
|
}
|
|
|
|
const taxes = data.taxes as Array<Record<string, unknown>>
|
|
if (data.tax_per_item !== 'YES' && taxes.length) {
|
|
data.tax_type_ids = taxes.map((tax) => tax.tax_type_id)
|
|
}
|
|
|
|
try {
|
|
const action = isEdit.value
|
|
? estimateStore.updateEstimate
|
|
: estimateStore.addEstimate
|
|
|
|
const res = await action(data)
|
|
if (res.data.data) {
|
|
router.push(`/admin/estimates/${res.data.data.id}/view`)
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
|
|
isSaving.value = false
|
|
}
|
|
</script>
|