5a5f43f2de
- WorkOrder and InjectorProtocol PDF routes now respond with Content-Disposition: inline (opens in browser tab so the user can read/print/save from the built-in PDF viewer). ?download=1 forces the classic attachment download. - CalendarBoard exportPdf() switched to the same inline default. - WO edit action + WO table row action + injector protocol actions now use ->url(...)->openUrlInNewTab() instead of streaming the PDF inline into the current tab. - Dashboard Docs tab has both a preview link and a "Descarcă" link. - Vehicle card photo falls back to WO's first uploaded photo when Vehicle model has no MediaLibrary integration of its own. Test CalendarEnhancementsTest updated to assert the new inline response headers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
861 lines
40 KiB
PHP
861 lines
40 KiB
PHP
<x-filament-panels::page>
|
||
@php
|
||
/** @var \App\Models\Tenant\WorkOrder $wo */
|
||
$wo = $this->record;
|
||
$finance = $this->getFinance();
|
||
$history = $this->getRepairHistory();
|
||
$timeline = $this->getTimeline();
|
||
$messages = $this->getNotifications();
|
||
$prev = $this->getPrevWo();
|
||
$next = $this->getNextWo();
|
||
$tabs = [
|
||
'works' => ['label' => __('Lucrări'), 'count' => $wo->works->count()],
|
||
'parts' => ['label' => __('Piese'), 'count' => $wo->parts->count()],
|
||
'diag' => ['label' => __('Diagnostic'), 'count' => null],
|
||
'photos' => ['label' => __('Foto'), 'count' => $wo->getMedia('photos')->count() ?? 0],
|
||
'docs' => ['label' => __('Documente'), 'count' => null],
|
||
'notes' => ['label' => __('Note'), 'count' => null],
|
||
];
|
||
$statusClass = match ($wo->status) {
|
||
'in_work' => 'wd-status-active',
|
||
'awaiting_parts' => 'wd-status-warn',
|
||
'ready' => 'wd-status-ready',
|
||
'done' => 'wd-status-done',
|
||
'cancelled' => 'wd-status-cancel',
|
||
default => 'wd-status-new',
|
||
};
|
||
@endphp
|
||
|
||
<style>
|
||
/* ══ WORK-ORDER DASHBOARD (Mitchell1-style) ══ */
|
||
:root {
|
||
--wd-bg: #F7F7F5;
|
||
--wd-surface: #FFFFFF;
|
||
--wd-border: #E5E7EB;
|
||
--wd-border-md: #D1D5DB;
|
||
--wd-text: #111827;
|
||
--wd-text-2: #4B5563;
|
||
--wd-text-3: #9CA3AF;
|
||
--wd-blue: #2563EB;
|
||
--wd-blue-bg: #EFF6FF;
|
||
--wd-green: #16A34A;
|
||
--wd-green-bg: #DCFCE7;
|
||
--wd-amber: #D97706;
|
||
--wd-amber-bg: #FEF3C7;
|
||
--wd-red: #DC2626;
|
||
--wd-red-bg: #FEE2E2;
|
||
--wd-slate: #64748B;
|
||
}
|
||
.dark {
|
||
--wd-bg: #0F172A;
|
||
--wd-surface: #1E293B;
|
||
--wd-border: #334155;
|
||
--wd-border-md: #475569;
|
||
--wd-text: #F1F5F9;
|
||
--wd-text-2: #CBD5E1;
|
||
--wd-text-3: #94A3B8;
|
||
}
|
||
|
||
/* Break out of Filament content wrapper */
|
||
.fi-main-ctn:has(.wd-shell) { padding: 0 !important; }
|
||
.fi-main:has(.wd-shell) { padding: 0 !important; }
|
||
.fi-page:has(.wd-shell) > div { padding: 0 !important; gap: 0 !important; }
|
||
.fi-page:has(.wd-shell) .fi-header { display: none !important; }
|
||
|
||
.wd-shell {
|
||
background: var(--wd-bg);
|
||
color: var(--wd-text);
|
||
min-height: calc(100vh - 64px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* ── TOP BAR ── */
|
||
.wd-topbar {
|
||
background: var(--wd-surface);
|
||
border-bottom: 1px solid var(--wd-border);
|
||
padding: 10px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.wd-wo-title { font-size: 15px; font-weight: 700; letter-spacing: -.2px; }
|
||
.wd-status {
|
||
padding: 4px 10px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: .3px;
|
||
cursor: pointer;
|
||
display: inline-flex; align-items: center; gap: 4px;
|
||
}
|
||
.wd-status-active { background: #DBEAFE; color: #1E40AF; }
|
||
.wd-status-warn { background: var(--wd-amber-bg); color: var(--wd-amber); }
|
||
.wd-status-ready { background: var(--wd-green-bg); color: #166534; }
|
||
.wd-status-done { background: #F3F4F6; color: #6B7280; }
|
||
.wd-status-cancel { background: var(--wd-red-bg); color: var(--wd-red); }
|
||
.wd-status-new { background: #EDE9FE; color: #6D28D9; }
|
||
|
||
.wd-btn {
|
||
padding: 7px 12px;
|
||
border: 1px solid var(--wd-border-md);
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
background: var(--wd-surface);
|
||
color: var(--wd-text);
|
||
cursor: pointer;
|
||
display: inline-flex; align-items: center; gap: 5px;
|
||
text-decoration: none;
|
||
}
|
||
.wd-btn:hover { background: var(--wd-bg); }
|
||
.wd-btn-primary { background: var(--wd-green); color: #fff; border-color: var(--wd-green); }
|
||
.wd-btn-primary:hover { background: #15803d; }
|
||
.wd-btn-secondary { background: var(--wd-text); color: #fff; border-color: var(--wd-text); }
|
||
.wd-btn-secondary:hover { background: #000; }
|
||
.wd-topbar-actions { margin-left: auto; display: flex; gap: 6px; }
|
||
|
||
/* ── META HEADER ROW ── */
|
||
.wd-meta {
|
||
background: var(--wd-surface);
|
||
border-bottom: 1px solid var(--wd-border);
|
||
padding: 10px 20px;
|
||
display: grid;
|
||
grid-template-columns: repeat(6, 1fr);
|
||
gap: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
.wd-meta-item { display: flex; flex-direction: column; gap: 3px; }
|
||
.wd-meta-lbl { font-size: 10px; color: var(--wd-text-3); text-transform: uppercase; letter-spacing: .4px; font-weight: 600; }
|
||
.wd-meta-val { font-size: 13px; color: var(--wd-text); }
|
||
.wd-meta-val.person { display: flex; align-items: center; gap: 6px; }
|
||
.wd-avatar {
|
||
width: 22px; height: 22px; border-radius: 50%;
|
||
background: var(--wd-blue-bg); color: var(--wd-blue);
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
font-size: 10px; font-weight: 700;
|
||
}
|
||
.wd-priority-dot {
|
||
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
|
||
margin-right: 5px;
|
||
}
|
||
.wd-priority-normal { background: var(--wd-amber); }
|
||
.wd-priority-urgent, .wd-priority-express { background: var(--wd-red); }
|
||
|
||
/* ── 3-COLUMN GRID ── */
|
||
.wd-body {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: 300px 1fr 300px;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
@media (max-width: 1280px) {
|
||
.wd-body { grid-template-columns: 260px 1fr; }
|
||
.wd-col-right { display: none; }
|
||
}
|
||
@media (max-width: 900px) {
|
||
.wd-body { grid-template-columns: 1fr; }
|
||
.wd-col-left { display: none; }
|
||
}
|
||
|
||
.wd-col-left, .wd-col-mid, .wd-col-right {
|
||
display: flex; flex-direction: column; gap: 12px;
|
||
overflow-y: auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* ── CARDS (common) ── */
|
||
.wd-card {
|
||
background: var(--wd-surface);
|
||
border: 1px solid var(--wd-border);
|
||
border-radius: 10px;
|
||
padding: 14px;
|
||
}
|
||
.wd-card-hd {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.wd-card-hd h3 { font-size: 13px; font-weight: 600; margin: 0; }
|
||
.wd-card-hd a { font-size: 11px; color: var(--wd-blue); text-decoration: none; }
|
||
.wd-card-hd a:hover { text-decoration: underline; }
|
||
|
||
/* ── LEFT SIDEBAR ── */
|
||
.wd-person-row {
|
||
display: flex; align-items: center; gap: 10px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.wd-person-avatar {
|
||
width: 40px; height: 40px; border-radius: 50%;
|
||
background: var(--wd-bg); border: 1px solid var(--wd-border);
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
font-size: 15px; font-weight: 700; color: var(--wd-text-2);
|
||
flex-shrink: 0;
|
||
}
|
||
.wd-person-name { font-size: 13px; font-weight: 600; line-height: 1.2; }
|
||
.wd-person-phone { font-size: 11px; color: var(--wd-blue); display: block; margin-top: 2px; }
|
||
.wd-person-email { font-size: 11px; color: var(--wd-text-2); display: block; margin-top: 1px; }
|
||
.wd-tag {
|
||
display: inline-block;
|
||
background: var(--wd-blue-bg); color: var(--wd-blue);
|
||
padding: 2px 8px; border-radius: 4px;
|
||
font-size: 10px; font-weight: 600;
|
||
margin-top: 6px;
|
||
}
|
||
.wd-stat-grid {
|
||
display: grid; grid-template-columns: repeat(3, 1fr);
|
||
gap: 6px; margin-top: 10px;
|
||
}
|
||
.wd-stat-cell {
|
||
text-align: center;
|
||
background: var(--wd-bg);
|
||
border-radius: 6px;
|
||
padding: 6px 4px;
|
||
}
|
||
.wd-stat-cell .l { font-size: 9px; color: var(--wd-text-3); text-transform: uppercase; }
|
||
.wd-stat-cell .v { font-size: 12px; font-weight: 700; margin-top: 2px; }
|
||
.wd-stat-cell .v.money { color: var(--wd-green); }
|
||
.wd-stat-cell .v.debt { color: var(--wd-red); }
|
||
|
||
.wd-vehicle-photo {
|
||
width: 100%; aspect-ratio: 16/10; object-fit: cover;
|
||
background: var(--wd-bg);
|
||
border-radius: 6px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.wd-veh-title { font-size: 13px; font-weight: 700; line-height: 1.2; }
|
||
.wd-veh-facts {
|
||
display: grid; grid-template-columns: auto 1fr;
|
||
gap: 4px 10px; font-size: 11px;
|
||
margin-top: 6px;
|
||
}
|
||
.wd-veh-facts dt { color: var(--wd-text-3); }
|
||
.wd-veh-facts dd { color: var(--wd-text); font-weight: 500; margin: 0; }
|
||
|
||
.wd-history-item {
|
||
padding: 6px 0;
|
||
border-bottom: 1px solid var(--wd-border);
|
||
font-size: 11px;
|
||
display: flex; justify-content: space-between; gap: 8px;
|
||
}
|
||
.wd-history-item:last-child { border-bottom: 0; }
|
||
.wd-history-item .h-date { color: var(--wd-text-3); }
|
||
.wd-history-item .h-total { font-weight: 600; }
|
||
.wd-history-item a { color: var(--wd-text); text-decoration: none; }
|
||
.wd-history-item a:hover { color: var(--wd-blue); }
|
||
|
||
/* ── MIDDLE: TABS ── */
|
||
.wd-tabs-bar {
|
||
background: var(--wd-surface);
|
||
border: 1px solid var(--wd-border);
|
||
border-radius: 10px 10px 0 0;
|
||
border-bottom: 0;
|
||
display: flex;
|
||
padding: 0 8px;
|
||
overflow-x: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
.wd-tab {
|
||
padding: 12px 16px;
|
||
border-bottom: 2px solid transparent;
|
||
color: var(--wd-text-2);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
letter-spacing: .3px;
|
||
background: none;
|
||
border-left: 0; border-right: 0; border-top: 0;
|
||
white-space: nowrap;
|
||
display: inline-flex; align-items: center; gap: 5px;
|
||
}
|
||
.wd-tab.active { color: var(--wd-blue); border-bottom-color: var(--wd-blue); font-weight: 600; }
|
||
.wd-tab-count {
|
||
background: var(--wd-bg); color: var(--wd-text-2);
|
||
padding: 1px 6px; border-radius: 8px;
|
||
font-size: 10px; font-weight: 600;
|
||
}
|
||
|
||
.wd-tab-content {
|
||
background: var(--wd-surface);
|
||
border: 1px solid var(--wd-border);
|
||
border-top: 0;
|
||
border-radius: 0 0 10px 10px;
|
||
padding: 16px;
|
||
flex: 1;
|
||
overflow: auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Tables inside tabs */
|
||
.wd-tbl { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||
.wd-tbl th, .wd-tbl td {
|
||
padding: 8px 10px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--wd-border);
|
||
}
|
||
.wd-tbl th {
|
||
font-size: 10px; color: var(--wd-text-3);
|
||
text-transform: uppercase; letter-spacing: .3px;
|
||
font-weight: 600; background: var(--wd-bg);
|
||
}
|
||
.wd-tbl td.right, .wd-tbl th.right { text-align: right; font-variant-numeric: tabular-nums; }
|
||
.wd-tbl tr:last-child td { border-bottom: 0; }
|
||
.wd-tbl tr:hover td { background: rgba(37,99,235,.03); }
|
||
|
||
.wd-mini-badge {
|
||
display: inline-block;
|
||
padding: 2px 7px; border-radius: 4px;
|
||
font-size: 10px; font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
.wd-mini-todo { background: #F3F4F6; color: #6B7280; }
|
||
.wd-mini-progress { background: var(--wd-blue-bg); color: var(--wd-blue); }
|
||
.wd-mini-done { background: var(--wd-green-bg); color: #166534; }
|
||
|
||
.wd-empty { padding: 40px 20px; text-align: center; color: var(--wd-text-3); font-size: 12px; }
|
||
.wd-empty-add { margin-top: 8px; }
|
||
|
||
.wd-footer-row {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 10px 4px; margin-top: 8px;
|
||
font-size: 12px;
|
||
border-top: 1px solid var(--wd-border);
|
||
}
|
||
.wd-footer-total { font-weight: 700; font-size: 13px; }
|
||
|
||
/* ── TIMELINE ── */
|
||
.wd-timeline { display: flex; flex-direction: column; gap: 10px; max-height: 240px; overflow-y: auto; }
|
||
.wd-tl-item { display: flex; gap: 8px; font-size: 11px; }
|
||
.wd-tl-icon {
|
||
width: 24px; height: 24px; border-radius: 50%;
|
||
background: var(--wd-bg); border: 1px solid var(--wd-border);
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0; font-size: 11px;
|
||
}
|
||
.wd-tl-body { flex: 1; min-width: 0; }
|
||
.wd-tl-body .lbl { font-weight: 600; color: var(--wd-text); line-height: 1.3; }
|
||
.wd-tl-body .meta { color: var(--wd-text-3); font-size: 10px; margin-top: 2px; }
|
||
|
||
/* ── CHAT ── */
|
||
.wd-chat { display: flex; flex-direction: column; height: 260px; }
|
||
.wd-chat-list { flex: 1; overflow-y: auto; padding-right: 4px; }
|
||
.wd-msg { margin-bottom: 8px; }
|
||
.wd-msg-meta { font-size: 10px; color: var(--wd-text-3); margin-bottom: 2px; display: flex; gap: 6px; }
|
||
.wd-msg-body {
|
||
background: var(--wd-blue-bg); color: var(--wd-text);
|
||
padding: 6px 10px; border-radius: 8px 8px 8px 2px;
|
||
font-size: 11px; line-height: 1.4;
|
||
white-space: pre-wrap;
|
||
}
|
||
.wd-msg.failed .wd-msg-body { background: var(--wd-red-bg); color: var(--wd-red); }
|
||
.wd-chat-input { display: flex; gap: 6px; margin-top: 8px; }
|
||
.wd-chat-input input {
|
||
flex: 1; padding: 7px 10px; font-size: 12px;
|
||
border: 1px solid var(--wd-border-md); border-radius: 6px;
|
||
background: var(--wd-surface); color: var(--wd-text);
|
||
}
|
||
|
||
/* ── BOTTOM ACTION BAR ── */
|
||
.wd-bottom {
|
||
background: var(--wd-surface);
|
||
border-top: 1px solid var(--wd-border);
|
||
padding: 10px 20px;
|
||
display: flex; gap: 8px; align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.wd-bottom-nav { display: flex; gap: 4px; }
|
||
.wd-bottom-actions { margin-left: auto; display: flex; gap: 6px; }
|
||
.wd-btn-danger { background: var(--wd-red); color: #fff; border-color: var(--wd-red); }
|
||
.wd-btn-danger:hover { background: #991b1b; }
|
||
</style>
|
||
|
||
<div class="wd-shell" x-data="{ tab: @entangle('activeTab').live }">
|
||
|
||
{{-- ── TOP BAR ── --}}
|
||
<div class="wd-topbar">
|
||
<span class="wd-wo-title">{{ __('Fișă') }} {{ $wo->number }}</span>
|
||
<span class="wd-status {{ $statusClass }}">
|
||
{{ __(\App\Models\Tenant\WorkOrder::STATUSES[$wo->status] ?? $wo->status) }}
|
||
<span style="opacity:.6;">▾</span>
|
||
</span>
|
||
<div class="wd-topbar-actions">
|
||
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id]) }}">
|
||
✎ {{ __('Editează') }}
|
||
</a>
|
||
@if ($wo->tracking_token)
|
||
<a class="wd-btn" target="_blank" href="{{ $wo->trackingUrl() }}">
|
||
🔗 {{ __('Link tracking') }}
|
||
</a>
|
||
@endif
|
||
<button type="button" class="wd-btn wd-btn-primary">{{ __('Salvează') }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── META HEADER ── --}}
|
||
<div class="wd-meta">
|
||
<div class="wd-meta-item">
|
||
<span class="wd-meta-lbl">{{ __('Data creării') }}</span>
|
||
<span class="wd-meta-val">{{ $wo->created_at?->format('d.m.Y H:i') ?? '—' }}</span>
|
||
</div>
|
||
<div class="wd-meta-item">
|
||
<span class="wd-meta-lbl">{{ __('Deschis') }}</span>
|
||
<span class="wd-meta-val">{{ $wo->opened_at?->format('d.m.Y H:i') ?? '—' }}</span>
|
||
</div>
|
||
<div class="wd-meta-item">
|
||
<span class="wd-meta-lbl">{{ __('ETA') }}</span>
|
||
<span class="wd-meta-val">{{ $wo->eta_at?->format('d.m.Y H:i') ?? '—' }}</span>
|
||
</div>
|
||
<div class="wd-meta-item">
|
||
<span class="wd-meta-lbl">{{ __('Responsabil') }}</span>
|
||
<span class="wd-meta-val person">
|
||
@if ($wo->master)
|
||
<span class="wd-avatar">{{ mb_strtoupper(mb_substr($wo->master->name, 0, 2)) }}</span>
|
||
{{ $wo->master->name }}
|
||
@else — @endif
|
||
</span>
|
||
</div>
|
||
<div class="wd-meta-item">
|
||
<span class="wd-meta-lbl">{{ __('Urgență') }}</span>
|
||
<span class="wd-meta-val">
|
||
<span class="wd-priority-dot wd-priority-{{ $wo->urgency ?? 'normal' }}"></span>
|
||
{{ __(\App\Models\Tenant\PricingCoefficient::URGENCY[$wo->urgency ?? 'normal'] ?? 'Normal') }}
|
||
</span>
|
||
</div>
|
||
<div class="wd-meta-item">
|
||
<span class="wd-meta-lbl">{{ __('Data plății') }}</span>
|
||
<span class="wd-meta-val">{{ $wo->paidAmount() > 0 ? number_format($wo->paidAmount(), 2, '.', ' ') . ' MDL' : '—' }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── 3-COLUMN BODY ── --}}
|
||
<div class="wd-body">
|
||
{{-- ═══ LEFT SIDEBAR ═══ --}}
|
||
<div class="wd-col-left">
|
||
{{-- CLIENT CARD --}}
|
||
<div class="wd-card">
|
||
<div class="wd-card-hd">
|
||
<h3>{{ __('Client') }}</h3>
|
||
@if ($wo->client)
|
||
<a href="{{ route('filament.tenant.resources.clients.edit', ['record' => $wo->client_id]) }}">{{ __('Editează') }}</a>
|
||
@endif
|
||
</div>
|
||
@if ($wo->client)
|
||
<div class="wd-person-row">
|
||
<div class="wd-person-avatar">{{ mb_strtoupper(mb_substr($wo->client->name, 0, 1)) }}</div>
|
||
<div style="flex:1;min-width:0;">
|
||
<div class="wd-person-name">{{ $wo->client->name }}</div>
|
||
@if ($wo->client->phone)
|
||
<a href="tel:{{ $wo->client->phone }}" class="wd-person-phone">📞 {{ $wo->client->phone }}</a>
|
||
@endif
|
||
@if ($wo->client->email)
|
||
<span class="wd-person-email">{{ $wo->client->email }}</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@if (! empty($wo->client->status))
|
||
<span class="wd-tag">{{ __(ucfirst($wo->client->status)) }}</span>
|
||
@endif
|
||
@php
|
||
$totalOrders = \App\Models\Tenant\WorkOrder::where('client_id', $wo->client_id)->count();
|
||
$totalSpent = (float) \App\Models\Tenant\WorkOrder::where('client_id', $wo->client_id)->sum('total');
|
||
$debt = (float) $wo->client->balance;
|
||
@endphp
|
||
<div class="wd-stat-grid">
|
||
<div class="wd-stat-cell">
|
||
<div class="l">{{ __('Vizite') }}</div>
|
||
<div class="v">{{ $totalOrders }}</div>
|
||
</div>
|
||
<div class="wd-stat-cell">
|
||
<div class="l">{{ __('Total') }}</div>
|
||
<div class="v money">{{ number_format($totalSpent, 0, '.', ' ') }}</div>
|
||
</div>
|
||
<div class="wd-stat-cell">
|
||
<div class="l">{{ __('Datorie') }}</div>
|
||
<div class="v {{ $debt > 0 ? 'debt' : '' }}">{{ number_format($debt, 0, '.', ' ') }}</div>
|
||
</div>
|
||
</div>
|
||
@else
|
||
<div class="wd-empty">{{ __('Fără client') }}</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- VEHICLE CARD --}}
|
||
<div class="wd-card">
|
||
<div class="wd-card-hd">
|
||
<h3>{{ __('Automobil') }}</h3>
|
||
@if ($wo->vehicle)
|
||
<a href="{{ route('filament.tenant.resources.vehicles.edit', ['record' => $wo->vehicle_id]) }}">{{ __('Editează') }}</a>
|
||
@endif
|
||
</div>
|
||
@if ($wo->vehicle)
|
||
@php
|
||
// 1) Vehicle's own photo (if the model uses MediaLibrary), else
|
||
// 2) fall back to the first WO photo (uploaded via edit form).
|
||
$photoUrl = method_exists($wo->vehicle, 'getFirstMediaUrl')
|
||
? ($wo->vehicle->getFirstMediaUrl('photos') ?: null)
|
||
: null;
|
||
if (! $photoUrl) {
|
||
$woFirst = $wo->getFirstMedia('photos');
|
||
$photoUrl = $woFirst?->getFullUrl();
|
||
}
|
||
@endphp
|
||
@if ($photoUrl)
|
||
<img class="wd-vehicle-photo" src="{{ $photoUrl }}" alt="{{ $wo->vehicle->make }}">
|
||
@else
|
||
<div class="wd-vehicle-photo" style="display:flex;align-items:center;justify-content:center;color:var(--wd-text-3);font-size:24px;">🚗</div>
|
||
@endif
|
||
<div class="wd-veh-title">{{ $wo->vehicle->make }} {{ $wo->vehicle->model }} {{ $wo->vehicle->year }}</div>
|
||
<dl class="wd-veh-facts">
|
||
@if ($wo->vehicle->plate)
|
||
<dt>{{ __('Nr.') }}</dt><dd>{{ $wo->vehicle->plate }}</dd>
|
||
@endif
|
||
@if ($wo->vehicle->vin)
|
||
<dt>VIN</dt><dd style="font-family:monospace;font-size:10px;">{{ $wo->vehicle->vin }}</dd>
|
||
@endif
|
||
@if ($wo->vehicle->mileage)
|
||
<dt>{{ __('Km') }}</dt><dd>{{ number_format($wo->vehicle->mileage, 0, '.', ' ') }}</dd>
|
||
@endif
|
||
@if ($wo->vehicle->engine)
|
||
<dt>{{ __('Motor') }}</dt><dd>{{ $wo->vehicle->engine }}</dd>
|
||
@endif
|
||
@if ($wo->vehicle->gearbox)
|
||
<dt>{{ __('Cutie') }}</dt><dd>{{ $wo->vehicle->gearbox }}</dd>
|
||
@endif
|
||
</dl>
|
||
@else
|
||
<div class="wd-empty">{{ __('Fără mașină') }}</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- REPAIR HISTORY --}}
|
||
<div class="wd-card">
|
||
<div class="wd-card-hd">
|
||
<h3>{{ __('Istoric reparații') }}</h3>
|
||
</div>
|
||
@forelse ($history as $h)
|
||
<div class="wd-history-item">
|
||
<a href="{{ url('/app/work-orders/' . $h->id . '/dashboard') }}">
|
||
<span class="h-date">{{ $h->opened_at?->format('d.m.Y') }}</span>
|
||
<span>{{ $h->number }}</span>
|
||
</a>
|
||
<span class="h-total">{{ number_format($h->total, 0, '.', ' ') }} MDL</span>
|
||
</div>
|
||
@empty
|
||
<div class="wd-empty" style="padding:16px 0;">{{ __('Prima vizită') }}</div>
|
||
@endforelse
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ═══ MIDDLE: TABS ═══ --}}
|
||
<div class="wd-col-mid">
|
||
<div class="wd-tabs-bar">
|
||
@foreach ($tabs as $key => $t)
|
||
<button type="button" class="wd-tab" :class="tab === '{{ $key }}' && 'active'" @click="tab = '{{ $key }}'">
|
||
{{ $t['label'] }}
|
||
@if (! is_null($t['count']))
|
||
<span class="wd-tab-count">{{ $t['count'] }}</span>
|
||
@endif
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
<div class="wd-tab-content">
|
||
{{-- ── WORKS TAB ── --}}
|
||
<div x-show="tab === 'works'" x-cloak>
|
||
@if ($wo->works->isNotEmpty())
|
||
<table class="wd-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>{{ __('Manoperă') }}</th>
|
||
<th class="right">{{ __('Ore') }}</th>
|
||
<th>{{ __('Mecanic') }}</th>
|
||
<th class="right">{{ __('Preț/h') }}</th>
|
||
<th class="right">{{ __('Total') }}</th>
|
||
<th>{{ __('Status') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($wo->works as $i => $w)
|
||
<tr>
|
||
<td>{{ $i + 1 }}</td>
|
||
<td>{{ $w->labor?->label() ?: $w->name }}</td>
|
||
<td class="right">{{ rtrim(rtrim(number_format($w->hours, 2), '0'), '.') }}</td>
|
||
<td>{{ $w->master?->name ?? '—' }}</td>
|
||
<td class="right">{{ number_format($w->price_per_hour, 2, '.', ' ') }}</td>
|
||
<td class="right">{{ number_format($w->total, 2, '.', ' ') }}</td>
|
||
<td>
|
||
<span class="wd-mini-badge wd-mini-{{ $w->status === 'done' ? 'done' : ($w->status === 'in_progress' ? 'progress' : 'todo') }}">
|
||
{{ __(\App\Models\Tenant\WorkOrderWork::STATUSES[$w->status] ?? $w->status) }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
<div class="wd-footer-row">
|
||
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 0]) }}">+ {{ __('Adaugă manoperă') }}</a>
|
||
<span class="wd-footer-total">{{ __('Total manopere:') }} {{ number_format($finance['works_sum'], 2, '.', ' ') }} MDL</span>
|
||
</div>
|
||
@else
|
||
<div class="wd-empty">
|
||
{{ __('Nicio manoperă efectuată.') }}
|
||
<div class="wd-empty-add"><a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 0]) }}">+ {{ __('Adaugă manoperă') }}</a></div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- ── PARTS TAB ── --}}
|
||
<div x-show="tab === 'parts'" x-cloak>
|
||
@if ($wo->parts->isNotEmpty())
|
||
<table class="wd-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ __('Cod') }}</th>
|
||
<th>{{ __('Denumire') }}</th>
|
||
<th>{{ __('Brand') }}</th>
|
||
<th class="right">{{ __('Cant.') }}</th>
|
||
<th class="right">{{ __('Preț') }}</th>
|
||
<th class="right">{{ __('Total') }}</th>
|
||
<th>{{ __('Status') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@foreach ($wo->parts as $p)
|
||
<tr>
|
||
<td style="font-family:monospace;font-size:11px;">{{ $p->article ?? '—' }}</td>
|
||
<td>{{ $p->name }}</td>
|
||
<td>{{ $p->brand ?? '—' }}</td>
|
||
<td class="right">{{ rtrim(rtrim(number_format($p->qty, 2), '0'), '.') }} {{ $p->unitLabel() ?: '' }}</td>
|
||
<td class="right">{{ number_format($p->sell_price, 2, '.', ' ') }}</td>
|
||
<td class="right">{{ number_format($p->total, 2, '.', ' ') }}</td>
|
||
<td>
|
||
<span class="wd-mini-badge wd-mini-{{ $p->status === 'installed' ? 'done' : ($p->status === 'delivered' ? 'progress' : 'todo') }}">
|
||
{{ __(\App\Models\Tenant\WorkOrderPart::STATUSES[$p->status] ?? $p->status) }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
@endforeach
|
||
</tbody>
|
||
</table>
|
||
<div class="wd-footer-row">
|
||
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 1]) }}">+ {{ __('Adaugă piesă') }}</a>
|
||
<span class="wd-footer-total">{{ __('Total piese:') }} {{ number_format($finance['parts_sum'], 2, '.', ' ') }} MDL</span>
|
||
</div>
|
||
@else
|
||
<div class="wd-empty">
|
||
{{ __('Nicio piesă montată.') }}
|
||
<div class="wd-empty-add"><a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.edit', ['record' => $wo->id, 'relation' => 1]) }}">+ {{ __('Adaugă piesă') }}</a></div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- ── DIAGNOSTIC TAB ── --}}
|
||
<div x-show="tab === 'diag'" x-cloak>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||
<div>
|
||
<h4 style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin:0 0 8px;">{{ __('Plângere client') }}</h4>
|
||
<div style="background:var(--wd-bg);padding:10px 12px;border-radius:6px;font-size:12px;white-space:pre-wrap;min-height:60px;">{{ $wo->complaint ?: '—' }}</div>
|
||
</div>
|
||
<div>
|
||
<h4 style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin:0 0 8px;">{{ __('Diagnostic') }}</h4>
|
||
<div style="background:var(--wd-bg);padding:10px 12px;border-radius:6px;font-size:12px;white-space:pre-wrap;min-height:60px;">{{ $wo->diagnosis ?: '—' }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── PHOTOS TAB ── --}}
|
||
<div x-show="tab === 'photos'" x-cloak>
|
||
@php $photos = $wo->getMedia('photos'); @endphp
|
||
@if ($photos->isNotEmpty())
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;">
|
||
@foreach ($photos as $ph)
|
||
<a href="{{ $ph->getFullUrl() }}" target="_blank">
|
||
<img src="{{ $ph->getFullUrl() }}" style="width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:6px;border:1px solid var(--wd-border);">
|
||
</a>
|
||
@endforeach
|
||
</div>
|
||
@else
|
||
<div class="wd-empty">{{ __('Nicio fotografie') }}</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- ── DOCUMENTS TAB ── --}}
|
||
<div x-show="tab === 'docs'" x-cloak>
|
||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf') }}" target="_blank">
|
||
🔍 {{ __('Vizualizează factură (PDF)') }}
|
||
</a>
|
||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf?download=1') }}">
|
||
⬇️ {{ __('Descarcă factură (PDF)') }}
|
||
</a>
|
||
@php $signed = $wo->getMedia('signed_documents'); @endphp
|
||
@if ($signed->isNotEmpty())
|
||
<div style="font-size:11px;color:var(--wd-text-3);text-transform:uppercase;margin-top:12px;">{{ __('Documente semnate') }}</div>
|
||
@foreach ($signed as $doc)
|
||
<a class="wd-btn" href="{{ $doc->getFullUrl() }}" target="_blank">
|
||
📎 {{ $doc->name }} <span style="color:var(--wd-text-3);">({{ round($doc->size / 1024) }} KB)</span>
|
||
</a>
|
||
@endforeach
|
||
@else
|
||
<div class="wd-empty" style="padding:12px 0;font-size:11px;">{{ __('Niciun document semnat încă. Poți încărca documente din formularul de editare al fișei.') }}</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── NOTES TAB ── --}}
|
||
<div x-show="tab === 'notes'" x-cloak>
|
||
<div style="background:var(--wd-bg);padding:10px 12px;border-radius:6px;font-size:12px;white-space:pre-wrap;min-height:80px;">{{ $wo->notes ?: __('Nicio notă internă.') }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ═══ RIGHT COLUMN (placeholder — Phase 2) ═══ --}}
|
||
<div class="wd-col-right">
|
||
<div class="wd-card">
|
||
<div class="wd-card-hd">
|
||
<h3>{{ __('Finanțe') }}</h3>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:6px;font-size:12px;">
|
||
<div style="display:flex;justify-content:space-between;">
|
||
<span style="color:var(--wd-text-2);">{{ __('Cost manopere') }}</span>
|
||
<b>{{ number_format($finance['works_sum'], 2, '.', ' ') }}</b>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;">
|
||
<span style="color:var(--wd-text-2);">{{ __('Cost piese') }}</span>
|
||
<b>{{ number_format($finance['parts_sum'], 2, '.', ' ') }}</b>
|
||
</div>
|
||
@if ($finance['discount'] > 0)
|
||
<div style="display:flex;justify-content:space-between;">
|
||
<span style="color:var(--wd-text-2);">{{ __('Discount') }}</span>
|
||
<b style="color:var(--wd-red);">−{{ number_format($finance['discount'], 2, '.', ' ') }}</b>
|
||
</div>
|
||
@endif
|
||
<div style="border-top:1px solid var(--wd-border);padding-top:8px;display:flex;justify-content:space-between;font-size:15px;">
|
||
<span>{{ __('Total') }}</span>
|
||
<b>{{ number_format($finance['total'], 2, '.', ' ') }} MDL</b>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;color:var(--wd-green);">
|
||
<span>{{ __('Achitat') }}</span>
|
||
<b>{{ number_format($finance['paid'], 2, '.', ' ') }}</b>
|
||
</div>
|
||
@if ($finance['balance'] > 0)
|
||
<div style="display:flex;justify-content:space-between;color:var(--wd-red);font-weight:700;">
|
||
<span>{{ __('Rest de plată') }}</span>
|
||
<b>{{ number_format($finance['balance'], 2, '.', ' ') }} MDL</b>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
{{-- TIMELINE --}}
|
||
<div class="wd-card">
|
||
<div class="wd-card-hd">
|
||
<h3>{{ __('Timeline') }}</h3>
|
||
</div>
|
||
@if (! empty($timeline))
|
||
<div class="wd-timeline">
|
||
@foreach ($timeline as $ev)
|
||
<div class="wd-tl-item">
|
||
<div class="wd-tl-icon">{{ $ev['icon'] }}</div>
|
||
<div class="wd-tl-body">
|
||
<div class="lbl">{{ $ev['label'] }}</div>
|
||
<div class="meta">
|
||
{{ $ev['at']->format('d.m.Y H:i') }}
|
||
@if ($ev['meta']) · {{ $ev['meta'] }} @endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@else
|
||
<div class="wd-empty" style="padding:16px 0;">{{ __('Fără evenimente') }}</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- CHAT CLIENT --}}
|
||
@php
|
||
$tenantSettings = (array) (app(\App\Tenancy\TenantManager::class)->current()?->settings ?? []);
|
||
$integrations = (array) ($tenantSettings['integrations'] ?? []);
|
||
$telegramEnabled = (bool) data_get($integrations, 'telegram_bot.enabled')
|
||
&& filled(data_get($integrations, 'telegram_bot.fields.bot_token'));
|
||
$whatsappEnabled = (bool) data_get($integrations, 'whatsapp_business.enabled')
|
||
&& filled(data_get($integrations, 'whatsapp_business.fields.token'));
|
||
$messagingConfigured = $telegramEnabled || $whatsappEnabled;
|
||
@endphp
|
||
<div class="wd-card">
|
||
<div class="wd-card-hd">
|
||
<h3>{{ __('Chat client') }}</h3>
|
||
@if ($telegramEnabled && $wo->client?->telegram_chat_id)
|
||
<span style="font-size:10px;color:var(--wd-blue);">✓ Telegram</span>
|
||
@elseif ($whatsappEnabled && $wo->client?->phone)
|
||
<span style="font-size:10px;color:var(--wd-text-3);">✓ WhatsApp</span>
|
||
@endif
|
||
</div>
|
||
@if (! $messagingConfigured)
|
||
<div class="wd-empty" style="padding:16px 0;">
|
||
{{ __('Mesageria nu este configurată.') }}
|
||
<div class="wd-empty-add" style="margin-top:8px;">
|
||
<a class="wd-btn" href="{{ url('/app/integrations') }}">⚙️ {{ __('Configurează integrări') }}</a>
|
||
</div>
|
||
</div>
|
||
@elseif (! $wo->client)
|
||
<div class="wd-empty" style="padding:16px 0;">{{ __('Fișa nu are client asociat.') }}</div>
|
||
@else
|
||
<div class="wd-chat">
|
||
<div class="wd-chat-list">
|
||
@forelse ($messages as $m)
|
||
<div class="wd-msg {{ $m->status === 'failed' ? 'failed' : '' }}">
|
||
<div class="wd-msg-meta">
|
||
<span>{{ strtoupper($m->channel) }}</span>
|
||
<span>{{ $m->sent_at?->format('d.m H:i') }}</span>
|
||
@if ($m->status === 'failed')<span style="color:var(--wd-red);">✕ {{ __('Eșuat') }}</span>@endif
|
||
</div>
|
||
<div class="wd-msg-body">{{ $m->message_text }}</div>
|
||
</div>
|
||
@empty
|
||
<div class="wd-empty" style="padding:16px 0;">{{ __('Nicio conversație') }}</div>
|
||
@endforelse
|
||
</div>
|
||
<form class="wd-chat-input" wire:submit.prevent="sendMessage">
|
||
<input type="text" placeholder="{{ __('Scrie mesajul tău...') }}" wire:model="newMessage" maxlength="1000">
|
||
<button type="submit" class="wd-btn wd-btn-primary">➤</button>
|
||
</form>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── BOTTOM ACTION BAR ── --}}
|
||
<div class="wd-bottom">
|
||
<div class="wd-bottom-nav">
|
||
@if ($prev)
|
||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $prev->id . '/dashboard') }}">
|
||
← {{ __('Precedent') }} <span style="color:var(--wd-text-3);">({{ $prev->number }})</span>
|
||
</a>
|
||
@endif
|
||
@if ($next)
|
||
<a class="wd-btn" href="{{ url('/app/work-orders/' . $next->id . '/dashboard') }}">
|
||
{{ __('Următor') }} → <span style="color:var(--wd-text-3);">({{ $next->number }})</span>
|
||
</a>
|
||
@endif
|
||
</div>
|
||
<div class="wd-bottom-actions">
|
||
<a class="wd-btn" href="{{ route('filament.tenant.resources.work-orders.create') }}?client_id={{ $wo->client_id }}&vehicle_id={{ $wo->vehicle_id }}">
|
||
📄 {{ __('Comandă repetată') }}
|
||
</a>
|
||
@if ($wo->status !== 'done')
|
||
<button type="button" class="wd-btn wd-btn-danger" wire:click="closeWorkOrder" wire:confirm="{{ __('Închide fișa?') }}">
|
||
🔒 {{ __('Închide fișă') }}
|
||
</button>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</x-filament-panels::page>
|