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">
|
<v-tooltip :text="isEditMode ? 'بروزرسانی قالب' : 'ذخیره قالب'" location="bottom">
|
||||||
<template v-slot:activator="{ props }">
|
<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>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<v-tooltip text="پیشنمایش (HTML)" location="bottom">
|
<v-tooltip text="پیشنمایش (HTML)" location="bottom">
|
||||||
|
@ -21,7 +33,8 @@
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
<v-tooltip text="دانلود پیشنمایش PDF" location="bottom">
|
<v-tooltip text="دانلود پیشنمایش PDF" location="bottom">
|
||||||
<template v-slot:activator="{ props }">
|
<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>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
@ -30,31 +43,49 @@
|
||||||
<v-tab value="help">راهنما و آموزش</v-tab>
|
<v-tab value="help">راهنما و آموزش</v-tab>
|
||||||
<v-tab value="settings">تنظیمات ویرایشگر</v-tab>
|
<v-tab value="settings">تنظیمات ویرایشگر</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
<v-container>
|
<v-container fluid>
|
||||||
<v-window v-model="activeTab" class="template-window">
|
<v-window v-model="activeTab" class="template-window">
|
||||||
<!-- فرم قالب -->
|
<!-- فرم قالب -->
|
||||||
<v-window-item value="form">
|
<v-window-item value="form">
|
||||||
<div class="template-form-fields">
|
<div class="template-form-fields">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field v-model="templateData.name" label="نام قالب" placeholder="مثال: قالب استاندارد شرکت"
|
<v-text-field
|
||||||
variant="outlined" required :rules="[v => !!v || 'نام قالب الزامی است']" />
|
v-model="templateData.name"
|
||||||
|
label="نام قالب *"
|
||||||
|
placeholder="مثال: قالب استاندارد شرکت"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
:rules="nameRules"
|
||||||
|
:error-messages="nameError"
|
||||||
|
@input="clearNameError"
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select v-model="templateData.isPublic" label="وضعیت عمومی" :items="publicOptions" variant="outlined"
|
<v-select
|
||||||
required />
|
v-model="templateData.isPublic"
|
||||||
|
label="وضعیت عمومی"
|
||||||
|
:items="publicOptions"
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-label class="text-subtitle-2 mb-2 d-block">کد قالب</v-label>
|
<v-label class="text-subtitle-2 mb-2 d-block">کد قالب *</v-label>
|
||||||
<MonacoEditor v-model="templateData.code" :language="editorSettings.language"
|
<TemplateDesigner
|
||||||
:theme="editorSettings.theme" height="500px" :options="monacoOptions" @change="onCodeChange"
|
v-model="templateData.code"
|
||||||
ref="monacoEditor" />
|
@preview="previewHtml"
|
||||||
|
ref="templateDesigner"
|
||||||
|
/>
|
||||||
<div v-if="codeError" class="text-error text-caption mt-1">
|
<div v-if="codeError" class="text-error text-caption mt-1">
|
||||||
{{ codeError }}
|
{{ codeError }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="codeValidationError" class="text-error text-caption mt-1">
|
||||||
|
{{ codeValidationError }}
|
||||||
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,7 +103,8 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<p>
|
<p>
|
||||||
در این بخش میتوانید با استفاده از زبان قالببندی Twig و HTML/CSS، قالب فاکتور سفارشی خود را طراحی کنید.
|
در این بخش میتوانید با استفاده از زبان قالببندی Twig و HTML/CSS، قالب فاکتور سفارشی خود را طراحی کنید.
|
||||||
برای جلوگیری از مشکلات امنیتی، رندر کدها در <b>Sandbox</b> انجام میشود؛ بنابراین تنها بخشی از امکانات Twig مجاز است.
|
برای جلوگیری از مشکلات امنیتی، رندر کدها در <b>Sandbox</b> انجام میشود؛ بنابراین تنها بخشی از امکانات
|
||||||
|
Twig مجاز است.
|
||||||
</p>
|
</p>
|
||||||
<ul class="mt-3">
|
<ul class="mt-3">
|
||||||
<li>تگهای مجاز: <b>if</b> و <b>for</b></li>
|
<li>تگهای مجاز: <b>if</b> و <b>for</b></li>
|
||||||
|
@ -94,7 +126,9 @@
|
||||||
<li>
|
<li>
|
||||||
<b>business</b>: اطلاعات کسبوکار
|
<b>business</b>: اطلاعات کسبوکار
|
||||||
<ul>
|
<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>
|
<li><code>business.shenasemeli</code>, <code>business.codeeghtesadi</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
@ -102,22 +136,31 @@
|
||||||
<b>doc</b>: اطلاعات سند
|
<b>doc</b>: اطلاعات سند
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>doc.code</code>, <code>doc.date</code></li>
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>person</b>: اطلاعات مشتری/طرف حساب
|
<b>person</b>: اطلاعات مشتری/طرف حساب
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>rows</b>: آرایه اقلام فاکتور (هر ردیف یک شیء)
|
<b>rows</b>: آرایه اقلام فاکتور (هر ردیف یک شیء)
|
||||||
<ul>
|
<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.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.des</code>, <code>row.bs</code>, <code>row.bd</code> (در خرید/برگشت از خرید)،
|
||||||
<li>در فروش/برگشت از فروش: <code>row.showPercentDiscount</code>, <code>row.discountPercent</code></li>
|
<code>row.tax</code>, <code>row.discount</code>
|
||||||
|
</li>
|
||||||
|
<li>در فروش/برگشت از فروش: <code>row.showPercentDiscount</code>, <code>row.discountPercent</code>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -146,40 +189,41 @@
|
||||||
<div class="code-example" variant="outlined">
|
<div class="code-example" variant="outlined">
|
||||||
<v-card-text class="font-family-monospace">
|
<v-card-text class="font-family-monospace">
|
||||||
<pre class="text-body-2" v-pre><code>{% if person %}
|
<pre class="text-body-2" v-pre><code>{% if person %}
|
||||||
<p>مشتری: {{ person.name | escape }}</p>
|
<p>مشتری: {{ person.name | escape }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<table width="100%" cellspacing="0" cellpadding="6" border="1">
|
<table width="100%" cellspacing="0" cellpadding="6"
|
||||||
<thead>
|
border="1">
|
||||||
<tr>
|
<thead>
|
||||||
<th>#</th>
|
<tr>
|
||||||
<th>کالا</th>
|
<th>#</th>
|
||||||
<th>تعداد</th>
|
<th>کالا</th>
|
||||||
<th>فی توضیح</th>
|
<th>تعداد</th>
|
||||||
<th>مالیات</th>
|
<th>فی توضیح</th>
|
||||||
<th>تخفیف</th>
|
<th>مالیات</th>
|
||||||
</tr>
|
<th>تخفیف</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{% for item in rows %}
|
<tbody>
|
||||||
<tr>
|
{% for item in rows %}
|
||||||
<td>{{ loop.index }}</td>
|
<tr>
|
||||||
<td>{{ item.commodity.name ?? '-' }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ item.commodityCount }}</td>
|
<td>{{ item.commodity.name ?? '-' }}</td>
|
||||||
<td>{{ item.des | escape }}</td>
|
<td>{{ item.commodityCount }}</td>
|
||||||
<td>{{ item.tax | number_format(0, '.', ',') }}</td>
|
<td>{{ item.des | escape }}</td>
|
||||||
<td>
|
<td>{{ item.tax | number_format(0, '.', ',') }}</td>
|
||||||
{% if item.showPercentDiscount %}
|
<td>
|
||||||
{{ item.discountPercent }}%
|
{% if item.showPercentDiscount %}
|
||||||
{% else %}
|
{{ item.discountPercent }}%
|
||||||
{{ item.discount | number_format(0, '.', ',') }}
|
{% else %}
|
||||||
{% endif %}
|
{{ item.discount | number_format(0, '.', ',') }}
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
</td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
{% endfor %}
|
||||||
</table>
|
</tbody>
|
||||||
</code></pre>
|
</table>
|
||||||
|
</code></pre>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
@ -193,36 +237,41 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||||
<h2 style="text-align:center">{{ business.name }}</h2>
|
<h2 style="text-align:center">{{ business.name }}</h2>
|
||||||
<div>شماره فاکتور: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
<div>شماره فاکتور: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||||
{% if person %}<div>مشتری: {{ person.name }} | موبایل: {{ person.mobile }}</div>{% endif %}
|
{% 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"
|
||||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
border="1" style="margin-top:10px">
|
||||||
<tbody>
|
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||||
{% for r in rows %}
|
<tbody>
|
||||||
<tr>
|
{% for r in rows %}
|
||||||
<td>{{ loop.index }}</td>
|
<tr>
|
||||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ r.commodityCount }}</td>
|
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||||
<td>{{ r.des }}</td>
|
<td>{{ r.commodityCount }}</td>
|
||||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
<td>{{ r.des }}</td>
|
||||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||||
</tr>
|
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
|
||||||
{% endfor %}
|
number_format(0, '.', ',') }}{% endif %}</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<div style="margin-top:10px;text-align:right">
|
<div style="margin-top:10px;text-align:right">
|
||||||
{% if discount %}<div>جمع تخفیف: {{ discount | 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 transfer %}<div>هزینه ارسال/انتقال: {{ transfer | number_format(0, '.', ',')
|
||||||
|
}}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if note %}
|
{% 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
|
||||||
{% endif %}
|
}}</div>
|
||||||
</div>
|
{% endif %}
|
||||||
</code></pre>
|
</div>
|
||||||
|
</code></pre>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
@ -235,9 +284,10 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<p>در خرید/برگشت از خرید، مقادیر ردیفها شامل <code>bs</code> و <code>bd</code> نیز هستند.</p>
|
<p>در خرید/برگشت از خرید، مقادیر ردیفها شامل <code>bs</code> و <code>bd</code> نیز هستند.</p>
|
||||||
<pre class="text-body-2" v-pre><code>{% for r in rows %}
|
<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 }} | شرح: {{
|
||||||
{% endfor %}
|
r.des }} | بدهکار: {{ r.bd }} | بستانکار: {{ r.bs }}</div>
|
||||||
</code></pre>
|
{% endfor %}
|
||||||
|
</code></pre>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
@ -250,8 +300,12 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<ul>
|
<ul>
|
||||||
<li>از اجرای جاوااسکریپت، توابع سیستم یا درخواستهای خارجی در قالب پرهیز شده و امکانپذیر نیست.</li>
|
<li>از اجرای جاوااسکریپت، توابع سیستم یا درخواستهای خارجی در قالب پرهیز شده و امکانپذیر نیست.</li>
|
||||||
<li>برای جلوگیری از خطاهای سازگاری، از <b>commodityCount</b> استفاده کنید (هرچند برای سازگاری <b>commdityCount</b> نیز پشتیبانی میشود).</li>
|
<li>برای جلوگیری از خطاهای سازگاری، از <b>commodityCount</b> استفاده کنید (هرچند برای سازگاری
|
||||||
<li>به دلیل Sandbox، دسترسی به متد/پراپرتیهای آبجکتها محدود است؛ از دادههای آرایهای فراهمشده استفاده کنید.</li>
|
<b>commdityCount</b> نیز پشتیبانی میشود).
|
||||||
|
</li>
|
||||||
|
<li>به دلیل Sandbox، دسترسی به متد/پراپرتیهای آبجکتها محدود است؛ از دادههای آرایهای فراهمشده
|
||||||
|
استفاده
|
||||||
|
کنید.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -264,9 +318,13 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<ul>
|
<ul>
|
||||||
<li>خطای «Key X does not exist»: نام کلید را با فهرست بالا تطبیق دهید؛ برای تعداد از <code>commodityCount</code> استفاده کنید.</li>
|
<li>خطای «Key X does not exist»: نام کلید را با فهرست بالا تطبیق دهید؛ برای تعداد از
|
||||||
<li>عدم نمایش اطلاعات مشتری: ابتدا بررسی کنید <code>person</code> تهی نباشد: <code>{% if person %} ... {% endif %}</code></li>
|
<code>commodityCount</code> استفاده کنید.
|
||||||
<li>بههمریختگی چاپ: از CSS ساده و سازگار با چاپ استفاده کنید؛ عرض جدولها و اندازه فونتها را کنترل کنید.</li>
|
</li>
|
||||||
|
<li>عدم نمایش اطلاعات مشتری: ابتدا بررسی کنید <code>person</code> تهی نباشد: <code>{% if person %} ... {%
|
||||||
|
endif %}</code></li>
|
||||||
|
<li>بههمریختگی چاپ: از CSS ساده و سازگار با چاپ استفاده کنید؛ عرض جدولها و اندازه فونتها را کنترل
|
||||||
|
کنید.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -282,90 +340,96 @@
|
||||||
|
|
||||||
<h4 class="mt-4">فروش (نمونه ساده)</h4>
|
<h4 class="mt-4">فروش (نمونه ساده)</h4>
|
||||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||||
{% if person %}<div>مشتری: {{ person.name }}</div>{% endif %}
|
{% 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"
|
||||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
cellpadding="6" style="margin-top:8px">
|
||||||
<tbody>
|
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||||
{% for r in rows %}
|
<tbody>
|
||||||
<tr>
|
{% for r in rows %}
|
||||||
<td>{{ loop.index }}</td>
|
<tr>
|
||||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ r.commodityCount }}</td>
|
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||||
<td>{{ r.des }}</td>
|
<td>{{ r.commodityCount }}</td>
|
||||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
<td>{{ r.des }}</td>
|
||||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||||
</tr>
|
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
|
||||||
{% endfor %}
|
number_format(0, '.', ',') }}{% endif %}</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{% endfor %}
|
||||||
{% if note %}<div style="margin-top:8px">{{ note | escape }}</div>{% endif %}
|
</tbody>
|
||||||
</div></code></pre>
|
</table>
|
||||||
|
{% if note %}<div style="margin-top:8px">{{ note | escape }}</div>{% endif %}
|
||||||
|
</div></code></pre>
|
||||||
|
|
||||||
<h4 class="mt-6">خرید (نمونه ساده)</h4>
|
<h4 class="mt-6">خرید (نمونه ساده)</h4>
|
||||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
||||||
{% if person %}<div>فروشنده: {{ person.name }}</div>{% endif %}
|
{% 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"
|
||||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
cellpadding="6" style="margin-top:8px">
|
||||||
<tbody>
|
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
||||||
{% for r in rows %}
|
<tbody>
|
||||||
<tr>
|
{% for r in rows %}
|
||||||
<td>{{ loop.index }}</td>
|
<tr>
|
||||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ r.commodityCount }}</td>
|
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||||
<td>{{ r.des }}</td>
|
<td>{{ r.commodityCount }}</td>
|
||||||
<td>{{ r.bd | number_format(0, '.', ',') }}</td>
|
<td>{{ r.des }}</td>
|
||||||
<td>{{ r.bs | number_format(0, '.', ',') }}</td>
|
<td>{{ r.bd | number_format(0, '.', ',') }}</td>
|
||||||
</tr>
|
<td>{{ r.bs | number_format(0, '.', ',') }}</td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
{% endfor %}
|
||||||
</table>
|
</tbody>
|
||||||
</div></code></pre>
|
</table>
|
||||||
|
</div></code></pre>
|
||||||
|
|
||||||
<h4 class="mt-6">برگشت از خرید (نمونه ساده)</h4>
|
<h4 class="mt-6">برگشت از خرید (نمونه ساده)</h4>
|
||||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
<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"
|
||||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
cellpadding="6" style="margin-top:8px">
|
||||||
<tbody>
|
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>بدهکار</th><th>بستانکار</th></tr></thead>
|
||||||
{% for r in rows %}
|
<tbody>
|
||||||
<tr>
|
{% for r in rows %}
|
||||||
<td>{{ loop.index }}</td>
|
<tr>
|
||||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ r.commodityCount }}</td>
|
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||||
<td>{{ r.des }}</td>
|
<td>{{ r.commodityCount }}</td>
|
||||||
<td>{{ r.bd | number_format(0, '.', ',') }}</td>
|
<td>{{ r.des }}</td>
|
||||||
<td>{{ r.bs | number_format(0, '.', ',') }}</td>
|
<td>{{ r.bd | number_format(0, '.', ',') }}</td>
|
||||||
</tr>
|
<td>{{ r.bs | number_format(0, '.', ',') }}</td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
{% endfor %}
|
||||||
</table>
|
</tbody>
|
||||||
</div></code></pre>
|
</table>
|
||||||
|
</div></code></pre>
|
||||||
|
|
||||||
<h4 class="mt-6">برگشت از فروش (نمونه ساده)</h4>
|
<h4 class="mt-6">برگشت از فروش (نمونه ساده)</h4>
|
||||||
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
<pre class="text-body-2" v-pre><code><div class="invoice-template">
|
||||||
<h3 style="text-align:center">{{ business.name }}</h3>
|
<h3 style="text-align:center">{{ business.name }}</h3>
|
||||||
<div>کد: {{ doc.code }} | تاریخ: {{ doc.date }}</div>
|
<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"
|
||||||
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
cellpadding="6" style="margin-top:8px">
|
||||||
<tbody>
|
<thead><tr><th>#</th><th>کالا</th><th>تعداد</th><th>شرح</th><th>مالیات</th><th>تخفیف</th></tr></thead>
|
||||||
{% for r in rows %}
|
<tbody>
|
||||||
<tr>
|
{% for r in rows %}
|
||||||
<td>{{ loop.index }}</td>
|
<tr>
|
||||||
<td>{{ r.commodity.name ?? '-' }}</td>
|
<td>{{ loop.index }}</td>
|
||||||
<td>{{ r.commodityCount }}</td>
|
<td>{{ r.commodity.name ?? '-' }}</td>
|
||||||
<td>{{ r.des }}</td>
|
<td>{{ r.commodityCount }}</td>
|
||||||
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
<td>{{ r.des }}</td>
|
||||||
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}</td>
|
<td>{{ r.tax | number_format(0, '.', ',') }}</td>
|
||||||
</tr>
|
<td>{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
|
||||||
{% endfor %}
|
number_format(0, '.', ',') }}{% endif %}</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
{% endfor %}
|
||||||
</div></code></pre>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div></code></pre>
|
||||||
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -448,16 +512,45 @@
|
||||||
</v-window-item>
|
</v-window-item>
|
||||||
</v-window>
|
</v-window>
|
||||||
</v-container>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MonacoEditor from '@/components/MonacoEditor.vue'
|
import MonacoEditor from '@/components/MonacoEditor.vue'
|
||||||
|
import TemplateDesigner from '@/components/TemplateDesigner.vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CustomInvoiceTemplateForm',
|
name: 'CustomInvoiceTemplateForm',
|
||||||
components: {
|
components: {
|
||||||
MonacoEditor
|
MonacoEditor,
|
||||||
|
TemplateDesigner
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -472,6 +565,14 @@ export default {
|
||||||
code: ''
|
code: ''
|
||||||
},
|
},
|
||||||
codeError: '',
|
codeError: '',
|
||||||
|
nameError: '',
|
||||||
|
codeValidationError: '',
|
||||||
|
snackbar: {
|
||||||
|
show: false,
|
||||||
|
message: '',
|
||||||
|
color: 'success',
|
||||||
|
timeout: 3000
|
||||||
|
},
|
||||||
publicOptions: [
|
publicOptions: [
|
||||||
{ title: 'خصوصی', value: false },
|
{ title: 'خصوصی', value: false },
|
||||||
{ title: 'عمومی', value: true }
|
{ title: 'عمومی', value: true }
|
||||||
|
@ -504,12 +605,26 @@ export default {
|
||||||
{ title: 'HTML', value: 'html' },
|
{ title: 'HTML', value: 'html' },
|
||||||
{ title: 'CSS', value: 'css' },
|
{ title: 'CSS', value: 'css' },
|
||||||
{ title: 'JavaScript', value: 'javascript' },
|
{ title: 'JavaScript', value: 'javascript' },
|
||||||
{ title: 'TypeScript', value: 'typescript' },
|
{ title: 'TypeScript', value: 'typescript' },
|
||||||
{ title: 'Twig', value: 'twig' }
|
{ title: 'Twig', value: 'twig' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
monacoOptions() {
|
||||||
return {
|
return {
|
||||||
fontSize: this.editorSettings.fontSize,
|
fontSize: this.editorSettings.fontSize,
|
||||||
|
@ -557,28 +672,116 @@ export default {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e?.response?.data?.message || 'خطا در بارگذاری قالب'
|
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() {
|
async saveTemplate() {
|
||||||
if (!this.templateData.name || !this.templateData.code) {
|
// Clear previous errors
|
||||||
this.$toast?.error('نام و کد قالب الزامی است.');
|
this.nameError = '';
|
||||||
|
this.codeValidationError = '';
|
||||||
|
|
||||||
|
// Validate form
|
||||||
|
if (!this.validateForm()) {
|
||||||
|
this.showErrorSnackbar('لطفاً خطاهای فرم را برطرف کنید');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: this.templateData.name,
|
name: this.templateData.name.trim(),
|
||||||
isPublic: this.templateData.isPublic,
|
isPublic: this.templateData.isPublic,
|
||||||
code: this.templateData.code,
|
code: this.templateData.code.trim(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (this.isEditMode && this.templateId) {
|
if (this.isEditMode && this.templateId) {
|
||||||
response = await axios.post(`/api/plugins/custominvoice/template/${this.templateId}`, payload);
|
response = await axios.post(`/api/plugins/custominvoice/template/${this.templateId}`, payload);
|
||||||
|
this.showSuccessSnackbar('قالب با موفقیت بروزرسانی شد');
|
||||||
} else {
|
} else {
|
||||||
response = await axios.post('/api/plugins/custominvoice/template', payload);
|
response = await axios.post('/api/plugins/custominvoice/template', payload);
|
||||||
|
this.showSuccessSnackbar('قالب با موفقیت ایجاد شد');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = response.data?.data || {};
|
const data = response.data?.data || {};
|
||||||
|
@ -586,11 +789,14 @@ export default {
|
||||||
this.templateId = data.id;
|
this.templateId = data.id;
|
||||||
this.isEditMode = true;
|
this.isEditMode = true;
|
||||||
}
|
}
|
||||||
this.$toast?.success('قالب با موفقیت ذخیره شد.');
|
|
||||||
this.$router.push('/acc/plugins/custominvoice/templates');
|
// Redirect after a short delay to show success message
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push('/acc/plugins/custominvoice/templates');
|
||||||
|
}, 1500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const msg = e?.response?.data?.message || 'خطا در ذخیره قالب';
|
const msg = e?.response?.data?.message || 'خطا در ذخیره قالب';
|
||||||
this.$toast?.error(msg);
|
this.showErrorSnackbar(msg);
|
||||||
} finally {
|
} finally {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}
|
}
|
||||||
|
@ -1102,4 +1308,44 @@ export default {
|
||||||
font-size: 0.9rem;
|
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>
|
</style>
|
Loading…
Reference in a new issue