Faza 8: PDF generation pentru fișa lucru (DomPDF)

- barryvdh/laravel-dompdf instalat
- WorkOrderPdfService: încarcă WO cu toate relațiile (works/parts/payments),
  embed-ează logo ca data URI, foloseste theme_color din settings
- Blade template /resources/views/pdf/work-order.blade.php:
  - Header cu logo + date companie + nr fișă + data
  - Box-uri client + auto (kilometraj/VIN/plate)
  - Plângere + diagnostic
  - Tabel manopere (h, preț/h, total) cu maistru pe fiecare rând
  - Tabel piese (cod, brand, qty, preț, total)
  - Box total cu discount + plăți efectuate + rest de achitat
  - Block recomandări cu fundal galben (warning)
  - Linii semnătură client + maistru
  - Footer cu timestamp generare
- Action 'PDF' (icon descărcare) pe rând în lista de WO
- Action 'Descarcă PDF' în header-ul paginii Edit WO
This commit is contained in:
2026-05-07 13:01:42 +00:00
parent f1d196f018
commit 19a7afac27
6 changed files with 904 additions and 2 deletions
+295
View File
@@ -0,0 +1,295 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<title>{{ $wo->number }} Fișă lucru</title>
<style>
* { box-sizing: border-box; }
body {
font-family: DejaVu Sans, sans-serif;
font-size: 11px;
color: #1f2937;
margin: 0; padding: 24px;
}
h1, h2, h3 { margin: 0; }
.row { display: table; width: 100%; }
.col { display: table-cell; vertical-align: top; }
.col-half { width: 50%; }
.col-third { width: 33.33%; }
.header { border-bottom: 2px solid {{ $themeColor }}; padding-bottom: 12px; margin-bottom: 16px; }
.brand { font-size: 18px; font-weight: 700; color: {{ $themeColor }}; }
.brand-sub { font-size: 11px; color: #6b7280; margin-top: 4px; }
.doc-title {
text-align: right;
font-size: 22px; font-weight: 700;
color: {{ $themeColor }};
}
.doc-meta {
text-align: right;
font-size: 11px; color: #6b7280; margin-top: 4px;
}
.box {
border: 1px solid #e5e7eb; border-radius: 6px;
padding: 12px; margin-bottom: 12px;
}
.box h3 {
font-size: 12px; text-transform: uppercase;
color: #6b7280; margin-bottom: 8px;
border-bottom: 1px solid #f3f4f6; padding-bottom: 4px;
}
.kv { font-size: 11px; line-height: 1.6; }
.kv b { display: inline-block; min-width: 90px; color: #6b7280; font-weight: 500; }
table.items {
width: 100%; border-collapse: collapse;
margin: 8px 0 12px;
}
table.items th {
background: #f9fafb;
text-align: left; padding: 6px 8px;
font-size: 10px; text-transform: uppercase;
color: #6b7280;
border-bottom: 2px solid #e5e7eb;
}
table.items th.r, table.items td.r { text-align: right; }
table.items td {
padding: 6px 8px;
border-bottom: 1px solid #f3f4f6;
font-size: 11px;
}
table.items tfoot td {
font-weight: 700;
border-top: 2px solid #e5e7eb;
border-bottom: none;
}
.total-box {
margin-top: 16px;
padding: 12px;
background: #f9fafb;
border-radius: 6px;
text-align: right;
}
.total-line { font-size: 11px; color: #6b7280; padding: 2px 0; }
.total-line.grand {
font-size: 16px; font-weight: 700;
color: {{ $themeColor }};
border-top: 1px solid #e5e7eb;
margin-top: 6px; padding-top: 8px;
}
.recommendations {
background: #fef3c7; border-left: 4px solid #f59e0b;
padding: 10px 12px; margin: 12px 0;
font-size: 11px;
}
.recommendations h3 {
color: #92400e; font-size: 11px;
margin-bottom: 4px; text-transform: uppercase;
}
.signatures {
margin-top: 24px;
}
.sig-cell {
width: 50%;
padding: 8px 16px;
font-size: 10px;
color: #6b7280;
}
.sig-line {
border-bottom: 1px solid #1f2937;
height: 28px;
}
.footer {
margin-top: 16px; padding-top: 8px;
border-top: 1px solid #f3f4f6;
font-size: 9px; color: #9ca3af;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<div class="row">
<div class="col col-half">
@if (!empty($logoData))
<img src="{{ $logoData }}" style="max-height: 50px; max-width: 200px;" alt="logo">
<div class="brand-sub">{{ $company->display_name ?? $company->name }}</div>
@else
<div class="brand">{{ $company->display_name ?? $company->name }}</div>
@endif
@if ($company->city)
<div class="brand-sub">{{ $company->city }}</div>
@endif
@if ($company->phone)
<div class="brand-sub">📞 {{ $company->phone }}</div>
@endif
@if ($company->email)
<div class="brand-sub"> {{ $company->email }}</div>
@endif
</div>
<div class="col col-half">
<div class="doc-title">FIȘĂ DE LUCRU</div>
<div class="doc-meta">Nr.: <b>{{ $wo->number }}</b></div>
<div class="doc-meta">Data deschiderii: <b>{{ $wo->opened_at?->format('d.m.Y') }}</b></div>
@if ($wo->closed_at)
<div class="doc-meta">Data închiderii: <b>{{ $wo->closed_at->format('d.m.Y') }}</b></div>
@endif
<div class="doc-meta">
Status: <b>{{ \App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status }}</b>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-half" style="padding-right: 8px;">
<div class="box">
<h3>Client</h3>
<div class="kv">
<div><b>Nume:</b> {{ $wo->client?->name ?? '—' }}</div>
<div><b>Telefon:</b> {{ $wo->client?->phone ?? '—' }}</div>
@if ($wo->client?->email)
<div><b>Email:</b> {{ $wo->client->email }}</div>
@endif
</div>
</div>
</div>
<div class="col col-half" style="padding-left: 8px;">
<div class="box">
<h3>Auto</h3>
<div class="kv">
<div><b>Marca/Model:</b> {{ $wo->vehicle?->make }} {{ $wo->vehicle?->model }} {{ $wo->vehicle?->year }}</div>
<div><b>Nr.:</b> {{ $wo->vehicle?->plate ?? '—' }}</div>
@if ($wo->vehicle?->vin)
<div><b>VIN:</b> {{ $wo->vehicle->vin }}</div>
@endif
<div><b>Km la intrare:</b> {{ $wo->mileage_in ? number_format($wo->mileage_in, 0, '.', ' ') : '—' }}</div>
@if ($wo->mileage_out)
<div><b>Km la ieșire:</b> {{ number_format($wo->mileage_out, 0, '.', ' ') }}</div>
@endif
</div>
</div>
</div>
</div>
@if ($wo->complaint || $wo->diagnosis)
<div class="box">
@if ($wo->complaint)
<h3>Plângere client</h3>
<div style="margin-bottom: 8px;">{{ $wo->complaint }}</div>
@endif
@if ($wo->diagnosis)
<h3>Diagnostic</h3>
<div>{{ $wo->diagnosis }}</div>
@endif
</div>
@endif
@if ($wo->works->isNotEmpty())
<h3 style="font-size: 12px; margin: 12px 0 4px; text-transform: uppercase; color: {{ $themeColor }};">Manopere</h3>
<table class="items">
<thead>
<tr>
<th style="width: 50%;">Denumire</th>
<th class="r" style="width: 12%;">Ore</th>
<th class="r" style="width: 18%;">Preț/h</th>
<th class="r" style="width: 20%;">Total</th>
</tr>
</thead>
<tbody>
@foreach ($wo->works as $w)
<tr>
<td>{{ $w->name }}@if ($w->master?->name) <span style="color:#9ca3af;"> {{ $w->master->name }}</span>@endif</td>
<td class="r">{{ number_format((float) $w->hours, 2) }}</td>
<td class="r">{{ number_format((float) $w->price_per_hour, 2, '.', ' ') }}</td>
<td class="r"><b>{{ number_format((float) $w->total, 2, '.', ' ') }}</b></td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="3" class="r">Subtotal manopere:</td>
<td class="r">{{ number_format((float) $worksTotal, 2, '.', ' ') }} {{ $currency }}</td>
</tr>
</tfoot>
</table>
@endif
@if ($wo->parts->isNotEmpty())
<h3 style="font-size: 12px; margin: 12px 0 4px; text-transform: uppercase; color: {{ $themeColor }};">Piese</h3>
<table class="items">
<thead>
<tr>
<th style="width: 40%;">Denumire</th>
<th style="width: 18%;">Cod</th>
<th class="r" style="width: 10%;">Cant.</th>
<th class="r" style="width: 14%;">Preț</th>
<th class="r" style="width: 18%;">Total</th>
</tr>
</thead>
<tbody>
@foreach ($wo->parts as $p)
<tr>
<td>{{ $p->name }}@if ($p->brand) <span style="color:#9ca3af;">({{ $p->brand }})</span>@endif</td>
<td>{{ $p->article ?? '—' }}</td>
<td class="r">{{ number_format((float) $p->qty, 2) }} {{ $p->unit }}</td>
<td class="r">{{ number_format((float) $p->sell_price, 2, '.', ' ') }}</td>
<td class="r"><b>{{ number_format((float) $p->total, 2, '.', ' ') }}</b></td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="4" class="r">Subtotal piese:</td>
<td class="r">{{ number_format((float) $partsTotal, 2, '.', ' ') }} {{ $currency }}</td>
</tr>
</tfoot>
</table>
@endif
<div class="total-box">
@if ($wo->discount_pct > 0)
<div class="total-line">Subtotal: {{ number_format((float) ($worksTotal + $partsTotal), 2, '.', ' ') }} {{ $currency }}</div>
<div class="total-line">Discount: -{{ $wo->discount_pct }}%</div>
@endif
<div class="total-line grand">
TOTAL DE PLATĂ: {{ number_format((float) $wo->total, 2, '.', ' ') }} {{ $currency }}
</div>
@if ($paid > 0)
<div class="total-line">Achitat: {{ number_format($paid, 2, '.', ' ') }} {{ $currency }}</div>
<div class="total-line"><b>Rest de achitat: {{ number_format($balance, 2, '.', ' ') }} {{ $currency }}</b></div>
@endif
</div>
@if ($wo->recommendations)
<div class="recommendations">
<h3> Recomandări</h3>
<div>{{ $wo->recommendations }}</div>
</div>
@endif
<div class="signatures">
<table style="width: 100%;">
<tr>
<td class="sig-cell">
<div>Client (nume, semnătură):</div>
<div class="sig-line"></div>
</td>
<td class="sig-cell">
<div>Maistru / Recepție:</div>
<div class="sig-line"></div>
</td>
</tr>
</table>
</div>
<div class="footer">
Document generat automat {{ now()->format('d.m.Y H:i') }} {{ $company->display_name ?? $company->name }}
</div>
</body>
</html>