# Slip/Fiş Yazdırma Sistemi - Dokümantasyon

## Genel Bakış

Bu doküman, poscloud.apper projesindeki fiş/slip yazdırma sisteminin detaylı analizini sunmaktadır. Sistem, restoran ve benzeri işletmelerde mutfak/bar fişleri, iptal fişleri ve hesap fişlerinin yazdırılmasını yönetir.

---

## İçindekiler

1. [Yazdırma Çözümleri](#yazdırma-çözümleri)
2. [Fiş Türleri](#fiş-türleri)
3. [Printer Modeli](#printer-modeli)
4. [Tracking Section ve Printer İlişkisi](#tracking-section-ve-printer-ilişkisi)
5. [Yeni Sipariş Fişi Yazdırma](#yeni-sipariş-fişi-yazdırma)
6. [İptal Fişi Yazdırma](#iptal-fişi-yazdırma)
7. [Hesap Fişi (Bill)](#hesap-fişi-bill)
8. [QZ Tray Yazdırma Sistemi](#qz-tray-yazdırma-sistemi)
9. [Google Cloud Print (GCP) Entegrasyonu](#google-cloud-print-gcp-entegrasyonu)
10. [Print Queue (Yazdırma Kuyruğu)](#print-queue-yazdırma-kuyruğu)
11. [Yazdırma Akış Diyagramları](#yazdırma-akış-diyagramları)
12. [Fiş Template'leri](#fiş-template-leri)
13. [Konfigürasyon Ayarları](#konfigürasyon-ayarları)
14. [API Endpoints](#api-endpoints)
15. [Frontend Yazdırma Mantığı](#frontend-yazdırma-mantığı)
16. [Troubleshooting](#troubleshooting)

---

## Yazdırma Çözümleri

Sistem iki farklı yazdırma çözümünü destekler:

### 1. QZ Tray (Önerilen)

- **Tanım:** Yerel ağ yazıcılarıyla doğrudan iletişim kuran Java tabanlı bir çözüm
- **Avantajlar:**
    - Hızlı ve güvenilir
    - Yerel yazıcılara doğrudan erişim
    - HTML içeriği yazdırma desteği
    - Offline çalışabilir
- **Dezavantajlar:**
    - Client makinede QZ Tray software kurulumu gerektirir
    - Java runtime environment gerektirir

### 2. Google Cloud Print (GCP)

- **Tanım:** Google'ın bulut tabanlı yazdırma servisi (NOT: GCP 2021'de kapatıldı, legacy destek)
- **Avantajlar:**
    - Bulut tabanlı, her yerden erişim
    - Kurulum kolaylığı
- **Dezavantajlar:**
    - Google hesabı gerektirir
    - İnternet bağlantısı zorunlu
    - Servis artık kullanımdan kaldırıldı

**Konfigürasyon:** `companySetting('printer_solution')` ile belirlenir

- Değerler: `'qz'` veya `'gcp'`
- Varsayılan: `'gcp'`

---

## Fiş Türleri

Sistem üç ana fiş türünü destekler:

### 1. Tracking Section Slip (Mutfak/Bar Fişi)

- **Amaç:** Yeni siparişleri mutfak veya bar bölümlerine bildirmek
- **Tetiklenme:** Yeni sipariş oluşturulduğunda otomatik
- **İçerik:**
    - Restoran adı
    - Masa numarası veya teslimat adresi
    - Garson adı
    - Satış numarası
    - Tarih ve saat
    - Ürün listesi (adet ile birlikte)
    - Ürün özellikleri (specs)
    - Sipariş notu
    - İleri tarihli sipariş bilgisi (varsa)

### 2. Cancel Slip (İptal Fişi)

- **Amaç:** İptal edilen siparişleri ilgili bölüme bildirmek
- **Tetiklenme:** Sipariş iptal edildiğinde
- **İçerik:**
    - "SİPARİŞ İPTAL" başlığı
    - Restoran adı
    - Tracking section adı
    - Masa numarası veya adres
    - Tarih
    - İptal edilen ürünler
    - Ürün özellikleri
    - İptal sebebi

### 3. Bill/Hesap Fişi

- **Amaç:** Müşteriye verilen hesap özeti
- **Tetiklenme:** Checkout işlemi sırasında manuel tetiklenebilir
- **İçerik:**
    - Restoran adı ve adresi
    - Telefon numarası
    - Masa numarası
    - Satış numarası
    - Açılış saati
    - Garson adı
    - Ödeme tipi
    - Ürün listesi (kategori ve KDV bazında gruplanmış)
    - Ara toplamlar
    - İndirim
    - İkram
    - İptal tutarı
    - KDV toplamı
    - Servis ücreti
    - Genel toplam

---

## Printer Modeli

**Dosya:** `app/Models/Printer.php`

### Özellikler:

- **Tablo:** `printers`
- **Soft Deletes:** Evet
- **Traits:** `CompanySpecificTrait`, `SoftDeletes`

### Fillable Alanlar:

```php
protected $fillable = ['display_name', 'enabled'];
```

### Validasyon Kuralları:

```php
public static $rules = [
    'display_name' => 'required|max:100',
];
```

### İlişkiler:

```php
// Şirket ilişkisi
public function company() → BelongsTo(Company)

// Bağlı tracking section'lar
public function trackingSections() → HasMany(TrackingSection)
```

### Ana Metod: `print($title, $content, $contentType, $queue)`

```php
public function print($title, $content, $contentType = 'text/html', $queue = true)
{
    if (companySetting('printer_solution') === 'gcp') {
        // Google Cloud Print çözümü
        if ($queue) {
            // Asenkron job olarak gönder
            dispatch(new SendPrintJob(
                $this->company_id,
                $this->gcp_id,
                $title,
                $content,
                $contentType
            ));
        } else {
            // Senkron olarak gönder
            $client = $this->company->googleClient(null, false);
            $gcp = new GoogleCloudPrint($client);
            return $gcp->submit($this->gcp_id, $title, $content, $contentType);
        }
    } else {
        // QZ Tray çözümü
        addToPrintQueue($this, $content, [
            'content_type' => $contentType,
        ]);
    }
}
```

**Parametreler:**

- `$title`: Yazdırma işinin başlığı
- `$content`: Yazdırılacak HTML içeriği
- `$contentType`: İçerik tipi (varsayılan: 'text/html')
- `$queue`: Kuyruğa alınsın mı (varsayılan: true)

---

## Tracking Section ve Printer İlişkisi

**Dosya:** `app/Models/TrackingSection.php`

Tracking Section (İzleme Bölümü), mutfaktaki farklı çalışma alanlarını temsil eder:

- Ana Mutfak
- Bar
- Tatlı Bölümü
- Izgara
- vb.

Her tracking section bir printer'a atanabilir.

### Ana Metod: `printSlip($orders)`

```php
public function printSlip($orders)
{
    // İlgili tracking section'ın printer'ı yoksa çık
    if (!$this->printer) return;

    // GCP seçili ama Google token yoksa çık
    if (companySetting('printer_solution') === 'gcp' && !company()->google_refresh_token)
        return;

    // Fişi oluştur ve yazdır
    $this->printer->print('New Orders', $this->slip($orders));
}
```

### Fiş Oluşturma: `slip($orders)`

```php
public function slip($orders)
{
    $sale = $orders[0]->sale;
    $user = $orders[0]->user;

    // Fişler şirketin dil tercihine göre basılmalı
    $lang = companySetting('locale');

    return view('common.tracking-section-slip')
        ->with('trackingSection', $this)
        ->with('company', company())
        ->with('sale', $sale)
        ->with('user', $user)
        ->with('orders', $orders)
        ->with('lang', $lang)
        ->__toString();
}
```

---

## Yeni Sipariş Fişi Yazdırma

### Süreç Akışı:

```
1. OrderController::store() veya SaleRepository::create()
   ↓
2. Order'lar oluşturulur ve kaydedilir
   ↓
3. Order'lar tracking_section_id'ye göre gruplanır
   ↓
4. Her tracking section için:
   IF printer varsa:
     → trackingSection->printSlip(orders)
       ↓
     → Printer::print('New Orders', slip_content)
       ↓
     → addToPrintQueue() [QZ] veya SendPrintJob [GCP]
```

### Kod Implementasyonu:

**SaleRepository::create() içinde:**

```php
// Order'ları oluşturduktan sonra
$trackingSections = collect($orders)->groupBy('tracking_section_id');

foreach ($trackingSections as $orders) {
    /** @var TrackingSection $trackingSection */
    $trackingSection = $orders[0]->trackingSection;

    if ($trackingSection) {
        $trackingSection->printSlip($orders);
    }
}
```

**Önemli Noktalar:**

1. Aynı tracking section'a ait order'lar tek fişte toplanır
2. Her tracking section için ayrı fiş basılır
3. Printer yoksa yazdırma atlanır (hata vermez)
4. GCP seçili ama token yoksa yazdırma atlanır

---

## İptal Fişi Yazdırma

### Tetiklenme Noktaları:

1. **SaleController::update()** - Manuel iptal
2. **CheckoutRepository::checkout()** - Checkout sırasında iptal

### Süreç Akışı:

```
1. Kullanıcı iptal action'ını tetikler
   ↓
2. Order'lar iptal edilir (payment_type = CANCEL, deleted_at set edilir)
   ↓
3. Sale::printCancelSlip($orderIds) çağrılır
   ↓
4. Order'lar tracking_section_id'ye göre gruplanır
   ↓
5. Her tracking section için:
   IF printer varsa:
     → Printer::print('Order Cancel', cancel_slip_content)
```

### Sale::printCancelSlip() Metodu:

```php
public function printCancelSlip($orderIds = false)
{
    // Belirli order ID'leri filtrele
    if ($orderIds) {
        $orders = $this->allOrders->filter(function ($order) use ($orderIds) {
            return in_array($order->id, $orderIds);
        });
    } else {
        $orders = $this->orders;
    }

    // Tracking section'lara göre grupla
    $grouped = $orders->groupBy('tracking_section_id');

    foreach ($grouped as $trackingSectionId => $orders) {
        $trackingSection = $orders[0]->trackingSection;

        // Printer kontrolü
        if (!$trackingSection || !$trackingSection->printer) continue;
        if (companySetting('printer_solution') === 'gcp' && !company()->google_refresh_token)
            continue;

        // İptal fişini yazdır
        $trackingSection->printer->print(
            'Order Cancel',
            $this->cancelSlip($orders, $trackingSection)
        );
    }
}
```

### Partial vs Full Cancel:

**Partial Mode (Kısmi İptal):**

```php
// Seçili order'ları böl ve iptal et
$processedIds = [];
foreach ($orders as $order) {
    $processedIds[] = $order->split($selectedOrders[$order->id])->id;
}

// Sadece iptal edilen order'lar için fiş bas
$sale->printCancelSlip($processedIds);
```

**Full Mode (Tam İptal):**

```php
// Tüm aktif order ID'lerini al
$toBeDeletedIds = $sale->orders()
    ->where('payment_type', '!=', PAYMENT_TYPE['cancel'])
    ->pluck('id')
    ->toArray();

// Tüm order'lar için fiş bas
$sale->printCancelSlip($toBeDeletedIds);
```

---

## Hesap Fişi (Bill)

### Sale::bill() Metodu:

```php
public function bill()
{
    // Getir Yemek için özel fatura
    if ($this->channel === 'getir')
        return $this->getirYemekBill();

    // Yığınlanmış siparişleri al (aynı ürünleri grupla)
    $stackedOrders = $this->getStackedOrders();

    // Ödenebilir siparişleri al
    $payableOrders = $this->orders()
        ->with('product.categories')
        ->payable()
        ->get();

    // Siparişleri kategori invoice_title'a göre grupla
    $orderGroups = $payableOrders->map(function ($order) {
        $order->invoice_title = array_first($order->getCategoryInvoiceTitles());
        return $order;
    })
    ->groupBy('invoice_title')
    ->map(function ($groupedOrders) {
        return $groupedOrders->groupBy(function ($order) {
            return object_get($order, 'product.vat_pct');
        });
    });

    // Grup istatistiklerini hesapla
    $groupStats = [];
    foreach ($orderGroups as $invoiceTitle => $vatGroups) {
        foreach ($vatGroups as $vat => $orders) {
            $groupStats[] = [
                'invoice_title' => $invoiceTitle ?: transTitleCase('generic.other'),
                'vat_pct'       => $vat,
                'total_price'   => $orders->sum('total_price'),
            ];
        }
    }

    return view('common.bill')
        ->with('stackedOrders', $stackedOrders)
        ->with('groupStats', $groupStats)
        ->with('sale', $this)
        ->__toString();
}
```

### Gruplama Mantığı:

1. **Birinci Seviye:** Kategori `invoice_title`'a göre grupla
    - Örnek: "ANA YEMEKLER", "İÇECEKLER", "TATLILAR"

2. **İkinci Seviye:** Her kategori içinde KDV oranına göre alt grupla
    - Örnek: %1 KDV, %8 KDV, %18 KDV

3. **İstatistikler:** Her grup için toplam fiyat hesapla

### Getir Yemek Faturası:

```php
public function getirYemekBill()
{
    if (data_get($this, 'delivery_type') === DeliveryTypes::GETIR) {
        $customer = data_get($this, 'payer');
    } else {
        $customer = data_get($this, 'address.customer');
    }

    return view('common.getir-bill')
        ->with('getirSale', $this)
        ->with('customer', $customer)
        ->with('getirPartner', company()->getirPartner)
        ->__toString();
}
```

---

## QZ Tray Yazdırma Sistemi

### QZ Tray Nedir?

QZ Tray, web tarayıcılarından yerel yazıcılara doğrudan erişim sağlayan açık kaynaklı bir çözümdür.

### Kurulum Gereksinimleri:

1. **Client Tarafı:**
    - QZ Tray software kurulmalı
    - Java Runtime Environment (JRE) gerekli
    - QZ Tray servisi çalışıyor olmalı
    - Sertifika kurulumu yapılmalı

2. **Frontend:**
    - `qz-tray.js` library dahil edilmeli
    - WebSocket bağlantısı kurulmalı

### Frontend Implementation:

**Dosya:** `resources/views/common/qz-print-js.blade.php`

```javascript
$(function () {
    let printJobs = []
    let syncEndpoint = "{{ route('firm.sync') }}"
    let checks = 0
    let completedJobIds = []

    // Mevcut print job'ları al
    @if(sessionPrinterConnectionStatus() === SESSION_HAS_PRINTERS)
    console.log('Printers are connected.')
    printJobs = {!! json_encode(getJobsForPrinting()) !!}
    @endif

    startConnection()

    // WebSocket bağlantısını kontrol et
    let interval = setInterval(function () {
        if (!qz.websocket.isActive()) {
            console.log('Printer connection is not ready yet.')
            checks++

            // 20 denemeden sonra vazgeç
            if (checks >= 20) {
                clearInterval(interval)
                console.log('Printers are not connected.')
                performRequest([], 0)
            }
        } else {
            console.log('Printer connection has established.')
            clearInterval(interval)

            // Mevcut job'ları yazdır
            processPrintJobs(printJobs)

            // Her 5 saniyede bir yeni job kontrolü
            setInterval(syncLoop, 5000)
        }
    }, 300)

    // Print job'ları işle
    async function processPrintJobs(printJobs) {
        if (Object.keys(printJobs).length)
            console.log(Object.keys(printJobs).length + ' job(s) found.')

        for (let id in printJobs) {
            let printJob = printJobs[id]

            // Printer'ı bul ve yazdır
            await qz.printers.find(printJob.printer_name).then(function (printer) {
                let config = qz.configs.create(printer)
                let data = [
                    {
                        type: 'html',
                        format: 'plain',
                        data: printJob.content
                    }
                ]

                return qz.print(config, data).then(function () {
                    completedJobIds.push(id)
                    console.log('print job sent')
                }).catch(function (e) {
                    console.log(e)
                })
            }).catch(function (e) {
                console.error(e)
            })
        }
    }

    // Sync loop - her 5 saniyede bir çalışır
    function syncLoop() {
        let _completedJobIds = completedJobIds
        completedJobIds = []

        if (_completedJobIds.length) {
            console.log('ABOUT TO MARK SOME JOBS AS DONE', _completedJobIds)
        }

        performRequest(_completedJobIds)
    }

    // Server ile senkronizasyon
    function performRequest(completedJobIds = [], isPrintersConnected = 1) {
        $.ajax({
            type: 'POST',
            url: syncEndpoint,
            data: {
                printed_jobs: completedJobIds,
                printers_connected: isPrintersConnected,
            },
            success: function (response) {
                if (Object.keys(response.print_jobs).length) {
                    processPrintJobs(response.print_jobs)
                }
            },
            dataType: 'json',
            contentType: 'application/x-www-form-urlencoded'
        })
    }
})
```

### QZ Tray İşleyişi:

```
┌─────────────────────────────────────────────────────────────┐
│                    FRONTEND (Browser)                        │
│                                                              │
│  1. Sayfa yüklendiğinde QZ Tray bağlantısı başlat           │
│  2. Her 300ms'de bağlantı durumu kontrol edilir             │
│  3. Bağlantı kurulunca mevcut job'lar alınır                │
│  4. Her 5 saniyede bir server'dan yeni job'lar sorgulanır   │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│                 BACKEND (Laravel)                            │
│                                                              │
│  1. getJobsForPrinting() ile cache'den job'lar alınır       │
│  2. Job'lar frontend'e JSON olarak gönderilir               │
│  3. Frontend tamamlanan job ID'lerini gönderir              │
│  4. removePrintJobs() ile tamamlanan job'lar silinir        │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              QZ TRAY (Desktop App)                          │
│                                                              │
│  1. WebSocket üzerinden HTML content alır                   │
│  2. Belirtilen printer'a gönderir                           │
│  3. Başarı/hata durumunu frontend'e bildirir                │
└─────────────────────────────────────────────────────────────┘
```

---

## Google Cloud Print (GCP) Entegrasyonu

**Not:** Google Cloud Print servisi Aralık 2020'de kapatılmıştır. Bu dokümantasyon legacy sistemler içindir.

### GoogleCloudPrint Library:

**Dosya:** `app/Libraries/GoogleCloudPrint.php`

### Temel İşlevler:

#### 1. Initialization:

```php
$gcp = new GoogleCloudPrint($accessToken, $opts);
```

#### 2. Printer Arama:

```php
$gcp->search(['q' => 'printer_name']);
$printers = $gcp->response;
```

#### 3. Yazdırma:

```php
$gcp->submit(
    $printerId,      // GCP printer ID
    $title,          // Job başlığı
    $content,        // HTML content
    'text/html',     // Content type
    'default'        // Ticket (print settings)
);
```

### Default Print Ticket:

```php
'default-cloud-job-ticket' => [
    'version' => "1.0",
    'print'   => [
        "vendor_ticket_item" => [],
        "color" => ["type" => "STANDARD_MONOCHROME"],
        "copies" => ["copies" => 1],
        "margins" => [
            "top_microns"    => 5000,
            "right_microns"  => 5000,
            "left_microns"   => 5000,
            "bottom_microns" => 5000,
        ],
    ],
]
```

### GCP ile Yazdırma Akışı:

```
1. Printer::print() çağrılır
   ↓
2. IF $queue == true:
     → SendPrintJob dispatch edilir (async)
   ELSE:
     → Google client oluşturulur
     → GoogleCloudPrint::submit() senkron çağrılır
   ↓
3. GCP API'ye POST request gönderilir
   ↓
4. Google Cloud Print job oluşturur
   ↓
5. Printer job'ı alır ve yazdırır
```

### Error Handling:

```php
try {
    $this->request('submit', $parameters);
} catch (GoogleCloudPrintException $e) {
    \Log::warning(var_export([
        'error'     => 'Printing error',
        'code'      => $e->getCode(),
        'message'   => $e->getMessage(),
        'request'   => $e->request,
        'printerId' => $printerid,
    ], true));
}
```

---

## Print Queue (Yazdırma Kuyruğu)

QZ Tray kullanıldığında, yazdırma işleri cache-based bir kuyruk sisteminde saklanır.

### addToPrintQueue() Fonksiyonu:

**Dosya:** `app/helpers.php`

```php
function addToPrintQueue($printer, $content, $options)
{
    $companyId = company()->id;
    $cacheKey = "c{$companyId}_print_queue";

    // Mevcut kuyruğu al
    $queueJobs = Cache::get($cacheKey, []);

    // Unique ID oluştur
    $uuid = uniqid();

    // Job'ı kuyruğa ekle
    $queueJobs[$uuid] = [
        'printer_id'           => $printer->id,
        'printer_name'         => $printer->name,
        'printer_display_name' => $printer->display_name,
        'content'              => $content,
        'options'              => $options,
        'created_at'           => Carbon::now()->toDateTimeString(),
    ];

    // 5 dakika TTL ile cache'e kaydet
    Cache::put($cacheKey, $queueJobs, 300);
}
```

### getJobsForPrinting() Fonksiyonu:

```php
function getJobsForPrinting()
{
    // Job rezervasyon süresi (saniye)
    $reserveFor = 30;
    $reserve = isBetaUser();

    $companyId = company()->id;
    $cacheKey = "c{$companyId}_print_queue";

    // Tüm job'ları cache'den al ve temizle
    $printBacklog = collect(Cache::pull($cacheKey, []));

    if ($reserve) {
        // Beta kullanıcılar için rezervasyon sistemi
        // Zaten işlenmekte olan job'ları filtrele
        $jobsToPrint = $printBacklog->filter(function ($job) use ($reserveFor) {
            $reservedAt = array_get($job, 'reserved_at');

            // Rezerve edilmemiş veya süresi dolmuş job'ları al
            return !$reservedAt || $reservedAt + $reserveFor < now()->timestamp;
        });

        // Seçili job'ları "rezerve" olarak işaretle
        $jobsToPrint = $jobsToPrint->map(function ($job) {
            $job['reserved_at'] = now()->timestamp;
            return $job;
        });

        // Güncellenmiş listeyi cache'e geri koy
        $printBacklog = $printBacklog->merge($jobsToPrint);
        Cache::put($cacheKey, $printBacklog->toArray(), 300);
    } else {
        // Normal kullanıcılar için tüm job'ları döndür
        $jobsToPrint = $printBacklog;
    }

    return $jobsToPrint;
}
```

### removePrintJobs() Fonksiyonu:

```php
function removePrintJobs($ids = null)
{
    $companyId = company()->id;
    $cacheKey = "c{$companyId}_print_queue";

    if ($ids === null) {
        // Tüm job'ları sil
        Cache::forget($cacheKey);
    } else if (count($ids)) {
        // Belirli job'ları sil
        $queueJobs = Cache::get($cacheKey, []);

        foreach ($ids as $id) {
            unset($queueJobs[$id]);
        }

        Cache::put($cacheKey, $queueJobs, 300);
    }
}
```

### Queue Yapısı:

```php
[
    "5f8a9b2c3d4e5" => [
        "printer_id" => 1,
        "printer_name" => "Kitchen_Printer_01",
        "printer_display_name" => "Ana Mutfak",
        "content" => "<div>...HTML content...</div>",
        "options" => [
            "content_type" => "text/html"
        ],
        "created_at" => "2026-04-09 14:30:00",
        "reserved_at" => 1712667000  // optional
    ],
    // ... daha fazla job
]
```

### Queue Lifecycle:

```
1. addToPrintQueue() ile job eklenir
   ↓
2. Cache'de 5 dakika saklanır
   ↓
3. Frontend her 5 saniyede bir getJobsForPrinting() çağırır
   ↓
4. Job'lar frontend'e gönderilir
   ↓
5. QZ Tray job'ı yazdırır
   ↓
6. Frontend completed_job_ids ile server'a bildirir
   ↓
7. removePrintJobs() ile job cache'den silinir
```

---

## Yazdırma Akış Diyagramları

### 1. Yeni Sipariş Fişi Yazdırma Akışı:

```
┌─────────────────────────────────────────────────────────────┐
│              ORDER CONTROLLER / REPOSITORY                   │
│                                                              │
│  1. Order'lar oluşturulur ve save edilir                    │
│  2. Order::saved event tetiklenir                           │
│  3. Sale::updateTotalPrice() çalışır                        │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              TRACKING SECTION GROUPING                       │
│                                                              │
│  $trackingSections = orders->groupBy('tracking_section_id') │
│                                                              │
│  FOR each tracking section:                                 │
│    → trackingSection->printSlip(orders)                     │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              TRACKING SECTION :: printSlip()                 │
│                                                              │
│  1. Printer var mı kontrol et                               │
│  2. GCP ise Google token var mı kontrol et                  │
│  3. slip() metodu ile HTML oluştur                          │
│  4. printer->print('New Orders', html) çağır                │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ├──────────────┬──────────────┐
                   │              │              │
                   ▼              ▼              │
        ┌─────────────────┐ ┌──────────────┐    │
        │   QZ TRAY       │ │  GCP         │    │
        └────────┬────────┘ └──────┬───────┘    │
                 │                 │              │
                 ▼                 ▼              │
┌──────────────────────────┐ ┌──────────────────────┐
│ addToPrintQueue()        │ │ SendPrintJob         │
│                          │ │ dispatch veya        │
│ Cache key:               │ │ GoogleCloudPrint     │
│ c{company_id}_print_queue│ │ ::submit()           │
│                          │ │                      │
│ Job structure:           │ │ GCP API'ye POST:     │
│ - printer_id             │ │ - printerid          │
│ - printer_name           │ │ - title              │
│ - printer_display_name   │ │ - content            │
│ - content (HTML)         │ │ - contentType        │
│ - options                │ │ - ticket             │
│ - created_at             │ │                      │
└────────┬─────────────────┘ └──────────┬───────────┘
         │                              │
         └──────────┬───────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────┐
│              FRONTEND POLLING                                │
│                                                              │
│  Her 5 saniyede bir:                                        │
│  → AJAX POST /firm/sync                                     │
│  → getJobsForPrinting() çağrılır                            │
│  → Job'lar JSON olarak döner                                │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              QZ TRAY PRINTING                                │
│                                                              │
│  FOR each job:                                              │
│    1. qz.printers.find(printer_name)                        │
│    2. qz.configs.create(printer)                            │
│    3. qz.print(config, [{type:'html', data:content}])       │
│    4. completedJobIds.push(job_id)                          │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              SYNC COMPLETED JOBS                             │
│                                                              │
│  Frontend → AJAX POST /firm/sync                            │
│  Data: {printed_jobs: [id1, id2, ...]}                      │
│                                                              │
│  Backend:                                                   │
│  → removePrintJobs(completedJobIds)                         │
│  → Job'lar cache'den silinir                                │
└─────────────────────────────────────────────────────────────┘
```

### 2. İptal Fişi Yazdırma Akışı:

```
┌─────────────────────────────────────────────────────────────┐
│              SALE CONTROLLER :: update()                     │
│                                                              │
│  Action: 'cancel'                                           │
│  Partial Mode veya Full Mode                                │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ├──────────────┬──────────────┐
                   │              │              │
                   ▼              ▼              │
        ┌─────────────────┐ ┌──────────────┐    │
        │  PARTIAL MODE   │ │  FULL MODE   │    │
        └────────┬────────┘ └──────┬───────┘    │
                 │                 │              │
                 ▼                 ▼              │
┌──────────────────────────┐ ┌──────────────────────┐
│ 1. IDs ve quantities'e   │ │ 1. Aktif order       │
│    ayır                   │ │    ID'lerini al      │
│                          │ │                      │
│ 2. Order'ları split et   │ │ 2. Tüm order'ları    │
│                          │ │    iptal et:         │
│ 3. Processed ID'leri     │ │    - payment_type    │
│    topla                 │ │      = CANCEL        │
│                          │ │    - deleted_at      │
│ 4. Order'ları iptal et   │ │      = now()         │
│    - payment_type        │ │    - canceller_id    │
│      = CANCEL            │ │    - cancel_reason   │
│    - canceller_id        │ │                      │
│    - cancel_reason       │ │ 3. Sale'i iptal et   │
└────────┬─────────────────┘ └──────────┬───────────┘
         │                              │
         └──────────┬───────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────┐
│              SALE :: updateTotalPrice(true)                  │
│                                                              │
│  Sale fiyatları yeniden hesaplanır                          │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              SALE :: printCancelSlip(orderIds)               │
│                                                              │
│  1. Belirtilen order ID'lerini filtrele                     │
│  2. Order'ları tracking_section_id'ye göre grupla           │
│  3. FOR each tracking section:                              │
│       IF printer varsa:                                     │
│         → cancelSlip() ile HTML oluştur                     │
│         → printer->print('Order Cancel', html)              │
└──────────────────┬──────────────────────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────────────────────┐
│              ACTIVITY LOG                                    │
│                                                              │
│  Activity::orderCancel(destination, quantity, price)        │
│  veya                                                       │
│  Activity::checkoutCancel(destination)                      │
└─────────────────────────────────────────────────────────────┘
```

---

## Fiş Template'leri

### 1. Tracking Section Slip Template

**Dosya:** `resources/views/common/tracking-section-slip.blade.php`

#### Yapı:

```blade
<div style="font-size:{{ companySetting('slip-font-size') }}px; font-family: Consolas, monospace;">
    <!-- Header -->
    <div style="text-align: center">
        <p>YENİ SİPARİŞ - {{ $company->name }}</p>
    </div>

    <!-- Info Table -->
    <table>
        <tr>
            @if($sale->destination_type == 'table')
                <td><strong>MASA</strong></td>
                <td>: Masa {{ $sale->destinationName }}</td>
            @elseif($sale->destination_type == 'address')
                <td><strong>ADRES</strong></td>
                <td>: {{ $sale->destinationDetail }}</td>
            @endif
        </tr>
        @if(companySetting('show_waiter_names_in_tacking_sections'))
        <tr>
            <td><strong>GARSON</strong></td>
            <td>: {{ $user->fullname }}</td>
        </tr>
        @endif
        <tr>
            <td><strong>SATIŞ NO</strong></td>
            <td>: #{{ $sale->saleNo }}</td>
        </tr>
        <tr>
            <td><strong>TARİH</strong></td>
            <td>: {{ Carbon::now()->format('Y-m-d H:i') }}</td>
        </tr>
        @if($sale->scheduled_to)
        <tr>
            <td colspan="2"><strong>-- İLERİ TARİHLİ SİPARİŞ --</strong></td>
        </tr>
        <tr>
            <td><strong>TESLİM SAATİ</strong></td>
            <td>: {{ $sale->scheduled_to->format('Y-m-d H:i') }}</td>
        </tr>
        @endif
    </table>

    <!-- Orders List -->
    <table>
        @foreach($orders as $order)
            <hr>
            <!-- Product Name & Quantity -->
            <tr>
                <td><b>{{ $order->product->name }}</b></td>
                <td><b>{{ $order->visibleQuantity }}</b> Ad.</td>
            </tr>

            <!-- Specs -->
            @if(count($order->specs))
            <tr>
                <td colspan="2">
                    @foreach($order->product->specs as $spec)
                        @foreach($spec->options as $option)
                            @if($order->isOptionSelected($option))
                                <div style="border: 1px solid black; padding: 2px 3px;">
                                    {{ $option->name }}
                                </div>
                            @endif
                        @endforeach
                    @endforeach
                </td>
            </tr>
            @endif

            <!-- Note -->
            @if($order->note)
            <tr>
                <td colspan="2">
                    <b>Not:</b> {{ $order->note }}
                </td>
            </tr>
            @endif
        @endforeach
    </table>
</div>
```

#### Özellikler:

- Font boyutu: `companySetting('slip-font-size')` (varsayılan: 25px)
- Font ailesi: Consolas, monospace
- Uzun ürün isimleri için otomatik font küçültme
- Spec'ler kutucuk içinde gösterilir
- İptal edilen default spec'ler üstü çizili gösterilir

### 2. Cancel Slip Template

**Dosya:** `resources/views/common/cancel-slip.blade.php`

#### Yapı:

```blade
<div style="font-size:{{ companySetting('slip-font-size') }}px">
    <!-- Header -->
    <div style="text-align: center">
        <h5>SİPARİŞ İPTAL - {{ $sale->company->name }} - {{ $trackingSection->name }}</h5>
    </div>

    <!-- Info -->
    <table>
        <tr>
            @if($sale->destination_type == 'table')
                <td><strong>MASA :</strong></td>
                <td>Masa {{ $sale->destination->name }}</td>
            @elseif($sale->destination_type == 'address')
                <td><strong>ADRES :</strong></td>
                <td>{{ $sale->destination->title }}</td>
            @endif
        </tr>
        <tr>
            <td><strong>TARİH :</strong></td>
            <td>{{ Carbon::now() }}</td>
        </tr>
    </table>

    <hr>

    <!-- Orders -->
    @foreach($orders as $order)
        <b>{{ $order->product->name }}</b> &nbsp; <b>{{ $order->visibleQuantity }} Ad.</b>

        <!-- Specs -->
        <div style="font-size: 11px;">
            @foreach($order->product->specs as $spec)
                @foreach($spec->options as $option)
                    @if($order->isOptionSelected($option))
                        <div style="border: 1px solid black; padding: 2px 3px;">
                            {{ $option->name }}
                        </div>
                    @endif
                @endforeach
            @endforeach
        </div>

        <!-- Note -->
        @if($order->note)
            <div><b>Not:</b> {{ $order->note }}</div>
        @endif
    @endforeach

    <!-- Cancel Reason -->
    <div><b>İptal sebebi:</b> {{ $order->cancel_reason }}</div>
</div>
```

### 3. Bill Template

**Dosya:** `resources/views/common/bill.blade.php`

#### Yapı:

```blade
<style>
    @media print {
        html { background: none; }
        td, th { padding: 0 2px; }
        table { width: 100%; }
        .ta-r { text-align: right; }
        table, th, td { white-space: nowrap; }
    }
</style>

<div id="bill" style="font-size:{{ companySetting('bill-font-size') }}px">
    <!-- Restaurant Info -->
    <div style="text-align: center">
        <p>{{ company()->name }}</p>
        <p>{{ company()->address }}</p>
        @if(company()->phone)
            <p>TEL: {{ company()->phone }}</p>
        @endif
    </div>

    <!-- Sale Info -->
    <table>
        <tr>
            <td><b>MASA :</b></td>
            <td class="ta-r">Masa {{ $sale->destinationName }}</td>
        </tr>
        <tr>
            <td><b>SATIŞ NO :</b></td>
            <td class="ta-r">#{{ $sale->saleNo }}</td>
        </tr>
        <tr>
            <td><b>AÇILIŞ :</b></td>
            <td class="ta-r">{{ $sale->started_at->format('H:i:s') }}</td>
        </tr>
        <tr>
            <td><b>GARSON :</b></td>
            <td class="ta-r">{{ $sale->user->fullname }}</td>
        </tr>
        <tr>
            <td><b>ÖDEME :</b></td>
            <td class="ta-r">{{ trans('payment_types.' . $sale->payment_type) }}</td>
        </tr>
    </table>

    <hr>

    <!-- Orders by Category and VAT -->
    @foreach($groupStats as $group)
        <table>
            <tr>
                <td colspan="2"><b>{{ $group['invoice_title'] }}</b> (KDV: {{ $group['vat_pct'] }}%)</td>
            </tr>
            @foreach($stackedOrders as $order)
                @if($order->product->vat_pct == $group['vat_pct'])
                <tr>
                    <td>{{ $order->product->name }} x{{ $order->quantity }}</td>
                    <td class="ta-r">{{ currencyFormat($order->total_price) }}</td>
                </tr>
                @endif
            @endforeach
            <tr>
                <td><b>Ara Toplam</b></td>
                <td class="ta-r"><b>{{ currencyFormat($group['total_price']) }}</b></td>
            </tr>
        </table>
        <br>
    @endforeach

    <hr>

    <!-- Totals -->
    <table>
        @if($sale->discount)
        <tr>
            <td>İndirim</td>
            <td class="ta-r">-{{ currencyFormat($sale->discount) }}</td>
        </tr>
        @endif
        @if($sale->treat)
        <tr>
            <td>İkram</td>
            <td class="ta-r">{{ currencyFormat($sale->treat) }}</td>
        </tr>
        @endif
        <tr>
            <td>KDV</td>
            <td class="ta-r">{{ currencyFormat($sale->vat) }}</td>
        </tr>
        <tr>
            <td>Servis Ücreti</td>
            <td class="ta-r">{{ currencyFormat($sale->service_fee) }}</td>
        </tr>
        <tr>
            <td><b>TOPLAM</b></td>
            <td class="ta-r"><b>{{ currencyFormat($sale->net_price) }}</b></td>
        </tr>
    </table>
</div>
```

---

## Konfigürasyon Ayarları

### Company Settings:

**Dosya:** `config/settings.php`

#### 1. Printer Solution:

```php
'printer_solution' => [
    'type'    => 'select',
    'options' => ['qz', 'gcp'],
    'default' => 'gcp',
],
```

#### 2. Slip Font Size:

```php
'slip-font-size' => [
    'type'    => 'numeric',
    'default' => 25,
],
```

- Mutfak/bar fişleri ve iptal fişleri için font boyutu
- Birim: pixel

#### 3. Bill Font Size:

```php
'bill-font-size' => [
    'type'    => 'numeric',
    'default' => 25,
],
```

- Hesap fişleri için font boyutu
- Birim: pixel

#### 4. Show Waiter Names:

```php
'show_waiter_names_in_tacking_sections' => [
    'type'    => 'boolean',
    'default' => true,
],
```

- Mutfak fişlerinde garson adını göster/gizle

### Ayarların Kullanımı:

```blade
<!-- Fiş template'lerinde -->
<div style="font-size:{{ companySetting('slip-font-size') }}px">
    ...
</div>

<!-- Bill template'inde -->
<div style="font-size:{{ companySetting('bill-font-size') }}px">
    ...
</div>

<!-- Printer solution kontrolü -->
@if(companySetting('printer_solution') === 'gcp')
    <!-- GCP logic -->
@else
    <!-- QZ logic -->
@endif
```

---

## API Endpoints

### 1. Receipts List Endpoint

**Route:** `GET /api/receipts`  
**Controller:** `ApiController::listReceipts()`

```php
public function listReceipts()
{
    $auth = $this->authorizeRequest();
    if ($auth !== true) return $auth;

    $lastNMinutes = request()->get('minutes', 5);

    $receiptsToPrint = [];
    $time = Carbon::now()->subMinutes($lastNMinutes);

    // Yazdırılmamış satışları al
    $sales = Sale::with('allOrders.product')
        ->onlyTrashed()
        ->whereIn('payment_type', [
            PAYMENT_TYPE['cash'],
            PAYMENT_TYPE['credit-card'],
            PAYMENT_TYPE['online']
        ])
        ->where('is_slip_printed', 0)
        ->where('deleted_at', '>', $time)
        ->get();

    foreach ($sales as $sale) {
        $orders = [];

        foreach ($sale->allOrders as $order) {
            $orders[] = [
                'id'          => $order->id,
                'quantity'    => $order->quantity,
                'totalPrice'  => $order->total_price,
                'unitPrice'   => currencyRound($order->unit_price),
                'name'        => $order->product->name,
                'uom'         => $order->product->uom,
                'price'       => $order->price,
                'vat'         => $order->vat,
                'serviceFee'  => $order->service_fee,
                'vatPct'      => $order->product->vat_pct,
                'paymentType' => $this->mapPaymentType($order->payment_type),
            ];
        }

        $receiptsToPrint[] = [
            'id'     => $sale->id,
            'detail' => [
                'grossPrice'  => $sale->gross_price,
                'netPrice'    => $sale->net_price,
                'vat'         => $sale->vat,
                'serviceFee'  => $sale->service_fee,
                'discount'    => $sale->discount,
                'treat'       => $sale->treat,
                'cancel'      => $sale->cancel,
                'numOrders'   => $sale->num_orders,
                'paymentType' => $this->mapPaymentType($sale->payment_type),
            ],
            'orders' => $orders,
        ];
    }

    return $this->response($receiptsToPrint);
}
```

**Kullanım:**

- POS cihazları veya external sistemler tarafından çağrılır
- Son N dakikadaki yazdırılmamış fişleri döner
- `is_slip_printed` flag'i ile takip edilir

### 2. Update Receipts Endpoint

**Route:** `POST /api/receipts`  
**Controller:** `ApiController::updateReceipts()`

```php
public function updateReceipts()
{
    $auth = $this->authorizeRequest();
    if ($auth !== true) return $auth;

    $ids = request()->get('receipt_ids');

    // Yazdırılan fişleri işaretle
    $this->company->sales()
        ->whereIn('id', $ids)
        ->withTrashed()
        ->update(['is_slip_printed' => 1]);

    return $this->response(['success' => true]);
}
```

**Kullanım:**

- External sistem fişleri yazdırdıktan sonra çağırır
- `is_slip_printed` flag'ini 1 yapar
- Aynı fişlerin tekrar yazdırılmasını önler

### 3. Firm Sync Endpoint

**Route:** `POST /firm/sync`  
**Kullanım:** Frontend QZ Tray sync için

```php
// Frontend'den gelen data:
{
    "printed_jobs": ["job_id_1", "job_id_2"],
    "printers_connected": 1
}

// Response:
{
    "print_jobs": {
        "new_job_id": {
            "printer_id": 1,
            "printer_name": "Kitchen_01",
            "content": "<div>...</div>",
            "options": {...}
        }
    }
}
```

---

## Frontend Yazdırma Mantığı

### QZ Tray Initialization:

**Dosya:** `resources/views/common/qz-print-js.blade.php`

#### 1. Connection Check Loop:

```javascript
let checks = 0;
let interval = setInterval(function () {
    if (!qz.websocket.isActive()) {
        console.log("Printer connection is not ready yet.");
        checks++;

        // 20 deneme (6 saniye) sonra vazgeç
        if (checks >= 20) {
            clearInterval(interval);
            console.log("Printers are not connected.");
            performRequest([], 0);
        }
    } else {
        console.log("Printer connection has established.");
        clearInterval(interval);

        // Mevcut job'ları yazdır
        processPrintJobs(printJobs);

        // Her 5 saniyede bir sync
        setInterval(syncLoop, 5000);
    }
}, 300);
```

#### 2. Print Job Processing:

```javascript
async function processPrintJobs(printJobs) {
    if (Object.keys(printJobs).length)
        console.log(Object.keys(printJobs).length + " job(s) found.");

    for (let id in printJobs) {
        let printJob = printJobs[id];

        // Printer'ı bul
        await qz.printers
            .find(printJob.printer_name)
            .then(function (printer) {
                // Config oluştur
                let config = qz.configs.create(printer);

                // Print data hazırla
                let data = [
                    {
                        type: "html",
                        format: "plain",
                        data: printJob.content,
                    },
                ];

                // Yazdır
                return qz
                    .print(config, data)
                    .then(function () {
                        completedJobIds.push(id);
                        console.log("print job sent");
                    })
                    .catch(function (e) {
                        console.log(e);
                    });
            })
            .catch(function (e) {
                console.error(e);
            });
    }
}
```

#### 3. Sync Loop:

```javascript
function syncLoop() {
    // Tamamlanan job ID'lerini al ve sıfırla
    let _completedJobIds = completedJobIds;
    completedJobIds = [];

    if (_completedJobIds.length) {
        console.log("ABOUT TO MARK SOME JOBS AS DONE", _completedJobIds);
    }

    // Server'a bildir
    performRequest(_completedJobIds);
}

function performRequest(completedJobIds = [], isPrintersConnected = 1) {
    $.ajax({
        type: "POST",
        url: syncEndpoint,
        data: {
            printed_jobs: completedJobIds,
            printers_connected: isPrintersConnected,
        },
        success: function (response) {
            // Yeni job'lar varsa yazdır
            if (Object.keys(response.print_jobs).length) {
                processPrintJobs(response.print_jobs);
            }
        },
        dataType: "json",
        contentType: "application/x-www-form-urlencoded",
    });
}
```

### Manual Print Button:

Bazı sayfalarda manuel yazdırma butonu bulunur:

```javascript
$("#print-btn").click(printReceipt);

function printReceipt() {
    var orders = [];
    var totalPrice = 0;

    // Orders topla
    $(".order-item").each(function () {
        orders.push({
            name: $(this).data("name"),
            quantity: $(this).data("quantity"),
            price: $(this).data("price"),
        });
        totalPrice += $(this).data("price");
    });

    // HTML content oluştur
    var content = generateReceiptHTML(orders, totalPrice);

    // QZ Tray ile yazdır
    qz.printers.find(defaultPrinter).then(function (printer) {
        let config = qz.configs.create(printer);
        let data = [
            {
                type: "html",
                format: "plain",
                data: content,
            },
        ];

        return qz.print(config, data);
    });
}
```

---

## Troubleshooting

### 1. QZ Tray Bağlantı Sorunları

**Problem:** QZ Tray bağlantısı kurulamıyor

**Çözüm Adımları:**

1. QZ Tray servisinin çalıştığını kontrol et

    ```bash
    # Windows
    tasklist | findstr qz

    # Linux/Mac
    ps aux | grep qz
    ```

2. WebSocket portunu kontrol et (varsayılan: 8181)

    ```
    http://localhost:8181
    ```

3. Sertifika kurulumunu doğrula
    - QZ Tray tray icon → About → Certificate Status

4. Browser console'da hata mesajlarını kontrol et

    ```javascript
    console.log(qz.websocket.isActive());
    ```

5. Firewall ayarlarını kontrol et
    - Port 8181'in açık olduğundan emin ol

### 2. Fiş Yazdırılmıyor

**Problem:** Sipariş oluşturuluyor ama fiş basılmıyor

**Kontrol Listesi:**

1. Tracking section'a printer atanmış mı?

    ```php
    $trackingSection->printer // null olmamalı
    ```

2. Printer enabled mı?

    ```php
    $printer->enabled // true olmalı
    ```

3. Printer solution doğru ayarlanmış mı?

    ```php
    companySetting('printer_solution') // 'qz' veya 'gcp'
    ```

4. GCP kullanılıyorsa Google token var mı?

    ```php
    company()->google_refresh_token // null olmamalı
    ```

5. Cache temizle

    ```bash
    php artisan cache:clear
    ```

6. Print queue'yu kontrol et
    ```php
    $jobs = getJobsForPrinting();
    dd($jobs);
    ```

### 3. GCP Yazdırma Hataları

**Problem:** Google Cloud Print ile yazdırma başarısız

**Çözüm:**

1. Google OAuth token'ı yenile

    ```php
    $client = company()->googleClient();
    $token = $client->getAccessToken();
    ```

2. Printer ID'yi kontrol et

    ```php
    $printer->gcp_id // geçerli GCP printer ID olmalı
    ```

3. GCP API logs'u kontrol et

    ```php
    \Log::info('GCP print attempt', [
        'printer_id' => $printer->gcp_id,
        'title' => $title,
    ]);
    ```

4. Google Cloud Console'dan API quota'yı kontrol et

### 4. Font Boyutu Sorunları

**Problem:** Fiş üzerindeki yazılar çok küçük/büyük

**Çözüm:**

1. Company settings'den font boyutunu ayarla

    ```
    Admin Panel → Company Settings → Slip Font Size
    ```

2. Varsayılan değerler:
    - Slip font size: 25px
    - Bill font size: 25px

3. Custom CSS ile override et
    ```blade
    <div style="font-size:{{ companySetting('slip-font-size') * 1.2 }}px">
    ```

### 5. Duplicate Printing (Çift Yazdırma)

**Problem:** Aynı fiş birden fazla kez basılıyor

**Çözüm:**

1. Print queue reservation sistemini kontrol et

    ```php
    // Beta users için reservation aktif
    isBetaUser() // true ise reservation devrede
    ```

2. Frontend sync interval'ı kontrol et

    ```javascript
    // Varsayılan: 5000ms (5 saniye)
    setInterval(syncLoop, 5000);
    ```

3. Completed job IDs'nin düzgün gönderildiğini doğrula

    ```javascript
    console.log("Completed jobs:", completedJobIds);
    ```

4. Cache TTL'yi kontrol et
    ```php
    // Varsayılan: 300 saniye (5 dakika)
    Cache::put($cacheKey, $queueJobs, 300);
    ```

### 6. Tracking Section Printer Ataması

**Problem:** Belirli bir tracking section için fiş basılmıyor

**Çözüm:**

1. Tracking section'ın printer'ı var mı kontrol et

    ```php
    $trackingSection = TrackingSection::find($id);
    dd($trackingSection->printer);
    ```

2. Admin panelinden printer ata

    ```
    Admin → Restaurant → Tracking Sections → Edit
    ```

3. Printer'ın aynı şirkete ait olduğunu doğrula
    ```php
    $printer->company_id === $trackingSection->company_id
    ```

### Debugging Tools:

#### 1. Print Queue Inspector:

```php
// Tinker'da çalıştır
$companyId = company()->id;
$cacheKey = "c{$companyId}_print_queue";
$jobs = Cache::get($cacheKey, []);
dd($jobs);
```

#### 2. Test Print:

```php
// Admin panelinde test print özelliği var
route('admin.restaurant.printers.diagnose.print-test', $printerId)
```

#### 3. Log Monitoring:

```bash
# Real-time log izleme
tail -f storage/logs/laravel.log | grep -i print
```

#### 4. Database Check:

```sql
-- Printer status kontrolü
SELECT id, display_name, enabled, gcp_id
FROM printers
WHERE company_id = ?;

-- Sales with is_slip_printed flag
SELECT id, is_slip_printed, deleted_at
FROM sales
WHERE is_slip_printed = 0
ORDER BY deleted_at DESC
LIMIT 10;
```

---

## Best Practices

### 1. Printer Assignment Strategy

- Her tracking section için dedicated printer ata
- Backup printer tanımla (manuel fallback için)
- Printer isimlerini anlamlı yap: "Kitchen_Main", "Bar_Cocktails"

### 2. Error Handling

- Printer hatalarını logla ama user experience'ı bloklama
- Async printing kullan (queue = true)
- Failed print job'ları retry mekanizması ekle

### 3. Performance Optimization

- Print queue TTL'yi ihtiyaca göre ayarla (default: 5 min)
- Frontend polling interval'ı optimize et (default: 5 sec)
- Beta users için reservation sistemi kullan

### 4. User Experience

- Print success/failure feedback ver
- Offline durumda kullanıcıyı bilgilendir
- Manual reprint seçeneği sun

### 5. Security

- API endpoints için authentication zorunlu
- Printer access control implement et
- Sensitive data'yı fişlerde mask'le

---

## Gelecek Geliştirmeler

### 1. Multi-Printer Support

- Aynı tracking section için birden fazla printer
- Load balancing veya failover

### 2. Print Templates Customization

- Admin panelinden template editor
- Custom CSS injection
- Conditional printing rules

### 3. Advanced Queue Management

- Priority queues
- Scheduled printing
- Print job analytics

### 4. Mobile Printing

- Bluetooth printer support
- WiFi direct printing
- Mobile app integration

### 5. Cloud Printing Alternatives

- PrintNode entegrasyonu
- Star CloudPRNT
- Epson ePOS

---

## Sonuç

Bu doküman, poscloud.apper projesindeki fiş/slip yazdırma sisteminin kapsamlı bir analizini sunmaktadır. Sistem, QZ Tray ve Google Cloud Print çözümlerini destekleyen esnek bir mimariye sahiptir.

**Ana Bileşenler:**

- **Printer Model:** Yazıcı yönetimi
- **Tracking Section:** Mutfak/bar bölüm organizasyonu
- **Print Queue:** Cache-based job management
- **QZ Tray Integration:** Local printer communication
- **GCP Integration:** Cloud printing (legacy)
- **Blade Templates:** Fiş template'leri

**Desteklenen Fiş Türleri:**

1. Tracking Section Slip (Mutfak/Bar fişi)
2. Cancel Slip (İptal fişi)
3. Bill/Hesap fişi

Sistem, restaurant, delivery ve quick order senaryolarını destekler ve genişletilebilir bir yapıya sahiptir.

---

## Dosya Referansları

### Core Models

- `app/Models/Printer.php` - Printer modeli
- `app/Models/TrackingSection.php` - Tracking section modeli
- `app/Models/Sale.php` - Sale modeli (printCancelSlip, bill metodları)

### Libraries

- `app/Libraries/GoogleCloudPrint.php` - GCP integration

### Helpers

- `app/helpers.php` - addToPrintQueue, getJobsForPrinting, removePrintJobs

### Controllers

- `app/Http/Controllers/Firm/OrderController.php` - Order creation
- `app/Http/Controllers/Firm/SaleController.php` - Cancel handling
- `app/Http/Controllers/ApiController.php` - Receipt API endpoints

### Repositories

- `app/Repositories/SaleRepository.php` - Order creation & printing
- `app/Repositories/CheckoutRepository.php` - Checkout & cancel printing

### Views (Templates)

- `resources/views/common/tracking-section-slip.blade.php` - Kitchen/bar slip
- `resources/views/common/cancel-slip.blade.php` - Cancel slip
- `resources/views/common/bill.blade.php` - Customer bill
- `resources/views/common/qz-print-js.blade.php` - QZ Tray frontend logic

### Routes

- `routes/api.php` - Receipt API endpoints

### Configuration

- `config/settings.php` - Printer settings

### Migrations

- `database/migrations/2023_12_03_203717_add_is_slip_printed_to_sales.php`

---

**Doküman Versiyonu:** 1.0  
**Son Güncelleme:** 2026-04-09  
**Yazar:** AI Assistant  
**Proje:** poscloud.apper
