mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 11:41:09 -04:00
523 lines
15 KiB
Vue
523 lines
15 KiB
Vue
<template>
|
|
<PaymentModeModal />
|
|
|
|
<BasePage class="relative payment-create">
|
|
<form action="" @submit.prevent="submitPaymentData">
|
|
<BasePageHeader :title="pageTitle" class="mb-5">
|
|
<BaseBreadcrumb>
|
|
<BaseBreadcrumbItem
|
|
:title="$t('general.home')"
|
|
to="/admin/dashboard"
|
|
/>
|
|
<BaseBreadcrumbItem
|
|
:title="$tc('payments.payment', 2)"
|
|
to="/admin/payments"
|
|
/>
|
|
<BaseBreadcrumbItem :title="pageTitle" to="#" active />
|
|
</BaseBreadcrumb>
|
|
|
|
<template #actions>
|
|
<BaseButton
|
|
:loading="isSaving"
|
|
:disabled="isSaving"
|
|
variant="primary"
|
|
type="submit"
|
|
class="hidden sm:flex"
|
|
>
|
|
<template #left="slotProps">
|
|
<BaseIcon
|
|
v-if="!isSaving"
|
|
name="SaveIcon"
|
|
:class="slotProps.class"
|
|
/>
|
|
</template>
|
|
{{
|
|
isEdit
|
|
? $t('payments.update_payment')
|
|
: $t('payments.save_payment')
|
|
}}
|
|
</BaseButton>
|
|
</template>
|
|
</BasePageHeader>
|
|
|
|
<BaseCard>
|
|
<BaseInputGrid>
|
|
<BaseInputGroup
|
|
:label="$t('payments.date')"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
:error="
|
|
v$.currentPayment.payment_date.$error &&
|
|
v$.currentPayment.payment_date.$errors[0].$message
|
|
"
|
|
>
|
|
<BaseDatePicker
|
|
v-model="paymentStore.currentPayment.payment_date"
|
|
:content-loading="isLoadingContent"
|
|
:calendar-button="true"
|
|
calendar-button-icon="calendar"
|
|
:invalid="v$.currentPayment.payment_date.$error"
|
|
@update:modelValue="v$.currentPayment.payment_date.$touch()"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
:label="$t('payments.payment_number')"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
>
|
|
<BaseInput
|
|
v-model="paymentStore.currentPayment.payment_number"
|
|
:content-loading="isLoadingContent"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
:label="$t('payments.customer')"
|
|
:error="
|
|
v$.currentPayment.customer_id.$error &&
|
|
v$.currentPayment.customer_id.$errors[0].$message
|
|
"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
>
|
|
<BaseCustomerSelectInput
|
|
v-model="paymentStore.currentPayment.customer_id"
|
|
:content-loading="isLoadingContent"
|
|
:invalid="v$.currentPayment.customer_id.$error"
|
|
:placeholder="$t('customers.select_a_customer')"
|
|
:fetch-all="isEdit"
|
|
show-action
|
|
@update:modelValue="
|
|
selectNewCustomer(paymentStore.currentPayment.customer_id)
|
|
"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
:content-loading="isLoadingContent"
|
|
:label="$t('payments.invoice')"
|
|
:help-text="
|
|
selectedInvoice
|
|
? `Due Amount: ${
|
|
paymentStore.currentPayment.maxPayableAmount / 100
|
|
}`
|
|
: ''
|
|
"
|
|
>
|
|
<BaseMultiselect
|
|
v-model="paymentStore.currentPayment.invoice_id"
|
|
:content-loading="isLoadingContent"
|
|
value-prop="id"
|
|
track-by="invoice_number"
|
|
label="invoice_number"
|
|
:options="invoiceList"
|
|
:loading="isLoadingInvoices"
|
|
:placeholder="$t('invoices.select_invoice')"
|
|
@select="onSelectInvoice"
|
|
>
|
|
<template #singlelabel="{ value }">
|
|
<div class="absolute left-3.5">
|
|
{{ value.invoice_number }} ({{
|
|
utils.formatMoney(value.total, value.customer.currency)
|
|
}})
|
|
</div>
|
|
</template>
|
|
|
|
<template #option="{ option }">
|
|
{{ option.invoice_number }} ({{
|
|
utils.formatMoney(option.total, option.customer.currency)
|
|
}})
|
|
</template>
|
|
</BaseMultiselect>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
:label="$t('payments.amount')"
|
|
:content-loading="isLoadingContent"
|
|
:error="
|
|
v$.currentPayment.amount.$error &&
|
|
v$.currentPayment.amount.$errors[0].$message
|
|
"
|
|
required
|
|
>
|
|
<div class="relative w-full">
|
|
<BaseMoney
|
|
:key="paymentStore.currentPayment.currency"
|
|
v-model="amount"
|
|
:currency="paymentStore.currentPayment.currency"
|
|
:content-loading="isLoadingContent"
|
|
:invalid="v$.currentPayment.amount.$error"
|
|
@update:modelValue="v$.currentPayment.amount.$touch()"
|
|
/>
|
|
</div>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
:content-loading="isLoadingContent"
|
|
:label="$t('payments.payment_mode')"
|
|
>
|
|
<BaseMultiselect
|
|
v-model="paymentStore.currentPayment.payment_method_id"
|
|
:content-loading="isLoadingContent"
|
|
label="name"
|
|
value-prop="id"
|
|
track-by="name"
|
|
:options="paymentStore.paymentModes"
|
|
:placeholder="$t('payments.select_payment_mode')"
|
|
searchable
|
|
>
|
|
<template #action>
|
|
<BaseSelectAction @click="addPaymentMode">
|
|
<BaseIcon
|
|
name="PlusIcon"
|
|
class="h-4 mr-2 -ml-2 text-center text-primary-400"
|
|
/>
|
|
{{ $t('settings.payment_modes.add_payment_mode') }}
|
|
</BaseSelectAction>
|
|
</template>
|
|
</BaseMultiselect>
|
|
</BaseInputGroup>
|
|
|
|
<ExchangeRateConverter
|
|
:store="paymentStore"
|
|
store-prop="currentPayment"
|
|
:v="v$.currentPayment"
|
|
:is-loading="isLoadingContent"
|
|
:is-edit="isEdit"
|
|
:customer-currency="paymentStore.currentPayment.currency_id"
|
|
/>
|
|
</BaseInputGrid>
|
|
|
|
<!-- Payment Custom Fields -->
|
|
<PaymentCustomFields
|
|
type="Payment"
|
|
:is-edit="isEdit"
|
|
:is-loading="isLoadingContent"
|
|
:store="paymentStore"
|
|
store-prop="currentPayment"
|
|
:custom-field-scope="paymentValidationScope"
|
|
class="mt-6"
|
|
/>
|
|
|
|
<!-- Payment Note field -->
|
|
<div class="relative mt-6">
|
|
<div
|
|
class="
|
|
z-20
|
|
float-right
|
|
text-sm
|
|
font-semibold
|
|
leading-5
|
|
text-primary-400
|
|
"
|
|
>
|
|
<SelectNotePopup type="Payment" @select="onSelectNote" />
|
|
</div>
|
|
|
|
<label class="mb-4 text-sm font-medium text-primary-800">
|
|
{{ $t('estimates.notes') }}
|
|
</label>
|
|
|
|
<BaseCustomInput
|
|
v-model="paymentStore.currentPayment.notes"
|
|
:content-loading="isLoadingContent"
|
|
:fields="PaymentFields"
|
|
class="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<BaseButton
|
|
:loading="isSaving"
|
|
:content-loading="isLoadingContent"
|
|
variant="primary"
|
|
type="submit"
|
|
class="flex justify-center w-full mt-4 sm:hidden md:hidden"
|
|
>
|
|
<template #left="slotProps">
|
|
<BaseIcon
|
|
v-if="!isSaving"
|
|
name="SaveIcon"
|
|
:class="slotProps.class"
|
|
/>
|
|
</template>
|
|
{{
|
|
isEdit ? $t('payments.update_payment') : $t('payments.save_payment')
|
|
}}
|
|
</BaseButton>
|
|
</BaseCard>
|
|
</form>
|
|
</BasePage>
|
|
</template>
|
|
|
|
<script setup>
|
|
import ExchangeRateConverter from '@/scripts/components/estimate-invoice-common/ExchangeRateConverter.vue'
|
|
|
|
import {
|
|
ref,
|
|
reactive,
|
|
computed,
|
|
inject,
|
|
watchEffect,
|
|
onBeforeUnmount,
|
|
} from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import {
|
|
required,
|
|
numeric,
|
|
helpers,
|
|
between,
|
|
requiredIf,
|
|
decimal,
|
|
} from '@vuelidate/validators'
|
|
|
|
import useVuelidate from '@vuelidate/core'
|
|
import { useCustomerStore } from '@/scripts/stores/customer'
|
|
import { usePaymentStore } from '@/scripts/stores/payment'
|
|
import { useNotificationStore } from '@/scripts/stores/notification'
|
|
import { useCustomFieldStore } from '@/scripts/stores/custom-field'
|
|
import { useCompanyStore } from '@/scripts/stores/company'
|
|
import { useModalStore } from '@/scripts/stores/modal'
|
|
import { useInvoiceStore } from '@/scripts/stores/invoice'
|
|
import { useGlobalStore } from '@/scripts/stores/global'
|
|
|
|
import SelectNotePopup from '@/scripts/components/SelectNotePopup.vue'
|
|
import PaymentCustomFields from '@/scripts/components/custom-fields/CreateCustomFields.vue'
|
|
import PaymentModeModal from '@/scripts/components/modal-components/PaymentModeModal.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const paymentStore = usePaymentStore()
|
|
const notificationStore = useNotificationStore()
|
|
const customerStore = useCustomerStore()
|
|
const customFieldStore = useCustomFieldStore()
|
|
const companyStore = useCompanyStore()
|
|
const modalStore = useModalStore()
|
|
const invoiceStore = useInvoiceStore()
|
|
const globalStore = useGlobalStore()
|
|
|
|
const utils = inject('utils')
|
|
const { t } = useI18n()
|
|
|
|
let isSaving = ref(false)
|
|
let isLoadingInvoices = ref(false)
|
|
let invoiceList = ref([])
|
|
const selectedInvoice = ref(null)
|
|
|
|
const paymentValidationScope = 'newEstimate'
|
|
|
|
const PaymentFields = reactive([
|
|
'customer',
|
|
'company',
|
|
'customerCustom',
|
|
'payment',
|
|
'paymentCustom',
|
|
])
|
|
|
|
const amount = computed({
|
|
get: () => paymentStore.currentPayment.amount / 100,
|
|
set: (value) => {
|
|
paymentStore.currentPayment.amount = Math.round(value * 100)
|
|
},
|
|
})
|
|
|
|
const isLoadingContent = computed(() => paymentStore.isFetchingInitialData)
|
|
|
|
const isEdit = computed(() => route.name === 'payments.edit')
|
|
|
|
const pageTitle = computed(() => {
|
|
if (isEdit.value) {
|
|
return t('payments.edit_payment')
|
|
}
|
|
return t('payments.new_payment')
|
|
})
|
|
|
|
const rules = computed(() => {
|
|
return {
|
|
currentPayment: {
|
|
customer_id: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
},
|
|
payment_date: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
},
|
|
amount: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
between: helpers.withMessage(
|
|
t('validation.payment_greater_than_due_amount'),
|
|
between(1, paymentStore.currentPayment.maxPayableAmount)
|
|
),
|
|
},
|
|
exchange_rate: {
|
|
required: requiredIf(function () {
|
|
helpers.withMessage(t('validation.required'), required)
|
|
return paymentStore.showExchangeRate
|
|
}),
|
|
decimal: helpers.withMessage(
|
|
t('validation.valid_exchange_rate'),
|
|
decimal
|
|
),
|
|
},
|
|
},
|
|
}
|
|
})
|
|
|
|
const v$ = useVuelidate(rules, paymentStore, {
|
|
$scope: paymentValidationScope,
|
|
})
|
|
|
|
watchEffect(() => {
|
|
// fetch customer and its invoices
|
|
paymentStore.currentPayment.customer_id
|
|
? onCustomerChange(paymentStore.currentPayment.customer_id)
|
|
: ''
|
|
if (route.query.customer) {
|
|
paymentStore.currentPayment.customer_id = route.query.customer
|
|
}
|
|
})
|
|
|
|
// Reset State on Create
|
|
paymentStore.resetCurrentPayment()
|
|
|
|
if (route.query.customer) {
|
|
paymentStore.currentPayment.customer_id = route.query.customer
|
|
}
|
|
|
|
paymentStore.fetchPaymentInitialData(isEdit.value)
|
|
|
|
if (route.params.id && !isEdit.value) {
|
|
setInvoiceFromUrl()
|
|
}
|
|
|
|
async function addPaymentMode() {
|
|
modalStore.openModal({
|
|
title: t('settings.payment_modes.add_payment_mode'),
|
|
componentName: 'PaymentModeModal',
|
|
})
|
|
}
|
|
|
|
function onSelectNote(data) {
|
|
paymentStore.currentPayment.notes = '' + data.notes
|
|
}
|
|
|
|
async function setInvoiceFromUrl() {
|
|
let res = await invoiceStore.fetchInvoice(route?.params?.id)
|
|
|
|
paymentStore.currentPayment.customer_id = res.data.data.customer.id
|
|
paymentStore.currentPayment.invoice_id = res.data.data.id
|
|
}
|
|
|
|
async function onSelectInvoice(id) {
|
|
if (id) {
|
|
selectedInvoice.value = invoiceList.value.find((inv) => inv.id === id)
|
|
|
|
amount.value = selectedInvoice.value.due_amount / 100
|
|
paymentStore.currentPayment.maxPayableAmount =
|
|
selectedInvoice.value.due_amount
|
|
}
|
|
}
|
|
|
|
function onCustomerChange(customer_id) {
|
|
if (customer_id) {
|
|
let data = {
|
|
customer_id: customer_id,
|
|
status: 'DUE',
|
|
limit: 'all',
|
|
}
|
|
|
|
if (isEdit.value) {
|
|
data.status = ''
|
|
}
|
|
|
|
isLoadingInvoices.value = true
|
|
|
|
Promise.all([
|
|
invoiceStore.fetchInvoices(data),
|
|
customerStore.fetchCustomer(customer_id),
|
|
])
|
|
.then(async ([res1, res2]) => {
|
|
if (res1) {
|
|
invoiceList.value = [...res1.data.data]
|
|
}
|
|
|
|
if (res2 && res2.data) {
|
|
paymentStore.currentPayment.selectedCustomer = res2.data.data
|
|
paymentStore.currentPayment.customer = res2.data.data
|
|
paymentStore.currentPayment.currency = res2.data.data.currency
|
|
}
|
|
|
|
if (isEdit.value && paymentStore.currentPayment.invoice_id) {
|
|
selectedInvoice.value = invoiceList.value.find(
|
|
(inv) => inv.id === paymentStore.currentPayment.invoice_id
|
|
)
|
|
|
|
paymentStore.currentPayment.maxPayableAmount =
|
|
selectedInvoice.value.due_amount +
|
|
paymentStore.currentPayment.amount
|
|
}
|
|
|
|
if (isEdit.value) {
|
|
invoiceList.value = invoiceList.value.filter((v) => {
|
|
return (
|
|
v.due_amount > 0 || v.id == paymentStore.currentPayment.invoice_id
|
|
)
|
|
})
|
|
}
|
|
|
|
isLoadingInvoices.value = false
|
|
})
|
|
.catch((error) => {
|
|
isLoadingInvoices.value = false
|
|
console.error(error, 'error')
|
|
})
|
|
}
|
|
}
|
|
onBeforeUnmount(() => {
|
|
paymentStore.resetCurrentPayment()
|
|
invoiceList.value = []
|
|
})
|
|
|
|
async function submitPaymentData() {
|
|
v$.value.$touch()
|
|
|
|
if (v$.value.$invalid) {
|
|
return false
|
|
}
|
|
|
|
isSaving.value = true
|
|
|
|
let data = {
|
|
...paymentStore.currentPayment,
|
|
}
|
|
|
|
let response = null
|
|
|
|
try {
|
|
const action = isEdit.value
|
|
? paymentStore.updatePayment
|
|
: paymentStore.addPayment
|
|
|
|
response = await action(data)
|
|
|
|
router.push(`/admin/payments/${response.data.data.id}/view`)
|
|
} catch (err) {
|
|
isSaving.value = false
|
|
}
|
|
}
|
|
|
|
function selectNewCustomer(id) {
|
|
let params = {
|
|
userId: id,
|
|
}
|
|
|
|
if (route.params.id) params.model_id = route.params.id
|
|
|
|
paymentStore.currentPayment.invoice_id = selectedInvoice.value = null
|
|
paymentStore.currentPayment.amount = 100
|
|
invoiceList.value = []
|
|
paymentStore.getNextNumber(params, true)
|
|
}
|
|
</script>
|