first release of custome invoice designer
This commit is contained in:
parent
29625b7afa
commit
51d68b9874
418
webUI/src/components/InvoiceElements.vue
Normal file
418
webUI/src/components/InvoiceElements.vue
Normal file
|
@ -0,0 +1,418 @@
|
|||
<template>
|
||||
<div class="invoice-element" :class="elementType">
|
||||
<div class="element-header">
|
||||
<v-icon>{{ getElementIcon() }}</v-icon>
|
||||
<span>{{ getElementTitle() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="element-content">
|
||||
<!-- Business Info Element -->
|
||||
<div v-if="elementType === 'business-info'" class="business-info">
|
||||
<div class="info-row">
|
||||
<span class="label">نام:</span>
|
||||
<span class="value">{{ businessData.name || 'نام کسبوکار' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">تلفن:</span>
|
||||
<span class="value">{{ businessData.tel || 'تلفن کسبوکار' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">آدرس:</span>
|
||||
<span class="value">{{ businessData.address || 'آدرس کسبوکار' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Customer Info Element -->
|
||||
<div v-else-if="elementType === 'customer-info'" class="customer-info">
|
||||
<div class="info-row">
|
||||
<span class="label">نام:</span>
|
||||
<span class="value">{{ customerData.name || 'نام مشتری' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">موبایل:</span>
|
||||
<span class="value">{{ customerData.mobile || 'موبایل مشتری' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">آدرس:</span>
|
||||
<span class="value">{{ customerData.address || 'آدرس مشتری' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Header Element -->
|
||||
<div v-else-if="elementType === 'invoice-header'" class="invoice-header">
|
||||
<div class="header-row">
|
||||
<div class="logo-section">
|
||||
<div class="logo-placeholder">لوگو</div>
|
||||
</div>
|
||||
<div class="title-section">
|
||||
<h2>صورتحساب فروش کالا و خدمات</h2>
|
||||
</div>
|
||||
<div class="info-section">
|
||||
<div class="info-item">
|
||||
<span class="label">تاریخ:</span>
|
||||
<span class="value">{{ invoiceData.date || 'تاریخ فاکتور' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">شماره:</span>
|
||||
<span class="value">{{ invoiceData.code || 'شماره فاکتور' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Table Element -->
|
||||
<div v-else-if="elementType === 'items-table'" class="items-table">
|
||||
<table class="invoice-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ردیف</th>
|
||||
<th>کالا/خدمات</th>
|
||||
<th>شرح</th>
|
||||
<th>تعداد</th>
|
||||
<th>فی واحد</th>
|
||||
<th>تخفیف</th>
|
||||
<th>مالیات</th>
|
||||
<th>مبلغ کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in sampleItems" :key="index">
|
||||
<td>{{ index + 1 }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ formatNumber(item.unitPrice) }}</td>
|
||||
<td>{{ item.discount }}%</td>
|
||||
<td>{{ formatNumber(item.tax) }}</td>
|
||||
<td>{{ formatNumber(item.total) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Totals Section Element -->
|
||||
<div v-else-if="elementType === 'totals-section'" class="totals-section">
|
||||
<div class="totals-row">
|
||||
<span class="label">جمع کل:</span>
|
||||
<span class="value">{{ formatNumber(totalsData.subtotal) }}</span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span class="label">تخفیف:</span>
|
||||
<span class="value">{{ formatNumber(totalsData.discount) }}</span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span class="label">مالیات:</span>
|
||||
<span class="value">{{ formatNumber(totalsData.tax) }}</span>
|
||||
</div>
|
||||
<div class="totals-row total">
|
||||
<span class="label">مبلغ نهایی:</span>
|
||||
<span class="value">{{ formatNumber(totalsData.final) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section Element -->
|
||||
<div v-else-if="elementType === 'signature-section'" class="signature-section">
|
||||
<div class="signature-row">
|
||||
<div class="signature-box">
|
||||
<div class="signature-placeholder">مهر و امضا خریدار</div>
|
||||
</div>
|
||||
<div class="signature-box">
|
||||
<div class="signature-placeholder">مهر و امضا فروشنده</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Element -->
|
||||
<div v-else class="default-element">
|
||||
{{ content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InvoiceElement',
|
||||
props: {
|
||||
elementType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
elementData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Sample data for preview
|
||||
businessData: {
|
||||
name: 'شرکت نمونه',
|
||||
tel: '021-12345678',
|
||||
address: 'تهران، خیابان نمونه'
|
||||
},
|
||||
customerData: {
|
||||
name: 'مشتری نمونه',
|
||||
mobile: '09123456789',
|
||||
address: 'آدرس مشتری'
|
||||
},
|
||||
invoiceData: {
|
||||
date: '1402/12/15',
|
||||
code: 'INV-001'
|
||||
},
|
||||
sampleItems: [
|
||||
{
|
||||
name: 'کالای نمونه 1',
|
||||
description: 'توضیحات کالا',
|
||||
quantity: 2,
|
||||
unitPrice: 100000,
|
||||
discount: 10,
|
||||
tax: 9000,
|
||||
total: 189000
|
||||
},
|
||||
{
|
||||
name: 'کالای نمونه 2',
|
||||
description: 'توضیحات کالا',
|
||||
quantity: 1,
|
||||
unitPrice: 50000,
|
||||
discount: 0,
|
||||
tax: 4500,
|
||||
total: 54500
|
||||
}
|
||||
],
|
||||
totalsData: {
|
||||
subtotal: 250000,
|
||||
discount: 20000,
|
||||
tax: 13500,
|
||||
final: 243500
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getElementIcon() {
|
||||
const icons = {
|
||||
'business-info': 'mdi-domain',
|
||||
'customer-info': 'mdi-account',
|
||||
'invoice-header': 'mdi-file-document',
|
||||
'items-table': 'mdi-format-list-bulleted',
|
||||
'totals-section': 'mdi-calculator',
|
||||
'signature-section': 'mdi-pen'
|
||||
}
|
||||
return icons[this.elementType] || 'mdi-view-grid'
|
||||
},
|
||||
|
||||
getElementTitle() {
|
||||
const titles = {
|
||||
'business-info': 'اطلاعات کسبوکار',
|
||||
'customer-info': 'اطلاعات مشتری',
|
||||
'invoice-header': 'سربرگ فاکتور',
|
||||
'items-table': 'جدول اقلام',
|
||||
'totals-section': 'جمعها',
|
||||
'signature-section': 'امضا'
|
||||
}
|
||||
return titles[this.elementType] || 'عنصر'
|
||||
},
|
||||
|
||||
formatNumber(value) {
|
||||
return new Intl.NumberFormat('fa-IR').format(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.invoice-element {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.element-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.element-content {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Business Info Styles */
|
||||
.business-info .info-row,
|
||||
.customer-info .info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.info-row .label {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-row .value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Invoice Header Styles */
|
||||
.invoice-header .header-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-section .logo-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #f0f0f0;
|
||||
border: 2px dashed #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.title-section h2 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-section .info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Items Table Styles */
|
||||
.items-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.invoice-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.invoice-table th,
|
||||
.invoice-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invoice-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.invoice-table td {
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Totals Section Styles */
|
||||
.totals-section .totals-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.totals-row .label {
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.totals-row .value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.totals-row.total {
|
||||
border-top: 2px solid #333;
|
||||
padding-top: 8px;
|
||||
margin-top: 8px;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Signature Section Styles */
|
||||
.signature-section .signature-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.signature-box {
|
||||
height: 80px;
|
||||
border: 2px dashed #ccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.signature-placeholder {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Default Element Styles */
|
||||
.default-element {
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.invoice-header .header-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.signature-section .signature-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.invoice-table {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.invoice-table th,
|
||||
.invoice-table td {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
295
webUI/src/components/LivePreview.vue
Normal file
295
webUI/src/components/LivePreview.vue
Normal file
|
@ -0,0 +1,295 @@
|
|||
<template>
|
||||
<div class="live-preview">
|
||||
<v-dialog v-model="showPreview" max-width="1200px" fullscreen>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-btn icon @click="showPreview = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>پیشنمایش قالب</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="printPreview" prepend-icon="mdi-printer">چاپ</v-btn>
|
||||
<v-btn @click="downloadPDF" prepend-icon="mdi-file-pdf-box">دانلود PDF</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card-text class="preview-content">
|
||||
<div class="preview-toolbar">
|
||||
<v-select
|
||||
v-model="selectedPaper"
|
||||
label="اندازه کاغذ"
|
||||
:items="paperSizes"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
style="max-width: 200px"
|
||||
/>
|
||||
<v-switch
|
||||
v-model="showGrid"
|
||||
label="نمایش گرید"
|
||||
color="primary"
|
||||
/>
|
||||
<v-switch
|
||||
v-model="showMargins"
|
||||
label="نمایش حاشیه"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preview-container" :class="{ 'show-grid': showGrid, 'show-margins': showMargins }">
|
||||
<div
|
||||
class="preview-frame"
|
||||
:class="selectedPaper"
|
||||
v-html="renderedTemplate"
|
||||
></div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LivePreview',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
templateCode: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'print', 'download-pdf'],
|
||||
data() {
|
||||
return {
|
||||
selectedPaper: 'A4',
|
||||
showGrid: false,
|
||||
showMargins: true,
|
||||
paperSizes: [
|
||||
{ title: 'A4', value: 'A4' },
|
||||
{ title: 'A4 Landscape', value: 'A4-L' },
|
||||
{ title: 'A5', value: 'A5' },
|
||||
{ title: 'Letter', value: 'Letter' },
|
||||
{ title: 'Legal', value: 'Legal' }
|
||||
],
|
||||
sampleData: {
|
||||
business: {
|
||||
name: 'شرکت نمونه',
|
||||
tel: '021-12345678',
|
||||
mobile: '09123456789',
|
||||
address: 'تهران، خیابان نمونه، پلاک 123',
|
||||
shenasemeli: '1234567890',
|
||||
codeeghtesadi: '12345678901'
|
||||
},
|
||||
doc: {
|
||||
code: 'INV-001',
|
||||
date: '1402/12/15',
|
||||
amount: 1500000,
|
||||
money: { shortName: 'ریال' }
|
||||
},
|
||||
person: {
|
||||
name: 'مشتری نمونه',
|
||||
mobile: '09123456789',
|
||||
tel: '021-98765432',
|
||||
address: 'آدرس مشتری نمونه'
|
||||
},
|
||||
rows: [
|
||||
{
|
||||
commodity: { name: 'کالای نمونه 1', code: 'ITEM-001', unit: { name: 'عدد' } },
|
||||
commodityCount: 2,
|
||||
des: 'توضیحات کالای نمونه 1',
|
||||
bs: 500000,
|
||||
tax: 45000,
|
||||
discount: 50000,
|
||||
showPercentDiscount: false,
|
||||
discountPercent: 10
|
||||
},
|
||||
{
|
||||
commodity: { name: 'کالای نمونه 2', code: 'ITEM-002', unit: { name: 'عدد' } },
|
||||
commodityCount: 1,
|
||||
des: 'توضیحات کالای نمونه 2',
|
||||
bs: 300000,
|
||||
tax: 27000,
|
||||
discount: 0,
|
||||
showPercentDiscount: true,
|
||||
discountPercent: 0
|
||||
}
|
||||
],
|
||||
discount: 50000,
|
||||
transfer: 25000,
|
||||
note: 'یادداشت نمونه برای فاکتور'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPreview: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
renderedTemplate() {
|
||||
if (!this.templateCode) return '<div>قالب خالی است</div>'
|
||||
|
||||
try {
|
||||
// Simple template rendering - in production, use a proper Twig engine
|
||||
let rendered = this.templateCode
|
||||
|
||||
// Replace basic variables
|
||||
rendered = rendered.replace(/\{\{\s*business\.name\s*\}\}/g, this.sampleData.business.name)
|
||||
rendered = rendered.replace(/\{\{\s*business\.tel\s*\}\}/g, this.sampleData.business.tel)
|
||||
rendered = rendered.replace(/\{\{\s*business\.address\s*\}\}/g, this.sampleData.business.address)
|
||||
rendered = rendered.replace(/\{\{\s*doc\.code\s*\}\}/g, this.sampleData.doc.code)
|
||||
rendered = rendered.replace(/\{\{\s*doc\.date\s*\}\}/g, this.sampleData.doc.date)
|
||||
rendered = rendered.replace(/\{\{\s*person\.name\s*\}\}/g, this.sampleData.person.name)
|
||||
rendered = rendered.replace(/\{\{\s*person\.mobile\s*\}\}/g, this.sampleData.person.mobile)
|
||||
rendered = rendered.replace(/\{\{\s*discount\s*\}\}/g, this.sampleData.discount.toLocaleString('fa-IR'))
|
||||
rendered = rendered.replace(/\{\{\s*transfer\s*\}\}/g, this.sampleData.transfer.toLocaleString('fa-IR'))
|
||||
rendered = rendered.replace(/\{\{\s*note\s*\}\}/g, this.sampleData.note)
|
||||
|
||||
// Replace for loops
|
||||
const forLoopRegex = /\{%\s*for\s+(\w+)\s+in\s+rows\s*%\}([\s\S]*?)\{%\s*endfor\s*%\}/g
|
||||
rendered = rendered.replace(forLoopRegex, (match, variable, content) => {
|
||||
let result = ''
|
||||
this.sampleData.rows.forEach((row, index) => {
|
||||
let rowContent = content
|
||||
rowContent = rowContent.replace(/\{\{\s*loop\.index\s*\}\}/g, index + 1)
|
||||
rowContent = rowContent.replace(/\{\{\s*item\.commodity\.name\s*\}\}/g, row.commodity.name)
|
||||
rowContent = rowContent.replace(/\{\{\s*item\.commodityCount\s*\}\}/g, row.commodityCount)
|
||||
rowContent = rowContent.replace(/\{\{\s*item\.des\s*\}\}/g, row.des)
|
||||
rowContent = rowContent.replace(/\{\{\s*item\.bs\s*\}\}/g, row.bs.toLocaleString('fa-IR'))
|
||||
rowContent = rowContent.replace(/\{\{\s*item\.tax\s*\}\}/g, row.tax.toLocaleString('fa-IR'))
|
||||
rowContent = rowContent.replace(/\{\{\s*item\.discount\s*\}\}/g, row.discount.toLocaleString('fa-IR'))
|
||||
result += rowContent
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
// Replace if conditions
|
||||
rendered = rendered.replace(/\{%\s*if\s+person\s*%\}([\s\S]*?)\{%\s*endif\s*%\}/g, '$1')
|
||||
|
||||
return rendered
|
||||
} catch (error) {
|
||||
console.error('Template rendering error:', error)
|
||||
return `<div style="color: red; padding: 20px;">خطا در رندر قالب: ${error.message}</div>`
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
printPreview() {
|
||||
this.$emit('print', this.renderedTemplate)
|
||||
},
|
||||
|
||||
downloadPDF() {
|
||||
this.$emit('download-pdf', this.renderedTemplate)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.live-preview {
|
||||
/* Component styles */
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
background: white;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Paper sizes */
|
||||
.preview-frame.A4 {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
padding: 20mm;
|
||||
}
|
||||
|
||||
.preview-frame.A4-L {
|
||||
width: 297mm;
|
||||
min-height: 210mm;
|
||||
padding: 20mm;
|
||||
}
|
||||
|
||||
.preview-frame.A5 {
|
||||
width: 148mm;
|
||||
min-height: 210mm;
|
||||
padding: 15mm;
|
||||
}
|
||||
|
||||
.preview-frame.Letter {
|
||||
width: 216mm;
|
||||
min-height: 279mm;
|
||||
padding: 20mm;
|
||||
}
|
||||
|
||||
.preview-frame.Legal {
|
||||
width: 216mm;
|
||||
min-height: 356mm;
|
||||
padding: 20mm;
|
||||
}
|
||||
|
||||
/* Grid and margins */
|
||||
.preview-container.show-grid .preview-frame {
|
||||
background-image:
|
||||
linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px);
|
||||
background-size: 10mm 10mm;
|
||||
}
|
||||
|
||||
.preview-container.show-margins .preview-frame {
|
||||
border: 2px dashed #ccc;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.preview-frame.A4,
|
||||
.preview-frame.A4-L {
|
||||
transform: scale(0.8);
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.preview-frame.A4,
|
||||
.preview-frame.A4-L {
|
||||
transform: scale(0.6);
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.preview-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
196
webUI/src/components/TemplateDesigner-README.md
Normal file
196
webUI/src/components/TemplateDesigner-README.md
Normal file
|
@ -0,0 +1,196 @@
|
|||
# راهنمای سیستم طراحی قالب فاکتور
|
||||
|
||||
## مقدمه
|
||||
|
||||
سیستم طراحی قالب فاکتور یک ابزار قدرتمند برای ایجاد قالبهای سفارشی فاکتور است که شامل دو حالت اصلی میباشد:
|
||||
|
||||
1. **حالت کد نویسی**: ویرایشگر Monaco برای نوشتن کد HTML/Twig
|
||||
2. **حالت طراحی**: رابط drag & drop برای طراحی بصری
|
||||
|
||||
## ویژگیهای اصلی
|
||||
|
||||
### 🔧 حالت کد نویسی
|
||||
- ویرایشگر Monaco با syntax highlighting
|
||||
- پشتیبانی از HTML، CSS، و Twig
|
||||
- Auto-completion و IntelliSense
|
||||
- خطایابی کد
|
||||
|
||||
### 🎨 حالت طراحی
|
||||
- **Toolbox**: مجموعه عناصر آماده
|
||||
- **Canvas**: فضای طراحی با grid system
|
||||
- **Properties Panel**: تنظیمات المانها
|
||||
- **Drag & Drop**: کشیدن و رها کردن المانها
|
||||
|
||||
## عناصر موجود در Toolbox
|
||||
|
||||
### عناصر پایه
|
||||
- **کانتینر**: برای گروهبندی المانها
|
||||
- **متن**: برای اضافه کردن متن ساده
|
||||
- **عنوان**: برای عناوین مختلف
|
||||
- **جدول**: برای نمایش دادهها
|
||||
- **تصویر**: برای اضافه کردن لوگو یا تصاویر
|
||||
- **خط جداکننده**: برای تفکیک بخشها
|
||||
|
||||
### عناصر خاص فاکتور
|
||||
- **اطلاعات کسبوکار**: نمایش اطلاعات فروشنده
|
||||
- **اطلاعات مشتری**: نمایش اطلاعات خریدار
|
||||
- **سربرگ فاکتور**: شامل لوگو، عنوان و شماره فاکتور
|
||||
- **جدول اقلام**: برای نمایش محصولات/خدمات
|
||||
- **جمعها**: نمایش محاسبات نهایی
|
||||
- **امضا**: بخش مهر و امضا
|
||||
|
||||
### متغیرها
|
||||
- متغیرهای Twig برای نمایش دادههای پویا
|
||||
- قابلیت فرمتبندی اعداد
|
||||
- پشتیبانی از شرطها و حلقهها
|
||||
|
||||
## نحوه استفاده
|
||||
|
||||
### شروع کار
|
||||
1. روی دکمه "طراحی" کلیک کنید
|
||||
2. از Toolbox المان مورد نظر را انتخاب کنید
|
||||
3. آن را روی Canvas بکشید و رها کنید
|
||||
4. روی المان کلیک کنید تا Properties Panel باز شود
|
||||
|
||||
### تنظیمات المان
|
||||
- **ویژگیهای پایه**: متن، کلاس CSS، شناسه
|
||||
- **موقعیت و اندازه**: تنظیم grid position و span
|
||||
- **استایلها**: فونت، رنگ، تراز متن
|
||||
- **تنظیمات خاص**: برای جداول و متغیرها
|
||||
|
||||
### کتابخانه قالبها
|
||||
- قالبهای آماده برای فروش، خرید و برگشت
|
||||
- قالبهای سفارشی
|
||||
- امکان import/export قالبها
|
||||
|
||||
### پیشنمایش
|
||||
- پیشنمایش زنده با دادههای نمونه
|
||||
- تنظیم اندازه کاغذ
|
||||
- نمایش گرید و حاشیهها
|
||||
- چاپ و دانلود PDF
|
||||
|
||||
## متغیرهای در دسترس
|
||||
|
||||
### اطلاعات کسبوکار
|
||||
- `{{ business.name }}`: نام کسبوکار
|
||||
- `{{ business.tel }}`: تلفن
|
||||
- `{{ business.address }}`: آدرس
|
||||
|
||||
### اطلاعات فاکتور
|
||||
- `{{ doc.code }}`: شماره فاکتور
|
||||
- `{{ doc.date }}`: تاریخ فاکتور
|
||||
- `{{ doc.amount }}`: مبلغ کل
|
||||
|
||||
### اطلاعات مشتری
|
||||
- `{{ person.name }}`: نام مشتری
|
||||
- `{{ person.mobile }}`: موبایل
|
||||
- `{{ person.address }}`: آدرس
|
||||
|
||||
### اقلام فاکتور
|
||||
```twig
|
||||
{% for item in rows %}
|
||||
{{ item.commodity.name }} - {{ item.commodityCount }}
|
||||
{{ item.bs | number_format(0, '.', ',') }}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
### جمعها
|
||||
- `{{ discount }}`: تخفیف
|
||||
- `{{ transfer }}`: هزینه ارسال
|
||||
- `{{ note }}`: یادداشت
|
||||
|
||||
## نکات مهم
|
||||
|
||||
### همگامسازی
|
||||
- تغییرات در حالت طراحی به صورت خودکار به کد تبدیل میشود
|
||||
- تغییرات در کد نیز روی طراحی اعمال میشود
|
||||
- همیشه آخرین تغییرات ذخیره میشود
|
||||
|
||||
### امنیت
|
||||
- کدها در Sandbox اجرا میشوند
|
||||
- فقط دستورات مجاز Twig قابل استفاده است
|
||||
- از اجرای JavaScript جلوگیری میشود
|
||||
|
||||
### بهینهسازی
|
||||
- از CSS ساده استفاده کنید
|
||||
- اندازههای مناسب برای چاپ تنظیم کنید
|
||||
- از فونتهای استاندارد استفاده کنید
|
||||
|
||||
## عیبیابی
|
||||
|
||||
### مشکلات متداول
|
||||
1. **عدم نمایش المان**: بررسی کنید که grid position درست تنظیم شده باشد
|
||||
2. **بههمریختگی چاپ**: از CSS سازگار با چاپ استفاده کنید
|
||||
3. **خطای متغیر**: نام متغیر را با لیست بالا تطبیق دهید
|
||||
|
||||
### پشتیبانی
|
||||
- برای مشکلات فنی با تیم توسعه تماس بگیرید
|
||||
- مستندات کامل در بخش راهنما موجود است
|
||||
- نمونههای آماده برای الهام گرفتن استفاده کنید
|
||||
|
||||
## نمونههای کاربردی
|
||||
|
||||
### قالب ساده فروش
|
||||
```html
|
||||
<div class="invoice">
|
||||
<h1>{{ business.name }}</h1>
|
||||
<p>شماره: {{ doc.code }} | تاریخ: {{ doc.date }}</p>
|
||||
|
||||
<table>
|
||||
<tr><th>کالا</th><th>تعداد</th><th>قیمت</th></tr>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ item.commodity.name }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>{{ item.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<p>جمع کل: {{ doc.amount | number_format(0, '.', ',') }}</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### قالب پیشرفته
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" direction="rtl">
|
||||
<head>
|
||||
<style>
|
||||
.invoice-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.info-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<!-- محتوای قالب -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
این سیستم به شما امکان میدهد تا قالبهای حرفهای و سفارشی برای فاکتورهای خود ایجاد کنید.
|
1208
webUI/src/components/TemplateDesigner.vue
Normal file
1208
webUI/src/components/TemplateDesigner.vue
Normal file
File diff suppressed because it is too large
Load diff
890
webUI/src/components/TemplateLibrary.vue
Normal file
890
webUI/src/components/TemplateLibrary.vue
Normal file
|
@ -0,0 +1,890 @@
|
|||
<template>
|
||||
<div class="template-library">
|
||||
<v-dialog v-model="showLibrary" max-width="800px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon class="mr-3">mdi-library</v-icon>
|
||||
کتابخانه قالبهای آماده
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-tabs v-model="activeCategory" color="primary">
|
||||
<v-tab value="sales">فروش</v-tab>
|
||||
<v-tab value="purchase">خرید</v-tab>
|
||||
<v-tab value="return">برگشت</v-tab>
|
||||
<v-tab value="custom">سفارشی</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeCategory" class="mt-4">
|
||||
<v-window-item value="sales">
|
||||
<div class="templates-grid">
|
||||
<div
|
||||
v-for="template in salesTemplates"
|
||||
:key="template.id"
|
||||
class="template-card"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<div class="template-preview">
|
||||
<img :src="template.preview" :alt="template.name" />
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h4>{{ template.name }}</h4>
|
||||
<p>{{ template.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="purchase">
|
||||
<div class="templates-grid">
|
||||
<div
|
||||
v-for="template in purchaseTemplates"
|
||||
:key="template.id"
|
||||
class="template-card"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<div class="template-preview">
|
||||
<img :src="template.preview" :alt="template.name" />
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h4>{{ template.name }}</h4>
|
||||
<p>{{ template.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="return">
|
||||
<div class="templates-grid">
|
||||
<div
|
||||
v-for="template in returnTemplates"
|
||||
:key="template.id"
|
||||
class="template-card"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<div class="template-preview">
|
||||
<img :src="template.preview" :alt="template.name" />
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h4>{{ template.name }}</h4>
|
||||
<p>{{ template.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="custom">
|
||||
<div class="templates-grid">
|
||||
<div
|
||||
v-for="template in customTemplates"
|
||||
:key="template.id"
|
||||
class="template-card"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<div class="template-preview">
|
||||
<img :src="template.preview" :alt="template.name" />
|
||||
</div>
|
||||
<div class="template-info">
|
||||
<h4>{{ template.name }}</h4>
|
||||
<p>{{ template.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showLibrary = false" variant="text">انصراف</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TemplateLibrary',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'select-template'],
|
||||
data() {
|
||||
return {
|
||||
activeCategory: 'sales',
|
||||
salesTemplates: [
|
||||
{
|
||||
id: 'sales-standard',
|
||||
name: 'قالب استاندارد فروش',
|
||||
description: 'قالب ساده و کاربردی برای فاکتورهای فروش',
|
||||
preview: '/templates/sales-standard.png',
|
||||
code: this.getStandardSalesTemplate()
|
||||
},
|
||||
{
|
||||
id: 'sales-luxury',
|
||||
name: 'قالب لوکس فروش',
|
||||
description: 'قالب زیبا و حرفهای برای فاکتورهای فروش',
|
||||
preview: '/templates/sales-luxury.png',
|
||||
code: this.getLuxurySalesTemplate()
|
||||
}
|
||||
],
|
||||
purchaseTemplates: [
|
||||
{
|
||||
id: 'purchase-standard',
|
||||
name: 'قالب استاندارد خرید',
|
||||
description: 'قالب مناسب برای فاکتورهای خرید',
|
||||
preview: '/templates/purchase-standard.png',
|
||||
code: this.getStandardPurchaseTemplate()
|
||||
}
|
||||
],
|
||||
returnTemplates: [
|
||||
{
|
||||
id: 'return-standard',
|
||||
name: 'قالب استاندارد برگشت',
|
||||
description: 'قالب مناسب برای برگشت از فروش/خرید',
|
||||
preview: '/templates/return-standard.png',
|
||||
code: this.getStandardReturnTemplate()
|
||||
}
|
||||
],
|
||||
customTemplates: [
|
||||
{
|
||||
id: 'custom-minimal',
|
||||
name: 'قالب مینیمال',
|
||||
description: 'قالب ساده و تمیز',
|
||||
preview: '/templates/custom-minimal.png',
|
||||
code: this.getMinimalTemplate()
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showLibrary: {
|
||||
get() {
|
||||
return this.modelValue
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectTemplate(template) {
|
||||
this.$emit('select-template', template)
|
||||
this.showLibrary = false
|
||||
},
|
||||
|
||||
getStandardSalesTemplate() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="fa" direction="rtl">
|
||||
<head>
|
||||
<style>
|
||||
.invoice-container {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-box {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.info-box h3 {
|
||||
margin: 0 0 10px 0;
|
||||
background: #f5f5f5;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.items-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
.totals {
|
||||
text-align: right;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.totals div {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.signatures {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.signature-box {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
min-height: 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="header">
|
||||
<h1>{{ business.name }}</h1>
|
||||
<h2>صورتحساب فروش کالا و خدمات</h2>
|
||||
<p>شماره: {{ doc.code }} | تاریخ: {{ doc.date }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="info-box">
|
||||
<h3>فروشنده</h3>
|
||||
<p><strong>نام:</strong> {{ business.name }}</p>
|
||||
<p><strong>تلفن:</strong> {{ business.tel }}</p>
|
||||
<p><strong>آدرس:</strong> {{ business.address }}</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h3>خریدار</h3>
|
||||
{% if person %}
|
||||
<p><strong>نام:</strong> {{ person.name }}</p>
|
||||
<p><strong>موبایل:</strong> {{ person.mobile }}</p>
|
||||
<p><strong>آدرس:</strong> {{ person.address }}</p>
|
||||
{% else %}
|
||||
<p>مشتری ناشناس</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ردیف</th>
|
||||
<th>کالا/خدمات</th>
|
||||
<th>شرح</th>
|
||||
<th>تعداد</th>
|
||||
<th>فی واحد</th>
|
||||
<th>تخفیف</th>
|
||||
<th>مالیات</th>
|
||||
<th>مبلغ کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||
<td>{{ item.des }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>{{ (item.bs / item.commodityCount) | number_format(0, '.', ',') }}</td>
|
||||
<td>
|
||||
{% if item.showPercentDiscount %}
|
||||
{{ item.discountPercent }}%
|
||||
{% else %}
|
||||
{{ item.discount | number_format(0, '.', ',') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ item.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="totals">
|
||||
{% if discount %}
|
||||
<div><strong>جمع تخفیف:</strong> {{ discount | number_format(0, '.', ',') }}</div>
|
||||
{% endif %}
|
||||
{% if transfer %}
|
||||
<div><strong>هزینه ارسال:</strong> {{ transfer | number_format(0, '.', ',') }}</div>
|
||||
{% endif %}
|
||||
<div><strong>جمع کل:</strong> {{ doc.amount | number_format(0, '.', ',') }}</div>
|
||||
</div>
|
||||
|
||||
{% if note %}
|
||||
<div style="margin-top: 20px; padding: 10px; background: #f9f9f9; border-radius: 5px;">
|
||||
<strong>یادداشت:</strong> {{ note }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="signatures">
|
||||
<div class="signature-box">
|
||||
<h4>مهر و امضا خریدار</h4>
|
||||
</div>
|
||||
<div class="signature-box">
|
||||
<h4>مهر و امضا فروشنده</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
},
|
||||
|
||||
getLuxurySalesTemplate() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="fa" direction="rtl">
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Tahoma', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
.invoice-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.header h2 {
|
||||
margin: 10px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.invoice-info {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
.info-box {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
||||
}
|
||||
.info-box h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #667eea;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.items-table th {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 15px 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.items-table td {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.items-table tr:nth-child(even) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
.totals {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.final-total {
|
||||
border-top: 2px solid #667eea;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
.signatures {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
padding: 30px;
|
||||
}
|
||||
.signature-box {
|
||||
border: 2px dashed #667eea;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="header">
|
||||
<h1>{{ business.name }}</h1>
|
||||
<h2>صورتحساب فروش کالا و خدمات</h2>
|
||||
<p>شماره: {{ doc.code }} | تاریخ: {{ doc.date }}</p>
|
||||
</div>
|
||||
|
||||
<div class="invoice-info">
|
||||
<div class="info-box">
|
||||
<h3>اطلاعات فروشنده</h3>
|
||||
<p><strong>نام:</strong> {{ business.name }}</p>
|
||||
<p><strong>تلفن:</strong> {{ business.tel }}</p>
|
||||
<p><strong>آدرس:</strong> {{ business.address }}</p>
|
||||
</div>
|
||||
<div class="info-box">
|
||||
<h3>اطلاعات خریدار</h3>
|
||||
{% if person %}
|
||||
<p><strong>نام:</strong> {{ person.name }}</p>
|
||||
<p><strong>موبایل:</strong> {{ person.mobile }}</p>
|
||||
<p><strong>آدرس:</strong> {{ person.address }}</p>
|
||||
{% else %}
|
||||
<p>مشتری ناشناس</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 0 20px;">
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ردیف</th>
|
||||
<th>کالا/خدمات</th>
|
||||
<th>شرح</th>
|
||||
<th>تعداد</th>
|
||||
<th>فی واحد</th>
|
||||
<th>تخفیف</th>
|
||||
<th>مالیات</th>
|
||||
<th>مبلغ کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||
<td>{{ item.des }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>{{ (item.bs / item.commodityCount) | number_format(0, '.', ',') }}</td>
|
||||
<td>
|
||||
{% if item.showPercentDiscount %}
|
||||
{{ item.discountPercent }}%
|
||||
{% else %}
|
||||
{{ item.discount | number_format(0, '.', ',') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ item.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="totals">
|
||||
{% if discount %}
|
||||
<div class="total-row">
|
||||
<span>جمع تخفیف:</span>
|
||||
<span>{{ discount | number_format(0, '.', ',') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if transfer %}
|
||||
<div class="total-row">
|
||||
<span>هزینه ارسال:</span>
|
||||
<span>{{ transfer | number_format(0, '.', ',') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="total-row final-total">
|
||||
<span>جمع کل:</span>
|
||||
<span>{{ doc.amount | number_format(0, '.', ',') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note %}
|
||||
<div style="margin: 20px; padding: 15px; background: #e3f2fd; border-radius: 10px; border-right: 4px solid #2196f3;">
|
||||
<strong>یادداشت:</strong> {{ note }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="signatures">
|
||||
<div class="signature-box">
|
||||
<h4>مهر و امضا خریدار</h4>
|
||||
</div>
|
||||
<div class="signature-box">
|
||||
<h4>مهر و امضا فروشنده</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
},
|
||||
|
||||
getStandardPurchaseTemplate() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="fa" direction="rtl">
|
||||
<head>
|
||||
<style>
|
||||
.invoice-container {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.items-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="header">
|
||||
<h1>{{ business.name }}</h1>
|
||||
<h2>فاکتور خرید</h2>
|
||||
<p>شماره: {{ doc.code }} | تاریخ: {{ doc.date }}</p>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ردیف</th>
|
||||
<th>کالا/خدمات</th>
|
||||
<th>شرح</th>
|
||||
<th>تعداد</th>
|
||||
<th>بدهکار</th>
|
||||
<th>بستانکار</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||
<td>{{ item.des }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>{{ item.bd | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ item.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
},
|
||||
|
||||
getStandardReturnTemplate() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="fa" direction="rtl">
|
||||
<head>
|
||||
<style>
|
||||
.invoice-container {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #333;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.items-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="header">
|
||||
<h1>{{ business.name }}</h1>
|
||||
<h2>برگشت از فروش</h2>
|
||||
<p>شماره: {{ doc.code }} | تاریخ: {{ doc.date }}</p>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ردیف</th>
|
||||
<th>کالا/خدمات</th>
|
||||
<th>شرح</th>
|
||||
<th>تعداد</th>
|
||||
<th>تخفیف</th>
|
||||
<th>مالیات</th>
|
||||
<th>مبلغ کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||
<td>{{ item.des }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>
|
||||
{% if item.showPercentDiscount %}
|
||||
{{ item.discountPercent }}%
|
||||
{% else %}
|
||||
{{ item.discount | number_format(0, '.', ',') }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ item.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
},
|
||||
|
||||
getMinimalTemplate() {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="fa" direction="rtl">
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.invoice-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
.info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.info-section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.items-table th {
|
||||
background: #f8f9fa;
|
||||
padding: 12px 8px;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
.items-table td {
|
||||
padding: 12px 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.total {
|
||||
text-align: right;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
border-top: 2px solid #333;
|
||||
padding-top: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="invoice-container">
|
||||
<div class="header">
|
||||
<h1>{{ business.name }}</h1>
|
||||
<p>شماره: {{ doc.code }} | تاریخ: {{ doc.date }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="info-section">
|
||||
<h3>فروشنده</h3>
|
||||
<p>{{ business.name }}</p>
|
||||
<p>{{ business.tel }}</p>
|
||||
<p>{{ business.address }}</p>
|
||||
</div>
|
||||
<div class="info-section">
|
||||
<h3>خریدار</h3>
|
||||
{% if person %}
|
||||
<p>{{ person.name }}</p>
|
||||
<p>{{ person.mobile }}</p>
|
||||
<p>{{ person.address }}</p>
|
||||
{% else %}
|
||||
<p>مشتری ناشناس</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>کالا/خدمات</th>
|
||||
<th>تعداد</th>
|
||||
<th>فی واحد</th>
|
||||
<th>مبلغ کل</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in rows %}
|
||||
<tr>
|
||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||
<td>{{ item.commodityCount }}</td>
|
||||
<td>{{ (item.bs / item.commodityCount) | number_format(0, '.', ',') }}</td>
|
||||
<td>{{ item.bs | number_format(0, '.', ',') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="total">
|
||||
جمع کل: {{ doc.amount | number_format(0, '.', ',') }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-library {
|
||||
/* Component styles */
|
||||
}
|
||||
|
||||
.templates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.template-preview {
|
||||
height: 150px;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.template-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.template-info {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.template-info h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.template-info p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.templates-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -11,7 +11,19 @@
|
|||
|
||||
<v-tooltip :text="isEditMode ? 'بروزرسانی قالب' : 'ذخیره قالب'" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-content-save" color="primary" @click="saveTemplate" :loading="saving"></v-btn>
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon="mdi-content-save"
|
||||
color="primary"
|
||||
@click="saveTemplate"
|
||||
:loading="saving"
|
||||
:disabled="!isFormValid"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="بررسی فرم" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-check-circle" color="secondary" @click="showValidationStatus" class="ml-2"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="پیشنمایش (HTML)" location="bottom">
|
||||
|
@ -21,7 +33,8 @@
|
|||
</v-tooltip>
|
||||
<v-tooltip text="دانلود پیشنمایش PDF" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" icon="mdi-file-pdf-box" color="secondary" class="ml-2" @click="previewPdf" :disabled="!templateData.code"></v-btn>
|
||||
<v-btn v-bind="props" icon="mdi-file-pdf-box" color="secondary" class="ml-2" @click="previewPdf"
|
||||
:disabled="!templateData.code"></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</v-toolbar>
|
||||
|
@ -30,31 +43,49 @@
|
|||
<v-tab value="help">راهنما و آموزش</v-tab>
|
||||
<v-tab value="settings">تنظیمات ویرایشگر</v-tab>
|
||||
</v-tabs>
|
||||
<v-container>
|
||||
<v-container fluid>
|
||||
<v-window v-model="activeTab" class="template-window">
|
||||
<!-- فرم قالب -->
|
||||
<v-window-item value="form">
|
||||
<div class="template-form-fields">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field v-model="templateData.name" label="نام قالب" placeholder="مثال: قالب استاندارد شرکت"
|
||||
variant="outlined" required :rules="[v => !!v || 'نام قالب الزامی است']" />
|
||||
<v-text-field
|
||||
v-model="templateData.name"
|
||||
label="نام قالب *"
|
||||
placeholder="مثال: قالب استاندارد شرکت"
|
||||
variant="outlined"
|
||||
required
|
||||
:rules="nameRules"
|
||||
:error-messages="nameError"
|
||||
@input="clearNameError"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select v-model="templateData.isPublic" label="وضعیت عمومی" :items="publicOptions" variant="outlined"
|
||||
required />
|
||||
<v-select
|
||||
v-model="templateData.isPublic"
|
||||
label="وضعیت عمومی"
|
||||
:items="publicOptions"
|
||||
variant="outlined"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-label class="text-subtitle-2 mb-2 d-block">کد قالب</v-label>
|
||||
<MonacoEditor v-model="templateData.code" :language="editorSettings.language"
|
||||
:theme="editorSettings.theme" height="500px" :options="monacoOptions" @change="onCodeChange"
|
||||
ref="monacoEditor" />
|
||||
<v-label class="text-subtitle-2 mb-2 d-block">کد قالب *</v-label>
|
||||
<TemplateDesigner
|
||||
v-model="templateData.code"
|
||||
@preview="previewHtml"
|
||||
ref="templateDesigner"
|
||||
/>
|
||||
<div v-if="codeError" class="text-error text-caption mt-1">
|
||||
{{ codeError }}
|
||||
</div>
|
||||
<div v-if="codeValidationError" class="text-error text-caption mt-1">
|
||||
{{ codeValidationError }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
@ -72,7 +103,8 @@
|
|||
<v-card-text>
|
||||
<p>
|
||||
در این بخش میتوانید با استفاده از زبان قالببندی Twig و HTML/CSS، قالب فاکتور سفارشی خود را طراحی کنید.
|
||||
برای جلوگیری از مشکلات امنیتی، رندر کدها در <b>Sandbox</b> انجام میشود؛ بنابراین تنها بخشی از امکانات Twig مجاز است.
|
||||
برای جلوگیری از مشکلات امنیتی، رندر کدها در <b>Sandbox</b> انجام میشود؛ بنابراین تنها بخشی از امکانات
|
||||
Twig مجاز است.
|
||||
</p>
|
||||
<ul class="mt-3">
|
||||
<li>تگهای مجاز: <b>if</b> و <b>for</b></li>
|
||||
|
@ -94,7 +126,9 @@
|
|||
<li>
|
||||
<b>business</b>: اطلاعات کسبوکار
|
||||
<ul>
|
||||
<li><code>business.name</code>, <code>business.tel</code>, <code>business.mobile</code>, <code>business.address</code></li>
|
||||
<li><code>business.name</code>, <code>business.tel</code>, <code>business.mobile</code>,
|
||||
<code>business.address</code>
|
||||
</li>
|
||||
<li><code>business.shenasemeli</code>, <code>business.codeeghtesadi</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -102,22 +136,31 @@
|
|||
<b>doc</b>: اطلاعات سند
|
||||
<ul>
|
||||
<li><code>doc.code</code>, <code>doc.date</code></li>
|
||||
<li>در فروش: <code>doc.taxPercent</code>, <code>doc.discountPercent</code>, <code>doc.discountType</code></li>
|
||||
<li>در فروش: <code>doc.taxPercent</code>, <code>doc.discountPercent</code>,
|
||||
<code>doc.discountType</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>person</b>: اطلاعات مشتری/طرف حساب
|
||||
<ul>
|
||||
<li><code>person.name</code>, <code>person.mobile</code>, <code>person.tel</code>, <code>person.address</code></li>
|
||||
<li><code>person.name</code>, <code>person.mobile</code>, <code>person.tel</code>,
|
||||
<code>person.address</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<b>rows</b>: آرایه اقلام فاکتور (هر ردیف یک شیء)
|
||||
<ul>
|
||||
<li><code>row.commodity.name</code>, <code>row.commodity.code</code> (ممکن است <code>commodity</code> تهی باشد)</li>
|
||||
<li><code>row.commodity.name</code>, <code>row.commodity.code</code> (ممکن است
|
||||
<code>commodity</code>
|
||||
تهی باشد)</li>
|
||||
<li><code>row.commodityCount</code> (و برای سازگاری قدیمی: <code>row.commdityCount</code>)</li>
|
||||
<li><code>row.des</code>, <code>row.bs</code>, <code>row.bd</code> (در خرید/برگشت از خرید)، <code>row.tax</code>, <code>row.discount</code></li>
|
||||
<li>در فروش/برگشت از فروش: <code>row.showPercentDiscount</code>, <code>row.discountPercent</code></li>
|
||||
<li><code>row.des</code>, <code>row.bs</code>, <code>row.bd</code> (در خرید/برگشت از خرید)،
|
||||
<code>row.tax</code>, <code>row.discount</code>
|
||||
</li>
|
||||
<li>در فروش/برگشت از فروش: <code>row.showPercentDiscount</code>, <code>row.discountPercent</code>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -149,7 +192,8 @@
|
|||
<p>مشتری: {{ person.name | escape }}</p>
|
||||
{% endif %}
|
||||
|
||||
<table width="100%" cellspacing="0" cellpadding="6" border="1">
|
||||
<table width="100%" cellspacing="0" cellpadding="6"
|
||||
border="1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
|
@ -197,7 +241,8 @@
|
|||
<div>شماره فاکتور: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
{% if person %}<div>مشتری: {{ person.name }} | موبایل: {{ person.mobile }}</div>{% endif %}
|
||||
|
||||
<table width="100%" cellspacing="0" cellpadding="6" border="1" style="margin-top:10px">
|
||||
<table width="100%" cellspacing="0" cellpadding="6"
|
||||
border="1" style="margin-top:10px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
|
@ -207,19 +252,23 @@
|
|||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
|
||||
number_format(0, '.', ',') }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top:10px;text-align:right">
|
||||
{% if discount %}<div>جمع تخفیف: {{ discount | number_format(0, '.', ',') }}</div>{% endif %}
|
||||
{% if transfer %}<div>هزینه ارسال/انتقال: {{ transfer | number_format(0, '.', ',') }}</div>{% endif %}
|
||||
{% if discount %}<div>جمع تخفیف: {{ discount | number_format(0, '.', ',') }}</div>{% endif
|
||||
%}
|
||||
{% if transfer %}<div>هزینه ارسال/انتقال: {{ transfer | number_format(0, '.', ',')
|
||||
}}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
{% if note %}
|
||||
<div style="margin-top:12px;border-top:1px dashed #ccc;padding-top:8px">{{ note | escape }}</div>
|
||||
<div style="margin-top:12px;border-top:1px dashed #ccc;padding-top:8px">{{ note | escape
|
||||
}}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</code></pre>
|
||||
|
@ -235,7 +284,8 @@
|
|||
<v-card-text>
|
||||
<p>در خرید/برگشت از خرید، مقادیر ردیفها شامل <code>bs</code> و <code>bd</code> نیز هستند.</p>
|
||||
<pre class="text-body-2" v-pre><code>{% for r in rows %}
|
||||
<div>{{ loop.index }}. {{ r.commodity.name ?? '-' }} | تعداد: {{ r.commodityCount }} | شرح: {{ r.des }} | بدهکار: {{ r.bd }} | بستانکار: {{ r.bs }}</div>
|
||||
<div>{{ loop.index }}. {{ r.commodity.name ?? '-' }} | تعداد: {{ r.commodityCount }} | شرح: {{
|
||||
r.des }} | بدهکار: {{ r.bd }} | بستانکار: {{ r.bs }}</div>
|
||||
{% endfor %}
|
||||
</code></pre>
|
||||
</v-card-text>
|
||||
|
@ -250,8 +300,12 @@
|
|||
<v-card-text>
|
||||
<ul>
|
||||
<li>از اجرای جاوااسکریپت، توابع سیستم یا درخواستهای خارجی در قالب پرهیز شده و امکانپذیر نیست.</li>
|
||||
<li>برای جلوگیری از خطاهای سازگاری، از <b>commodityCount</b> استفاده کنید (هرچند برای سازگاری <b>commdityCount</b> نیز پشتیبانی میشود).</li>
|
||||
<li>به دلیل Sandbox، دسترسی به متد/پراپرتیهای آبجکتها محدود است؛ از دادههای آرایهای فراهمشده استفاده کنید.</li>
|
||||
<li>برای جلوگیری از خطاهای سازگاری، از <b>commodityCount</b> استفاده کنید (هرچند برای سازگاری
|
||||
<b>commdityCount</b> نیز پشتیبانی میشود).
|
||||
</li>
|
||||
<li>به دلیل Sandbox، دسترسی به متد/پراپرتیهای آبجکتها محدود است؛ از دادههای آرایهای فراهمشده
|
||||
استفاده
|
||||
کنید.</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -264,9 +318,13 @@
|
|||
</v-card-title>
|
||||
<v-card-text>
|
||||
<ul>
|
||||
<li>خطای «Key X does not exist»: نام کلید را با فهرست بالا تطبیق دهید؛ برای تعداد از <code>commodityCount</code> استفاده کنید.</li>
|
||||
<li>عدم نمایش اطلاعات مشتری: ابتدا بررسی کنید <code>person</code> تهی نباشد: <code>{% if person %} ... {% endif %}</code></li>
|
||||
<li>بههمریختگی چاپ: از CSS ساده و سازگار با چاپ استفاده کنید؛ عرض جدولها و اندازه فونتها را کنترل کنید.</li>
|
||||
<li>خطای «Key X does not exist»: نام کلید را با فهرست بالا تطبیق دهید؛ برای تعداد از
|
||||
<code>commodityCount</code> استفاده کنید.
|
||||
</li>
|
||||
<li>عدم نمایش اطلاعات مشتری: ابتدا بررسی کنید <code>person</code> تهی نباشد: <code>{% if person %} ... {%
|
||||
endif %}</code></li>
|
||||
<li>بههمریختگی چاپ: از CSS ساده و سازگار با چاپ استفاده کنید؛ عرض جدولها و اندازه فونتها را کنترل
|
||||
کنید.</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -285,7 +343,8 @@
|
|||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
{% if person %}<div>مشتری: {{ person.name }}</div>{% endif %}
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<table width="100%" border="1" cellspacing="0"
|
||||
cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
|
@ -295,7 +354,8 @@
|
|||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
|
||||
number_format(0, '.', ',') }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -308,7 +368,8 @@
|
|||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
{% if person %}<div>فروشنده: {{ person.name }}</div>{% endif %}
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<table width="100%" border="1" cellspacing="0"
|
||||
cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
|
@ -329,7 +390,8 @@
|
|||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<table width="100%" border="1" cellspacing="0"
|
||||
cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
|
@ -350,7 +412,8 @@
|
|||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||
<table width="100%" border="1" cellspacing="0" cellpadding="6" style="margin-top:8px">
|
||||
<table width="100%" border="1" cellspacing="0"
|
||||
cellpadding="6" style="margin-top:8px">
|
||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
|
@ -360,7 +423,8 @@
|
|||
<td>{{ r.commodityCount }}</td>
|
||||
<td>{{ r.des }}</td>
|
||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
|
||||
number_format(0, '.', ',') }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -448,16 +512,45 @@
|
|||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
|
||||
<!-- Snackbar for notifications -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="snackbar.timeout"
|
||||
location="bottom"
|
||||
class="snackbar-custom"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon
|
||||
:icon="getSnackbarIcon(snackbar.color)"
|
||||
class="mr-2"
|
||||
></v-icon>
|
||||
{{ snackbar.message }}
|
||||
</div>
|
||||
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
color="white"
|
||||
variant="text"
|
||||
@click="closeSnackbar"
|
||||
>
|
||||
بستن
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MonacoEditor from '@/components/MonacoEditor.vue'
|
||||
import TemplateDesigner from '@/components/TemplateDesigner.vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export default {
|
||||
name: 'CustomInvoiceTemplateForm',
|
||||
components: {
|
||||
MonacoEditor
|
||||
MonacoEditor,
|
||||
TemplateDesigner
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -472,6 +565,14 @@ export default {
|
|||
code: ''
|
||||
},
|
||||
codeError: '',
|
||||
nameError: '',
|
||||
codeValidationError: '',
|
||||
snackbar: {
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success',
|
||||
timeout: 3000
|
||||
},
|
||||
publicOptions: [
|
||||
{ title: 'خصوصی', value: false },
|
||||
{ title: 'عمومی', value: true }
|
||||
|
@ -510,6 +611,20 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
nameRules() {
|
||||
return [
|
||||
v => !!v || 'نام قالب الزامی است',
|
||||
v => (v && v.length >= 3) || 'نام قالب باید حداقل 3 کاراکتر باشد',
|
||||
v => (v && v.length <= 100) || 'نام قالب نمیتواند بیشتر از 100 کاراکتر باشد'
|
||||
]
|
||||
},
|
||||
isFormValid() {
|
||||
return this.templateData.name &&
|
||||
this.templateData.name.trim().length >= 3 &&
|
||||
this.templateData.name.trim().length <= 100 &&
|
||||
this.templateData.code &&
|
||||
this.templateData.code.trim().length >= 50;
|
||||
},
|
||||
monacoOptions() {
|
||||
return {
|
||||
fontSize: this.editorSettings.fontSize,
|
||||
|
@ -557,28 +672,116 @@ export default {
|
|||
}
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || 'خطا در بارگذاری قالب'
|
||||
this.$toast?.error(msg)
|
||||
this.showErrorSnackbar(msg)
|
||||
}
|
||||
},
|
||||
validateForm() {
|
||||
let isValid = true;
|
||||
|
||||
// Validate name
|
||||
if (!this.templateData.name || this.templateData.name.trim().length === 0) {
|
||||
this.nameError = 'نام قالب الزامی است';
|
||||
isValid = false;
|
||||
} else if (this.templateData.name.trim().length < 3) {
|
||||
this.nameError = 'نام قالب باید حداقل 3 کاراکتر باشد';
|
||||
isValid = false;
|
||||
} else if (this.templateData.name.trim().length > 100) {
|
||||
this.nameError = 'نام قالب نمیتواند بیشتر از 100 کاراکتر باشد';
|
||||
isValid = false;
|
||||
} else {
|
||||
this.nameError = '';
|
||||
}
|
||||
|
||||
// Validate code
|
||||
if (!this.templateData.code || this.templateData.code.trim().length === 0) {
|
||||
this.codeValidationError = 'کد قالب الزامی است';
|
||||
isValid = false;
|
||||
} else if (this.templateData.code.trim().length < 50) {
|
||||
this.codeValidationError = 'کد قالب باید حداقل 50 کاراکتر باشد';
|
||||
isValid = false;
|
||||
} else {
|
||||
this.codeValidationError = '';
|
||||
}
|
||||
|
||||
return isValid;
|
||||
},
|
||||
|
||||
clearNameError() {
|
||||
this.nameError = '';
|
||||
},
|
||||
|
||||
showSnackbar(message, color = 'success', timeout = 3000) {
|
||||
this.snackbar.message = message;
|
||||
this.snackbar.color = color;
|
||||
this.snackbar.timeout = timeout;
|
||||
this.snackbar.show = true;
|
||||
},
|
||||
|
||||
closeSnackbar() {
|
||||
this.snackbar.show = false;
|
||||
},
|
||||
|
||||
showSuccessSnackbar(message, timeout = 3000) {
|
||||
this.showSnackbar(message, 'success', timeout);
|
||||
},
|
||||
|
||||
showErrorSnackbar(message, timeout = 5000) {
|
||||
this.showSnackbar(message, 'error', timeout);
|
||||
},
|
||||
|
||||
showWarningSnackbar(message, timeout = 4000) {
|
||||
this.showSnackbar(message, 'warning', timeout);
|
||||
},
|
||||
|
||||
showInfoSnackbar(message, timeout = 3000) {
|
||||
this.showSnackbar(message, 'info', timeout);
|
||||
},
|
||||
|
||||
getSnackbarIcon(color) {
|
||||
const icons = {
|
||||
success: 'mdi-check-circle',
|
||||
error: 'mdi-alert-circle',
|
||||
warning: 'mdi-alert',
|
||||
info: 'mdi-information'
|
||||
};
|
||||
return icons[color] || 'mdi-information';
|
||||
},
|
||||
|
||||
showValidationStatus() {
|
||||
const isValid = this.validateForm();
|
||||
if (isValid) {
|
||||
this.showSuccessSnackbar('فرم معتبر است و آماده ذخیره میباشد');
|
||||
} else {
|
||||
this.showErrorSnackbar('لطفاً خطاهای فرم را برطرف کنید');
|
||||
}
|
||||
},
|
||||
|
||||
async saveTemplate() {
|
||||
if (!this.templateData.name || !this.templateData.code) {
|
||||
this.$toast?.error('نام و کد قالب الزامی است.');
|
||||
// Clear previous errors
|
||||
this.nameError = '';
|
||||
this.codeValidationError = '';
|
||||
|
||||
// Validate form
|
||||
if (!this.validateForm()) {
|
||||
this.showErrorSnackbar('لطفاً خطاهای فرم را برطرف کنید');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const payload = {
|
||||
name: this.templateData.name,
|
||||
name: this.templateData.name.trim(),
|
||||
isPublic: this.templateData.isPublic,
|
||||
code: this.templateData.code,
|
||||
code: this.templateData.code.trim(),
|
||||
};
|
||||
|
||||
let response;
|
||||
if (this.isEditMode && this.templateId) {
|
||||
response = await axios.post(`/api/plugins/custominvoice/template/${this.templateId}`, payload);
|
||||
this.showSuccessSnackbar('قالب با موفقیت بروزرسانی شد');
|
||||
} else {
|
||||
response = await axios.post('/api/plugins/custominvoice/template', payload);
|
||||
this.showSuccessSnackbar('قالب با موفقیت ایجاد شد');
|
||||
}
|
||||
|
||||
const data = response.data?.data || {};
|
||||
|
@ -586,11 +789,14 @@ export default {
|
|||
this.templateId = data.id;
|
||||
this.isEditMode = true;
|
||||
}
|
||||
this.$toast?.success('قالب با موفقیت ذخیره شد.');
|
||||
|
||||
// Redirect after a short delay to show success message
|
||||
setTimeout(() => {
|
||||
this.$router.push('/acc/plugins/custominvoice/templates');
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
const msg = e?.response?.data?.message || 'خطا در ذخیره قالب';
|
||||
this.$toast?.error(msg);
|
||||
this.showErrorSnackbar(msg);
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
|
@ -1102,4 +1308,44 @@ export default {
|
|||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
.text-error {
|
||||
color: #d32f2f !important;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.v-text-field--error .v-field__outline {
|
||||
border-color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.v-text-field--error .v-field__outline__start {
|
||||
border-color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.v-text-field--error .v-field__outline__end {
|
||||
border-color: #d32f2f !important;
|
||||
}
|
||||
|
||||
/* Snackbar styles */
|
||||
.snackbar-custom {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.snackbar-custom .v-snackbar__content {
|
||||
font-family: 'Vazir', 'Tahoma', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.snackbar-custom .v-snackbar__actions {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.snackbar-custom .v-btn {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue