feat: add InvoiceTemplatePicker split-pane component
This commit is contained in:
136
src/components/InvoiceTemplatePicker.vue
Normal file
136
src/components/InvoiceTemplatePicker.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import InvoicePreview from './InvoicePreview.vue'
|
||||
import {
|
||||
TEMPLATE_CATEGORIES,
|
||||
getTemplatesByCategory,
|
||||
getTemplateById,
|
||||
} from '../utils/invoiceTemplates'
|
||||
import type { InvoiceItem } from '../utils/invoicePdf'
|
||||
import type { BusinessInfo } from '../utils/invoicePdfRenderer'
|
||||
import type { Invoice } from '../stores/invoices'
|
||||
import type { Client } from '../stores/clients'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
invoice?: Invoice
|
||||
client?: Client | null
|
||||
items?: InvoiceItem[]
|
||||
businessInfo?: BusinessInfo
|
||||
}>(),
|
||||
{
|
||||
invoice: undefined,
|
||||
client: undefined,
|
||||
items: undefined,
|
||||
businessInfo: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default sample data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const defaultInvoice: Invoice = {
|
||||
client_id: 0,
|
||||
invoice_number: 'INV-2026-001',
|
||||
date: '2026-02-18',
|
||||
due_date: '2026-03-18',
|
||||
subtotal: 8800,
|
||||
tax_rate: 10,
|
||||
tax_amount: 880,
|
||||
discount: 0,
|
||||
total: 9680,
|
||||
notes: 'Payment due within 30 days. Thank you for your business!',
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
const defaultClient: Client = {
|
||||
id: 1,
|
||||
name: 'Acme Corporation',
|
||||
email: 'billing@acme.com',
|
||||
address: '123 Business Ave\nSuite 100\nNew York, NY 10001',
|
||||
}
|
||||
|
||||
const defaultBusinessInfo: BusinessInfo = {
|
||||
name: 'Your Business Name',
|
||||
address: '456 Creative St, Design City',
|
||||
email: 'hello@business.com',
|
||||
phone: '(555) 123-4567',
|
||||
logo: '',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Computed
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const selectedTemplate = computed(() => getTemplateById(props.modelValue))
|
||||
|
||||
const previewInvoice = computed(() => props.invoice ?? defaultInvoice)
|
||||
|
||||
const previewClient = computed(() =>
|
||||
props.client !== undefined ? props.client : defaultClient,
|
||||
)
|
||||
|
||||
const previewItems = computed(() =>
|
||||
props.items && props.items.length > 0 ? props.items : [],
|
||||
)
|
||||
|
||||
const previewBusinessInfo = computed(() => props.businessInfo ?? defaultBusinessInfo)
|
||||
|
||||
function selectTemplate(id: string) {
|
||||
emit('update:modelValue', id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex border border-border-subtle rounded-lg overflow-hidden"
|
||||
style="height: 480px"
|
||||
>
|
||||
<!-- Left panel: Template list -->
|
||||
<div class="w-[30%] border-r border-border-subtle overflow-y-auto bg-bg-surface">
|
||||
<div v-for="cat in TEMPLATE_CATEGORIES" :key="cat.id">
|
||||
<div
|
||||
class="text-[0.5625rem] text-text-tertiary uppercase tracking-[0.1em] font-medium px-3 pt-3 pb-1"
|
||||
>
|
||||
{{ cat.label }}
|
||||
</div>
|
||||
<button
|
||||
v-for="tmpl in getTemplatesByCategory(cat.id)"
|
||||
:key="tmpl.id"
|
||||
class="w-full flex items-center gap-2 px-3 py-1.5 text-[0.75rem] transition-colors"
|
||||
:class="
|
||||
tmpl.id === modelValue
|
||||
? 'bg-accent/10 text-accent-text'
|
||||
: 'text-text-secondary hover:bg-bg-elevated hover:text-text-primary'
|
||||
"
|
||||
@click="selectTemplate(tmpl.id)"
|
||||
>
|
||||
<span
|
||||
class="w-2.5 h-2.5 rounded-full shrink-0 border border-black/10"
|
||||
:style="{ backgroundColor: tmpl.colors.primary }"
|
||||
/>
|
||||
<span class="truncate">{{ tmpl.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right panel: Live preview -->
|
||||
<div class="w-[70%] bg-bg-inset p-4 flex items-start justify-center overflow-y-auto">
|
||||
<div class="w-full max-w-sm">
|
||||
<InvoicePreview
|
||||
:template="selectedTemplate"
|
||||
:invoice="previewInvoice"
|
||||
:client="previewClient"
|
||||
:items="previewItems"
|
||||
:business-info="previewBusinessInfo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user