- VCardBuilder: Titel (N-Präfix), Abteilung (ORG), Privat-E-Mail, Mobil, Zentrale, Fax, Website, Geschäfts-/Privatadresse, Foto (PHOTO URI) - Profilseite: Foto über echte URL, Kontakt-Sektion (alle Nummern/Mails/Web), Adresse geschäftlich+privat, Branding via BrandingService (Reseller-Vererbung) - Mitarbeiter-Formular nutzt das breite Modal (kein Overflow mehr) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
3.8 KiB
PHP
106 lines
3.8 KiB
PHP
<?php
|
||
|
||
namespace App\Controller;
|
||
|
||
use App\Entity\Employee;
|
||
use App\Repository\EmployeeRepository;
|
||
use App\Service\BrandingService;
|
||
use App\Service\ResolvedTenant;
|
||
use App\Service\VCardBuilder;
|
||
use App\Service\WalletService;
|
||
use Endroid\QrCode\Builder\Builder;
|
||
use Endroid\QrCode\Encoding\Encoding;
|
||
use Endroid\QrCode\ErrorCorrectionLevel;
|
||
use Endroid\QrCode\Writer\PngWriter;
|
||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||
use Symfony\Component\HttpFoundation\Response;
|
||
use Symfony\Component\Routing\Attribute\Route;
|
||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||
|
||
/**
|
||
* Öffentliche, serverseitig gerenderte Profilseiten (SSR, SEO).
|
||
* Nicht mandantengefiltert und ohne Auth – bewusst öffentlich.
|
||
*/
|
||
final class PublicProfileController extends AbstractController
|
||
{
|
||
public function __construct(private readonly EmployeeRepository $employees)
|
||
{
|
||
}
|
||
|
||
#[Route('/p/{companySlug}/{slug}', name: 'public_profile', methods: ['GET'])]
|
||
public function show(string $companySlug, string $slug, WalletService $wallet, BrandingService $branding): Response
|
||
{
|
||
$employee = $this->resolve($companySlug, $slug);
|
||
$company = $employee->getCompany();
|
||
$tenant = new ResolvedTenant(ResolvedTenant::KIND_COMPANY, $company->getReseller(), $company);
|
||
|
||
return $this->render('public/profile.html.twig', [
|
||
'e' => $employee,
|
||
'branding' => $branding->forTenant($tenant)['branding'],
|
||
'profileUrl' => $this->profileUrl($employee),
|
||
'shareUrl' => $this->shareUrl($employee),
|
||
'walletEnabled' => null !== $employee->getShortCode()
|
||
&& ($wallet->isAppleConfigured() || $wallet->isGoogleConfigured()),
|
||
]);
|
||
}
|
||
|
||
#[Route('/p/{companySlug}/{slug}/vcard.vcf', name: 'public_profile_vcard', methods: ['GET'])]
|
||
public function vcard(string $companySlug, string $slug, VCardBuilder $builder): Response
|
||
{
|
||
$employee = $this->resolve($companySlug, $slug);
|
||
|
||
return new Response($builder->build($employee), 200, [
|
||
'Content-Type' => 'text/vcard; charset=utf-8',
|
||
'Content-Disposition' => sprintf('attachment; filename="%s.vcf"', $employee->getSlug()),
|
||
]);
|
||
}
|
||
|
||
#[Route('/p/{companySlug}/{slug}/qr.png', name: 'public_profile_qr', methods: ['GET'])]
|
||
public function qr(string $companySlug, string $slug): Response
|
||
{
|
||
$employee = $this->resolve($companySlug, $slug);
|
||
|
||
$result = (new Builder(
|
||
writer: new PngWriter(),
|
||
data: $this->shareUrl($employee),
|
||
encoding: new Encoding('UTF-8'),
|
||
errorCorrectionLevel: ErrorCorrectionLevel::Medium,
|
||
size: 320,
|
||
margin: 12,
|
||
))->build();
|
||
|
||
return new Response($result->getString(), 200, ['Content-Type' => $result->getMimeType()]);
|
||
}
|
||
|
||
private function resolve(string $companySlug, string $slug): Employee
|
||
{
|
||
$employee = $this->employees->findPublic($companySlug, $slug);
|
||
if (null === $employee) {
|
||
throw $this->createNotFoundException('Profil nicht gefunden.');
|
||
}
|
||
|
||
return $employee;
|
||
}
|
||
|
||
private function profileUrl(Employee $employee): string
|
||
{
|
||
return $this->generateUrl('public_profile', [
|
||
'companySlug' => $employee->getCompany()->getSlug(),
|
||
'slug' => $employee->getSlug(),
|
||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||
}
|
||
|
||
/**
|
||
* Stabile Teilen-URL: bevorzugt den Kurz-Code (/t/{code}, NFC/QR-tauglich),
|
||
* fällt sonst auf die Profil-URL zurück.
|
||
*/
|
||
private function shareUrl(Employee $employee): string
|
||
{
|
||
if (null !== $employee->getShortCode()) {
|
||
return $this->generateUrl('short_link', ['code' => $employee->getShortCode()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||
}
|
||
|
||
return $this->profileUrl($employee);
|
||
}
|
||
}
|