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