with([ 'callable', 'translations', 'tag.contexts.category.translations', 'tag.contexts.category.ancestors.translations', 'location.city.translations', 'location.district.translations', 'location.country.translations', 'location.division.translations', 'loveReactant.reactionCounters', ])->findOrFail($id); $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $isAdmin = $activeProfileType === \App\Models\Admin::class; $isCallable = $activeProfileType === $call->callable_type && (int) $activeProfileId === (int) $call->callable_id; // Deleted calls are only visible to admins and the callable itself $isDeleted = $call->trashed(); if ($isDeleted && !$isAdmin && !$isCallable) { return view('calls.unavailable'); } // Blocked calls are only visible to admins and the callable itself if ($call->is_suppressed && !$isAdmin && !$isCallable) { return view('calls.unavailable'); } // Paused calls are only visible to admins and the callable itself if ($call->is_paused && !$isAdmin && !$isCallable) { return view('calls.unavailable'); } // Guests cannot view private calls if (!$call->is_public && !Auth::check()) { return redirect()->route('login') ->with('status', __('This call is private. Please log in to view it.')); } // Expired calls are only visible to the owner and admins $isExpired = $call->till !== null && $call->till->isPast(); if ($isExpired && !$isCallable && !$isAdmin) { return view('calls.unavailable'); } $locale = App::getLocale(); // Build category hierarchy $tagCategory = $call->tag?->contexts->first()?->category; $tagColor = $tagCategory?->relatedColor ?? 'gray'; $tagCategories = []; if ($tagCategory) { foreach ($tagCategory->ancestorsAndSelf()->get()->reverse() as $cat) { $name = $cat->translations->firstWhere('locale', $locale)?->name ?? $cat->translations->first()?->name ?? ''; if ($name) { $tagCategories[] = ['id' => $cat->id, 'name' => $name, 'color' => $cat->relatedColor ?? 'gray']; } } } // Build callable profile's location short string (city first, fallback to division or country) $callableLocation = null; if ($call->callable && method_exists($call->callable, 'locations')) { $firstLoc = $call->callable->locations()->with(['city.translations', 'division.translations', 'country.translations'])->first(); if ($firstLoc) { $cCity = optional($firstLoc->city?->translations->first())->name; $cDivision = optional($firstLoc->division?->translations->first())->name; $cCountry = optional($firstLoc->country?->translations->first())->name; $callableLocation = $cCity ?? $cDivision ?? $cCountry ?: null; } } // Build call exchange location: "City District, Division, COUNTRY_CODE" $callLocation = null; if ($call->location) { $loc = $call->location; $parts = []; $cityPart = null; if ($loc->city) { $cityPart = optional($loc->city->translations->first())->name; } if ($loc->district) { $districtName = optional($loc->district->translations->first())->name; $cityPart = $cityPart ? $cityPart . ' ' . $districtName : $districtName; } if ($cityPart) $parts[] = $cityPart; if ($loc->division) { $divisionName = optional($loc->division->translations->first())->name; if ($divisionName) $parts[] = $divisionName; } if ($loc->country && $loc->country->code !== 'XX') { $parts[] = strtoupper($loc->country->code); } elseif ($loc->country && $loc->country->code === 'XX') { $parts[] = __('Location not specified'); } $callLocation = $parts ? implode(', ', $parts) : null; } // Determine whether to show the expiry badge (within configured warning window) $expiryBadgeText = $call->till ? __('Expires') . ': ' . $call->till->translatedFormat('d F Y') : null; $callableType = $call->callable ? strtolower(class_basename($call->callable)) : null; $callDescription = $call->translations->firstWhere('locale', App::getLocale())?->content ?? $call->translations->firstWhere('locale', config('app.fallback_locale'))?->content ?? $call->translations->first()?->content; $viewName = Auth::check() ? 'calls.show' : 'calls.show-guest'; return view($viewName, compact('call', 'tagColor', 'tagCategories', 'callableType', 'callableLocation', 'callLocation', 'expiryBadgeText', 'isExpired', 'isDeleted', 'isAdmin', 'callDescription')); } public function callPdf($id) { $data = $this->buildCallPdfData($id); $pdf = Pdf::loadView('calls.pdf', $data) ->setPaper('A6', 'portrait') ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), ]); return $pdf->download('call-' . $data['call']->id . '.pdf'); } public function callPdfBw($id) { $data = $this->buildCallPdfData($id); $pdf = Pdf::loadView('calls.pdf-bw', $data) ->setPaper('A6', 'portrait') ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), ]); return $pdf->download('call-' . $data['call']->id . '-bw.pdf'); } public function callPdfA4($id) { $data = $this->buildCallPdfData($id); $pdf = Pdf::loadView('calls.pdf-a4', $data) ->setPaper('A4', 'portrait') ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), ]); return $pdf->download('call-' . $data['call']->id . '-a4.pdf'); } public function callPdfA4Bw($id) { $data = $this->buildCallPdfData($id); $pdf = Pdf::loadView('calls.pdf-a4-bw', $data) ->setPaper('A4', 'portrait') ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), ]); return $pdf->download('call-' . $data['call']->id . '-a4-bw.pdf'); } public function callPdfBulk(\Illuminate\Http\Request $request) { $ids = array_filter(array_map('intval', explode(',', $request->input('ids', '')))); $perPage = in_array((int) $request->input('per_page', 1), [1, 2, 4]) ? (int) $request->input('per_page', 1) : 1; $color = $request->input('color', 'bw') === 'color' ? 'color' : 'bw'; if (empty($ids)) abort(400); $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $isAdmin = $activeProfileType === \App\Models\Admin::class || ($activeProfileType === \App\Models\Bank::class && \App\Models\Bank::find($activeProfileId)?->level === 0); // Build per-call data, each in the call's own locale $calls = []; foreach ($ids as $id) { $call = Call::withTrashed()->find($id); if (!$call) continue; // Authorization: non-admins can only print their own calls if (!$isAdmin) { $isOwner = $activeProfileType === $call->callable_type && (int) $activeProfileId === (int) $call->callable_id; if (!$isOwner) continue; } // Use the call's translation locale, fall back to callable's lang_preference $callLocale = $call->translations->first()?->locale ?? $call->callable?->lang_preference ?? App::getLocale(); $calls[] = $this->buildCallPdfData($id, $callLocale); } if (empty($calls)) abort(403); $view = $color === 'color' ? 'calls.pdf-bulk' : 'calls.pdf-bulk-bw'; $orientation = $perPage === 2 ? 'landscape' : 'portrait'; $pdf = Pdf::loadView($view, compact('calls', 'perPage')) ->setPaper('A4', $orientation) ->setOptions([ 'isHtml5ParserEnabled' => true, 'isRemoteEnabled' => false, 'defaultFont' => 'DejaVu Sans', 'dpi' => 150, 'chroot' => public_path(), 'margin_top' => 0, 'margin_right' => 0, 'margin_bottom' => 0, 'margin_left' => 0, ]); return $pdf->download('calls-bulk-' . now()->format('Ymd-His') . '.pdf'); } protected function buildCallPdfData($id, ?string $locale = null): array { $previousLocale = App::getLocale(); $locale = $locale ?? $previousLocale; App::setLocale($locale); try { $data = $this->doBuildCallPdfData($id, $locale); } finally { App::setLocale($previousLocale); } return $data; } private function doBuildCallPdfData($id, string $locale): array { $call = Call::withTrashed()->with([ 'callable', 'translations', 'tag.contexts.category.translations', 'tag.contexts.category.ancestors.translations', 'location.city.translations', 'location.district.translations', 'location.country.translations', 'location.division.translations', ])->findOrFail($id); $activeProfileType = session('activeProfileType'); $activeProfileId = session('activeProfileId'); $isAdmin = $activeProfileType === \App\Models\Admin::class; $isCallable = $activeProfileType === $call->callable_type && (int) $activeProfileId === (int) $call->callable_id; $isDeleted = $call->trashed(); if ($isDeleted && !$isAdmin && !$isCallable) abort(403); if ($call->is_suppressed && !$isAdmin && !$isCallable) abort(403); if ($call->is_paused && !$isAdmin && !$isCallable) abort(403); if (!$call->is_public && !Auth::check()) abort(403); $isExpired = $call->till !== null && $call->till->isPast(); if ($isExpired && !$isCallable && !$isAdmin) abort(403); $tagCategory = $call->tag?->contexts->first()?->category; $tagColor = $tagCategory?->relatedColor ?? 'gray'; $tagColorHex = $this->tailwindColorToHex($tagColor); $bgHex = $tagColorHex; $bgDarkHex = $this->darkenHex($tagColorHex, 0.50); $tagCategories = []; if ($tagCategory) { foreach ($tagCategory->ancestorsAndSelf()->get()->reverse() as $cat) { $name = $cat->translations->firstWhere('locale', $locale)?->name ?? $cat->translations->first()?->name ?? ''; if ($name) { $tagCategories[] = [ 'id' => $cat->id, 'name' => $name, 'colorHex' => $this->tailwindColorToHex($cat->relatedColor ?? 'gray'), ]; } } } $callableLocation = null; if ($call->callable && method_exists($call->callable, 'locations')) { $firstLoc = $call->callable->locations()->with(['city.translations', 'division.translations', 'country.translations'])->first(); if ($firstLoc) { $cCity = optional($firstLoc->city?->translations->first())->name; $cDivision = optional($firstLoc->division?->translations->first())->name; $cCountry = optional($firstLoc->country?->translations->first())->name; $callableLocation = $cCity ?? $cDivision ?? $cCountry ?: null; } } $callLocation = null; if ($call->location) { $loc = $call->location; $parts = []; $cityPart = null; if ($loc->city) $cityPart = optional($loc->city->translations->first())->name; if ($loc->district) { $districtName = optional($loc->district->translations->first())->name; $cityPart = $cityPart ? $cityPart . ' ' . $districtName : $districtName; } if ($cityPart) $parts[] = $cityPart; if ($loc->division) { $divisionName = optional($loc->division->translations->first())->name; if ($divisionName) $parts[] = $divisionName; } if ($loc->country && $loc->country->code !== 'XX') { $parts[] = strtoupper($loc->country->code); } elseif ($loc->country && $loc->country->code === 'XX') { $parts[] = __('Location not specified'); } $callLocation = $parts ? implode(', ', $parts) : null; } $expiryBadgeText = $call->till ? __('Expires') . ': ' . $call->till->translatedFormat('d F Y') : null; // Pre-translate status labels in the call's locale (used by PDF templates) $statusLabelDeleted = __('DELETED'); $statusLabelBlocked = __('BLOCKED'); $statusLabelPaused = __('PAUSED'); $statusLabelExpired = __('EXPIRED'); $rawContent = $call->translations->firstWhere('locale', $locale)?->content ?? $call->translations->firstWhere('locale', config('app.fallback_locale'))?->content ?? $call->translations->first()?->content ?? ''; $callContent = mb_strlen($rawContent) > 250 ? mb_substr($rawContent, 0, 247) . '…' : $rawContent; $avatarBase64 = null; if ($call->callable?->profile_photo_path) { $avatarPath = storage_path('app/public/' . $call->callable->profile_photo_path); $ext = strtolower(pathinfo($avatarPath, PATHINFO_EXTENSION)); if (file_exists($avatarPath)) { if ($ext === 'svg') { // SVG avatars cannot be rendered by GD — skip silently. } else { $mime = mime_content_type($avatarPath) ?: 'image/jpeg'; $avatarBase64 = 'data:' . $mime . ';base64,' . base64_encode(file_get_contents($avatarPath)); } } } $logoBase64 = null; $logoPath = theme_email_logo_file(); if (file_exists($logoPath)) { $logoBase64 = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath)); } // QR codes as inline SVG (no ImageMagick needed) $callUrl = route('call.show', ['id' => $call->id]); $qrSvg = 'data:image/svg+xml;base64,' . base64_encode( \SimpleSoftwareIO\QrCode\Facades\QrCode::format('svg') ->size(300)->margin(2)->errorCorrection('M')->color(0, 0, 0)->backgroundColor(255, 255, 255)->generate($callUrl) ); $qrSvgBw = 'data:image/svg+xml;base64,' . base64_encode( \SimpleSoftwareIO\QrCode\Facades\QrCode::format('svg') ->size(300)->margin(2)->errorCorrection('M')->color(0, 0, 0)->generate($callUrl) ); return compact( 'call', 'tagColor', 'tagColorHex', 'bgHex', 'bgDarkHex', 'tagCategories', 'callableLocation', 'callLocation', 'expiryBadgeText', 'isExpired', 'isDeleted', 'avatarBase64', 'logoBase64', 'callContent', 'qrSvg', 'qrSvgBw', 'statusLabelDeleted', 'statusLabelBlocked', 'statusLabelPaused', 'statusLabelExpired' ); } /** * Darken a 6-char hex color by the given factor (0–1). * Used to simulate the dark overlay on the call card background. */ private function darkenHex(string $hex, float $factor): string { $r = (int) hexdec(substr($hex, 0, 2)); $g = (int) hexdec(substr($hex, 2, 2)); $b = (int) hexdec(substr($hex, 4, 2)); $r = (int) round($r * (1 - $factor)); $g = (int) round($g * (1 - $factor)); $b = (int) round($b * (1 - $factor)); return sprintf('%02x%02x%02x', $r, $g, $b); } /** * Map a Tailwind color name to its -400 hex value for use in DomPDF. */ private function tailwindColorToHex(string $color): string { return match ($color) { 'red' => 'f87171', 'orange' => 'fb923c', 'amber' => 'fbbf24', 'yellow' => 'facc15', 'lime' => 'a3e635', 'green' => '4ade80', 'emerald' => '34d399', 'teal' => '2dd4bf', 'cyan' => '22d3ee', 'sky' => '38bdf8', 'blue' => '60a5fa', 'indigo' => '818cf8', 'violet' => 'a78bfa', 'purple' => 'c084fc', 'fuchsia' => 'e879f9', 'pink' => 'f472b6', 'rose' => 'fb7185', default => '9ca3af', // gray-400 }; } public function manage() { return view('calls.manage'); } }