Files
InvoiceShelf/resources/scripts-v2/features/company/estimates/views/EstimateCreateView.vue
2026-04-06 17:59:15 +02:00

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>