Eklentin kupon, çek, etiket ya da kendi raporunu kasa yazıcısından bastırabilir — yazıcıya hiç dokunmadan. Belgeyi sen tarif edersin, basma işini panel yapar.
İlk refleks window.print() olur ve çalışmaz — sebebini bilmek zaman kazandırır. Eklenti arayüzün ayrı bir origin'de, sandbox'lı iframe içinde çalışıyor:
allow-modals yok → window.print() çağrılamaz.allow-popups yok → window.open + yazdır da çalışmaz.window.flutter_inappwebview) erişim yok.Tek yol: belgeyi tarif et, basmayı panel yapsın. Sektörde aynı desen var — Clover PrintJob, Chrome chrome.printing, Odoo POS proxy: izin-kapılı, yapılandırılmış iş; ham bayt yok, hedefi host seçer.
// Köprü çağrısı — diğer action'larla aynı zarf.
// panelOrigin iframe'e query param olarak verilir; hedefi ONA pinleyin, "*" KULLANMAYIN.
const res = await bridgeCall("print", {
printer: "Mutfak", // OPSİYONEL — yazıcı ADI (ip/port değil). Aşağıdaki nota bakın.
elements: [
{ type: "text", text: "KUPON", size: "3", align: "center", bold: true },
{ type: "hr" },
{ type: "row", data: [
{ text: "Kod", width: 6, align: "left" },
{ text: "X-1234", width: 6, align: "right" }, // oranlar: 6/6
]},
{ type: "qr", text: "https://ornek.com/kupon/X-1234" },
{ type: "feed" },
],
});function bridgeCall(action, params) {
const requestId = crypto.randomUUID();
const target = readPanelOrigin(); // ?panelOrigin=… (doğrulanmış)
return new Promise((resolve, reject) => {
const timer = setTimeout(() => { cleanup(); reject(new Error("bridge-timeout")); }, 65000);
function onMessage(e) {
if (e.origin !== target) return; // origin doğrula
if (e.data?.type !== "restomenum-bridge-response") return;
if (e.data.requestId !== requestId) return;
cleanup(); resolve(e.data.result);
}
function cleanup() { clearTimeout(timer); removeEventListener("message", onMessage); }
addEventListener("message", onMessage);
parent.postMessage({ type: "restomenum-bridge", requestId, action, params }, target);
});
}| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| printer:print | scope | ✓ | Yazdırma için zorunlu. Yoksa printDenied. |
| printer:drawer | scope | – | Para çekmecesi (drawer elemanı) için ayrı scope. Yoksa iş tamamen reddedilir (printDrawerDenied) — o eleman atlanıp gerisi basılmaz. |
chrome.printing). İşletme "belge bastırabilir" ile "kasamı açabilir"i ayrı onaylar.printScopeUnknown döner. "Bilinmiyor" = "izin var" değildir. Bu hatadan sonra körlemesine tekrar gönderme; kullanıcıya durumu göster.İzinli tipler: text, row, hr, feed, qr, barcode, image, beep, cut (+ scope ile drawer). Başkası → printElementNotAllowed. line/divider YOK — çizgi hr'dir.
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| text | text + stiller | – | Satır. En fazla 300 karakter. |
| hr | text? | – | Ayraç. text çizgi karakteridir, tek karaktere kırpılır. |
| feed | — | – | Sabit 2 satır boşluk. |
| row | data: Column[] | – | En fazla 12 sütun (fazlası kırpılır). width göreli ağırlıktır, platform 12 birime normalize eder. |
| qr | text, size, align | – | size 1–8 (vars. 4). İçerik en fazla 512 karakter. Boş içerik işi reddettirir. |
| barcode | text, symbology, barWidth, barHeight | – | symbology vars. code128. İçerik en fazla 64 karakter. Veri sembolojinin hane/karakter kuralına uymazsa yalnız o eleman düşer ve dropped'a sayılır — iş reddedilmez. |
| image | text = URL | – | Yalnız https://, en fazla 500 karakter. Görseli cihaz indirir, 400px'e ölçeklenir. |
| beep | — | – | Yazıcı sesi (fişin sonunda). |
| cut | mode | – | "partial" · diğer değer → full. |
| drawer | pin | – | "5" · diğer değer → "2". printer:drawer ister. |
bold / underline — yalnız true geçer.size — "1"–"8". Aralık dışı değer kırpılır (clamp), sayıya çevrilemeyen değer hiç taşınmaz — ikisi de fişi düşürmez, en fazla stil yok sayılır.align — "left" | "right"; verilmezse center.linesAfter — elemandan sonra boş satır; 0–20 arasına kırpılır.codeTable göndermeyin — beyaz listede yoktur, hiçbir zaman yazıcıya ulaşmaz. Kod sayfası yazıcı ayarıdır, eklenti kararı değil.// row sütunu
interface Column {
text: string;
width: number; // GÖRELİ ağırlık — platform 12 birime normalize eder
align?: "left" | "right"; // verilmezse center
bold?: boolean; // yalnız true geçer
underline?: boolean; // yalnız true geçer
}width mutlak bir birim değil, göreli bir ağırlıktır. Platform (printBridge) genişlikleri her koşulda toplamı 12 olan pozitif tamsayılara dağıtır — oransal ölçekleme + en büyük artık yöntemiyle. Yani:
3+3 → 6+6).7/2/3 yazılmasının sebebi kuralı karşılamak değil, sütun genişliklerinin niyetini okunur kılmaktır.{ type: "row", data: [
{ text: "Ürün", width: 7, align: "left" },
{ text: "Adet", width: 2, align: "center" },
{ text: "Tutar", width: 3, align: "right" }, // oranlar: 7/2/3
]}printBridge'den geçiyor ve orada sütun genişlikleri 12'ye dağıtılıyor, size 1–8'e kırpılıyor, align normalize ediliyor, codeTable hiç taşınmıyor.Paralel çağrı güvenlidir — aynı anda birden çok print işi gönderebilirsin, sıraya alınırlar. Yine de success:false aldığında otomatik tekrar gönderme; sebebini ayırt et (aşağıdaki hata tablosu) ve indeterminate semantiğini gözet.
printer hiç gönderilmezse varsayılana gider: Electron/web'de host'un varsayılan yazıcısı; Flutter'da isDefault işaretli yazıcı, yoksa tek yazıcı varsa o. Birden fazla yazıcı var ve hiçbiri varsayılan değilse → printerMissing.printerNotFound. Sessizce başka yazıcıya basılmaz.printer alanını hiç gönderme. Kendi ayarlarında opsiyonel bir "yazıcı adı" alanı sun; boşsa alanı payload'a ekleme. Böylece tek yazıcılı kurulumlar sıfır konfigürasyonla çalışır.{ success: true, data: { jobId, printed, indeterminate, dropped } }
{ success: true, data: { duplicate: true, printed: false } } // KÂĞIT ÇIKMADI
{ success: false, message: "printDenied" | "printerNotFound" | … }| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| printed: true | başarı | – | İş gönderildi, host başarı bildirdi. |
| indeterminate: true | belirsiz | – | Sonuç güvenilir değil (eski native / Electron yolu bağlantı sonucunu döndürür, gerçek basımı değil). Kullanıcıya "yazıcıyı kontrol edin" de, otomatik tekrar gönderme — baytların bir kısmı ulaşmış olabilir, çift fiş çıkar. "Tekrar yazdır" seçeneği sun. |
| duplicate: true | ⚠️ başarı DEĞİL | – | Aynı içerik 5 sn içinde tekrar geldi → kâğıt çıkmadı. Yanıt success:true ama semantiği "basılmadı" — kullanıcıya "bastı" deme. |
| dropped: n | sayı | – | Cihazın desteklemediği için düşen eleman sayısı (v1 native'de barcode/drawer/cut sessizce atlanır). |
| jobId | string | – | Yerel denetim günlüğüne yazılan iş kimliği — destek talebinde bunu ilet. |
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| printDenied | kalıcı | – | Scope yok. Yazdır düğmesini gizle, tekrar deneme. |
| printScopeUnknown | geçici | – | İzin listesi alınamadı. Kullanıcıya bildir, tekrar denenebilir. |
| printerNotFound · printerMissing | yapılandırma | – | Hedef çözülemedi — kullanıcıyı POS yazıcı ayarlarına yönlendir. |
| printRateLimited | kota | – | Geri çekil, döngüye girme. |
| printTooLarge · printEmpty · printInvalid* · printElementNotAllowed · printDrawerDenied | payload | – | Geliştirici hatası — düzeltilmeli, tekrar denemek sonucu değiştirmez. |
| printFailed | cihaz | – | Cihaz basamadı; data.error ham sebebi taşır. "Tekrar yazdır" seçeneği sun. |
duplicate)printTooLarge — iş tamamen düşer). Uzun listeli belgelerde kendi tarafında kırp ve kırpmayı çıktıya yaz ("26 fişin 18'i basıldı"). Sessiz kırpma, özellikle mali belgelerde kabul edilemez.0x1B dahil) sökülür — text içine ESC/POS komutu gömerek izin duvarını delmek mümkün değildir. Geçersiz eleman atlanmaz, iş tamamen reddedilir: eklentinin "bastım" sanıp kasiyerin eksik fiş görmesi böyle engellenir.getContext yalnız serverId, pluginId, locale döner). Yani çalışmayacak bir düğmeyi önceden gizleyemezsin. Bugünkü yol: getSessionToken ile kendi backend'inden kurulumun onaylı scope'larını sor, ya da ilk printDenied sonrası düğmeyi gizle. Köprüye bir capabilities action'ı eklenmesi talebi kayıtlı.printDenied almaya devam edersin. Sayfayı yenile ya da eklentiyi yükselt.fiskaly TSE eklentisinin X-Bericht şablonundan türetildi; satır şablonlarının toplamları sahada test edildi.
// ── Platform sınırları (aşılırsa iş TAMAMEN reddedilir, kırpılmaz) ──
const MAX_ELEMENTS = 80; // → printTooLarge
const RESERVE = 8; // kırpma notu + toplamlar + qr + feed için ayrılan pay
// ── Satır şablonları — width GÖRELİ ağırlıktır, platform 12 birime normalize eder ──
const pText = (text, opts = {}) => ({ type: "text", text, ...opts });
const pHr = () => ({ type: "hr" });
const pPair = (label, value) => ({ type: "row", data: [
{ text: label, width: 8, align: "left" },
{ text: value, width: 4, align: "right" }, // oranlar: 8/4
]});
const pQuad = (a, b, c, d) => ({ type: "row", data: [
{ text: a, width: 3, align: "left" },
{ text: b, width: 3, align: "right" },
{ text: c, width: 3, align: "right" },
{ text: d, width: 3, align: "right" }, // oranlar: 3/3/3/3
]});
function buildRapor(rapor) {
const el = [];
el.push(pText("GÜN SONU RAPORU", { size: "2", align: "center", bold: true }));
el.push(pText(rapor.isletmeAdi, { align: "center" }));
el.push(pHr());
el.push(pPair("Nakit", rapor.nakit));
el.push(pPair("Kart", rapor.kart));
el.push(pPair("TOPLAM", rapor.toplam));
el.push(pHr());
el.push(pText("KDV KIRILIMI", { bold: true }));
el.push(pQuad("Oran", "Matrah", "KDV", "Top."));
rapor.kdv.forEach((k) => el.push(pQuad(k.oran, k.matrah, k.vergi, k.toplam)));
el.push(pHr());
// 80 eleman bütçesine göre kırp
const butce = MAX_ELEMENTS - el.length - RESERVE;
const basilan = rapor.fisler.slice(0, Math.max(0, butce));
basilan.forEach((f) => el.push(pPair(f.no, f.tutar)));
// ⚠️ Kırpma SESSİZ OLMAMALI — kâğıda yaz.
if (basilan.length < rapor.fisler.length) {
el.push(pText(`${rapor.fisler.length} fişin ${basilan.length}'i basıldı`, { bold: true }));
}
el.push(pHr());
el.push({ type: "qr", text: rapor.dogrulamaUrl });
el.push({ type: "feed" }); // "cut" KOYMA — aşağıdaki nota bakın
return el;
}
// ── Gönder ve sonucu operatöre DOĞRU cümleyle anlat ──
async function yazdir(rapor) {
// printer alanını HİÇ gönderme → varsayılan yazıcıya gider (tek yazıcılı kurulumda sıfır konfigürasyon)
const res = await bridgeCall("print", { elements: buildRapor(rapor) });
if (!res.success) {
switch (res.message) {
case "printDenied": return { ok: false, gizleDugme: true, mesaj: "Yazdırma izni verilmemiş." };
case "printScopeUnknown": return { ok: false, tekrarDenenebilir: true, mesaj: "İzin durumu okunamadı, tekrar deneyin." };
case "printerNotFound":
case "printerMissing": return { ok: false, mesaj: "Yazıcı bulunamadı — POS yazıcı ayarlarını kontrol edin." };
case "printRateLimited": return { ok: false, mesaj: "Çok sık yazdırma isteği, biraz bekleyin." };
default: return { ok: false, mesaj: "Yazdırılamadı." };
}
}
// duplicate: success:true ama KÂĞIT ÇIKMADI — başarı sayma.
if (res.data?.duplicate) return { ok: false, mesaj: "Aynı rapor az önce gönderildi, tekrar basılmadı." };
// indeterminate: gönderildi ama basıldığı DOĞRULANAMADI. Otomatik tekrar YOK (çift fiş riski).
if (res.data?.indeterminate) {
return { ok: true, belirsiz: true, mesaj: "Rapor gönderildi, yazıcıyı kontrol edin.", tekrarYazdirSun: true };
}
const not = res.data?.dropped > 0
? ` (${res.data.dropped} öğe bu cihazda desteklenmediği için basılmadı)` : "";
return { ok: true, mesaj: "Rapor yazdırıldı." + not };
}cut bilinçli olarak yok. v1 native'de cut, barcode ve drawer sessizce düşüyor → her basımda dropped ≥ 1 üretir ve gerçek bir düşmeyi maskeler. feed ile bitirmek daha dürüst bir sinyal verir.panelOrigin, köprü zarfı