current(); if (! $tenant) { throw new NotFoundHttpException('Tracking only available on tenant subdomain.'); } $wo = WorkOrder::with(['client', 'vehicle', 'master', 'media', 'works', 'parts']) ->where('tracking_token', $token) ->first(); if (! $wo) { throw new NotFoundHttpException('Fișa nu a fost găsită.'); } $pendingWorks = $wo->works->filter(fn ($w) => $w->isPendingApproval()); $pendingParts = $wo->parts->filter(fn ($p) => $p->isPendingApproval()); return view('tracking.show', [ 'wo' => $wo, 'tenant' => $tenant, 'photos' => $wo->getMedia('photos'), 'pendingWorks' => $pendingWorks, 'pendingParts' => $pendingParts, 'approvalStatus' => $request->session()->pull('approval_status'), ]); } /** * Client approves or declines a pending work/part line via the unique * approval_token. The line's approval_token IS the credential — anyone * with the URL can act (clients won't share it). */ public function approve(Request $request, string $token, string $kind, string $lineToken) { $tenant = app(TenantManager::class)->current(); if (! $tenant) throw new NotFoundHttpException(); $wo = WorkOrder::where('tracking_token', $token)->first(); if (! $wo) throw new NotFoundHttpException(); $decision = $request->input('decision', 'approve'); $line = match ($kind) { 'work' => WorkOrderWork::where('work_order_id', $wo->id)->where('approval_token', $lineToken)->first(), 'part' => WorkOrderPart::where('work_order_id', $wo->id)->where('approval_token', $lineToken)->first(), default => null, }; if (! $line || ! $line->isPendingApproval()) { $request->session()->flash('approval_status', ['kind' => 'error', 'message' => 'Linia nu mai necesită aprobare.']); return redirect()->route('tracking.show', ['token' => $token]); } if ($decision === 'approve') { $line->forceFill(['approved_at' => now()])->save(); $msg = '✅ Lucrarea „' . $line->name . '" a fost aprobată. Mulțumim!'; } else { $line->forceFill(['declined_at' => now()])->save(); $msg = '❌ Lucrarea „' . $line->name . '" a fost respinsă. Vă vom contacta.'; } $request->session()->flash('approval_status', ['kind' => 'success', 'message' => $msg]); return redirect()->route('tracking.show', ['token' => $token]); } /** * GET /api/track/{token} — JSON status payload for native apps. * Public, no auth (token IS the credential). Tenant-scoped via subdomain. */ public function jsonStatus(Request $request, string $token) { $tenant = app(TenantManager::class)->current(); if (! $tenant) { return response()->json(['error' => 'tenant_required'], 404); } $wo = WorkOrder::with(['client:id,name', 'vehicle:id,plate,make,model', 'master:id,name', 'works', 'parts']) ->where('tracking_token', $token) ->first(); if (! $wo) return response()->json(['error' => 'not_found'], 404); $statuses = WorkOrder::STATUSES; $flow = ['new', 'diagnosis', 'agreement', 'approved', 'in_work', 'awaiting_parts', 'ready', 'done']; $currentIdx = array_search($wo->status, $flow, true); $pendingApprovals = collect() ->merge($wo->works->filter(fn ($w) => $w->isPendingApproval())->map(fn ($w) => [ 'kind' => 'work', 'id' => $w->id, 'token' => $w->approval_token, 'name' => $w->name, 'amount' => (float) $w->total, 'approve_url' => url("/t/{$token}/approve/work/{$w->approval_token}"), ])) ->merge($wo->parts->filter(fn ($p) => $p->isPendingApproval())->map(fn ($p) => [ 'kind' => 'part', 'id' => $p->id, 'token' => $p->approval_token, 'name' => $p->name, 'amount' => (float) $p->total, 'approve_url' => url("/t/{$token}/approve/part/{$p->approval_token}"), ])); // Timeline from activity_log (best-effort — empty array if not configured) $timeline = []; try { $timeline = \DB::table('activity_log') ->where('subject_type', WorkOrder::class) ->where('subject_id', $wo->id) ->orderBy('created_at') ->limit(20) ->get(['event', 'description', 'created_at']) ->map(fn ($r) => [ 'event' => $r->event, 'description' => $r->description, 'at' => $r->created_at, ])->toArray(); } catch (\Throwable $e) { /* activity_log table may not exist in some tenants */ } return response()->json([ 'number' => $wo->number, 'status' => $wo->status, 'status_label' => $statuses[$wo->status] ?? $wo->status, 'progress' => $currentIdx !== false ? round(100 * ($currentIdx + 1) / count($flow)) : null, 'client' => $wo->client?->name, 'vehicle' => trim(($wo->vehicle?->make ?? '') . ' ' . ($wo->vehicle?->model ?? '')), 'plate' => $wo->vehicle?->plate, 'master' => $wo->master?->name, 'eta_promised' => $wo->eta_promised?->toIso8601String(), 'eta_current' => $wo->eta_at?->toIso8601String(), 'eta_change_reason' => $wo->eta_change_reason, 'total' => (float) $wo->total, 'pay_status' => $wo->pay_status, 'pending_approvals' => $pendingApprovals->values(), 'timeline' => $timeline, ]); } public function qr(Request $request, string $token) { $tenant = app(TenantManager::class)->current(); if (! $tenant) { throw new NotFoundHttpException(); } $wo = WorkOrder::where('tracking_token', $token)->first(); if (! $wo) { throw new NotFoundHttpException(); } $options = new \chillerlan\QRCode\QROptions([ 'outputType' => \chillerlan\QRCode\QRCode::OUTPUT_MARKUP_SVG, 'eccLevel' => \chillerlan\QRCode\QRCode::ECC_M, 'scale' => 6, 'imageBase64' => false, 'svgViewBoxSize' => 200, 'addQuietzone' => true, ]); $svg = (new \chillerlan\QRCode\QRCode($options))->render($wo->trackingUrl()); return response($svg, 200, [ 'Content-Type' => 'image/svg+xml', 'Cache-Control' => 'public, max-age=3600', ]); } }