mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-22 12:44:09 +00:00
Now that the legacy v1 frontend (commit 064bdf53) is gone, the v2 directory is the only frontend and the v2 suffix is just noise. Renames resources/scripts-v2 to resources/scripts via git mv (so git records the move as renames, preserving blame and log --follow), then bulk-rewrites the 152 files that imported via @v2/... to use @/scripts/... instead. The existing @ alias (resources/) covers the new path with no extra config needed.
Drops the now-unused @v2 alias from vite.config.js and points the laravel-vite-plugin entry at resources/scripts/main.ts. Updates the only blade reference (resources/views/app.blade.php) to match. The package.json test script (eslint ./resources/scripts) automatically targets the right place after the rename without any edit.
Verified: npm run build exits clean and the Vite warning lines now reference resources/scripts/plugins/i18n.ts, confirming every import resolved through the new path. git log --follow on any moved file walks back through its scripts-v2 history.
380 lines
11 KiB
Vue
380 lines
11 KiB
Vue
<template>
|
|
<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="$t('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="ArrowDownOnSquareIcon"
|
|
:class="slotProps.class"
|
|
/>
|
|
</template>
|
|
{{
|
|
isEdit
|
|
? $t('payments.update_payment')
|
|
: $t('payments.save_payment')
|
|
}}
|
|
</BaseButton>
|
|
</template>
|
|
</BasePageHeader>
|
|
|
|
<BaseCard>
|
|
<BaseInputGrid>
|
|
<!-- Payment Date -->
|
|
<BaseInputGroup
|
|
:label="$t('payments.date')"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
>
|
|
<BaseDatePicker
|
|
v-model="paymentStore.currentPayment.payment_date"
|
|
:content-loading="isLoadingContent"
|
|
:calendar-button="true"
|
|
calendar-button-icon="calendar"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<!-- Payment Number -->
|
|
<BaseInputGroup
|
|
:label="$t('payments.payment_number')"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
>
|
|
<BaseInput
|
|
v-model="paymentStore.currentPayment.payment_number"
|
|
:content-loading="isLoadingContent"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<!-- Customer -->
|
|
<BaseInputGroup
|
|
:label="$t('payments.customer')"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
>
|
|
<BaseCustomerSelectInput
|
|
v-if="!isLoadingContent"
|
|
v-model="paymentStore.currentPayment.customer_id"
|
|
:content-loading="isLoadingContent"
|
|
:placeholder="$t('customers.select_a_customer')"
|
|
show-action
|
|
@update:model-value="onManualCustomerSelect"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<!-- Invoice -->
|
|
<BaseInputGroup
|
|
:content-loading="isLoadingContent"
|
|
:label="$t('payments.invoice')"
|
|
>
|
|
<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"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<!-- Amount -->
|
|
<BaseInputGroup
|
|
:label="$t('payments.amount')"
|
|
:content-loading="isLoadingContent"
|
|
required
|
|
>
|
|
<div class="relative w-full">
|
|
<BaseMoney
|
|
:key="String(paymentStore.currentPayment.currency)"
|
|
v-model="amount"
|
|
:currency="paymentStore.currentPayment.currency"
|
|
:content-loading="isLoadingContent"
|
|
/>
|
|
</div>
|
|
</BaseInputGroup>
|
|
|
|
<!-- Exchange Rate -->
|
|
<ExchangeRateConverter
|
|
:store="paymentStore"
|
|
store-prop="currentPayment"
|
|
:v="{ exchange_rate: { $error: false, $errors: [], $touch: () => {} } }"
|
|
:is-loading="isLoadingContent"
|
|
:is-edit="isEdit"
|
|
:customer-currency="paymentStore.currentPayment.currency_id"
|
|
/>
|
|
|
|
<!-- Payment Mode -->
|
|
<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
|
|
/>
|
|
</BaseInputGroup>
|
|
</BaseInputGrid>
|
|
|
|
<!-- Notes -->
|
|
<div class="relative mt-6">
|
|
<label class="mb-4 text-sm font-medium text-heading">
|
|
{{ $t('estimates.notes') }}
|
|
</label>
|
|
|
|
<BaseCustomInput
|
|
v-model="paymentStore.currentPayment.notes"
|
|
:content-loading="isLoadingContent"
|
|
:fields="paymentFields"
|
|
class="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Mobile Save Button -->
|
|
<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="ArrowDownOnSquareIcon"
|
|
:class="slotProps.class"
|
|
/>
|
|
</template>
|
|
{{
|
|
isEdit
|
|
? $t('payments.update_payment')
|
|
: $t('payments.save_payment')
|
|
}}
|
|
</BaseButton>
|
|
</BaseCard>
|
|
</form>
|
|
</BasePage>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watchEffect, onBeforeUnmount } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import cloneDeep from 'lodash/cloneDeep'
|
|
import { usePaymentStore } from '../store'
|
|
import { useCompanyStore } from '../../../../stores/company.store'
|
|
import { invoiceService } from '../../../../api/services/invoice.service'
|
|
import { customerService } from '../../../../api/services/customer.service'
|
|
import { ExchangeRateConverter } from '../../../shared/document-form'
|
|
import type { Invoice } from '../../../../types/domain/invoice'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { t } = useI18n()
|
|
const paymentStore = usePaymentStore()
|
|
const companyStore = useCompanyStore()
|
|
|
|
const isSaving = ref<boolean>(false)
|
|
const isLoadingInvoices = ref<boolean>(false)
|
|
const invoiceList = ref<Invoice[]>([])
|
|
const selectedInvoice = ref<Invoice | null>(null)
|
|
|
|
const paymentFields = ref<string[]>([
|
|
'customer',
|
|
'company',
|
|
'customerCustom',
|
|
'payment',
|
|
'paymentCustom',
|
|
])
|
|
|
|
const amount = computed<number>({
|
|
get: () => paymentStore.currentPayment.amount / 100,
|
|
set: (value: number) => {
|
|
paymentStore.currentPayment.amount = Math.round(value * 100)
|
|
},
|
|
})
|
|
|
|
const isLoadingContent = computed<boolean>(
|
|
() => paymentStore.isFetchingInitialData,
|
|
)
|
|
|
|
const isEdit = computed<boolean>(() => route.name === 'payments.edit')
|
|
|
|
const pageTitle = computed<string>(() => {
|
|
return isEdit.value ? t('payments.edit_payment') : t('payments.new_payment')
|
|
})
|
|
|
|
// Reset state on create
|
|
paymentStore.resetCurrentPayment()
|
|
|
|
if (route.query.customer) {
|
|
paymentStore.currentPayment.customer_id = Number(route.query.customer)
|
|
}
|
|
|
|
paymentStore.fetchPaymentInitialData(
|
|
isEdit.value,
|
|
{ id: isEdit.value ? (route.params.id as string) : undefined },
|
|
companyStore.selectedCompanyCurrency ?? undefined,
|
|
)
|
|
|
|
// Create-from-invoice: pre-select the invoice and its customer
|
|
if (route.params.id && !isEdit.value) {
|
|
setInvoiceFromUrl()
|
|
}
|
|
|
|
async function setInvoiceFromUrl(): Promise<void> {
|
|
try {
|
|
const response = await invoiceService.get(Number(route.params.id))
|
|
const invoice = response.data
|
|
paymentStore.currentPayment.customer_id = invoice.customer_id ?? (invoice.customer as Record<string, unknown>)?.id as number
|
|
paymentStore.currentPayment.invoice_id = invoice.id
|
|
} catch {
|
|
// Invoice not found
|
|
}
|
|
}
|
|
|
|
// Reactively fetch invoices whenever customer_id changes
|
|
// Handles: edit data load, manual selection, create-from-invoice
|
|
watchEffect(() => {
|
|
if (paymentStore.currentPayment.customer_id) {
|
|
onCustomerChange(paymentStore.currentPayment.customer_id)
|
|
}
|
|
})
|
|
|
|
async function onCustomerChange(customerId: number): Promise<void> {
|
|
const params: Record<string, unknown> = {
|
|
customer_id: customerId,
|
|
status: isEdit.value ? '' : 'DUE',
|
|
limit: 'all',
|
|
}
|
|
|
|
isLoadingInvoices.value = true
|
|
try {
|
|
const [invoiceResponse, customerResponse] = await Promise.all([
|
|
invoiceService.list(params as never),
|
|
customerService.get(customerId),
|
|
])
|
|
|
|
invoiceList.value = [...(invoiceResponse.data as unknown as Invoice[])]
|
|
|
|
// Set currency from customer
|
|
if (customerResponse.data) {
|
|
const customer = customerResponse.data
|
|
paymentStore.currentPayment.customer = customer
|
|
paymentStore.currentPayment.selectedCustomer = customer
|
|
if (customer.currency) {
|
|
paymentStore.currentPayment.currency = customer.currency
|
|
paymentStore.currentPayment.currency_id = customer.currency.id
|
|
}
|
|
}
|
|
|
|
if (paymentStore.currentPayment.invoice_id) {
|
|
selectedInvoice.value =
|
|
invoiceList.value.find(
|
|
(inv) => inv.id === paymentStore.currentPayment.invoice_id,
|
|
) ?? null
|
|
|
|
if (selectedInvoice.value) {
|
|
paymentStore.currentPayment.maxPayableAmount =
|
|
selectedInvoice.value.due_amount +
|
|
paymentStore.currentPayment.amount
|
|
|
|
if (amount.value === 0) {
|
|
amount.value = selectedInvoice.value.due_amount / 100
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isEdit.value) {
|
|
invoiceList.value = invoiceList.value.filter(
|
|
(v) =>
|
|
v.due_amount > 0 ||
|
|
v.id === paymentStore.currentPayment.invoice_id,
|
|
)
|
|
}
|
|
} catch {
|
|
invoiceList.value = []
|
|
} finally {
|
|
isLoadingInvoices.value = false
|
|
}
|
|
}
|
|
|
|
function onManualCustomerSelect(): void {
|
|
const params: Record<string, unknown> = {
|
|
userId: paymentStore.currentPayment.customer_id,
|
|
}
|
|
if (route.params.id) {
|
|
params.model_id = route.params.id
|
|
}
|
|
|
|
paymentStore.currentPayment.invoice_id = null
|
|
selectedInvoice.value = null
|
|
paymentStore.currentPayment.amount = 0
|
|
paymentStore.getNextNumber(params, true)
|
|
}
|
|
|
|
function onSelectInvoice(id: number): void {
|
|
if (id) {
|
|
selectedInvoice.value =
|
|
invoiceList.value.find((inv) => inv.id === id) ?? null
|
|
if (selectedInvoice.value) {
|
|
amount.value = selectedInvoice.value.due_amount / 100
|
|
paymentStore.currentPayment.maxPayableAmount =
|
|
selectedInvoice.value.due_amount
|
|
}
|
|
}
|
|
}
|
|
|
|
async function submitPaymentData(): Promise<void> {
|
|
isSaving.value = true
|
|
|
|
const data = {
|
|
...cloneDeep(paymentStore.currentPayment),
|
|
}
|
|
|
|
try {
|
|
const action = isEdit.value
|
|
? paymentStore.updatePayment
|
|
: paymentStore.addPayment
|
|
|
|
const response = await action(data)
|
|
router.push(`/admin/payments/${response.data.data.id}/view`)
|
|
} catch {
|
|
isSaving.value = false
|
|
}
|
|
}
|
|
|
|
onBeforeUnmount(() => {
|
|
paymentStore.resetCurrentPayment()
|
|
invoiceList.value = []
|
|
})
|
|
</script>
|