Complete scripts-v2 TypeScript migration — all imports resolved,

build passes

Create all missing components (modals, dropdowns, icons, tabs, mail
drivers, customer partials), fix all @/scripts/ imports to @v2/,
wire up vite entry point and blade template. 382 files, 48883 lines.

- 27 settings components: modals (tax, payment, custom field, note,
  category, role, exchange rate, unit, mail test), dropdowns (6),
  customization tabs (4), mail driver forms (4)
- 22 icon components: 5 utility icons, 4 dashboard icons, 13 editor
  toolbar icons with typed barrel export
- 3 customer components: info, chart placeholder, custom fields single
- Fixed usePopper composable, client/format-money import patterns
- Zero remaining @/scripts/ imports in scripts-v2/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darko Gjorgjijoski
2026-04-04 09:30:00 +02:00
parent 812956abcc
commit a46cca5cd8
156 changed files with 6246 additions and 213 deletions

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useNotificationStore } from '@v2/stores/notification.store'
const notificationStore = useNotificationStore()
const { t } = useI18n()
const publicUrl = ref<HTMLElement | null>(null)
const props = defineProps<{
token: string
}>()
function selectText(element: HTMLElement): void {
if (window.getSelection) {
const range = document.createRange()
range.selectNode(element)
window.getSelection()?.removeAllRanges()
window.getSelection()?.addRange(range)
}
}
function copyUrl(): void {
if (publicUrl.value) {
selectText(publicUrl.value)
document.execCommand('copy')
notificationStore.showNotification({
type: 'success',
message: t('general.copied_url_clipboard'),
})
}
}
</script>
<template>
<div
class="relative flex px-4 py-2 rounded-lg bg-surface-muted/40 whitespace-nowrap flex-col mt-1"
>
<span
ref="publicUrl"
class="pr-10 text-sm font-medium text-heading truncate select-all select-color"
>
{{ token }}
</span>
<svg
v-tooltip="{ content: $t('general.copy_to_clipboard') }"
class="absolute right-0 h-full inset-y-0 cursor-pointer focus:outline-hidden text-primary-500"
width="37"
viewBox="0 0 37 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click="copyUrl"
>
<rect width="37" height="37" rx="10" fill="currentColor" />
<path
d="M16 10C15.7348 10 15.4804 10.1054 15.2929 10.2929C15.1054 10.4804 15 10.7348 15 11C15 11.2652 15.1054 11.5196 15.2929 11.7071C15.4804 11.8946 15.7348 12 16 12H18C18.2652 12 18.5196 11.8946 18.7071 11.7071C18.8946 11.5196 19 11.2652 19 11C19 10.7348 18.8946 10.4804 18.7071 10.2929C18.5196 10.1054 18.2652 10 18 10H16Z"
fill="white"
/>
<path
d="M11 13C11 12.4696 11.2107 11.9609 11.5858 11.5858C11.9609 11.2107 12.4696 11 13 11C13 11.7956 13.3161 12.5587 13.8787 13.1213C14.4413 13.6839 15.2044 14 16 14H18C18.7956 14 19.5587 13.6839 20.1213 13.1213C20.6839 12.5587 21 11.7956 21 11C21.5304 11 22.0391 11.2107 22.4142 11.5858C22.7893 11.9609 23 12.4696 23 13V19H18.414L19.707 17.707C19.8892 17.5184 19.99 17.2658 19.9877 17.0036C19.9854 16.7414 19.8802 16.4906 19.6948 16.3052C19.5094 16.1198 19.2586 16.0146 18.9964 16.0123C18.7342 16.01 18.4816 16.1108 18.293 16.293L15.293 19.293C15.1055 19.4805 15.0002 19.7348 15.0002 20C15.0002 20.2652 15.1055 20.5195 15.293 20.707L18.293 23.707C18.4816 23.8892 18.7342 23.99 18.9964 23.9877C19.2586 23.9854 19.5094 23.8802 19.6948 23.6948C19.8802 23.5094 19.9854 23.2586 19.9877 22.9964C19.99 22.7342 19.8892 22.4816 19.707 22.293L18.414 21H23V24C23 24.5304 22.7893 25.0391 22.4142 25.4142C22.0391 25.7893 21.5304 26 21 26H13C12.4696 26 11.9609 25.7893 11.5858 25.4142C11.2107 25.0391 11 24.5304 11 24V13ZM23 19H25C25.2652 19 25.5196 19.1054 25.7071 19.2929C25.8946 19.4804 26 19.7348 26 20C26 20.2652 25.8946 20.5196 25.7071 20.7071C25.5196 20.8946 25.2652 21 25 21H23V19Z"
fill="white"
/>
</svg>
</div>
</template>

View File

@@ -0,0 +1,138 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import lodash from 'lodash'
import moment from 'moment'
import { customFieldService } from '@v2/api/services/custom-field.service'
import SingleField from './CreateCustomFieldsSingle.vue'
interface CustomFieldItem {
id: number
value: string | boolean | number | null
default_answer: string | boolean | number | null
label: string
options: string[] | null
is_required: boolean
placeholder: string | null
order: number | null
type: string
custom_field_id?: number
custom_field?: {
label: string
options: string[] | null
is_required: boolean
placeholder: string | null
order: number | null
type: string
}
}
interface StoreWithProp {
[key: string]: {
customFields: CustomFieldItem[]
fields: CustomFieldItem[]
}
}
const props = withDefaults(
defineProps<{
store: StoreWithProp
storeProp: string
isEdit?: boolean
type?: string | null
gridLayout?: string
isLoading?: boolean | null
customFieldScope: string
}>(),
{
isEdit: false,
type: null,
gridLayout: 'two-column',
isLoading: null,
}
)
const customFields = ref<CustomFieldItem[]>([])
getInitialCustomFields()
function mergeExistingValues(): void {
if (props.isEdit && props.store[props.storeProp]) {
props.store[props.storeProp].fields.forEach((field) => {
const existingIndex = props.store[
props.storeProp
].customFields.findIndex((f) => f.id === field.custom_field_id)
if (existingIndex > -1) {
let value: string | boolean | number | null = field.default_answer
if (value && field.custom_field?.type === 'DateTime') {
value = moment(
String(field.default_answer),
'YYYY-MM-DD HH:mm:ss'
).format('YYYY-MM-DD HH:mm')
}
props.store[props.storeProp].customFields[existingIndex] = {
...field,
id: field.custom_field_id ?? field.id,
value,
label: field.custom_field?.label ?? '',
options: field.custom_field?.options ?? null,
is_required: field.custom_field?.is_required ?? false,
placeholder: field.custom_field?.placeholder ?? null,
order: field.custom_field?.order ?? null,
type: field.custom_field?.type ?? field.type,
}
}
})
}
}
async function getInitialCustomFields(): Promise<void> {
const res = await customFieldService.list({
type: props.type ?? undefined,
limit: 'all',
})
const data = (res as Record<string, unknown>).data as CustomFieldItem[]
data.forEach((d) => {
d.value = d.default_answer
})
props.store[props.storeProp].customFields = lodash.sortBy(
data,
(_cf: CustomFieldItem) => _cf.order
)
mergeExistingValues()
}
watch(
() => props.store[props.storeProp]?.fields,
() => {
mergeExistingValues()
}
)
</script>
<template>
<div
v-if="
store[storeProp] &&
store[storeProp].customFields.length > 0 &&
!isLoading
"
>
<BaseInputGrid :layout="gridLayout">
<SingleField
v-for="(field, index) in store[storeProp].customFields"
:key="field.id"
:custom-field-scope="customFieldScope"
:store="store"
:store-prop="storeProp"
:index="index"
:field="field"
/>
</BaseInputGrid>
</div>
</template>

View File

@@ -0,0 +1,57 @@
<template>
<BaseInputGroup
:label="field.label"
:required="field.is_required ? true : false"
:error="v$.value.$error && v$.value.$errors[0].$message"
>
<component
:is="getTypeComponent"
v-model="field.value"
:options="field.options"
:invalid="v$.value.$error"
:placeholder="field.placeholder"
/>
</BaseInputGroup>
</template>
<script setup lang="ts">
import { defineAsyncComponent, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { helpers, requiredIf } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
const props = defineProps<{
field: Record<string, any>
customFieldScope: string
index: number
store: Record<string, any>
storeProp: string
}>()
const { t } = useI18n()
const rules = {
value: {
required: helpers.withMessage(
t('validation.required'),
requiredIf(props.field.is_required)
),
},
}
const v$ = useVuelidate(
rules,
computed(() => props.field),
{ $scope: props.customFieldScope }
)
const getTypeComponent = computed(() => {
if (props.field.type) {
return defineAsyncComponent(() =>
import(`./types/${props.field.type}Type.vue`)
)
}
return false
})
</script>

View File

@@ -0,0 +1,208 @@
<script setup lang="ts">
import { ref, computed, watch, reactive } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useCustomerStore } from '../store'
import { useCompanyStore } from '@v2/stores/company.store'
import { customerService } from '@v2/api/services/customer.service'
import LineChart from '@v2/components/charts/LineChart.vue'
import ChartPlaceholder from './CustomerChartPlaceholder.vue'
import CustomerInfo from './CustomerInfo.vue'
interface ChartData {
salesTotal: number
totalReceipts: number
totalExpenses: number
netProfit: number
expenseTotals: number[]
netProfits: number[]
months: string[]
receiptTotals: number[]
invoiceTotals: number[]
}
interface YearOption {
label: string
value: string
}
const companyStore = useCompanyStore()
const customerStore = useCustomerStore()
const { t } = useI18n()
const route = useRoute()
const isLoading = ref<boolean>(false)
const chartData = reactive<Partial<ChartData>>({})
const years = reactive<YearOption[]>([
{ label: t('dateRange.this_year'), value: 'This year' },
{ label: t('dateRange.previous_year'), value: 'Previous year' },
])
const selectedYear = ref<string>('This year')
const getChartExpenses = computed<number[]>(() => chartData.expenseTotals ?? [])
const getNetProfits = computed<number[]>(() => chartData.netProfits ?? [])
const getChartMonths = computed<string[]>(() => chartData.months ?? [])
const getReceiptTotals = computed<number[]>(() => chartData.receiptTotals ?? [])
const getChartInvoices = computed<number[]>(() => chartData.invoiceTotals ?? [])
watch(
route,
() => {
if (route.params.id) {
loadCustomer()
}
selectedYear.value = 'This year'
},
{ immediate: true }
)
async function loadCustomer(): Promise<void> {
isLoading.value = false
const response = await customerService.getStats(Number(route.params.id))
if (response.data) {
const meta = (response as Record<string, unknown>).meta as Record<string, unknown> | undefined
if (meta?.chartData) {
Object.assign(chartData, meta.chartData)
}
}
isLoading.value = true
}
async function onChangeYear(data: string): Promise<boolean> {
const params: Record<string, unknown> = {
id: route.params.id,
}
if (data === 'Previous year') {
params.previous_year = true
} else {
params.this_year = true
}
const response = await customerService.getStats(
Number(route.params.id),
params
)
const meta = (response as Record<string, unknown>).meta as Record<string, unknown> | undefined
if (meta?.chartData) {
Object.assign(chartData, meta.chartData)
}
return true
}
</script>
<template>
<BaseCard class="flex flex-col mt-6">
<ChartPlaceholder v-if="!isLoading" />
<div v-else class="grid grid-cols-12">
<div class="col-span-12 xl:col-span-9 xxl:col-span-10">
<div class="flex justify-between mt-1 mb-6">
<h6 class="flex items-center">
<BaseIcon name="ChartBarSquareIcon" class="h-5 text-primary-400" />
{{ $t('dashboard.monthly_chart.title') }}
</h6>
<div class="w-40 h-10">
<BaseMultiselect
v-model="selectedYear"
:options="years"
:allow-empty="false"
:show-labels="false"
:placeholder="$t('dashboard.select_year')"
:can-deselect="false"
@select="onChangeYear"
/>
</div>
</div>
<LineChart
v-if="isLoading"
:invoices="getChartInvoices"
:expenses="getChartExpenses"
:receipts="getReceiptTotals"
:income="getNetProfits"
:labels="getChartMonths"
class="sm:w-full"
/>
</div>
<div
class="grid col-span-12 mt-6 text-center xl:mt-0 sm:grid-cols-4 xl:text-right xl:col-span-3 xl:grid-cols-1 xxl:col-span-2"
>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_sales') }}
</span>
<br />
<span
v-if="isLoading"
class="block mt-1 text-xl font-semibold leading-8"
>
<BaseFormatMoney
:amount="chartData.salesTotal"
:currency="companyStore.selectedCompanyCurrency"
/>
</span>
</div>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_receipts') }}
</span>
<br />
<span
v-if="isLoading"
class="block mt-1 text-xl font-semibold leading-8"
style="color: #00c99c"
>
<BaseFormatMoney
:amount="chartData.totalReceipts"
:currency="companyStore.selectedCompanyCurrency"
/>
</span>
</div>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.total_expense') }}
</span>
<br />
<span
v-if="isLoading"
class="block mt-1 text-xl font-semibold leading-8"
style="color: #fb7178"
>
<BaseFormatMoney
:amount="chartData.totalExpenses"
:currency="companyStore.selectedCompanyCurrency"
/>
</span>
</div>
<div class="px-6 py-2">
<span class="text-xs leading-5 lg:text-sm">
{{ $t('dashboard.chart_info.net_income') }}
</span>
<br />
<span
v-if="isLoading"
class="block mt-1 text-xl font-semibold leading-8"
style="color: #5851d8"
>
<BaseFormatMoney
:amount="chartData.netProfit"
:currency="companyStore.selectedCompanyCurrency"
/>
</span>
</div>
</div>
</div>
<CustomerInfo />
</BaseCard>
</template>

View File

@@ -0,0 +1,79 @@
<template>
<BaseContentPlaceholders class="grid grid-cols-12">
<div class="col-span-12 xl:col-span-9 xxl:col-span-10">
<div class="flex justify-between mt-1 mb-6">
<BaseContentPlaceholdersText class="h-10 w-36" :lines="1" />
<BaseContentPlaceholdersText class="h-10 w-40 !mt-0" :lines="1" />
</div>
<BaseContentPlaceholdersBox class="h-80 xl:h-72 sm:w-full" />
</div>
<div
class="
grid
col-span-12
mt-6
text-center
xl:mt-0
sm:grid-cols-4
xl:text-right xl:col-span-3 xl:grid-cols-1
xxl:col-span-2
"
>
<div
class="
flex flex-col
items-center
justify-center
px-6
py-2
lg:justify-end lg:items-end
"
>
<BaseContentPlaceholdersText class="h-3 w-14 xl:h-4" :lines="1" />
<BaseContentPlaceholdersText class="w-20 h-5 xl:h-6" :lines="1" />
</div>
<div
class="
flex flex-col
items-center
justify-center
px-6
py-2
lg:justify-end lg:items-end
"
>
<BaseContentPlaceholdersText class="h-3 w-14 xl:h-4" :lines="1" />
<BaseContentPlaceholdersText class="w-20 h-5 xl:h-6" :lines="1" />
</div>
<div
class="
flex flex-col
items-center
justify-center
px-6
py-2
lg:justify-end lg:items-end
"
>
<BaseContentPlaceholdersText class="h-3 w-14 xl:h-4" :lines="1" />
<BaseContentPlaceholdersText class="w-20 h-5 xl:h-6" :lines="1" />
</div>
<div
class="
flex flex-col
items-center
justify-center
px-6
py-2
lg:justify-end lg:items-end
"
>
<BaseContentPlaceholdersText class="h-3 w-14 xl:h-4" :lines="1" />
<BaseContentPlaceholdersText class="w-20 h-5 xl:h-6" :lines="1" />
</div>
</div>
</BaseContentPlaceholders>
</template>

View File

@@ -0,0 +1,124 @@
<template>
<div class="pt-6 mt-5 border-t border-solid lg:pt-8 md:pt-4 border-line-default">
<!-- Basic Info -->
<BaseHeading>
{{ $t('customers.basic_info') }}
</BaseHeading>
<BaseDescriptionList>
<BaseDescriptionListItem
v-if="selectedViewCustomer.name"
:content-loading="contentLoading"
:label="$t('customers.display_name')"
:value="selectedViewCustomer?.name"
/>
<BaseDescriptionListItem
v-if="selectedViewCustomer.contact_name"
:content-loading="contentLoading"
:label="$t('customers.primary_contact_name')"
:value="selectedViewCustomer?.contact_name"
/>
<BaseDescriptionListItem
v-if="selectedViewCustomer.email"
:content-loading="contentLoading"
:label="$t('customers.email')"
:value="selectedViewCustomer?.email"
/>
</BaseDescriptionList>
<BaseDescriptionList class="mt-5">
<BaseDescriptionListItem
:content-loading="contentLoading"
:label="$t('wizard.currency')"
:value="
selectedViewCustomer?.currency
? `${selectedViewCustomer?.currency?.code} (${selectedViewCustomer?.currency?.symbol})`
: ''
"
/>
<BaseDescriptionListItem
v-if="selectedViewCustomer.phone"
:content-loading="contentLoading"
:label="$t('customers.phone_number')"
:value="selectedViewCustomer?.phone"
/>
<BaseDescriptionListItem
v-if="selectedViewCustomer.website"
:content-loading="contentLoading"
:label="$t('customers.website')"
:value="selectedViewCustomer?.website"
/>
</BaseDescriptionList>
<!-- Address -->
<BaseHeading
v-if="selectedViewCustomer.billing || selectedViewCustomer.shipping"
class="mt-8"
>
{{ $t('customers.address') }}
</BaseHeading>
<BaseDescriptionList class="mt-5">
<BaseDescriptionListItem
v-if="selectedViewCustomer.billing"
:content-loading="contentLoading"
:label="$t('customers.billing_address')"
>
<BaseCustomerAddressDisplay :address="selectedViewCustomer.billing" />
</BaseDescriptionListItem>
<BaseDescriptionListItem
v-if="selectedViewCustomer.shipping"
:content-loading="contentLoading"
:label="$t('customers.shipping_address')"
>
<BaseCustomerAddressDisplay :address="selectedViewCustomer.shipping" />
</BaseDescriptionListItem>
</BaseDescriptionList>
<!-- Custom Fields -->
<BaseHeading v-if="customerCustomFields.length > 0" class="mt-8">
{{ $t('settings.custom_fields.title') }}
</BaseHeading>
<BaseDescriptionList class="mt-5">
<BaseDescriptionListItem
v-for="(field, index) in customerCustomFields"
:key="index"
:content-loading="contentLoading"
:label="field.custom_field.label"
>
<p
v-if="field.type === 'Switch'"
class="text-sm font-bold leading-5 text-heading non-italic"
>
<span v-if="field.default_answer === 1"> {{ $t('general.yes') }} </span>
<span v-else> {{ $t('general.no') }} </span>
</p>
<p v-else class="text-sm font-bold leading-5 text-heading non-italic">
{{ field.default_answer }}
</p>
</BaseDescriptionListItem>
</BaseDescriptionList>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useCustomerStore } from '../store'
const customerStore = useCustomerStore()
const selectedViewCustomer = computed(() => customerStore.selectedViewCustomer)
const contentLoading = computed(() => customerStore.isFetchingViewData)
const customerCustomFields = computed(() => {
if (selectedViewCustomer?.value?.fields) {
return selectedViewCustomer?.value?.fields
}
return []
})
</script>

View File

@@ -18,12 +18,12 @@ import { useCustomerStore } from '../store'
import { useCompanyStore } from '../../../../stores/company.store'
import { useGlobalStore } from '../../../../stores/global.store'
import { useNotificationStore } from '../../../../stores/notification.store'
import CopyInputField from '@/scripts/admin/components/CopyInputField.vue'
import CopyInputField from '@v2/features/company/customers/components/CopyInputField.vue'
// These stores are needed for auto-selecting customer after creation
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
import { useRecurringInvoiceStore } from '@/scripts/admin/stores/recurring-invoice'
import { useEstimateStore } from '@v2/features/company/estimates/store'
import { useInvoiceStore } from '@v2/features/company/invoices/store'
import { useRecurringInvoiceStore } from '@v2/features/company/recurring-invoices/store'
const recurringInvoiceStore = useRecurringInvoiceStore()
const modalStore = useModalStore()

View File

@@ -0,0 +1,272 @@
<script setup lang="ts">
import { computed, ref, reactive } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { useCustomerStore } from '../store'
import { useDebounceFn } from '@vueuse/core'
import LoadingIcon from '@v2/components/icons/LoadingIcon.vue'
interface SearchData {
orderBy: string | null
orderByField: string | null
searchText: string | null
}
interface CustomerListItem {
id: number
name: string
contact_name: string | null
due_amount: number | null
currency: Record<string, unknown> | null
}
const customerStore = useCustomerStore()
const route = useRoute()
const { t } = useI18n()
const isFetching = ref<boolean>(false)
const searchData = reactive<SearchData>({
orderBy: null,
orderByField: null,
searchText: null,
})
const customerList = ref<CustomerListItem[] | null>(null)
const currentPageNumber = ref<number>(1)
const lastPageNumber = ref<number>(1)
const customerListSection = ref<HTMLElement | null>(null)
const onSearch = useDebounceFn(async () => {
customerList.value = []
loadCustomers()
}, 500)
const getOrderBy = computed<boolean>(() => {
return searchData.orderBy === 'asc' || searchData.orderBy === null
})
function hasActiveUrl(id: number): boolean {
return Number(route.params.id) === id
}
async function loadCustomers(
pageNumber?: number,
fromScrollListener = false
): Promise<void> {
if (isFetching.value) return
const params: Record<string, unknown> = {}
if (searchData.searchText) {
params.display_name = searchData.searchText
}
if (searchData.orderBy) {
params.orderBy = searchData.orderBy
}
if (searchData.orderByField) {
params.orderByField = searchData.orderByField
}
isFetching.value = true
const response = await customerStore.fetchCustomers({
page: pageNumber ?? 1,
...params,
limit: 15,
})
isFetching.value = false
if (!customerList.value) customerList.value = []
customerList.value = [...customerList.value, ...response.data]
currentPageNumber.value = pageNumber ?? 1
lastPageNumber.value = response.meta.last_page
const customerFound = customerList.value.find(
(cust) => cust.id === Number(route.params.id)
)
if (
!fromScrollListener &&
!customerFound &&
currentPageNumber.value < lastPageNumber.value &&
Object.keys(params).length === 0
) {
loadCustomers(++currentPageNumber.value)
}
if (customerFound) {
setTimeout(() => {
if (!fromScrollListener) {
scrollToCustomer()
}
}, 500)
}
}
function scrollToCustomer(): void {
const el = document.getElementById(`customer-${route.params.id}`)
if (el) {
el.scrollIntoView({ behavior: 'smooth' })
el.classList.add('shake')
addScrollListener()
}
}
function addScrollListener(): void {
customerListSection.value?.addEventListener('scroll', (ev: Event) => {
const target = ev.target as HTMLElement
if (
target.scrollTop > 0 &&
target.scrollTop + target.clientHeight > target.scrollHeight - 200
) {
if (currentPageNumber.value < lastPageNumber.value) {
loadCustomers(++currentPageNumber.value, true)
}
}
})
}
function sortData(): void {
if (searchData.orderBy === 'asc') {
searchData.orderBy = 'desc'
} else {
searchData.orderBy = 'asc'
}
onSearch()
}
loadCustomers()
</script>
<template>
<div
class="fixed top-0 left-0 hidden h-full pt-16 pb-[6.6rem] ml-56 bg-surface xl:ml-64 w-88 xl:block"
>
<div
class="flex items-center justify-between px-4 pt-8 pb-2 border border-line-default border-solid height-full"
>
<BaseInput
v-model="searchData.searchText"
:placeholder="$t('general.search')"
container-class="mb-6"
type="text"
variant="gray"
@input="onSearch()"
>
<BaseIcon name="MagnifyingGlassIcon" class="text-muted" />
</BaseInput>
<div class="flex mb-6 ml-3" role="group" aria-label="First group">
<BaseDropdown
:close-on-select="false"
position="bottom-start"
width-class="w-40"
position-class="left-0"
>
<template #activator>
<BaseButton variant="gray">
<BaseIcon name="FunnelIcon" />
</BaseButton>
</template>
<div
class="px-4 py-3 pb-2 mb-2 text-sm border-b border-line-default border-solid"
>
{{ $t('general.sort_by') }}
</div>
<div class="px-2">
<BaseDropdownItem
class="flex px-1 py-2 mt-1 cursor-pointer hover:rounded-md"
>
<BaseInputGroup class="pt-2 -mt-4">
<BaseRadio
id="filter_create_date"
v-model="searchData.orderByField"
:label="$t('customers.create_date')"
size="sm"
name="filter"
value="invoices.created_at"
@update:model-value="onSearch"
/>
</BaseInputGroup>
</BaseDropdownItem>
</div>
<div class="px-2">
<BaseDropdownItem
class="flex px-1 cursor-pointer hover:rounded-md"
>
<BaseInputGroup class="pt-2 -mt-4">
<BaseRadio
id="filter_display_name"
v-model="searchData.orderByField"
:label="$t('customers.display_name')"
size="sm"
name="filter"
value="name"
@update:model-value="onSearch"
/>
</BaseInputGroup>
</BaseDropdownItem>
</div>
</BaseDropdown>
<BaseButton class="ml-1" size="md" variant="gray" @click="sortData">
<BaseIcon v-if="getOrderBy" name="SortAscendingIcon" />
<BaseIcon v-else name="SortDescendingIcon" />
</BaseButton>
</div>
</div>
<div
ref="customerListSection"
class="h-full overflow-y-scroll border-l border-line-default border-solid sidebar base-scroll"
>
<div v-for="(customer, index) in customerList" :key="index">
<router-link
v-if="customer"
:id="'customer-' + customer.id"
:to="`/admin/customers/${customer.id}/view`"
:class="[
'flex justify-between p-4 items-center cursor-pointer hover:bg-hover-strong border-l-4 border-l-transparent',
{
'bg-surface-tertiary border-l-4 border-l-primary-500 border-solid':
hasActiveUrl(customer.id),
},
]"
style="border-top: 1px solid rgba(185, 193, 209, 0.41)"
>
<div>
<BaseText
:text="customer.name"
class="pr-2 text-sm not-italic font-normal leading-5 text-heading capitalize truncate"
/>
<BaseText
v-if="customer.contact_name"
:text="customer.contact_name"
class="mt-1 text-xs not-italic font-medium leading-5 text-body"
/>
</div>
<div class="flex-1 font-bold text-right whitespace-nowrap">
<BaseFormatMoney
:amount="customer.due_amount !== null ? customer.due_amount : 0"
:currency="customer.currency"
/>
</div>
</router-link>
</div>
<div v-if="isFetching" class="flex justify-center p-4 items-center">
<LoadingIcon class="h-6 m-1 animate-spin text-primary-400" />
</div>
<p
v-if="!customerList?.length && !isFetching"
class="flex justify-center px-4 mt-5 text-sm text-body"
>
{{ $t('customers.no_matching_customers') }}
</p>
</div>
</div>
</template>