Phase 3: Typed Vue components in scripts-v2/

Migrate all shared components to TypeScript SFCs with script setup
lang=ts. 72 files, 7144 lines, zero any types.

- components/base/ (42 files): Button, Input, Textarea, Checkbox,
  Radio, Switch, Badge, Card, Modal, Dialog, Dropdown, DatePicker,
  TimePicker, Money, FileUploader, Select, Icon, Loader, Multiselect,
  TabGroup, Wizard, CustomerSelect, ItemSelect, CustomInput, alerts,
  status badges (Invoice/Estimate/Paid/RecurringInvoice), List/ListItem
- components/table/ (3 files): DataTable, TablePagination
- components/form/ (4 files): FormGroup, FormGrid, SwitchSection
- components/layout/ (11 files): Page, PageHeader, Breadcrumb,
  FilterWrapper, EmptyPlaceholder, ContentPlaceholders, SettingCard
- components/editor/ (2 files): RichEditor with Tiptap
- components/charts/ (2 files): LineChart with Chart.js
- components/notifications/ (3 files): NotificationRoot, NotificationItem
- components/icons/ (2 files): MainLogo

All use defineProps<Props>(), defineEmits<Emits>(), typed refs,
and import domain types from types/domain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Darko Gjorgjijoski
2026-04-04 05:45:00 +02:00
parent 2b996d30bf
commit e43e515614
72 changed files with 7144 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue'
import type { Ref } from 'vue'
interface CustomFieldOption {
label: string
value: string
}
interface CustomFieldData {
label: string
value: string
slug: string
model_type: string
}
interface FieldGroup {
label: string
fields: CustomFieldOption[]
}
type FieldType =
| 'shipping'
| 'billing'
| 'customer'
| 'invoice'
| 'estimate'
| 'payment'
| 'company'
interface Props {
contentLoading?: boolean
modelValue?: string
fields?: FieldType[] | null
customFields?: CustomFieldData[]
}
interface Emits {
(e: 'update:modelValue', value: string): void
}
const props = withDefaults(defineProps<Props>(), {
contentLoading: false,
modelValue: '',
fields: null,
customFields: () => [],
})
const emit = defineEmits<Emits>()
const fieldList = ref<FieldGroup[]>([])
const invoiceFields = ref<CustomFieldData[]>([])
const estimateFields = ref<CustomFieldData[]>([])
const paymentFields = ref<CustomFieldData[]>([])
const customerFields = ref<CustomFieldData[]>([])
watch(
() => props.fields,
() => {
if (props.fields && props.fields.length > 0) {
getFields()
}
}
)
watch(
() => props.customFields,
(newValue: CustomFieldData[] | undefined) => {
const data = newValue ?? []
invoiceFields.value = data.filter((field) => field.model_type === 'Invoice')
customerFields.value = data.filter(
(field) => field.model_type === 'Customer'
)
paymentFields.value = data.filter(
(field) => field.model_type === 'Payment'
)
estimateFields.value = data.filter(
(field) => field.model_type === 'Estimate'
)
getFields()
}
)
const value = computed<string>({
get: () => props.modelValue,
set: (val: string) => {
emit('update:modelValue', val)
},
})
function getFields(): void {
fieldList.value = []
if (!props.fields || props.fields.length === 0) return
if (props.fields.includes('shipping')) {
fieldList.value.push({
label: 'Shipping Address',
fields: [
{ label: 'Address name', value: 'SHIPPING_ADDRESS_NAME' },
{ label: 'Country', value: 'SHIPPING_COUNTRY' },
{ label: 'State', value: 'SHIPPING_STATE' },
{ label: 'City', value: 'SHIPPING_CITY' },
{ label: 'Address Street 1', value: 'SHIPPING_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'SHIPPING_ADDRESS_STREET_2' },
{ label: 'Phone', value: 'SHIPPING_PHONE' },
{ label: 'Zip Code', value: 'SHIPPING_ZIP_CODE' },
],
})
}
if (props.fields.includes('billing')) {
fieldList.value.push({
label: 'Billing Address',
fields: [
{ label: 'Address name', value: 'BILLING_ADDRESS_NAME' },
{ label: 'Country', value: 'BILLING_COUNTRY' },
{ label: 'State', value: 'BILLING_STATE' },
{ label: 'City', value: 'BILLING_CITY' },
{ label: 'Address Street 1', value: 'BILLING_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'BILLING_ADDRESS_STREET_2' },
{ label: 'Phone', value: 'BILLING_PHONE' },
{ label: 'Zip Code', value: 'BILLING_ZIP_CODE' },
],
})
}
if (props.fields.includes('customer')) {
fieldList.value.push({
label: 'Customer',
fields: [
{ label: 'Display Name', value: 'CONTACT_DISPLAY_NAME' },
{ label: 'Contact Name', value: 'PRIMARY_CONTACT_NAME' },
{ label: 'Email', value: 'CONTACT_EMAIL' },
{ label: 'Phone', value: 'CONTACT_PHONE' },
{ label: 'Website', value: 'CONTACT_WEBSITE' },
{ label: 'Tax ID', value: 'CONTACT_TAX_ID' },
...customerFields.value.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (props.fields.includes('invoice')) {
fieldList.value.push({
label: 'Invoice',
fields: [
{ label: 'Date', value: 'INVOICE_DATE' },
{ label: 'Due Date', value: 'INVOICE_DUE_DATE' },
{ label: 'Number', value: 'INVOICE_NUMBER' },
{ label: 'Ref Number', value: 'INVOICE_REF_NUMBER' },
...invoiceFields.value.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (props.fields.includes('estimate')) {
fieldList.value.push({
label: 'Estimate',
fields: [
{ label: 'Date', value: 'ESTIMATE_DATE' },
{ label: 'Expiry Date', value: 'ESTIMATE_EXPIRY_DATE' },
{ label: 'Number', value: 'ESTIMATE_NUMBER' },
{ label: 'Ref Number', value: 'ESTIMATE_REF_NUMBER' },
...estimateFields.value.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (props.fields.includes('payment')) {
fieldList.value.push({
label: 'Payment',
fields: [
{ label: 'Date', value: 'PAYMENT_DATE' },
{ label: 'Number', value: 'PAYMENT_NUMBER' },
{ label: 'Mode', value: 'PAYMENT_MODE' },
{ label: 'Amount', value: 'PAYMENT_AMOUNT' },
...paymentFields.value.map((i) => ({
label: i.label,
value: i.slug,
})),
],
})
}
if (props.fields.includes('company')) {
fieldList.value.push({
label: 'Company',
fields: [
{ label: 'Company Name', value: 'COMPANY_NAME' },
{ label: 'Country', value: 'COMPANY_COUNTRY' },
{ label: 'State', value: 'COMPANY_STATE' },
{ label: 'City', value: 'COMPANY_CITY' },
{ label: 'Address Street 1', value: 'COMPANY_ADDRESS_STREET_1' },
{ label: 'Address Street 2', value: 'COMPANY_ADDRESS_STREET_2' },
{ label: 'Phone', value: 'COMPANY_PHONE' },
{ label: 'Zip Code', value: 'COMPANY_ZIP_CODE' },
{ label: 'Vat Id', value: 'COMPANY_VAT' },
{ label: 'Tax Id', value: 'COMPANY_TAX' },
],
})
}
}
getFields()
</script>
<template>
<BaseContentPlaceholders v-if="contentLoading">
<BaseContentPlaceholdersBox
:rounded="true"
class="w-full"
style="height: 200px"
/>
</BaseContentPlaceholders>
<div v-else class="relative">
<div class="absolute bottom-0 right-0 z-10">
<BaseDropdown
:close-on-select="true"
max-height="220"
position="top-end"
width-class="w-92"
class="mb-2"
>
<template #activator>
<BaseButton type="button" variant="primary-outline" class="mr-4">
{{ $t('settings.customization.insert_fields') }}
<template #left="slotProps">
<BaseIcon name="PlusSmIcon" :class="slotProps.class" />
</template>
</BaseButton>
</template>
<div class="flex p-2">
<ul v-for="(type, index) in fieldList" :key="index" class="list-none">
<li class="mb-1 ml-2 text-xs font-semibold text-muted uppercase">
{{ type.label }}
</li>
<li
v-for="(field, fieldIndex) in type.fields"
:key="fieldIndex"
class="
w-48
text-sm
font-normal
cursor-pointer
hover:bg-hover-strong
rounded
ml-1
py-0.5
"
@click="value += `{${field.value}}`"
>
<div class="flex pl-1">
<BaseIcon
name="ChevronDoubleRightIcon"
class="h-3 mt-1 mr-2 text-subtle"
/>
{{ field.label }}
</div>
</li>
</ul>
</div>
</BaseDropdown>
</div>
<BaseEditor v-model="value" />
</div>
</template>