feat: add InvoicePreview.vue with all 7 header styles and 5 table styles
This commit is contained in:
723
src/components/InvoicePreview.vue
Normal file
723
src/components/InvoicePreview.vue
Normal file
@@ -0,0 +1,723 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { InvoiceTemplateConfig } from '../utils/invoiceTemplates'
|
||||||
|
import type { Invoice } from '../stores/invoices'
|
||||||
|
import type { Client } from '../stores/clients'
|
||||||
|
import type { InvoiceItem } from '../utils/invoicePdf'
|
||||||
|
import type { BusinessInfo } from '../utils/invoicePdfRenderer'
|
||||||
|
import { formatCurrency, formatDate } from '../utils/locale'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
template: InvoiceTemplateConfig
|
||||||
|
invoice: Invoice
|
||||||
|
client: Client | null
|
||||||
|
items: InvoiceItem[]
|
||||||
|
businessInfo?: BusinessInfo
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sampleItems: InvoiceItem[] = [
|
||||||
|
{ description: 'Web Design & Development', quantity: 40, rate: 150, amount: 6000 },
|
||||||
|
{ description: 'UI/UX Consultation', quantity: 8, rate: 200, amount: 1600 },
|
||||||
|
{ description: 'Project Management', quantity: 12, rate: 100, amount: 1200 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const displayItems = computed(() =>
|
||||||
|
props.items.length > 0 ? props.items : sampleItems,
|
||||||
|
)
|
||||||
|
|
||||||
|
const config = computed(() => props.template)
|
||||||
|
|
||||||
|
const clientName = computed(() => props.client?.name || 'N/A')
|
||||||
|
const clientEmail = computed(() => props.client?.email || '')
|
||||||
|
const clientAddressLines = computed(() =>
|
||||||
|
props.client?.address ? props.client.address.split('\n') : [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const biz = computed(() => props.businessInfo)
|
||||||
|
const bizAddressLines = computed(() =>
|
||||||
|
biz.value?.address ? biz.value.address.split('\n') : [],
|
||||||
|
)
|
||||||
|
|
||||||
|
const numberFont = computed(() =>
|
||||||
|
config.value.typography.numberStyle === 'monospace-feel'
|
||||||
|
? "'Courier New', Courier, monospace"
|
||||||
|
: 'inherit',
|
||||||
|
)
|
||||||
|
|
||||||
|
const sideOffset = computed(() =>
|
||||||
|
config.value.layout.headerStyle === 'sidebar'
|
||||||
|
? `${config.value.decorative.sidebarWidth + 6}%`
|
||||||
|
: '0px',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Divider style helper
|
||||||
|
const dividerBorder = computed(() => {
|
||||||
|
if (!config.value.layout.showDividers || config.value.layout.dividerStyle === 'none') return 'none'
|
||||||
|
switch (config.value.layout.dividerStyle) {
|
||||||
|
case 'thin': return `1px solid ${config.value.colors.tableBorder}`
|
||||||
|
case 'double': return `3px double ${config.value.colors.tableBorder}`
|
||||||
|
case 'thick': return `2px solid ${config.value.colors.primary}`
|
||||||
|
default: return 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const showDivider = computed(() =>
|
||||||
|
config.value.layout.showDividers && config.value.layout.dividerStyle !== 'none',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logo position style
|
||||||
|
const logoContainerStyle = computed(() => {
|
||||||
|
const pos = config.value.layout.logoPosition
|
||||||
|
const base: Record<string, string> = { marginBottom: '2%' }
|
||||||
|
if (pos === 'top-center') base.textAlign = 'center'
|
||||||
|
else if (pos === 'top-right') base.textAlign = 'right'
|
||||||
|
else base.textAlign = 'left'
|
||||||
|
return base
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Outer A4 container -->
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
backgroundColor: config.colors.background,
|
||||||
|
color: config.colors.bodyText,
|
||||||
|
fontFamily: '\'Helvetica Neue\', Helvetica, Arial, sans-serif',
|
||||||
|
position: 'relative',
|
||||||
|
aspectRatio: '8.5 / 11',
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: '4px',
|
||||||
|
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Background tint -->
|
||||||
|
<div
|
||||||
|
v-if="config.decorative.backgroundTint && config.decorative.backgroundTintColor"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
backgroundColor: config.decorative.backgroundTintColor,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Decorative: Sidebar -->
|
||||||
|
<div
|
||||||
|
v-if="config.decorative.sidebarWidth > 0"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
bottom: '0',
|
||||||
|
width: config.decorative.sidebarWidth + '%',
|
||||||
|
backgroundColor: config.decorative.sidebarColor,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Decorative: Corner block -->
|
||||||
|
<div
|
||||||
|
v-if="config.decorative.cornerShape === 'colored-block'"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '28%',
|
||||||
|
height: '20%',
|
||||||
|
backgroundColor: config.colors.primary,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Decorative: Triangle (SVG in top-right) -->
|
||||||
|
<svg
|
||||||
|
v-if="config.decorative.cornerShape === 'triangle'"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
right: '0',
|
||||||
|
width: '28%',
|
||||||
|
height: '28%',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<polygon points="100,0 100,100 0,0" :fill="config.colors.primary" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Decorative: Diagonal -->
|
||||||
|
<svg
|
||||||
|
v-if="config.decorative.cornerShape === 'diagonal'"
|
||||||
|
:style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '100%',
|
||||||
|
height: '20%',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}"
|
||||||
|
viewBox="0 0 210 60"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<polygon points="0,0 210,0 0,60" :fill="config.colors.primary" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Content area with padding -->
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
padding: '5%',
|
||||||
|
fontSize: config.typography.bodySize * 0.85 + 'px',
|
||||||
|
lineHeight: '1.4',
|
||||||
|
height: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- ===== HEADER: MINIMAL ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'minimal'">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div v-if="biz?.logo" :style="logoContainerStyle">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '5%', maxWidth: '20%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Business info -->
|
||||||
|
<div v-if="biz?.name" :style="{ marginBottom: '2%' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', fontSize: config.typography.headerSize * 0.85 + 'px', color: config.colors.headerText }">
|
||||||
|
{{ biz.name }}
|
||||||
|
</div>
|
||||||
|
<div v-for="(line, i) in bizAddressLines" :key="'ba' + i" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ line }}</div>
|
||||||
|
<div v-if="biz.email" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz.phone" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.phone }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div :style="{
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
color: config.colors.primary,
|
||||||
|
marginBottom: '1%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Accent line -->
|
||||||
|
<div :style="{ width: '15%', height: '2px', backgroundColor: config.colors.primary, marginBottom: '3%' }" />
|
||||||
|
|
||||||
|
<!-- Details + Bill To -->
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '3%' }">
|
||||||
|
<div>
|
||||||
|
<div>Invoice #: {{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due Date: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
<div :style="{ textAlign: 'right' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px' }">Bill To:</div>
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'ca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div v-if="showDivider" :style="{ borderBottom: dividerBorder, marginBottom: '3%' }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== HEADER: FULL-WIDTH ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'full-width'">
|
||||||
|
<!-- Full-width banner (negative margins to fill) -->
|
||||||
|
<div :style="{
|
||||||
|
margin: '-5% -5% 0 -5%',
|
||||||
|
padding: '4% 5%',
|
||||||
|
backgroundColor: config.colors.headerBg,
|
||||||
|
color: config.colors.headerText,
|
||||||
|
marginBottom: '3%',
|
||||||
|
}">
|
||||||
|
<!-- Logo inside banner -->
|
||||||
|
<div v-if="biz?.logo" :style="logoContainerStyle">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '5%', maxWidth: '20%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between' }">
|
||||||
|
<div>
|
||||||
|
<div :style="{
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
marginBottom: '2%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
<div :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">
|
||||||
|
<div>#{{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="biz?.name && config.layout.logoPosition !== 'top-right'" :style="{ textAlign: 'right', fontSize: config.typography.bodySize * 0.8 + 'px' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', fontSize: config.typography.bodySize * 0.9 + 'px' }">{{ biz.name }}</div>
|
||||||
|
<div v-if="biz.email">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz.phone">{{ biz.phone }}</div>
|
||||||
|
<div v-for="(line, i) in bizAddressLines" :key="'fba' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bill To below banner -->
|
||||||
|
<div :style="{ marginBottom: '3%' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px', color: config.colors.bodyText }">Bill To:</div>
|
||||||
|
<div :style="{ color: config.colors.bodyText }">
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'fca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== HEADER: SPLIT ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'split'">
|
||||||
|
<div :style="{
|
||||||
|
display: 'flex',
|
||||||
|
margin: '-5% -5% 0 -5%',
|
||||||
|
marginBottom: '3%',
|
||||||
|
}">
|
||||||
|
<!-- Left half: colored -->
|
||||||
|
<div :style="{
|
||||||
|
width: '50%',
|
||||||
|
backgroundColor: config.colors.headerBg,
|
||||||
|
color: config.colors.headerText,
|
||||||
|
padding: '4% 5%',
|
||||||
|
}">
|
||||||
|
<div :style="{
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
marginBottom: '3%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
<div :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">
|
||||||
|
<div v-if="biz?.name" :style="{ fontWeight: 'bold' }">{{ biz.name }}</div>
|
||||||
|
<div v-if="biz?.email">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz?.phone">{{ biz.phone }}</div>
|
||||||
|
<div v-for="(line, i) in bizAddressLines" :key="'sba' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
<!-- Logo in left half -->
|
||||||
|
<div v-if="biz?.logo" :style="{ marginTop: '3%' }">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '14%', maxWidth: '40%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Right half: light -->
|
||||||
|
<div :style="{
|
||||||
|
width: '50%',
|
||||||
|
padding: '4% 5%',
|
||||||
|
color: config.colors.bodyText,
|
||||||
|
}">
|
||||||
|
<div :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">
|
||||||
|
<div>Invoice #: {{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due Date: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bill To below split -->
|
||||||
|
<div :style="{ marginBottom: '3%' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px' }">Bill To:</div>
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'sca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== HEADER: SIDEBAR ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'sidebar'">
|
||||||
|
<div :style="{ paddingLeft: sideOffset }">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div v-if="biz?.logo" :style="logoContainerStyle">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '5%', maxWidth: '20%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Business info -->
|
||||||
|
<div v-if="biz?.name" :style="{ marginBottom: '2%' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', fontSize: config.typography.headerSize * 0.85 + 'px', color: config.colors.headerText }">
|
||||||
|
{{ biz.name }}
|
||||||
|
</div>
|
||||||
|
<div v-for="(line, i) in bizAddressLines" :key="'sbba' + i" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ line }}</div>
|
||||||
|
<div v-if="biz?.email" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz?.phone" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.phone }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div :style="{
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
color: config.colors.primary,
|
||||||
|
marginBottom: '1%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details + Bill To -->
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '3%' }">
|
||||||
|
<div>
|
||||||
|
<div>Invoice #: {{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due Date: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
<div :style="{ textAlign: 'right' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px' }">Bill To:</div>
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'sbca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div v-if="showDivider" :style="{ borderBottom: dividerBorder, marginBottom: '3%' }" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== HEADER: GRADIENT ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'gradient'">
|
||||||
|
<div :style="{
|
||||||
|
margin: '-5% -5% 0 -5%',
|
||||||
|
padding: '4% 5%',
|
||||||
|
background: `linear-gradient(to right, ${config.decorative.gradientFrom || config.colors.headerBg}, ${config.decorative.gradientTo || config.colors.primary})`,
|
||||||
|
color: config.colors.headerText,
|
||||||
|
marginBottom: '3%',
|
||||||
|
}">
|
||||||
|
<!-- Logo inside gradient -->
|
||||||
|
<div v-if="biz?.logo" :style="logoContainerStyle">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '5%', maxWidth: '20%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between' }">
|
||||||
|
<div>
|
||||||
|
<div :style="{
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
marginBottom: '2%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
<div :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">
|
||||||
|
<div>#{{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="biz?.name && config.layout.logoPosition !== 'top-right'" :style="{ textAlign: 'right', fontSize: config.typography.bodySize * 0.8 + 'px' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', fontSize: config.typography.bodySize * 0.9 + 'px' }">{{ biz.name }}</div>
|
||||||
|
<div v-if="biz.email">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz.phone">{{ biz.phone }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bill To below gradient -->
|
||||||
|
<div :style="{ marginBottom: '3%', color: config.colors.bodyText }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px' }">Bill To:</div>
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'gca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== HEADER: GEOMETRIC ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'geometric'">
|
||||||
|
<!-- Logo (top-left to avoid triangle) -->
|
||||||
|
<div v-if="biz?.logo" :style="logoContainerStyle">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '5%', maxWidth: '20%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Business info -->
|
||||||
|
<div v-if="biz?.name" :style="{ marginBottom: '2%', maxWidth: '60%' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', fontSize: config.typography.headerSize * 0.85 + 'px', color: config.colors.headerText }">
|
||||||
|
{{ biz.name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="biz.email" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz.phone" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.phone }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div :style="{
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
color: config.colors.primary,
|
||||||
|
marginBottom: '1%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details + Bill To -->
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '3%' }">
|
||||||
|
<div>
|
||||||
|
<div>Invoice #: {{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due Date: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
<div :style="{ textAlign: 'right' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px' }">Bill To:</div>
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'geca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div v-if="showDivider" :style="{ borderBottom: dividerBorder, marginBottom: '3%' }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== HEADER: CENTERED ===== -->
|
||||||
|
<template v-if="config.layout.headerStyle === 'centered'">
|
||||||
|
<!-- Logo centered -->
|
||||||
|
<div v-if="biz?.logo" :style="{ textAlign: 'center', marginBottom: '2%' }">
|
||||||
|
<img :src="biz.logo" :style="{ maxHeight: '5%', maxWidth: '20%', objectFit: 'contain' }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top divider line -->
|
||||||
|
<div :style="{ borderBottom: '1px solid ' + config.colors.tableBorder, marginBottom: '2%' }" />
|
||||||
|
|
||||||
|
<!-- Title centered -->
|
||||||
|
<div :style="{
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: config.typography.titleWeight,
|
||||||
|
fontSize: config.typography.titleSize * 0.85 + 'px',
|
||||||
|
color: config.colors.primary,
|
||||||
|
marginBottom: '1%',
|
||||||
|
}">
|
||||||
|
INVOICE
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Business name centered -->
|
||||||
|
<div v-if="biz?.name" :style="{ textAlign: 'center', marginBottom: '2%' }">
|
||||||
|
<div :style="{ fontSize: config.typography.headerSize * 0.85 + 'px', color: config.colors.headerText }">
|
||||||
|
{{ biz.name }}
|
||||||
|
</div>
|
||||||
|
<div v-for="(line, i) in bizAddressLines" :key="'cba' + i" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ line }}</div>
|
||||||
|
<div v-if="biz.email" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.email }}</div>
|
||||||
|
<div v-if="biz.phone" :style="{ fontSize: config.typography.bodySize * 0.8 + 'px' }">{{ biz.phone }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom divider line -->
|
||||||
|
<div :style="{ borderBottom: '1px solid ' + config.colors.tableBorder, marginBottom: '2%' }" />
|
||||||
|
|
||||||
|
<!-- Details + Bill To side by side -->
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '3%' }">
|
||||||
|
<div>
|
||||||
|
<div>Invoice #: {{ invoice.invoice_number }}</div>
|
||||||
|
<div>Date: {{ formatDate(invoice.date) }}</div>
|
||||||
|
<div v-if="invoice.due_date">Due Date: {{ formatDate(invoice.due_date) }}</div>
|
||||||
|
<div>Status: {{ invoice.status }}</div>
|
||||||
|
</div>
|
||||||
|
<div :style="{ textAlign: 'right' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '2px' }">Bill To:</div>
|
||||||
|
<div>{{ clientName }}</div>
|
||||||
|
<div v-if="clientEmail">{{ clientEmail }}</div>
|
||||||
|
<div v-for="(line, i) in clientAddressLines" :key="'cca' + i">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<div v-if="showDivider" :style="{ borderBottom: dividerBorder, marginBottom: '3%' }" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ===== TABLE ===== -->
|
||||||
|
<div :style="{ paddingLeft: config.layout.headerStyle === 'sidebar' ? sideOffset : '0' }">
|
||||||
|
<table
|
||||||
|
:style="{
|
||||||
|
width: '100%',
|
||||||
|
borderCollapse: 'collapse',
|
||||||
|
fontSize: config.typography.bodySize * 0.85 + 'px',
|
||||||
|
marginBottom: '4%',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr
|
||||||
|
:style="{
|
||||||
|
backgroundColor: config.colors.tableHeaderBg,
|
||||||
|
color: config.colors.tableHeaderText,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<th
|
||||||
|
:style="{
|
||||||
|
textAlign: 'left',
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
width: '50%',
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
:style="{
|
||||||
|
textAlign: 'left',
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
width: '15%',
|
||||||
|
fontFamily: numberFont,
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Qty
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
:style="{
|
||||||
|
textAlign: 'left',
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
width: '17.5%',
|
||||||
|
fontFamily: numberFont,
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Rate
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
:style="{
|
||||||
|
textAlign: 'left',
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
width: '17.5%',
|
||||||
|
fontFamily: numberFont,
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
Amount
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(item, idx) in displayItems"
|
||||||
|
:key="idx"
|
||||||
|
:style="{
|
||||||
|
backgroundColor:
|
||||||
|
config.layout.tableStyle === 'striped' && idx % 2 === 1
|
||||||
|
? config.colors.tableRowAlt
|
||||||
|
: config.layout.tableStyle === 'colored-sections'
|
||||||
|
? idx % 2 === 1 ? config.colors.tableRowAlt : config.colors.background
|
||||||
|
: 'transparent',
|
||||||
|
borderBottom:
|
||||||
|
config.layout.tableStyle === 'minimal-lines'
|
||||||
|
? '1px solid ' + config.colors.tableBorder
|
||||||
|
: 'none',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
:style="{
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.description }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
:style="{
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontFamily: numberFont,
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.quantity }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
:style="{
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontFamily: numberFont,
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ formatCurrency(item.rate) }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
:style="{
|
||||||
|
padding: '1.5% 2%',
|
||||||
|
fontFamily: numberFont,
|
||||||
|
...(config.layout.tableStyle === 'bordered'
|
||||||
|
? { border: '1px solid ' + config.colors.tableBorder }
|
||||||
|
: {}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ formatCurrency(item.amount) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ===== TOTALS ===== -->
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: config.layout.totalsPosition === 'center' ? 'center' : 'flex-end',
|
||||||
|
marginBottom: '4%',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div :style="{ width: '40%', fontSize: config.typography.bodySize * 0.9 + 'px' }">
|
||||||
|
<!-- Subtotal -->
|
||||||
|
<div :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '1%' }">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span :style="{ fontFamily: numberFont }">{{ formatCurrency(invoice.subtotal) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tax -->
|
||||||
|
<div v-if="invoice.tax_rate > 0" :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '1%' }">
|
||||||
|
<span>Tax ({{ invoice.tax_rate }}%):</span>
|
||||||
|
<span :style="{ fontFamily: numberFont }">{{ formatCurrency(invoice.tax_amount) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discount -->
|
||||||
|
<div v-if="invoice.discount > 0" :style="{ display: 'flex', justifyContent: 'space-between', marginBottom: '1%' }">
|
||||||
|
<span>Discount:</span>
|
||||||
|
<span :style="{ fontFamily: numberFont }">-{{ formatCurrency(invoice.discount) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<div :style="{ borderBottom: '1px solid ' + config.colors.tableBorder, margin: '2% 0' }" />
|
||||||
|
|
||||||
|
<!-- Total -->
|
||||||
|
<div :style="{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: config.typography.bodySize * 1.1 + 'px',
|
||||||
|
color: config.colors.totalHighlight,
|
||||||
|
}">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span :style="{ fontFamily: numberFont }">{{ formatCurrency(invoice.total) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== NOTES ===== -->
|
||||||
|
<div v-if="invoice.notes" :style="{ borderTop: '1px solid ' + config.colors.tableBorder, paddingTop: '2%' }">
|
||||||
|
<div :style="{ fontWeight: 'bold', marginBottom: '1%' }">Notes:</div>
|
||||||
|
<div :style="{ fontSize: config.typography.bodySize * 0.8 + 'px', whiteSpace: 'pre-wrap' }">{{ invoice.notes }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user