Files
InvoiceShelf/resources/scripts/features/company/payments/views/PaymentCreateView.vue
Darko Gjorgjijoski 71388ec6a5 Rename resources/scripts-v2 to resources/scripts and drop @v2 alias
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.
2026-04-07 12:50:16 +02:00

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>