mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-05-07 03:54:04 +00:00
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.
This commit is contained in:
539
resources/scripts/components/base/BaseCustomerSelectPopup.vue
Normal file
539
resources/scripts/components/base/BaseCustomerSelectPopup.vue
Normal file
@@ -0,0 +1,539 @@
|
||||
<script setup lang="ts">
|
||||
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/scripts/stores/user.store'
|
||||
import { useModalStore } from '@/scripts/stores/modal.store'
|
||||
import { ABILITIES } from '@/scripts/config/abilities'
|
||||
import { useCustomerStore } from '@/scripts/features/company/customers/store'
|
||||
import { useInvoiceStore } from '@/scripts/features/company/invoices/store'
|
||||
import { useEstimateStore } from '@/scripts/features/company/estimates/store'
|
||||
import { useRecurringInvoiceStore } from '@/scripts/features/company/recurring-invoices/store'
|
||||
import CustomerModal from '@/scripts/features/company/customers/components/CustomerModal.vue'
|
||||
|
||||
type DocumentType = 'estimate' | 'invoice' | 'recurring-invoice'
|
||||
|
||||
interface ValidationError {
|
||||
$message: string
|
||||
}
|
||||
|
||||
interface Validation {
|
||||
$error: boolean
|
||||
$errors: ValidationError[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
valid?: Validation
|
||||
customerId?: number | null
|
||||
type?: DocumentType | null
|
||||
contentLoading?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
valid: () => ({ $error: false, $errors: [] }),
|
||||
customerId: null,
|
||||
type: null,
|
||||
contentLoading: false,
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const modalStore = useModalStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
const customerStore = useCustomerStore()
|
||||
const invoiceStore = useInvoiceStore()
|
||||
const estimateStore = useEstimateStore()
|
||||
const recurringInvoiceStore = useRecurringInvoiceStore()
|
||||
|
||||
const search = ref<string | null>(null)
|
||||
const isSearchingCustomer = ref<boolean>(false)
|
||||
|
||||
const selectedCustomer = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'invoice':
|
||||
return invoiceStore.newInvoice.customer
|
||||
case 'estimate':
|
||||
return estimateStore.newEstimate.customer
|
||||
case 'recurring-invoice':
|
||||
return recurringInvoiceStore.newRecurringInvoice.customer
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch initial customers on setup
|
||||
async function fetchInitialCustomers(): Promise<void> {
|
||||
await customerStore.fetchCustomers({
|
||||
orderByField: '',
|
||||
orderBy: '',
|
||||
})
|
||||
}
|
||||
|
||||
// Select customer on setup if customerId is provided
|
||||
if (props.customerId) {
|
||||
if (props.type === 'invoice') {
|
||||
invoiceStore.selectCustomer(props.customerId)
|
||||
} else if (props.type === 'estimate') {
|
||||
estimateStore.selectCustomer(props.customerId)
|
||||
} else if (props.type === 'recurring-invoice') {
|
||||
recurringInvoiceStore.selectCustomer(props.customerId)
|
||||
}
|
||||
}
|
||||
|
||||
fetchInitialCustomers()
|
||||
|
||||
const debounceSearchCustomer = useDebounceFn(() => {
|
||||
isSearchingCustomer.value = true
|
||||
searchCustomer()
|
||||
}, 500)
|
||||
|
||||
async function searchCustomer(): Promise<void> {
|
||||
await customerStore.fetchCustomers({
|
||||
display_name: search.value ?? '',
|
||||
page: 1,
|
||||
})
|
||||
isSearchingCustomer.value = false
|
||||
}
|
||||
|
||||
function selectNewCustomer(id: number, close: () => void): void {
|
||||
const params: Record<string, unknown> = { userId: id }
|
||||
if (route.params.id) params.model_id = route.params.id
|
||||
|
||||
if (props.type === 'invoice') {
|
||||
invoiceStore.getNextNumber(params, true)
|
||||
invoiceStore.selectCustomer(id)
|
||||
} else if (props.type === 'estimate') {
|
||||
estimateStore.getNextNumber(params, true)
|
||||
estimateStore.selectCustomer(id)
|
||||
} else if (props.type === 'recurring-invoice') {
|
||||
recurringInvoiceStore.selectCustomer(id)
|
||||
}
|
||||
|
||||
close()
|
||||
search.value = null
|
||||
}
|
||||
|
||||
function resetSelectedCustomer(): void {
|
||||
if (props.type === 'invoice') {
|
||||
invoiceStore.resetSelectedCustomer()
|
||||
} else if (props.type === 'estimate') {
|
||||
estimateStore.resetSelectedCustomer()
|
||||
} else if (props.type === 'recurring-invoice') {
|
||||
recurringInvoiceStore.resetSelectedCustomer()
|
||||
}
|
||||
}
|
||||
|
||||
async function editCustomer(): Promise<void> {
|
||||
if (!selectedCustomer.value) return
|
||||
await customerStore.fetchCustomer(selectedCustomer.value.id)
|
||||
modalStore.openModal({
|
||||
title: t('customers.edit_customer'),
|
||||
componentName: 'CustomerModal',
|
||||
})
|
||||
}
|
||||
|
||||
function openCustomerModal(): void {
|
||||
customerStore.resetCurrentCustomer()
|
||||
modalStore.openModal({
|
||||
title: t('customers.add_customer'),
|
||||
componentName: 'CustomerModal',
|
||||
variant: 'md',
|
||||
})
|
||||
}
|
||||
|
||||
function initGenerator(name: string): string {
|
||||
if (name) {
|
||||
const nameSplit = name.split(' ')
|
||||
return nameSplit[0].charAt(0).toUpperCase()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CustomerModal />
|
||||
|
||||
<BaseContentPlaceholders v-if="contentLoading">
|
||||
<BaseContentPlaceholdersBox
|
||||
:rounded="true"
|
||||
class="w-full"
|
||||
style="min-height: 170px"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<div v-else class="max-h-[173px]">
|
||||
<div
|
||||
v-if="selectedCustomer"
|
||||
class="
|
||||
flex flex-col
|
||||
p-4
|
||||
bg-surface
|
||||
border border-line-light border-solid
|
||||
min-h-[170px]
|
||||
rounded-xl
|
||||
shadow
|
||||
"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex relative justify-between mb-2">
|
||||
<BaseText
|
||||
:text="selectedCustomer.name"
|
||||
class="flex-1 text-base font-medium text-left text-heading"
|
||||
/>
|
||||
<div class="flex">
|
||||
<a
|
||||
class="
|
||||
relative
|
||||
my-0
|
||||
ml-6
|
||||
text-sm
|
||||
font-medium
|
||||
cursor-pointer
|
||||
text-primary-500
|
||||
items-center
|
||||
flex
|
||||
"
|
||||
@click.stop="editCustomer"
|
||||
>
|
||||
<BaseIcon name="PencilIcon" class="text-muted h-4 w-4 mr-1" />
|
||||
|
||||
{{ $t('general.edit') }}
|
||||
</a>
|
||||
<a
|
||||
class="
|
||||
relative
|
||||
my-0
|
||||
ml-6
|
||||
text-sm
|
||||
flex
|
||||
items-center
|
||||
font-medium
|
||||
cursor-pointer
|
||||
text-primary-500
|
||||
"
|
||||
@click="resetSelectedCustomer"
|
||||
>
|
||||
<BaseIcon name="XCircleIcon" class="text-muted h-4 w-4 mr-1" />
|
||||
{{ $t('general.deselect') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-8 mt-2">
|
||||
<div v-if="selectedCustomer.billing" class="flex flex-col">
|
||||
<label
|
||||
class="
|
||||
mb-1
|
||||
text-sm
|
||||
font-medium
|
||||
text-left text-subtle
|
||||
uppercase
|
||||
whitespace-nowrap
|
||||
"
|
||||
>
|
||||
{{ $t('general.bill_to') }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="selectedCustomer.billing"
|
||||
class="flex flex-col flex-1 p-0 text-left"
|
||||
>
|
||||
<label
|
||||
v-if="selectedCustomer.billing.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing.name }}
|
||||
</label>
|
||||
|
||||
<label class="relative w-11/12 text-sm truncate">
|
||||
<span v-if="selectedCustomer.billing.city">
|
||||
{{ selectedCustomer.billing.city }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
selectedCustomer.billing.city &&
|
||||
selectedCustomer.billing.state
|
||||
"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
<span v-if="selectedCustomer.billing.state">
|
||||
{{ selectedCustomer.billing.state }}
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.billing.zip"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.billing.zip }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedCustomer.shipping" class="flex flex-col">
|
||||
<label
|
||||
class="
|
||||
mb-1
|
||||
text-sm
|
||||
font-medium
|
||||
text-left text-subtle
|
||||
uppercase
|
||||
whitespace-nowrap
|
||||
"
|
||||
>
|
||||
{{ $t('general.ship_to') }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
v-if="selectedCustomer.shipping"
|
||||
class="flex flex-col flex-1 p-0 text-left"
|
||||
>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping.name"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping.name }}
|
||||
</label>
|
||||
|
||||
<label class="relative w-11/12 text-sm truncate">
|
||||
<span v-if="selectedCustomer.shipping.city">
|
||||
{{ selectedCustomer.shipping.city }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
selectedCustomer.shipping.city &&
|
||||
selectedCustomer.shipping.state
|
||||
"
|
||||
>
|
||||
,
|
||||
</span>
|
||||
<span v-if="selectedCustomer.shipping.state">
|
||||
{{ selectedCustomer.shipping.state }}
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="selectedCustomer.shipping.zip"
|
||||
class="relative w-11/12 text-sm truncate"
|
||||
>
|
||||
{{ selectedCustomer.shipping.zip }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popover v-else v-slot="{ open }" class="relative flex flex-col rounded-xl">
|
||||
<PopoverButton
|
||||
:class="{
|
||||
'': open,
|
||||
'border border-solid border-red-500 focus:ring-red-500 rounded':
|
||||
valid.$error,
|
||||
'focus:ring-2 focus:ring-primary-400': !valid.$error,
|
||||
}"
|
||||
class="w-full outline-hidden rounded-xl"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
relative
|
||||
flex
|
||||
justify-center
|
||||
px-0
|
||||
p-0
|
||||
py-16
|
||||
bg-surface
|
||||
border border-line-light border-solid
|
||||
rounded-xl
|
||||
shadow
|
||||
min-h-[170px]
|
||||
"
|
||||
>
|
||||
<BaseIcon
|
||||
name="UserIcon"
|
||||
class="
|
||||
flex
|
||||
justify-center
|
||||
!w-10
|
||||
!h-10
|
||||
p-2
|
||||
mr-5
|
||||
text-sm text-white
|
||||
bg-surface-muted
|
||||
rounded-full
|
||||
font-base
|
||||
"
|
||||
/>
|
||||
|
||||
<div class="mt-1">
|
||||
<label class="text-lg font-medium text-heading">
|
||||
{{ $t('customers.new_customer') }}
|
||||
<span class="text-red-500"> * </span>
|
||||
</label>
|
||||
|
||||
<p
|
||||
v-if="valid.$error && valid.$errors[0]?.$message"
|
||||
class="text-red-500 text-sm absolute right-3 bottom-3"
|
||||
>
|
||||
{{ $t('estimates.errors.required') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverButton>
|
||||
|
||||
<!-- Customer Select Popup -->
|
||||
<transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="translate-y-1 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-1 opacity-0"
|
||||
>
|
||||
<div v-if="open" class="absolute min-w-full z-10">
|
||||
<PopoverPanel
|
||||
v-slot="{ close }"
|
||||
focus
|
||||
static
|
||||
class="
|
||||
overflow-hidden
|
||||
rounded-xl
|
||||
shadow
|
||||
ring-1 ring-black/5
|
||||
bg-surface
|
||||
"
|
||||
>
|
||||
<div class="relative">
|
||||
<BaseInput
|
||||
v-model="search"
|
||||
container-class="m-4"
|
||||
:placeholder="$t('general.search')"
|
||||
type="text"
|
||||
icon="search"
|
||||
@update:modelValue="() => debounceSearchCustomer()"
|
||||
/>
|
||||
|
||||
<ul
|
||||
class="
|
||||
max-h-80
|
||||
flex flex-col
|
||||
overflow-auto
|
||||
list
|
||||
border-t border-line-light
|
||||
"
|
||||
>
|
||||
<li
|
||||
v-for="(customer, index) in customerStore.customers"
|
||||
:key="index"
|
||||
href="#"
|
||||
class="
|
||||
flex
|
||||
px-6
|
||||
py-2
|
||||
border-b border-line-light border-solid
|
||||
cursor-pointer
|
||||
hover:cursor-pointer hover:bg-hover-strong
|
||||
focus:outline-hidden focus:bg-surface-tertiary
|
||||
last:border-b-0
|
||||
"
|
||||
@click="selectNewCustomer(customer.id, close)"
|
||||
>
|
||||
<span
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
content-center
|
||||
justify-center
|
||||
w-10
|
||||
h-10
|
||||
mr-4
|
||||
text-xl
|
||||
font-semibold
|
||||
leading-9
|
||||
text-white
|
||||
bg-surface-muted
|
||||
rounded-full
|
||||
avatar
|
||||
"
|
||||
>
|
||||
{{ initGenerator(customer.name) }}
|
||||
</span>
|
||||
|
||||
<div class="flex flex-col justify-center text-left">
|
||||
<BaseText
|
||||
v-if="customer.name"
|
||||
:text="customer.name"
|
||||
class="
|
||||
m-0
|
||||
text-base
|
||||
font-normal
|
||||
leading-tight
|
||||
cursor-pointer
|
||||
"
|
||||
/>
|
||||
<BaseText
|
||||
v-if="customer.contact_name"
|
||||
:text="customer.contact_name"
|
||||
class="
|
||||
m-0
|
||||
text-sm
|
||||
font-medium
|
||||
text-subtle
|
||||
cursor-pointer
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<div
|
||||
v-if="customerStore.customers.length === 0"
|
||||
class="flex justify-center p-5 text-subtle"
|
||||
>
|
||||
<label class="text-base text-muted cursor-pointer">
|
||||
{{ $t('customers.no_customers_found') }}
|
||||
</label>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="userStore.hasAbilities(ABILITIES.CREATE_CUSTOMER)"
|
||||
type="button"
|
||||
class="
|
||||
h-10
|
||||
flex
|
||||
items-center
|
||||
justify-center
|
||||
w-full
|
||||
px-2
|
||||
py-3
|
||||
bg-surface-muted
|
||||
border-none
|
||||
outline-hidden
|
||||
focus:bg-surface-muted
|
||||
"
|
||||
@click="openCustomerModal"
|
||||
>
|
||||
<BaseIcon name="UserPlusIcon" class="text-primary-400" />
|
||||
|
||||
<label
|
||||
class="
|
||||
m-0
|
||||
ml-3
|
||||
text-sm
|
||||
leading-none
|
||||
cursor-pointer
|
||||
font-base
|
||||
text-primary-400
|
||||
"
|
||||
>
|
||||
{{ $t('customers.add_new_customer') }}
|
||||
</label>
|
||||
</button>
|
||||
</PopoverPanel>
|
||||
</div>
|
||||
</transition>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user