where('short_code', $shortCode)->firstOrFail(); $this->scanRecorder->record($qrCode, $request); if (! $qrCode->is_active) { return view('public.qr.inactive', ['qrCode' => $qrCode]); } if ($qrCode->resolvesToRedirect()) { $url = $qrCode->redirectUrl(); abort_unless($url, 404); return redirect()->away($url); } if ($qrCode->isDocumentType()) { return redirect()->route('qr.public.view', $shortCode); } if ($qrCode->isBookType()) { return view('public.qr.book-landing', ['qrCode' => $qrCode]); } if ($qrCode->usesLandingPage()) { return view('public.qr.landing', ['qrCode' => $qrCode]); } abort(404); } public function view(string $shortCode): View { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->with('document') ->firstOrFail(); abort_unless($qrCode->isDocumentType() && $qrCode->document, 404); return view('public.qr.document-viewer', [ 'qrCode' => $qrCode, 'fileUrl' => route('qr.public.file', $shortCode), 'allowDownload' => (bool) ($qrCode->content()['allow_download'] ?? true), ]); } public function file(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->with('document') ->firstOrFail(); $document = $qrCode->document; abort_unless($document && Storage::disk($document->disk)->exists($document->path), 404); return Storage::disk($document->disk)->response( $document->path, ($document->title ?: 'document') . '.pdf', ['Content-Type' => 'application/pdf'], ); } public function image(string $shortCode, int $index): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->isImageType(), 404); $images = $qrCode->content()['images'] ?? []; abort_unless(isset($images[$index]['path']), 404); $path = $images[$index]['path']; abort_unless(Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function itemImage(string $shortCode, int $sectionIndex, int $itemIndex): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless(in_array($qrCode->type, [QrCode::TYPE_MENU, QrCode::TYPE_SHOP], true), 404); $sections = $qrCode->content()['sections'] ?? []; $path = $sections[$sectionIndex]['items'][$itemIndex]['image_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function vcard(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_VCARD, 404); $c = $qrCode->content(); $lines = [ 'BEGIN:VCARD', 'VERSION:3.0', 'N:' . ($c['last_name'] ?? '') . ';' . ($c['first_name'] ?? '') . ';;;', 'FN:' . trim(($c['first_name'] ?? '') . ' ' . ($c['last_name'] ?? '')), ]; if (! empty($c['company'])) { $lines[] = 'ORG:' . $this->escapeVcard($c['company']); } if (! empty($c['phone'])) { $lines[] = 'TEL;TYPE=CELL:' . $this->escapeVcard($c['phone']); } if (! empty($c['email'])) { $lines[] = 'EMAIL;TYPE=INTERNET:' . $this->escapeVcard($c['email']); } if (! empty($c['website'])) { $lines[] = 'URL:' . $this->escapeVcard($c['website']); } if (! empty($c['address'])) { $lines[] = 'ADR;TYPE=WORK:;;' . $this->escapeVcard($c['address']) . ';;;;'; } if (! empty($c['note'])) { $lines[] = 'NOTE:' . $this->escapeVcard($c['note']); } $lines[] = 'END:VCARD'; $vcf = implode("\r\n", $lines) . "\r\n"; $filename = Str::slug($qrCode->label ?: 'contact') . '.vcf'; return response()->streamDownload(fn () => print($vcf), $filename, [ 'Content-Type' => 'text/vcard', ]); } public function vcardAvatar(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_VCARD, 404); $path = $qrCode->content()['avatar_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function bookCover(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->isBookType(), 404); $path = $qrCode->content()['cover_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function menuLogo(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless(in_array($qrCode->type, [QrCode::TYPE_MENU, QrCode::TYPE_SHOP], true), 404); $path = $qrCode->content()['logo_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function menuCover(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless(in_array($qrCode->type, [QrCode::TYPE_MENU, QrCode::TYPE_SHOP], true), 404); $path = $qrCode->content()['cover_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function businessLogo(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_BUSINESS, 404); $path = $qrCode->content()['logo_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function businessCover(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_BUSINESS, 404); $path = $qrCode->content()['cover_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function churchLogo(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_CHURCH, 404); $path = $qrCode->content()['logo_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function churchCover(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_CHURCH, 404); $path = $qrCode->content()['cover_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function eventLogo(string $shortCode): StreamedResponse { return $this->serveContentImage($shortCode, QrCode::TYPE_EVENT, 'logo_path'); } public function eventCover(string $shortCode): StreamedResponse { return $this->serveContentImage($shortCode, QrCode::TYPE_EVENT, 'cover_path'); } public function itineraryCover(string $shortCode): StreamedResponse { return $this->serveContentImage($shortCode, QrCode::TYPE_ITINERARY, 'cover_path'); } private function serveContentImage(string $shortCode, string $type, string $key): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === $type, 404); $path = $qrCode->content()[$key] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } public function appIcon(string $shortCode): StreamedResponse { $qrCode = QrCode::query() ->where('short_code', $shortCode) ->where('is_active', true) ->firstOrFail(); abort_unless($qrCode->type === QrCode::TYPE_APP, 404); $path = $qrCode->content()['icon_path'] ?? null; abort_unless($path && Storage::disk('qr')->exists($path), 404); return Storage::disk('qr')->response($path); } private function escapeVcard(string $value): string { return str_replace(["\n", "\r", ',', ';'], [' ', ' ', '\\,', '\\;'], $value); } }