vcard4reseller/backend/src/Service/VCardBuilder.php
Thomas Peterson fa321fb6a5 Profil + vCard mit neuen Mitarbeiterfeldern; Modal breiter
- 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>
2026-06-09 19:33:46 +02:00

146 lines
4.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Service;
use App\Entity\Employee;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Erzeugt eine vCard (RFC 6350, Version 3.0 breite Kompatibilität mit
* iOS, Android, Outlook) aus einem Mitarbeiterprofil.
*/
final class VCardBuilder
{
public function __construct(private readonly UrlGeneratorInterface $urls)
{
}
public function build(Employee $e): string
{
$company = $e->getCompany();
$lines = [];
$lines[] = 'BEGIN:VCARD';
$lines[] = 'VERSION:3.0';
// N: Nachname;Vorname;;Präfix(Titel);
$lines[] = 'N:'.$this->esc($e->getLastName()).';'.$this->esc($e->getFirstName()).';;'
.$this->esc((string) $e->getTitle()).';';
$lines[] = 'FN:'.$this->esc(trim(($e->getTitle() ? $e->getTitle().' ' : '').$e->getFirstName().' '.$e->getLastName()));
if ($company) {
// ORG:Firma;Abteilung
$org = $this->esc($company->getName());
if ($e->getDepartment()) {
$org .= ';'.$this->esc($e->getDepartment());
}
$lines[] = 'ORG:'.$org;
}
if ($e->getPosition()) {
$lines[] = 'TITLE:'.$this->esc($e->getPosition());
}
if ($e->getEmail()) {
$lines[] = 'EMAIL;TYPE=WORK:'.$this->esc($e->getEmail());
}
if ($e->getEmailPrivate()) {
$lines[] = 'EMAIL;TYPE=HOME:'.$this->esc($e->getEmailPrivate());
}
if ($e->getPhone()) {
$lines[] = 'TEL;TYPE=WORK,VOICE:'.$this->esc($e->getPhone());
}
if ($e->getMobile()) {
$lines[] = 'TEL;TYPE=CELL:'.$this->esc($e->getMobile());
}
if ($e->getPhoneCentral()) {
$lines[] = 'TEL;TYPE=WORK:'.$this->esc($e->getPhoneCentral());
}
if ($e->getFax()) {
$lines[] = 'TEL;TYPE=WORK,FAX:'.$this->esc($e->getFax());
}
if ($e->getWebsite()) {
$lines[] = 'URL:'.$this->esc($e->getWebsite());
}
$this->addAddress($lines, 'WORK', $e->getAddressBusiness() ?? $this->fromLocation($e));
$this->addAddress($lines, 'HOME', $e->getAddressPrivate());
foreach ($e->getContactLinks() as $link) {
$lines[] = 'URL:'.$this->esc($link->getUrl());
}
if (null !== $e->getPhotoPath()) {
$lines[] = 'PHOTO;VALUE=URI:'.$this->urls->generate(
'employee_photo_public',
['id' => (string) $e->getId()],
UrlGeneratorInterface::ABSOLUTE_URL,
);
}
if ($e->getBio()) {
$lines[] = 'NOTE:'.$this->esc($e->getBio());
}
$lines[] = 'REV:'.$e->getUpdatedAt()->format('Ymd\THis\Z');
$lines[] = 'END:VCARD';
// vCard verlangt CRLF-Zeilenenden
return implode("\r\n", $lines)."\r\n";
}
/**
* @param string[] $lines
* @param array<string, mixed>|null $a
*/
private function addAddress(array &$lines, string $type, ?array $a): void
{
if (null === $a) {
return;
}
$street = trim((string) ($a['street'] ?? '').' '.(string) ($a['houseNumber'] ?? ''));
$city = (string) ($a['city'] ?? '');
if ('' === $street && '' === $city) {
return;
}
// ADR: PO;Ext;Straße;Ort;Region;PLZ;Land
$lines[] = 'ADR;TYPE='.$type.':;'
.$this->esc((string) ($a['addressLine2'] ?? '')).';'
.$this->esc($street).';'
.$this->esc($city).';'
.$this->esc((string) ($a['state'] ?? '')).';'
.$this->esc((string) ($a['zip'] ?? '')).';'
.$this->esc((string) ($a['country'] ?? ''));
}
/**
* Fallback: Geschäftsadresse aus dem zugeordneten Standort.
*
* @return array<string, mixed>|null
*/
private function fromLocation(Employee $e): ?array
{
$l = $e->getLocation();
if (null === $l || (!$l->getStreet() && !$l->getCity())) {
return null;
}
return [
'street' => (string) $l->getStreet(),
'city' => (string) $l->getCity(),
'zip' => (string) $l->getPostalCode(),
'country' => (string) $l->getCountry(),
];
}
/** Escaping gemäß vCard-Spezifikation. */
private function esc(string $value): string
{
return str_replace(
['\\', "\n", ',', ';'],
['\\\\', '\\n', '\\,', '\\;'],
$value,
);
}
}