Commit effca1ea authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix broken print: add format_money helper + rewrite receipt views

Root cause: format_money() was used in 20+ views but never defined,
causing silent fatal errors on every print attempt. Also simplified
ReceiptController to render standalone HTML pages instead of depending
on ReceiptTemplate records that may not exist in the DB.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent c2683d48
<?php
if (!function_exists('format_money')) {
function format_money(int|float|null $piasters, string $currency = 'ج.م'): string
{
if ($piasters === null) {
$piasters = 0;
}
return number_format($piasters / 100, 2) . ' ' . $currency;
}
}
......@@ -4,37 +4,20 @@
use App\Domain\Financial\Models\Payment;
use App\Domain\POS\Models\POSTransaction;
use App\Domain\POS\Models\ReceiptTemplate;
use App\Domain\POS\Services\ReceiptService;
use Illuminate\Http\Request;
class ReceiptController extends Controller
{
public function __construct(
private ReceiptService $receiptService,
) {}
public function posPrint(POSTransaction $transaction)
{
$template = ReceiptTemplate::resolveFor(
$transaction->branch_id,
'pos'
);
$data = $this->receiptService->buildPosReceiptData($transaction, $template);
$transaction->load(['items', 'participant.person', 'branch', 'processedBy']);
return view('receipts.pos', ['data' => $data]);
return view('print.pos-receipt', compact('transaction'));
}
public function paymentPrint(Payment $payment)
{
$template = ReceiptTemplate::resolveFor(
$payment->branch_id ?? auth()->user()->branch_id,
'pos'
);
$data = $this->receiptService->buildPaymentReceiptData($payment, $template);
$payment->load(['invoice.billable.person', 'invoice.items', 'creator']);
return view('receipts.pos', ['data' => $data]);
return view('print.payment-receipt', compact('payment'));
}
}
......@@ -22,6 +22,7 @@
},
"autoload": {
"files": [
"app/Helpers/money.php",
"app/Helpers/whatsapp.php"
],
"psr-4": {
......
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ __('إيصال دفع') }} - {{ $payment->reference ?? $payment->uuid }}</title>
@php
$branding = app(\App\Domain\Shared\Services\SettingsService::class);
$brandLogo = $branding->get('branding.logo');
$brandPrimary = $branding->get('branding.primary_color', '#1e40af');
$academyName = app()->bound('current_academy') ? app('current_academy')->name_ar : 'الكابتن';
@endphp
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Cairo', 'Noto Sans Arabic', sans-serif;
direction: rtl;
font-size: 14px;
color: #333;
background: #f3f4f6;
padding: 16px;
}
.receipt-wrapper {
max-width: 400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
text-align: center;
padding: 20px 16px 12px;
border-bottom: 2px dashed #e2e8f0;
}
.header img { height: 40px; object-fit: contain; margin-bottom: 8px; }
.header h1 { font-size: 18px; color: {{ $brandPrimary }}; margin: 4px 0; }
.header .academy { font-size: 13px; color: #64748b; }
.amount-section {
text-align: center;
padding: 20px 16px;
border-bottom: 2px dashed #e2e8f0;
}
.amount-label { font-size: 12px; color: #64748b; margin-bottom: 4px; }
.amount-value { font-size: 28px; font-weight: bold; color: #1e293b; direction: ltr; }
.info-section {
padding: 12px 16px;
border-bottom: 1px dashed #e2e8f0;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 5px 0;
font-size: 13px;
}
.info-row .label { color: #64748b; }
.info-row .value { font-weight: 600; color: #1e293b; }
.status-badge {
display: inline-block;
padding: 3px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-confirmed { background: #dcfce7; color: #166534; }
.status-pending { background: #fef9c3; color: #854d0e; }
.status-failed { background: #fef2f2; color: #991b1b; }
.status-cancelled { background: #f3f4f6; color: #6b7280; }
.status-refunded { background: #f3e8ff; color: #6b21a8; }
.footer {
padding: 12px 16px;
text-align: center;
font-size: 11px;
color: #94a3b8;
border-top: 1px dashed #e2e8f0;
}
.actions {
padding: 16px;
display: flex;
justify-content: center;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: background 0.15s;
}
.btn-print { background: {{ $brandPrimary }}; color: white; }
.btn-print:active { opacity: 0.85; }
@media print {
body { background: white; padding: 0; }
.receipt-wrapper { box-shadow: none; border-radius: 0; max-width: 80mm; }
.actions { display: none !important; }
@page { size: 80mm auto; margin: 0; }
}
</style>
</head>
<body>
<div class="receipt-wrapper">
<div class="header">
@if($brandLogo)
<img src="{{ Storage::disk('public')->url($brandLogo) }}" alt="{{ $academyName }}">
@endif
<h1>{{ __('إيصال دفع') }}</h1>
<div class="academy">{{ $academyName }}</div>
</div>
<div class="amount-section">
<div class="amount-label">{{ __('المبلغ المدفوع') }}</div>
<div class="amount-value">{{ format_money($payment->amount) }}</div>
</div>
<div class="info-section">
<div class="info-row">
<span class="label">{{ __('رقم المرجع') }}</span>
<span class="value" dir="ltr">{{ $payment->reference ?? 'PAY-' . str_pad($payment->id, 6, '0', STR_PAD_LEFT) }}</span>
</div>
<div class="info-row">
<span class="label">{{ __('التاريخ') }}</span>
<span class="value" dir="ltr">{{ $payment->payment_date?->format('Y/m/d') ?? $payment->created_at?->format('Y/m/d') }}</span>
</div>
@php
$methodLabels = [
'cash' => 'نقدي',
'card' => 'بطاقة',
'bank_transfer' => 'تحويل بنكي',
'wallet' => 'محفظة',
'online' => 'إلكتروني',
'cheque' => 'شيك',
'other' => 'أخرى',
];
$methodValue = $payment->method?->value ?? $payment->method ?? '';
@endphp
<div class="info-row">
<span class="label">{{ __('طريقة الدفع') }}</span>
<span class="value">{{ $methodLabels[$methodValue] ?? $methodValue }}</span>
</div>
@if($payment->invoice)
<div class="info-row">
<span class="label">{{ __('رقم الفاتورة') }}</span>
<span class="value" dir="ltr">{{ $payment->invoice->number }}</span>
</div>
@endif
@php
$payerName = $payment->invoice?->contact_name
?? $payment->invoice?->billable?->person?->name_ar
?? $payment->invoice?->billable?->person?->name
?? null;
@endphp
@if($payerName)
<div class="info-row">
<span class="label">{{ __('الدافع') }}</span>
<span class="value">{{ $payerName }}</span>
</div>
@endif
@if($payment->creator)
<div class="info-row">
<span class="label">{{ __('استلم بواسطة') }}</span>
<span class="value">{{ $payment->creator->name ?? '' }}</span>
</div>
@endif
</div>
@php
$statusValue = $payment->status?->value ?? $payment->status ?? 'pending';
$statusLabels = [
'pending' => 'معلق',
'confirmed' => 'مؤكد',
'failed' => 'فاشل',
'cancelled' => 'ملغى',
'refunded' => 'مسترد',
];
@endphp
<div style="text-align: center; padding: 12px 16px; border-bottom: 1px dashed #e2e8f0;">
<span class="status-badge status-{{ $statusValue }}">{{ $statusLabels[$statusValue] ?? $statusValue }}</span>
</div>
@if($payment->notes)
<div class="info-section">
<div style="font-size: 12px; color: #64748b;">{{ __('ملاحظات') }}:</div>
<div style="font-size: 13px; margin-top: 4px;">{{ $payment->notes }}</div>
</div>
@endif
<div class="footer">
{{ __('تم إنشاء هذا المستند إلكترونياً ولا يحتاج إلى توقيع') }}
</div>
<div class="actions">
<button onclick="window.print()" class="btn btn-print">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
{{ __('طباعة') }}
</button>
</div>
</div>
<script>
window.onload = function() {
if (window.matchMedia('print').matches || window.location.hash === '#auto') {
window.print();
}
};
</script>
</body>
</html>
<!DOCTYPE html>
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ __('إيصال مبيعات') }} - {{ $transaction->receipt_number }}</title>
@php
$branding = app(\App\Domain\Shared\Services\SettingsService::class);
$brandLogo = $branding->get('branding.logo');
$brandPrimary = $branding->get('branding.primary_color', '#1e40af');
$academyName = app()->bound('current_academy') ? app('current_academy')->name_ar : 'الكابتن';
@endphp
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Cairo', 'Noto Sans Arabic', sans-serif;
direction: rtl;
font-size: 14px;
color: #333;
background: #f3f4f6;
padding: 16px;
}
.receipt-wrapper {
max-width: 400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
text-align: center;
padding: 20px 16px 12px;
border-bottom: 2px dashed #e2e8f0;
}
.header img { height: 40px; object-fit: contain; margin-bottom: 8px; }
.header h1 { font-size: 18px; color: {{ $brandPrimary }}; margin: 4px 0; }
.header .academy { font-size: 13px; color: #64748b; }
.header .branch { font-size: 11px; color: #94a3b8; }
.info-section {
padding: 12px 16px;
border-bottom: 1px dashed #e2e8f0;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
font-size: 12px;
}
.info-row .label { color: #64748b; }
.info-row .value { font-weight: 600; color: #1e293b; }
.items-section { padding: 12px 16px; }
table { width: 100%; border-collapse: collapse; }
th {
font-size: 11px;
font-weight: 600;
color: #64748b;
text-align: right;
padding: 6px 4px;
border-bottom: 1px solid #e2e8f0;
}
td {
font-size: 12px;
padding: 8px 4px;
border-bottom: 1px solid #f1f5f9;
}
td.num { text-align: left; direction: ltr; }
.totals-section {
padding: 12px 16px;
border-top: 2px dashed #e2e8f0;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 13px;
}
.total-row.grand {
font-size: 18px;
font-weight: bold;
padding-top: 8px;
margin-top: 4px;
border-top: 1px solid #e2e8f0;
}
.total-row .amount { direction: ltr; }
.footer {
padding: 12px 16px;
text-align: center;
font-size: 11px;
color: #94a3b8;
border-top: 1px dashed #e2e8f0;
}
.actions {
padding: 16px;
display: flex;
justify-content: center;
gap: 12px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 28px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: background 0.15s;
}
.btn-print { background: {{ $brandPrimary }}; color: white; }
.btn-print:active { opacity: 0.85; }
@media print {
body { background: white; padding: 0; }
.receipt-wrapper { box-shadow: none; border-radius: 0; max-width: 80mm; }
.actions { display: none !important; }
@page { size: 80mm auto; margin: 0; }
}
</style>
</head>
<body>
<div class="receipt-wrapper">
<div class="header">
@if($brandLogo)
<img src="{{ Storage::disk('public')->url($brandLogo) }}" alt="{{ $academyName }}">
@endif
<h1>{{ __('إيصال مبيعات') }}</h1>
<div class="academy">{{ $academyName }}</div>
@if($transaction->branch)
<div class="branch">{{ $transaction->branch->name_ar }}</div>
@endif
</div>
<div class="info-section">
<div class="info-row">
<span class="label">{{ __('رقم الإيصال') }}</span>
<span class="value" dir="ltr">{{ $transaction->receipt_number }}</span>
</div>
<div class="info-row">
<span class="label">{{ __('التاريخ') }}</span>
<span class="value" dir="ltr">{{ $transaction->processed_at?->format('Y/m/d H:i') ?? $transaction->created_at?->format('Y/m/d H:i') }}</span>
</div>
@if($transaction->participant?->person)
<div class="info-row">
<span class="label">{{ __('العميل') }}</span>
<span class="value">{{ $transaction->participant->person->name_ar ?? $transaction->participant->person->name ?? '-' }}</span>
</div>
@endif
@if($transaction->processedBy)
<div class="info-row">
<span class="label">{{ __('الكاشير') }}</span>
<span class="value">{{ $transaction->processedBy->name ?? '' }}</span>
</div>
@endif
</div>
@if($transaction->items->count())
<div class="items-section">
<table>
<thead>
<tr>
<th>{{ __('الصنف') }}</th>
<th class="num">{{ __('كمية') }}</th>
<th class="num">{{ __('السعر') }}</th>
<th class="num">{{ __('الإجمالي') }}</th>
</tr>
</thead>
<tbody>
@foreach($transaction->items as $item)
<tr>
<td>{{ $item->item_name_ar ?? $item->item_name ?? '-' }}</td>
<td class="num">{{ $item->quantity }}</td>
<td class="num">{{ format_money($item->unit_price) }}</td>
<td class="num">{{ format_money($item->line_total) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
<div class="totals-section">
<div class="total-row">
<span>{{ __('المجموع') }}</span>
<span class="amount">{{ format_money($transaction->subtotal) }}</span>
</div>
@if(($transaction->discount_amount ?? 0) > 0)
<div class="total-row" style="color: #16a34a;">
<span>{{ __('الخصم') }}</span>
<span class="amount">-{{ format_money($transaction->discount_amount) }}</span>
</div>
@endif
@if(($transaction->tax_amount ?? 0) > 0)
<div class="total-row">
<span>{{ __('الضريبة') }}</span>
<span class="amount">{{ format_money($transaction->tax_amount) }}</span>
</div>
@endif
<div class="total-row grand">
<span>{{ __('الإجمالي') }}</span>
<span class="amount">{{ format_money($transaction->total_amount) }}</span>
</div>
@php
$methodLabels = [
'cash' => 'نقدي',
'card' => 'بطاقة',
'wallet' => 'محفظة',
'split' => 'مقسم',
'bank_transfer' => 'تحويل بنكي',
'online' => 'إلكتروني',
'cheque' => 'شيك',
'other' => 'أخرى',
];
$methodValue = $transaction->payment_method?->value ?? $transaction->payment_method ?? '';
@endphp
<div class="total-row" style="margin-top: 8px;">
<span>{{ __('طريقة الدفع') }}</span>
<span class="value">{{ $methodLabels[$methodValue] ?? $methodValue }}</span>
</div>
</div>
<div class="footer">
{{ __('تم إنشاء هذا المستند إلكترونياً ولا يحتاج إلى توقيع') }}
</div>
<div class="actions">
<button onclick="window.print()" class="btn btn-print">
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"/>
</svg>
{{ __('طباعة') }}
</button>
</div>
</div>
<script>
window.onload = function() {
if (window.matchMedia('print').matches || window.location.hash === '#auto') {
window.print();
}
};
</script>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment