mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
to library
This commit is contained in:
@@ -1,28 +1,463 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import EditorCanvas from './components/editor/EditorCanvas.vue'
|
import { DreportEditor } from './lib'
|
||||||
import ToolboxPanel from './components/panels/ToolboxPanel.vue'
|
import type { Template, JsonSchema } from './lib'
|
||||||
import PropertiesPanel from './components/panels/PropertiesPanel.vue'
|
|
||||||
import { useTemplateStore } from './stores/template'
|
|
||||||
import { useEditorStore } from './stores/editor'
|
|
||||||
|
|
||||||
const templateStore = useTemplateStore()
|
// --- Full Invoice Schema ---
|
||||||
const editorStore = useEditorStore()
|
|
||||||
|
|
||||||
|
const invoiceSchema: JsonSchema = {
|
||||||
|
$id: 'fatura-schema',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
firma: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Firma',
|
||||||
|
properties: {
|
||||||
|
unvan: { type: 'string', title: 'Firma Unvani' },
|
||||||
|
vergiDairesi: { type: 'string', title: 'Vergi Dairesi' },
|
||||||
|
vergiNo: { type: 'string', title: 'Vergi No' },
|
||||||
|
adres: { type: 'string', title: 'Adres' },
|
||||||
|
il: { type: 'string', title: 'Il' },
|
||||||
|
telefon: { type: 'string', title: 'Telefon' },
|
||||||
|
email: { type: 'string', title: 'E-posta' },
|
||||||
|
logo: { type: 'string', title: 'Logo', format: 'image' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fatura: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Fatura',
|
||||||
|
properties: {
|
||||||
|
no: { type: 'string', title: 'Fatura No' },
|
||||||
|
seri: { type: 'string', title: 'Seri' },
|
||||||
|
tarih: { type: 'string', title: 'Duzenleme Tarihi', format: 'date' },
|
||||||
|
vadeTarihi: { type: 'string', title: 'Vade Tarihi', format: 'date' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
musteri: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Musteri',
|
||||||
|
properties: {
|
||||||
|
unvan: { type: 'string', title: 'Musteri Unvani' },
|
||||||
|
vergiDairesi: { type: 'string', title: 'Vergi Dairesi' },
|
||||||
|
vergiNo: { type: 'string', title: 'Vergi No' },
|
||||||
|
adres: { type: 'string', title: 'Adres' },
|
||||||
|
il: { type: 'string', title: 'Il' },
|
||||||
|
telefon: { type: 'string', title: 'Telefon' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
kalemler: {
|
||||||
|
type: 'array',
|
||||||
|
title: 'Fatura Kalemleri',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
siraNo: { type: 'integer', title: 'Sira No' },
|
||||||
|
adi: { type: 'string', title: 'Urun / Hizmet Adi' },
|
||||||
|
miktar: { type: 'number', title: 'Miktar' },
|
||||||
|
birim: { type: 'string', title: 'Birim' },
|
||||||
|
birimFiyat: { type: 'number', title: 'Birim Fiyat', format: 'currency' },
|
||||||
|
tutar: { type: 'number', title: 'Tutar', format: 'currency' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toplamlar: {
|
||||||
|
type: 'object',
|
||||||
|
title: 'Toplamlar',
|
||||||
|
properties: {
|
||||||
|
araToplam: { type: 'number', title: 'Ara Toplam', format: 'currency' },
|
||||||
|
kdvOrani: { type: 'number', title: 'KDV Orani', format: 'percentage' },
|
||||||
|
kdv: { type: 'number', title: 'KDV', format: 'currency' },
|
||||||
|
genelToplam: { type: 'number', title: 'Genel Toplam', format: 'currency' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sample Invoice Data ---
|
||||||
|
|
||||||
|
const sampleData: Record<string, unknown> = {
|
||||||
|
firma: {
|
||||||
|
unvan: 'Teknova Yazilim A.S.',
|
||||||
|
vergiDairesi: 'Besiktas',
|
||||||
|
vergiNo: '1234567890',
|
||||||
|
adres: 'Levent Mah. Inovasyon Sk. No:42 Kat:5',
|
||||||
|
il: 'Istanbul',
|
||||||
|
telefon: '+90 212 555 0042',
|
||||||
|
email: 'info@teknova.com.tr',
|
||||||
|
},
|
||||||
|
fatura: {
|
||||||
|
no: 'FTR-2026-001547',
|
||||||
|
seri: 'A',
|
||||||
|
tarih: '2026-03-29',
|
||||||
|
vadeTarihi: '2026-04-28',
|
||||||
|
},
|
||||||
|
musteri: {
|
||||||
|
unvan: 'Anadolu Lojistik Ltd. Sti.',
|
||||||
|
vergiDairesi: 'Kadikoy',
|
||||||
|
vergiNo: '9876543210',
|
||||||
|
adres: 'Caferaga Mah. Moda Cd. No:18',
|
||||||
|
il: 'Istanbul',
|
||||||
|
telefon: '+90 216 444 0018',
|
||||||
|
},
|
||||||
|
kalemler: [
|
||||||
|
{ siraNo: 1, adi: 'Web Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 45000, tutar: 45000 },
|
||||||
|
{ siraNo: 2, adi: 'Mobil Uygulama Gelistirme', miktar: 1, birim: 'Adet', birimFiyat: 35000, tutar: 35000 },
|
||||||
|
{ siraNo: 3, adi: 'UI/UX Tasarim Hizmeti', miktar: 40, birim: 'Saat', birimFiyat: 750, tutar: 30000 },
|
||||||
|
{ siraNo: 4, adi: 'Sunucu Bakim Sozlesmesi (Yillik)', miktar: 1, birim: 'Adet', birimFiyat: 12000, tutar: 12000 },
|
||||||
|
{ siraNo: 5, adi: 'SSL Sertifikasi', miktar: 3, birim: 'Adet', birimFiyat: 500, tutar: 1500 },
|
||||||
|
],
|
||||||
|
toplamlar: {
|
||||||
|
araToplam: 123500,
|
||||||
|
kdvOrani: 20,
|
||||||
|
kdv: 24700,
|
||||||
|
genelToplam: 148200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Default Invoice Template ---
|
||||||
|
|
||||||
|
const sz = {
|
||||||
|
fixed: (v: number) => ({ type: 'fixed' as const, value: v }),
|
||||||
|
auto: () => ({ type: 'auto' as const }),
|
||||||
|
fr: (v = 1) => ({ type: 'fr' as const, value: v }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultInvoiceTemplate: Template = {
|
||||||
|
id: 'tpl_fatura_demo',
|
||||||
|
name: 'Standart Fatura',
|
||||||
|
page: { width: 210, height: 297 },
|
||||||
|
fonts: ['Noto Sans'],
|
||||||
|
root: {
|
||||||
|
id: 'root',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
direction: 'column',
|
||||||
|
gap: 5,
|
||||||
|
padding: { top: 15, right: 15, bottom: 15, left: 15 },
|
||||||
|
align: 'stretch',
|
||||||
|
justify: 'start',
|
||||||
|
style: {},
|
||||||
|
children: [
|
||||||
|
// --- Header Row ---
|
||||||
|
{
|
||||||
|
id: 'c_header',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
direction: 'row',
|
||||||
|
gap: 5,
|
||||||
|
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
|
align: 'start',
|
||||||
|
justify: 'space-between',
|
||||||
|
style: {},
|
||||||
|
children: [
|
||||||
|
// Firma bilgileri (sol)
|
||||||
|
{
|
||||||
|
id: 'c_firma',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
direction: 'column',
|
||||||
|
gap: 1,
|
||||||
|
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
|
align: 'start',
|
||||||
|
justify: 'start',
|
||||||
|
style: {},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'el_firma_unvan',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 14, fontWeight: 'bold', color: '#1a1a1a' },
|
||||||
|
binding: { type: 'scalar', path: 'firma.unvan' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_firma_adres',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
binding: { type: 'scalar', path: 'firma.adres' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_firma_il',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
binding: { type: 'scalar', path: 'firma.il' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_firma_telefon',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
content: 'Tel: ',
|
||||||
|
binding: { type: 'scalar', path: 'firma.telefon' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_firma_vd',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
content: 'VD: ',
|
||||||
|
binding: { type: 'scalar', path: 'firma.vergiDairesi' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_firma_vn',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
content: 'VN: ',
|
||||||
|
binding: { type: 'scalar', path: 'firma.vergiNo' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Fatura basligi (sag)
|
||||||
|
{
|
||||||
|
id: 'c_fatura_baslik',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
direction: 'column',
|
||||||
|
gap: 2,
|
||||||
|
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
||||||
|
align: 'end',
|
||||||
|
justify: 'start',
|
||||||
|
style: {},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'el_fatura_baslik',
|
||||||
|
type: 'static_text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 18, fontWeight: 'bold', color: '#1a1a1a', align: 'right' },
|
||||||
|
content: 'FATURA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_fatura_no',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 10, color: '#333333', align: 'right' },
|
||||||
|
content: 'No: ',
|
||||||
|
binding: { type: 'scalar', path: 'fatura.no' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_fatura_tarih',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 10, color: '#333333', align: 'right' },
|
||||||
|
content: 'Tarih: ',
|
||||||
|
binding: { type: 'scalar', path: 'fatura.tarih' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_fatura_vade',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 10, color: '#333333', align: 'right' },
|
||||||
|
content: 'Vade: ',
|
||||||
|
binding: { type: 'scalar', path: 'fatura.vadeTarihi' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// --- Separator ---
|
||||||
|
{
|
||||||
|
id: 'el_cizgi_1',
|
||||||
|
type: 'line',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
style: { strokeColor: '#cccccc', strokeWidth: 0.5 },
|
||||||
|
},
|
||||||
|
// --- Musteri Bilgileri ---
|
||||||
|
{
|
||||||
|
id: 'c_musteri',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
direction: 'column',
|
||||||
|
gap: 1,
|
||||||
|
padding: { top: 3, right: 5, bottom: 3, left: 5 },
|
||||||
|
align: 'start',
|
||||||
|
justify: 'start',
|
||||||
|
style: { backgroundColor: '#f8f9fa', borderColor: '#e9ecef', borderWidth: 0.5 },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'el_musteri_baslik',
|
||||||
|
type: 'static_text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, fontWeight: 'bold', color: '#666666' },
|
||||||
|
content: 'MUSTERI BILGILERI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_musteri_unvan',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 11, fontWeight: 'bold', color: '#1a1a1a' },
|
||||||
|
binding: { type: 'scalar', path: 'musteri.unvan' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_musteri_adres',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
binding: { type: 'scalar', path: 'musteri.adres' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_musteri_vd',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
content: 'VD: ',
|
||||||
|
binding: { type: 'scalar', path: 'musteri.vergiDairesi' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_musteri_vn',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 9, color: '#555555' },
|
||||||
|
content: 'VN: ',
|
||||||
|
binding: { type: 'scalar', path: 'musteri.vergiNo' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// --- Kalemler Tablosu ---
|
||||||
|
{
|
||||||
|
id: 'el_tablo',
|
||||||
|
type: 'repeating_table',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
dataSource: { type: 'array', path: 'kalemler' },
|
||||||
|
columns: [
|
||||||
|
{ id: 'col_sira', field: 'siraNo', title: '#', width: sz.fixed(10), align: 'center' },
|
||||||
|
{ id: 'col_adi', field: 'adi', title: 'Urun / Hizmet', width: sz.fr(), align: 'left' },
|
||||||
|
{ id: 'col_miktar', field: 'miktar', title: 'Miktar', width: sz.fixed(18), align: 'right' },
|
||||||
|
{ id: 'col_birim', field: 'birim', title: 'Birim', width: sz.fixed(18), align: 'center' },
|
||||||
|
{ id: 'col_fiyat', field: 'birimFiyat', title: 'Birim Fiyat', width: sz.fixed(28), align: 'right', format: 'currency' as const },
|
||||||
|
{ id: 'col_tutar', field: 'tutar', title: 'Tutar', width: sz.fixed(28), align: 'right', format: 'currency' as const },
|
||||||
|
],
|
||||||
|
style: {
|
||||||
|
fontSize: 9,
|
||||||
|
headerFontSize: 9,
|
||||||
|
headerBg: '#1e293b',
|
||||||
|
headerColor: '#ffffff',
|
||||||
|
zebraOdd: '#ffffff',
|
||||||
|
zebraEven: '#f8fafc',
|
||||||
|
borderColor: '#e2e8f0',
|
||||||
|
borderWidth: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// --- Toplamlar ---
|
||||||
|
{
|
||||||
|
id: 'c_toplamlar_row',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
direction: 'row',
|
||||||
|
gap: 0,
|
||||||
|
padding: { top: 3, right: 0, bottom: 0, left: 0 },
|
||||||
|
align: 'start',
|
||||||
|
justify: 'end',
|
||||||
|
style: {},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'c_toplamlar',
|
||||||
|
type: 'container',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fixed(80), height: sz.auto() },
|
||||||
|
direction: 'column',
|
||||||
|
gap: 2,
|
||||||
|
padding: { top: 3, right: 5, bottom: 3, left: 5 },
|
||||||
|
align: 'stretch',
|
||||||
|
justify: 'start',
|
||||||
|
style: { borderColor: '#e2e8f0', borderWidth: 0.5 },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'el_ara_toplam',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 10, color: '#333333', align: 'right' },
|
||||||
|
content: 'Ara Toplam: ',
|
||||||
|
binding: { type: 'scalar', path: 'toplamlar.araToplam' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_kdv',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 10, color: '#333333', align: 'right' },
|
||||||
|
content: 'KDV (%20): ',
|
||||||
|
binding: { type: 'scalar', path: 'toplamlar.kdv' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_cizgi_2',
|
||||||
|
type: 'line',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.fr(), height: sz.auto() },
|
||||||
|
style: { strokeColor: '#1e293b', strokeWidth: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'el_genel_toplam',
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 12, fontWeight: 'bold', color: '#1a1a1a', align: 'right' },
|
||||||
|
content: 'GENEL TOPLAM: ',
|
||||||
|
binding: { type: 'scalar', path: 'toplamlar.genelToplam' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LocalStorage persistence ---
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'dreport-template'
|
||||||
|
|
||||||
|
function loadFromLocalStorage(): Template | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (!raw) return null
|
||||||
|
return JSON.parse(raw) as Template
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = ref<Template>(loadFromLocalStorage() ?? structuredClone(defaultInvoiceTemplate))
|
||||||
|
|
||||||
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
watch(template, (val) => {
|
||||||
|
if (saveTimeout) clearTimeout(saveTimeout)
|
||||||
|
saveTimeout = setTimeout(() => {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
|
||||||
|
}, 500)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// --- Editor ref ---
|
||||||
|
|
||||||
|
const editorRef = ref<InstanceType<typeof DreportEditor> | null>(null)
|
||||||
const pdfLoading = ref(false)
|
const pdfLoading = ref(false)
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
function exportTemplate() {
|
|
||||||
const json = templateStore.exportTemplate()
|
|
||||||
const blob = new Blob([json], { type: 'application/json' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.href = url
|
|
||||||
a.download = `${templateStore.template.name || 'sablon'}.json`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerImport() {
|
function triggerImport() {
|
||||||
fileInputRef.value?.click()
|
fileInputRef.value?.click()
|
||||||
}
|
}
|
||||||
@@ -34,7 +469,7 @@ function onImportFile(e: Event) {
|
|||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
try {
|
try {
|
||||||
templateStore.importTemplate(reader.result as string)
|
editorRef.value?.importTemplate(reader.result as string)
|
||||||
} catch {
|
} catch {
|
||||||
alert('Gecersiz sablon dosyasi')
|
alert('Gecersiz sablon dosyasi')
|
||||||
}
|
}
|
||||||
@@ -43,71 +478,40 @@ function onImportFile(e: Event) {
|
|||||||
input.value = ''
|
input.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadPdf() {
|
function exportTemplate() {
|
||||||
pdfLoading.value = true
|
const json = editorRef.value?.exportTemplate()
|
||||||
try {
|
if (!json) return
|
||||||
const res = await fetch('http://localhost:3001/api/render', {
|
const blob = new Blob([json], { type: 'application/json' })
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
template: templateStore.template,
|
|
||||||
data: templateStore.mockData,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
const text = await res.text()
|
|
||||||
alert('PDF olusturulamadi: ' + text)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const blob = await res.blob()
|
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
a.href = url
|
a.href = url
|
||||||
a.download = `${templateStore.template.name || 'belge'}.pdf`
|
a.download = `${template.value.name || 'sablon'}.json`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadPdf() {
|
||||||
|
pdfLoading.value = true
|
||||||
|
try {
|
||||||
|
const blob = await editorRef.value?.exportPdf()
|
||||||
|
if (!blob) return
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${template.value.name || 'belge'}.pdf`
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Backend baglantisi kurulamadi. Sunucu calisiyor mu?')
|
alert(err instanceof Error ? err.message : 'PDF olusturulamadi')
|
||||||
} finally {
|
} finally {
|
||||||
pdfLoading.value = false
|
pdfLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
function resetTemplate() {
|
||||||
// Delete / Backspace — seçili elemanı sil
|
template.value = structuredClone(defaultInvoiceTemplate)
|
||||||
if ((e.key === 'Delete' || e.key === 'Backspace') && editorStore.selectedElementId) {
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
// Input/textarea içindeyse yoksay
|
|
||||||
const tag = (e.target as HTMLElement)?.tagName
|
|
||||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
const id = editorStore.selectedElementId
|
|
||||||
if (id && id !== 'root') {
|
|
||||||
editorStore.clearSelection()
|
|
||||||
templateStore.removeElement(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape — seçimi temizle
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
editorStore.clearSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+Z — undo
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
templateStore.undo()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ctrl+Shift+Z — redo
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
templateStore.redo()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => window.addEventListener('keydown', onKeyDown))
|
|
||||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -117,21 +521,20 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown))
|
|||||||
<span class="app-header__subtitle">Belge Tasarim Araci</span>
|
<span class="app-header__subtitle">Belge Tasarim Araci</span>
|
||||||
<div style="flex: 1"></div>
|
<div style="flex: 1"></div>
|
||||||
<input ref="fileInputRef" type="file" accept=".json" style="display: none" @change="onImportFile" />
|
<input ref="fileInputRef" type="file" accept=".json" style="display: none" @change="onImportFile" />
|
||||||
|
<button class="header-btn header-btn--secondary" @click="resetTemplate">Sifirla</button>
|
||||||
<button class="header-btn header-btn--secondary" @click="triggerImport">Yukle</button>
|
<button class="header-btn header-btn--secondary" @click="triggerImport">Yukle</button>
|
||||||
<button class="header-btn header-btn--secondary" @click="exportTemplate">Kaydet</button>
|
<button class="header-btn header-btn--secondary" @click="exportTemplate">Kaydet</button>
|
||||||
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf">
|
<button class="header-btn" :disabled="pdfLoading" @click="downloadPdf">
|
||||||
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Indir' }}
|
{{ pdfLoading ? 'Hazirlaniyor...' : 'PDF Indir' }}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<main class="app-main">
|
<DreportEditor
|
||||||
<aside class="app-sidebar app-sidebar--left">
|
ref="editorRef"
|
||||||
<ToolboxPanel />
|
v-model="template"
|
||||||
</aside>
|
:schema="invoiceSchema"
|
||||||
<EditorCanvas />
|
:data="sampleData"
|
||||||
<aside class="app-sidebar app-sidebar--right">
|
:config="{ apiBaseUrl: 'http://localhost:3001/api' }"
|
||||||
<PropertiesPanel />
|
/>
|
||||||
</aside>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -196,23 +599,4 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown))
|
|||||||
background: #334155;
|
background: #334155;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar {
|
|
||||||
width: 260px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border-right: 1px solid #e2e8f0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar--right {
|
|
||||||
border-right: none;
|
|
||||||
border-left: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
|
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useTemplateStore } from '../../stores/template'
|
import { useTemplateStore } from '../../stores/template'
|
||||||
import { useEditorStore } from '../../stores/editor'
|
import { useEditorStore } from '../../stores/editor'
|
||||||
@@ -7,6 +7,12 @@ import { useTypstCompiler } from '../../composables/useTypstCompiler'
|
|||||||
import TypstSvgLayer from './TypstSvgLayer.vue'
|
import TypstSvgLayer from './TypstSvgLayer.vue'
|
||||||
import InteractionOverlay from './InteractionOverlay.vue'
|
import InteractionOverlay from './InteractionOverlay.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
handleErrors?: boolean
|
||||||
|
}>(), {
|
||||||
|
handleErrors: true,
|
||||||
|
})
|
||||||
|
|
||||||
const templateStore = useTemplateStore()
|
const templateStore = useTemplateStore()
|
||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
const { template, mockData } = storeToRefs(templateStore)
|
const { template, mockData } = storeToRefs(templateStore)
|
||||||
@@ -14,9 +20,15 @@ const { template, mockData } = storeToRefs(templateStore)
|
|||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const containerWidth = ref(800)
|
const containerWidth = ref(800)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'compile-error': [error: string | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
// Typst compiler — template + data'yı worker'a gönderir, WASM ile derlenir
|
// Typst compiler — template + data'yı worker'a gönderir, WASM ile derlenir
|
||||||
const { svg, error, compiling, layout, dispose } = useTypstCompiler(template, mockData)
|
const { svg, error, compiling, layout, dispose } = useTypstCompiler(template, mockData)
|
||||||
|
|
||||||
|
watch(error, (val) => emit('compile-error', val))
|
||||||
|
|
||||||
// mm → px dönüşüm katsayısı
|
// mm → px dönüşüm katsayısı
|
||||||
const scale = computed(() => {
|
const scale = computed(() => {
|
||||||
return (containerWidth.value / templateStore.template.page.width) * editorStore.zoom
|
return (containerWidth.value / templateStore.template.page.width) * editorStore.zoom
|
||||||
@@ -141,7 +153,7 @@ function onPointerUp(e: PointerEvent) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sabit overlay'ler — scroll dışında -->
|
<!-- Sabit overlay'ler — scroll dışında -->
|
||||||
<div v-if="error" class="editor-canvas__error">
|
<div v-if="props.handleErrors && error" class="editor-canvas__error">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="compiling" class="editor-canvas__compiling">
|
<div v-if="compiling" class="editor-canvas__compiling">
|
||||||
|
|||||||
196
frontend/src/lib/DreportEditor.vue
Normal file
196
frontend/src/lib/DreportEditor.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import type { Template } from '../core/types'
|
||||||
|
import type { JsonSchema } from '../core/schema-parser'
|
||||||
|
import { useTemplateStore } from '../stores/template'
|
||||||
|
import { useSchemaStore } from '../stores/schema'
|
||||||
|
import { useEditorStore } from '../stores/editor'
|
||||||
|
import EditorCanvas from '../components/editor/EditorCanvas.vue'
|
||||||
|
import ToolboxPanel from '../components/panels/ToolboxPanel.vue'
|
||||||
|
import PropertiesPanel from '../components/panels/PropertiesPanel.vue'
|
||||||
|
|
||||||
|
export interface DreportEditorConfig {
|
||||||
|
apiBaseUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
schema: JsonSchema
|
||||||
|
modelValue: Template
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
config?: DreportEditorConfig
|
||||||
|
handleErrors?: boolean
|
||||||
|
}>(), {
|
||||||
|
handleErrors: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: Template]
|
||||||
|
'compile-error': [error: string | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const templateStore = useTemplateStore()
|
||||||
|
const schemaStore = useSchemaStore()
|
||||||
|
const editorStore = useEditorStore()
|
||||||
|
|
||||||
|
// --- Prop ↔ Store sync ---
|
||||||
|
|
||||||
|
let syncing = false
|
||||||
|
|
||||||
|
// Schema sync
|
||||||
|
onMounted(() => {
|
||||||
|
schemaStore.setSchema(props.schema)
|
||||||
|
syncing = true
|
||||||
|
templateStore.template = JSON.parse(JSON.stringify(props.modelValue))
|
||||||
|
nextTick(() => { syncing = false })
|
||||||
|
templateStore.setOverrideData(props.data ?? null)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.schema, (val) => {
|
||||||
|
schemaStore.setSchema(val)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(() => props.data, (val) => {
|
||||||
|
templateStore.setOverrideData(val ?? null)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// Template: prop → store (only on reference change from parent)
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (syncing) return
|
||||||
|
syncing = true
|
||||||
|
templateStore.template = JSON.parse(JSON.stringify(val))
|
||||||
|
nextTick(() => { syncing = false })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Template: store → emit
|
||||||
|
watch(() => templateStore.template, (val) => {
|
||||||
|
if (syncing) return
|
||||||
|
syncing = true
|
||||||
|
emit('update:modelValue', JSON.parse(JSON.stringify(val)))
|
||||||
|
nextTick(() => { syncing = false })
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// --- Error forwarding ---
|
||||||
|
|
||||||
|
function onCompileError(error: string | null) {
|
||||||
|
emit('compile-error', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Keyboard shortcuts ---
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
const tag = (e.target as HTMLElement)?.tagName
|
||||||
|
const isInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
|
||||||
|
|
||||||
|
// Delete / Backspace
|
||||||
|
if ((e.key === 'Delete' || e.key === 'Backspace') && editorStore.selectedElementId) {
|
||||||
|
if (isInput) return
|
||||||
|
e.preventDefault()
|
||||||
|
const id = editorStore.selectedElementId
|
||||||
|
if (id && id !== 'root') {
|
||||||
|
editorStore.clearSelection()
|
||||||
|
templateStore.removeElement(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
editorStore.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Z — undo
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
templateStore.undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+Shift+Z — redo
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
templateStore.redo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => window.addEventListener('keydown', onKeyDown))
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown))
|
||||||
|
|
||||||
|
// --- Exposed API ---
|
||||||
|
|
||||||
|
function getTemplate(): Template {
|
||||||
|
return JSON.parse(JSON.stringify(templateStore.template))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTemplate(tpl: Template) {
|
||||||
|
templateStore.template = JSON.parse(JSON.stringify(tpl))
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportTemplate(): string {
|
||||||
|
return templateStore.exportTemplate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function importTemplate(json: string) {
|
||||||
|
templateStore.importTemplate(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportPdf(): Promise<Blob> {
|
||||||
|
const baseUrl = props.config?.apiBaseUrl ?? '/api'
|
||||||
|
const res = await fetch(`${baseUrl}/render`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
template: templateStore.template,
|
||||||
|
data: templateStore.mockData,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text()
|
||||||
|
throw new Error(`PDF olusturulamadi: ${text}`)
|
||||||
|
}
|
||||||
|
return res.blob()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getTemplate,
|
||||||
|
setTemplate,
|
||||||
|
exportTemplate,
|
||||||
|
importTemplate,
|
||||||
|
exportPdf,
|
||||||
|
undo: () => templateStore.undo(),
|
||||||
|
redo: () => templateStore.redo(),
|
||||||
|
canUndo: templateStore.canUndo,
|
||||||
|
canRedo: templateStore.canRedo,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dreport-editor">
|
||||||
|
<aside class="dreport-editor__sidebar dreport-editor__sidebar--left">
|
||||||
|
<ToolboxPanel />
|
||||||
|
</aside>
|
||||||
|
<EditorCanvas :handle-errors="handleErrors" @compile-error="onCompileError" />
|
||||||
|
<aside class="dreport-editor__sidebar dreport-editor__sidebar--right">
|
||||||
|
<PropertiesPanel />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dreport-editor {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dreport-editor__sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-right: 1px solid #e2e8f0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dreport-editor__sidebar--right {
|
||||||
|
border-right: none;
|
||||||
|
border-left: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
frontend/src/lib/index.ts
Normal file
35
frontend/src/lib/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export { default as DreportEditor } from './DreportEditor.vue'
|
||||||
|
export type { DreportEditorConfig } from './DreportEditor.vue'
|
||||||
|
|
||||||
|
// Core types
|
||||||
|
export type {
|
||||||
|
Template,
|
||||||
|
TemplateElement,
|
||||||
|
ContainerElement,
|
||||||
|
LeafElement,
|
||||||
|
StaticTextElement,
|
||||||
|
TextElement,
|
||||||
|
LineElement,
|
||||||
|
ImageElement,
|
||||||
|
PageNumberElement,
|
||||||
|
BarcodeElement,
|
||||||
|
RepeatingTableElement,
|
||||||
|
SizeValue,
|
||||||
|
SizeConstraint,
|
||||||
|
PositionMode,
|
||||||
|
ScalarBinding,
|
||||||
|
ArrayBinding,
|
||||||
|
ElementBinding,
|
||||||
|
TextStyle,
|
||||||
|
LineStyle,
|
||||||
|
ContainerStyle,
|
||||||
|
ImageStyle,
|
||||||
|
BarcodeStyle,
|
||||||
|
BarcodeFormat,
|
||||||
|
TableColumn,
|
||||||
|
TableStyle,
|
||||||
|
FormatType,
|
||||||
|
} from '../core/types'
|
||||||
|
|
||||||
|
// Schema types
|
||||||
|
export type { JsonSchema, SchemaNode } from '../core/schema-parser'
|
||||||
@@ -3,68 +3,14 @@ import { ref, computed } from 'vue'
|
|||||||
import type { JsonSchema, SchemaNode } from '../core/schema-parser'
|
import type { JsonSchema, SchemaNode } from '../core/schema-parser'
|
||||||
import { parseSchema, findArrayFields, findScalarFields } from '../core/schema-parser'
|
import { parseSchema, findArrayFields, findScalarFields } from '../core/schema-parser'
|
||||||
|
|
||||||
/** Varsayılan fatura schema'sı */
|
/** Minimal boş schema — gerçek schema dışarıdan (DreportEditor prop) gelir */
|
||||||
const defaultSchema: JsonSchema = {
|
const emptySchema: JsonSchema = {
|
||||||
$id: 'fatura-schema',
|
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {},
|
||||||
firma: {
|
|
||||||
type: 'object',
|
|
||||||
title: 'Firma',
|
|
||||||
properties: {
|
|
||||||
unvan: { type: 'string', title: 'Firma Unvani' },
|
|
||||||
vergiNo: { type: 'string', title: 'Vergi No' },
|
|
||||||
logo: { type: 'string', title: 'Logo', format: 'image' },
|
|
||||||
adres: { type: 'string', title: 'Adres' },
|
|
||||||
telefon: { type: 'string', title: 'Telefon' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fatura: {
|
|
||||||
type: 'object',
|
|
||||||
title: 'Fatura',
|
|
||||||
properties: {
|
|
||||||
no: { type: 'string', title: 'Fatura No' },
|
|
||||||
tarih: { type: 'string', title: 'Tarih', format: 'date' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
musteri: {
|
|
||||||
type: 'object',
|
|
||||||
title: 'Musteri',
|
|
||||||
properties: {
|
|
||||||
unvan: { type: 'string', title: 'Musteri Unvani' },
|
|
||||||
vergiNo: { type: 'string', title: 'Vergi No' },
|
|
||||||
adres: { type: 'string', title: 'Adres' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
kalemler: {
|
|
||||||
type: 'array',
|
|
||||||
title: 'Fatura Kalemleri',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
siraNo: { type: 'integer', title: 'Sira No' },
|
|
||||||
adi: { type: 'string', title: 'Urun / Hizmet Adi' },
|
|
||||||
miktar: { type: 'number', title: 'Miktar' },
|
|
||||||
birim: { type: 'string', title: 'Birim' },
|
|
||||||
birimFiyat: { type: 'number', title: 'Birim Fiyat', format: 'currency' },
|
|
||||||
tutar: { type: 'number', title: 'Tutar', format: 'currency' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
toplamlar: {
|
|
||||||
type: 'object',
|
|
||||||
title: 'Toplamlar',
|
|
||||||
properties: {
|
|
||||||
araToplam: { type: 'number', title: 'Ara Toplam', format: 'currency' },
|
|
||||||
kdv: { type: 'number', title: 'KDV', format: 'currency' },
|
|
||||||
genelToplam: { type: 'number', title: 'Genel Toplam', format: 'currency' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSchemaStore = defineStore('schema', () => {
|
export const useSchemaStore = defineStore('schema', () => {
|
||||||
const rawSchema = ref<JsonSchema>(defaultSchema)
|
const rawSchema = ref<JsonSchema>(emptySchema)
|
||||||
|
|
||||||
const schemaTree = computed<SchemaNode>(() => parseSchema(rawSchema.value))
|
const schemaTree = computed<SchemaNode>(() => parseSchema(rawSchema.value))
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,14 @@ function createDefaultTemplate(): Template {
|
|||||||
export const useTemplateStore = defineStore('template', () => {
|
export const useTemplateStore = defineStore('template', () => {
|
||||||
const template = ref<Template>(createDefaultTemplate())
|
const template = ref<Template>(createDefaultTemplate())
|
||||||
|
|
||||||
const mockData = computed(() => generateMockData(template.value))
|
/** Dışarıdan verilen önizleme verisi (null ise mock data üretilir) */
|
||||||
|
const overrideData = ref<Record<string, unknown> | null>(null)
|
||||||
|
|
||||||
|
const mockData = computed(() => overrideData.value ?? generateMockData(template.value))
|
||||||
|
|
||||||
|
function setOverrideData(data: Record<string, unknown> | null) {
|
||||||
|
overrideData.value = data
|
||||||
|
}
|
||||||
|
|
||||||
// Undo / Redo
|
// Undo / Redo
|
||||||
const { undo, redo, canUndo, canRedo } = useUndoRedo(template)
|
const { undo, redo, canUndo, canRedo } = useUndoRedo(template)
|
||||||
@@ -148,6 +155,7 @@ export const useTemplateStore = defineStore('template', () => {
|
|||||||
exportTemplate,
|
exportTemplate,
|
||||||
importTemplate,
|
importTemplate,
|
||||||
resetTemplate,
|
resetTemplate,
|
||||||
|
setOverrideData,
|
||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
canUndo,
|
canUndo,
|
||||||
|
|||||||
Reference in New Issue
Block a user