first release of custome invoice designer

This commit is contained in:
Hesabix 2025-08-15 22:19:00 +00:00
parent 29625b7afa
commit 51d68b9874
6 changed files with 3426 additions and 173 deletions

View 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>

View 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>

View 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>
```
این سیستم به شما امکان می‌دهد تا قالب‌های حرفه‌ای و سفارشی برای فاکتورهای خود ایجاد کنید.

File diff suppressed because it is too large Load diff

View 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>

View file

@ -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>
@ -146,10 +189,11 @@
<div class="code-example" variant="outlined">
<v-card-text class="font-family-monospace">
<pre class="text-body-2" v-pre><code>{% if person %}
&lt;p&gt;مشتری: {{ person.name | escape }}&lt;/p&gt;
{% endif %}
&lt;p&gt;مشتری: {{ person.name | escape }}&lt;/p&gt;
{% endif %}
&lt;table width=&quot;100%&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; border=&quot;1&quot;&gt;
&lt;table width=&quot;100%&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot;
border=&quot;1&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
@ -178,8 +222,8 @@
&lt;/tr&gt;
{% endfor %}
&lt;/tbody&gt;
&lt;/table&gt;
</code></pre>
&lt;/table&gt;
</code></pre>
</v-card-text>
</div>
</v-card-text>
@ -197,7 +241,8 @@
&lt;div&gt;شماره فاکتور: {{ doc.code }} | تاریخ: {{ doc.date }}&lt;/div&gt;
{% if person %}&lt;div&gt;مشتری: {{ person.name }} | موبایل: {{ person.mobile }}&lt;/div&gt;{% endif %}
&lt;table width=&quot;100%&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; border=&quot;1&quot; style=&quot;margin-top:10px&quot;&gt;
&lt;table width=&quot;100%&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot;
border=&quot;1&quot; style=&quot;margin-top:10px&quot;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;کالا&lt;/th&gt;&lt;th&gt;تعداد&lt;/th&gt;&lt;th&gt;شرح&lt;/th&gt;&lt;th&gt;مالیات&lt;/th&gt;&lt;th&gt;تخفیف&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
{% for r in rows %}
@ -207,22 +252,26 @@
&lt;td&gt;{{ r.commodityCount }}&lt;/td&gt;
&lt;td&gt;{{ r.des }}&lt;/td&gt;
&lt;td&gt;{{ r.tax | number_format(0, '.', ',') }}&lt;/td&gt;
&lt;td&gt;{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}&lt;/td&gt;
&lt;td&gt;{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
number_format(0, '.', ',') }}{% endif %}&lt;/td&gt;
&lt;/tr&gt;
{% endfor %}
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div style=&quot;margin-top:10px;text-align:right&quot;&gt;
{% if discount %}&lt;div&gt;جمع تخفیف: {{ discount | number_format(0, '.', ',') }}&lt;/div&gt;{% endif %}
{% if transfer %}&lt;div&gt;هزینه ارسال/انتقال: {{ transfer | number_format(0, '.', ',') }}&lt;/div&gt;{% endif %}
{% if discount %}&lt;div&gt;جمع تخفیف: {{ discount | number_format(0, '.', ',') }}&lt;/div&gt;{% endif
%}
{% if transfer %}&lt;div&gt;هزینه ارسال/انتقال: {{ transfer | number_format(0, '.', ',')
}}&lt;/div&gt;{% endif %}
&lt;/div&gt;
{% if note %}
&lt;div style=&quot;margin-top:12px;border-top:1px dashed #ccc;padding-top:8px&quot;&gt;{{ note | escape }}&lt;/div&gt;
&lt;div style=&quot;margin-top:12px;border-top:1px dashed #ccc;padding-top:8px&quot;&gt;{{ note | escape
}}&lt;/div&gt;
{% endif %}
&lt;/div&gt;
</code></pre>
&lt;/div&gt;
</code></pre>
</v-card-text>
</v-card>
@ -235,9 +284,10 @@
<v-card-text>
<p>در خرید/برگشت از خرید، مقادیر ردیفها شامل <code>bs</code> و <code>bd</code> نیز هستند.</p>
<pre class="text-body-2" v-pre><code>{% for r in rows %}
&lt;div&gt;{{ loop.index }}. {{ r.commodity.name ?? '-' }} | تعداد: {{ r.commodityCount }} | شرح: {{ r.des }} | بدهکار: {{ r.bd }} | بستانکار: {{ r.bs }}&lt;/div&gt;
{% endfor %}
</code></pre>
&lt;div&gt;{{ loop.index }}. {{ r.commodity.name ?? '-' }} | تعداد: {{ r.commodityCount }} | شرح: {{
r.des }} | بدهکار: {{ r.bd }} | بستانکار: {{ r.bs }}&lt;/div&gt;
{% endfor %}
</code></pre>
</v-card-text>
</v-card>
@ -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 @@
&lt;h3 style=&quot;text-align:center&quot;&gt;{{ business.name }}&lt;/h3&gt;
&lt;div&gt;کد: {{ doc.code }} | تاریخ: {{ doc.date }}&lt;/div&gt;
{% if person %}&lt;div&gt;مشتری: {{ person.name }}&lt;/div&gt;{% endif %}
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot;
cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;کالا&lt;/th&gt;&lt;th&gt;تعداد&lt;/th&gt;&lt;th&gt;شرح&lt;/th&gt;&lt;th&gt;مالیات&lt;/th&gt;&lt;th&gt;تخفیف&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
{% for r in rows %}
@ -295,20 +354,22 @@
&lt;td&gt;{{ r.commodityCount }}&lt;/td&gt;
&lt;td&gt;{{ r.des }}&lt;/td&gt;
&lt;td&gt;{{ r.tax | number_format(0, '.', ',') }}&lt;/td&gt;
&lt;td&gt;{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}&lt;/td&gt;
&lt;td&gt;{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
number_format(0, '.', ',') }}{% endif %}&lt;/td&gt;
&lt;/tr&gt;
{% endfor %}
&lt;/tbody&gt;
&lt;/table&gt;
{% if note %}&lt;div style=&quot;margin-top:8px&quot;&gt;{{ note | escape }}&lt;/div&gt;{% endif %}
&lt;/div&gt;</code></pre>
&lt;/div&gt;</code></pre>
<h4 class="mt-6">خرید (نمونه ساده)</h4>
<pre class="text-body-2" v-pre><code>&lt;div class=&quot;invoice-template&quot;&gt;
&lt;h3 style=&quot;text-align:center&quot;&gt;{{ business.name }}&lt;/h3&gt;
&lt;div&gt;کد: {{ doc.code }} | تاریخ: {{ doc.date }}&lt;/div&gt;
{% if person %}&lt;div&gt;فروشنده: {{ person.name }}&lt;/div&gt;{% endif %}
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot;
cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;کالا&lt;/th&gt;&lt;th&gt;تعداد&lt;/th&gt;&lt;th&gt;شرح&lt;/th&gt;&lt;th&gt;بدهکار&lt;/th&gt;&lt;th&gt;بستانکار&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
{% for r in rows %}
@ -323,13 +384,14 @@
{% endfor %}
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</code></pre>
&lt;/div&gt;</code></pre>
<h4 class="mt-6">برگشت از خرید (نمونه ساده)</h4>
<pre class="text-body-2" v-pre><code>&lt;div class=&quot;invoice-template&quot;&gt;
&lt;h3 style=&quot;text-align:center&quot;&gt;{{ business.name }}&lt;/h3&gt;
&lt;div&gt;کد: {{ doc.code }} | تاریخ: {{ doc.date }}&lt;/div&gt;
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot;
cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;کالا&lt;/th&gt;&lt;th&gt;تعداد&lt;/th&gt;&lt;th&gt;شرح&lt;/th&gt;&lt;th&gt;بدهکار&lt;/th&gt;&lt;th&gt;بستانکار&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
{% for r in rows %}
@ -344,13 +406,14 @@
{% endfor %}
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</code></pre>
&lt;/div&gt;</code></pre>
<h4 class="mt-6">برگشت از فروش (نمونه ساده)</h4>
<pre class="text-body-2" v-pre><code>&lt;div class=&quot;invoice-template&quot;&gt;
&lt;h3 style=&quot;text-align:center&quot;&gt;{{ business.name }}&lt;/h3&gt;
&lt;div&gt;کد: {{ doc.code }} | تاریخ: {{ doc.date }}&lt;/div&gt;
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot; cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;table width=&quot;100%&quot; border=&quot;1&quot; cellspacing=&quot;0&quot;
cellpadding=&quot;6&quot; style=&quot;margin-top:8px&quot;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;/th&gt;&lt;th&gt;کالا&lt;/th&gt;&lt;th&gt;تعداد&lt;/th&gt;&lt;th&gt;شرح&lt;/th&gt;&lt;th&gt;مالیات&lt;/th&gt;&lt;th&gt;تخفیف&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
{% for r in rows %}
@ -360,12 +423,13 @@
&lt;td&gt;{{ r.commodityCount }}&lt;/td&gt;
&lt;td&gt;{{ r.des }}&lt;/td&gt;
&lt;td&gt;{{ r.tax | number_format(0, '.', ',') }}&lt;/td&gt;
&lt;td&gt;{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount | number_format(0, '.', ',') }}{% endif %}&lt;/td&gt;
&lt;td&gt;{% if r.showPercentDiscount %}{{ r.discountPercent }}%{% else %}{{ r.discount |
number_format(0, '.', ',') }}{% endif %}&lt;/td&gt;
&lt;/tr&gt;
{% endfor %}
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</code></pre>
&lt;/div&gt;</code></pre>
</v-card-text>
</v-card>
@ -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>