feat(work-order-dashboard): PDF download in Docs tab + gate chat by config

- Documente tab: link to a new /app/work-orders/{id}/pdf route that
  streams the invoice PDF via WorkOrderPdfService; also lists any
  files uploaded to the 'signed_documents' media collection.
- Chat client card: only show the input form when telegram_bot or
  whatsapp_business is enabled AND credentialed in the tenant's
  Integrations page. Otherwise render an empty state with a shortcut
  to /app/integrations.

RU + EN translations added for the new strings (RO source).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-05 20:47:43 +00:00
parent 34f53383bf
commit e0b61fb3cb
4 changed files with 5435 additions and 5385 deletions
+5
View File
@@ -660,6 +660,7 @@
"Descarcă QR (SVG)": "Download QR (SVG)", "Descarcă QR (SVG)": "Download QR (SVG)",
"Descarcă backup": "Download backup", "Descarcă backup": "Download backup",
"Descarcă factura": "Download invoice", "Descarcă factura": "Download invoice",
"Descarcă factură (PDF)": "Download invoice (PDF)",
"Descarcă template": "Download template", "Descarcă template": "Download template",
"Deschide": "Open", "Deschide": "Open",
"Deschide Fișă": "Open order", "Deschide Fișă": "Open order",
@@ -717,6 +718,7 @@
"Document": "Document", "Document": "Document",
"Documentation": "Documentation", "Documentation": "Documentation",
"Documente": "Documents", "Documente": "Documents",
"Documente semnate": "Signed documents",
"Done": "Done", "Done": "Done",
"Downgrade": "Downgrade", "Downgrade": "Downgrade",
"Draft": "Draft", "Draft": "Draft",
@@ -848,6 +850,7 @@
"Fines": "Fines", "Fines": "Fines",
"Fisură": "Crack", "Fisură": "Crack",
"Fix": "Fixed", "Fix": "Fixed",
"Fișa nu are client asociat.": "This work order has no client attached.",
"Fișe": "Orders", "Fișe": "Orders",
"Fișe active": "Active orders", "Fișe active": "Active orders",
"Fișe deschise": "Open orders", "Fișe deschise": "Open orders",
@@ -1174,6 +1177,7 @@
"Medie": "Medium", "Medie": "Medium",
"Mediu": "Medium", "Mediu": "Medium",
"Merchant Code": "Merchant Code", "Merchant Code": "Merchant Code",
"Mesageria nu este configurată.": "Messaging is not configured.",
"Mesaj": "Message", "Mesaj": "Message",
"Mesaj client": "Client message", "Mesaj client": "Client message",
"Mesaj trimis": "Message sent", "Mesaj trimis": "Message sent",
@@ -1282,6 +1286,7 @@
"Niciun coeficient contextual nu se aplică (preț de bază).": "No contextual coefficient applies (base price).", "Niciun coeficient contextual nu se aplică (preț de bază).": "No contextual coefficient applies (base price).",
"Niciun depozit": "No warehouses", "Niciun depozit": "No warehouses",
"Niciun dispozitiv abonat": "No subscribed devices", "Niciun dispozitiv abonat": "No subscribed devices",
"Niciun document semnat încă. Poți încărca documente din formularul de editare al fișei.": "No signed documents yet. Upload documents from the work-order edit form.",
"Niciun istoric": "No history", "Niciun istoric": "No history",
"Niciun lead.": "No leads.", "Niciun lead.": "No leads.",
"Niciun lot înregistrat": "No batches recorded", "Niciun lot înregistrat": "No batches recorded",
+5
View File
@@ -660,6 +660,7 @@
"Descarcă QR (SVG)": "Скачать QR (SVG)", "Descarcă QR (SVG)": "Скачать QR (SVG)",
"Descarcă backup": "Скачать бэкап", "Descarcă backup": "Скачать бэкап",
"Descarcă factura": "Скачать счёт", "Descarcă factura": "Скачать счёт",
"Descarcă factură (PDF)": "Скачать счёт (PDF)",
"Descarcă template": "Скачать шаблон", "Descarcă template": "Скачать шаблон",
"Deschide": "Открыть", "Deschide": "Открыть",
"Deschide Fișă": "Открыть наряд", "Deschide Fișă": "Открыть наряд",
@@ -717,6 +718,7 @@
"Document": "Документ", "Document": "Документ",
"Documentation": "Documentation", "Documentation": "Documentation",
"Documente": "Документы", "Documente": "Документы",
"Documente semnate": "Подписанные документы",
"Done": "Готово", "Done": "Готово",
"Downgrade": "Понижение", "Downgrade": "Понижение",
"Draft": "Черновик", "Draft": "Черновик",
@@ -848,6 +850,7 @@
"Fines": "Штрафы", "Fines": "Штрафы",
"Fisură": "Трещина", "Fisură": "Трещина",
"Fix": "Фикс.", "Fix": "Фикс.",
"Fișa nu are client asociat.": "К заказ-наряду не привязан клиент.",
"Fișe": "Наряды", "Fișe": "Наряды",
"Fișe active": "Активные наряды", "Fișe active": "Активные наряды",
"Fișe deschise": "Открытые наряды", "Fișe deschise": "Открытые наряды",
@@ -1174,6 +1177,7 @@
"Medie": "Среднее", "Medie": "Среднее",
"Mediu": "Среднее", "Mediu": "Среднее",
"Merchant Code": "Merchant Code", "Merchant Code": "Merchant Code",
"Mesageria nu este configurată.": "Мессенджер не настроен.",
"Mesaj": "Сообщение", "Mesaj": "Сообщение",
"Mesaj client": "Сообщение клиента", "Mesaj client": "Сообщение клиента",
"Mesaj trimis": "Сообщение отправлено", "Mesaj trimis": "Сообщение отправлено",
@@ -1282,6 +1286,7 @@
"Niciun coeficient contextual nu se aplică (preț de bază).": "Контекстные коэффициенты не применяются (базовая цена).", "Niciun coeficient contextual nu se aplică (preț de bază).": "Контекстные коэффициенты не применяются (базовая цена).",
"Niciun depozit": "Нет складов", "Niciun depozit": "Нет складов",
"Niciun dispozitiv abonat": "Нет подписанных устройств", "Niciun dispozitiv abonat": "Нет подписанных устройств",
"Niciun document semnat încă. Poți încărca documente din formularul de editare al fișei.": "Пока нет подписанных документов. Загрузите документы из формы редактирования заказ-наряда.",
"Niciun istoric": "Нет истории", "Niciun istoric": "Нет истории",
"Niciun lead.": "Нет заявок.", "Niciun lead.": "Нет заявок.",
"Niciun lot înregistrat": "Партии не зарегистрированы", "Niciun lot înregistrat": "Партии не зарегистрированы",
@@ -681,7 +681,22 @@
{{-- ── DOCUMENTS TAB ── --}} {{-- ── DOCUMENTS TAB ── --}}
<div x-show="tab === 'docs'" x-cloak> <div x-show="tab === 'docs'" x-cloak>
<div class="wd-empty">{{ __('Nicio factură emisă încă') }}</div> <div style="display:flex;flex-direction:column;gap:8px;">
<a class="wd-btn" href="{{ url('/app/work-orders/' . $wo->id . '/pdf') }}" target="_blank">
📄 {{ __('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> </div>
{{-- ── NOTES TAB ── --}} {{-- ── NOTES TAB ── --}}
@@ -754,16 +769,34 @@
</div> </div>
{{-- CHAT CLIENT --}} {{-- 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">
<div class="wd-card-hd"> <div class="wd-card-hd">
<h3>{{ __('Chat client') }}</h3> <h3>{{ __('Chat client') }}</h3>
@if ($wo->client?->telegram_chat_id) @if ($telegramEnabled && $wo->client?->telegram_chat_id)
<span style="font-size:10px;color:var(--wd-blue);"> Telegram</span> <span style="font-size:10px;color:var(--wd-blue);"> Telegram</span>
@elseif ($wo->client?->phone) @elseif ($whatsappEnabled && $wo->client?->phone)
<span style="font-size:10px;color:var(--wd-text-3);">SMS/WhatsApp</span> <span style="font-size:10px;color:var(--wd-text-3);"> WhatsApp</span>
@endif @endif
</div> </div>
@if ($wo->client) @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">
<div class="wd-chat-list"> <div class="wd-chat-list">
@forelse ($messages as $m) @forelse ($messages as $m)
@@ -784,8 +817,6 @@
<button type="submit" class="wd-btn wd-btn-primary"></button> <button type="submit" class="wd-btn wd-btn-primary"></button>
</form> </form>
</div> </div>
@else
<div class="wd-empty" style="padding:16px 0;">{{ __('Fără client — nu se poate trimite mesaj.') }}</div>
@endif @endif
</div> </div>
</div> </div>
+9
View File
@@ -47,6 +47,15 @@ Route::post('/payments/paypal/webhook', [\App\Http\Controllers\PaymentController
Route::post('/payments/paynet/webhook', [\App\Http\Controllers\PaymentController::class, 'paynetWebhook']) Route::post('/payments/paynet/webhook', [\App\Http\Controllers\PaymentController::class, 'paynetWebhook'])
->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]); ->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
// PDF download for a work order (used from dashboard's Documents tab).
Route::get('/app/work-orders/{workOrder}/pdf', function (int $workOrder) {
abort_unless(auth('web')->check(), 403);
$wo = \App\Models\Tenant\WorkOrder::findOrFail($workOrder);
$svc = app(\App\Services\WorkOrderPdfService::class);
$pdf = $svc->generate($wo);
return response()->streamDownload(fn () => print($pdf->output()), $svc->filename($wo));
})->name('work-orders.pdf');
// User invitation accept flow (no auth required — token is the credential). // User invitation accept flow (no auth required — token is the credential).
Route::get('/invitations/{token}', [\App\Http\Controllers\InvitationController::class, 'show']) Route::get('/invitations/{token}', [\App\Http\Controllers\InvitationController::class, 'show'])
->name('invitation.show'); ->name('invitation.show');