vcard4reseller/backend/src/Entity/Employee.php
Thomas Peterson bcc06e697b Rechte: User in Employee verschmolzen (eine Identität pro Person)
Beseitigt die Doppelung Admin-Login vs. Mitarbeiter — jeder ist ein Employee
mit optionalem Login/Rechtegruppe (Voraussetzung für Mitarbeiter-Zeiterfassung).

- Employee implementiert UserInterface/PasswordAuthenticated (loginEmail unique,
  password, roles); User-Entität entfernt; Security-Provider → Employee.loginEmail
- Plattform = Reseller mit isPlatform + Org-Firma; Reseller haben Org-Firma
  (Company.isResellerOrg) für ihr Personal → alles = Reseller→Firma→Mitarbeiter
- TenantContext leitet Reseller/Company aus dem Mitarbeiter ab (Reseller-/
  Plattform-Admin = reseller-weit)
- UserAdminController: Login pro Mitarbeiter vergeben/entziehen
  (POST/DELETE /api/employees/{id}/login), /api/users = Logins-Übersicht
- Provisioning/Seed auf das neue Modell; Migrationen zu einer Baseline gesquasht
- Frontend: EmployeesView Login-Block + UsersView (Logins-Übersicht)

Verifiziert: Login, /me, Mandantenscoping, delegierter Grant (Eskalation→403),
öffentliches Profil, SPA-Flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 17:27:38 +02:00

440 lines
10 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\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\EmployeeRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid;
/**
* Mitarbeiterprofil — Single Source of Truth für alle Ausgabekanäle UND
* Login-Identität (Auth). Login/Passwort sind optional: ein Mitarbeiter kann
* ohne eigenes Login existieren (Visitenkarte/NFC) und bei Bedarf eine
* Rechtegruppe + Login erhalten.
*/
#[ORM\Entity(repositoryClass: EmployeeRepository::class)]
#[ORM\UniqueConstraint(name: 'uniq_employee_company_slug', fields: ['company', 'slug'])]
#[ORM\UniqueConstraint(name: 'uniq_employee_shortcode', fields: ['shortCode'])]
#[ORM\UniqueConstraint(name: 'uniq_employee_login_email', fields: ['loginEmail'])]
#[ApiResource(security: "is_granted('ROLE_COMPANY_ADMIN')")]
class Employee implements ResellerOwnedInterface, UserInterface, PasswordAuthenticatedUserInterface
{
public const ROLE_PLATFORM_ADMIN = 'ROLE_PLATFORM_ADMIN';
public const ROLE_RESELLER_ADMIN = 'ROLE_RESELLER_ADMIN';
public const ROLE_COMPANY_ADMIN = 'ROLE_COMPANY_ADMIN';
public const ROLE_EMPLOYEE = 'ROLE_EMPLOYEE';
#[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $id;
#[ORM\Column(length: 100)]
private string $firstName;
#[ORM\Column(length: 100)]
private string $lastName;
/** Öffentlicher Slug (eindeutig innerhalb der Firma). */
#[ORM\Column(length: 120)]
private string $slug;
/**
* Stabiler Kurz-Code für NFC-Tags / gedruckte QR-Codes. Verweist per
* Redirect (/t/{code}) immer auf das aktuelle Profil bleibt also gleich,
* auch wenn sich der Slug ändert. Generiert beim Anlegen, nicht editierbar.
*/
#[ORM\Column(length: 16, nullable: true)]
private ?string $shortCode = null;
#[ORM\Column(length: 150, nullable: true)]
private ?string $title = null;
#[ORM\Column(length: 150, nullable: true)]
private ?string $position = null;
#[ORM\Column(length: 150, nullable: true)]
private ?string $department = null;
#[ORM\Column(length: 180, nullable: true)]
private ?string $email = null;
#[ORM\Column(length: 50, nullable: true)]
private ?string $phone = null;
#[ORM\Column(length: 50, nullable: true)]
private ?string $mobile = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $photoPath = null;
#[ORM\Column(type: 'text', nullable: true)]
private ?string $bio = null;
#[ORM\Column(length: 20)]
private string $status = 'active';
/** Pro-Mitarbeiter-Freigabe für Self-Service (zusätzlich zu Company::selfEditEnabled). */
#[ORM\Column]
private bool $selfEditAllowed = false;
/** @var string[] Felder, die der Mitarbeiter selbst ändern darf (leer = alle erlaubten). */
#[ORM\Column(type: 'json')]
private array $editableFields = [];
#[ORM\ManyToOne(targetEntity: Company::class, inversedBy: 'employees')]
#[ORM\JoinColumn(nullable: false)]
private Company $company;
#[ORM\ManyToOne(targetEntity: Location::class)]
private ?Location $location = null;
// --- Login/Auth (optional) ---
/** Eindeutige Login-E-Mail; null = kein Login. */
#[ORM\Column(length: 180, nullable: true)]
private ?string $loginEmail = null;
#[ORM\Column(nullable: true)]
private ?string $password = null;
/** @var string[] Rechtegruppe(n); leer = nur Profil ohne Adminrechte. */
#[ORM\Column(type: 'json')]
private array $roles = [];
/** @var Collection<int, ContactLink> */
#[ORM\OneToMany(targetEntity: ContactLink::class, mappedBy: 'employee', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['position' => 'ASC'])]
private Collection $contactLinks;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $updatedAt;
public function __construct()
{
$this->id = Uuid::v7();
$this->shortCode = bin2hex(random_bytes(4));
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
$this->contactLinks = new ArrayCollection();
}
public function getShortCode(): ?string
{
return $this->shortCode;
}
public function ensureShortCode(): void
{
if (null === $this->shortCode) {
$this->shortCode = bin2hex(random_bytes(4));
}
}
public function getId(): Uuid
{
return $this->id;
}
public function getFirstName(): string
{
return $this->firstName;
}
public function setFirstName(string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): string
{
return $this->lastName;
}
public function setLastName(string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
public function getSlug(): string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): self
{
$this->title = $title;
return $this;
}
public function getPosition(): ?string
{
return $this->position;
}
public function setPosition(?string $position): self
{
$this->position = $position;
return $this;
}
public function getDepartment(): ?string
{
return $this->department;
}
public function setDepartment(?string $department): self
{
$this->department = $department;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getPhone(): ?string
{
return $this->phone;
}
public function setPhone(?string $phone): self
{
$this->phone = $phone;
return $this;
}
public function getMobile(): ?string
{
return $this->mobile;
}
public function setMobile(?string $mobile): self
{
$this->mobile = $mobile;
return $this;
}
public function getPhotoPath(): ?string
{
return $this->photoPath;
}
public function setPhotoPath(?string $photoPath): self
{
$this->photoPath = $photoPath;
return $this;
}
public function getBio(): ?string
{
return $this->bio;
}
public function setBio(?string $bio): self
{
$this->bio = $bio;
return $this;
}
public function getStatus(): string
{
return $this->status;
}
public function setStatus(string $status): self
{
$this->status = $status;
return $this;
}
public function isSelfEditAllowed(): bool
{
return $this->selfEditAllowed;
}
public function setSelfEditAllowed(bool $selfEditAllowed): self
{
$this->selfEditAllowed = $selfEditAllowed;
return $this;
}
/** @return string[] */
public function getEditableFields(): array
{
return $this->editableFields;
}
/** @param string[] $editableFields */
public function setEditableFields(array $editableFields): self
{
$this->editableFields = $editableFields;
return $this;
}
public function getCompany(): Company
{
return $this->company;
}
public function setCompany(Company $company): self
{
$this->company = $company;
return $this;
}
public function getLocation(): ?Location
{
return $this->location;
}
public function setLocation(?Location $location): self
{
$this->location = $location;
return $this;
}
// --- Login/Auth ---
public function getLoginEmail(): ?string
{
return $this->loginEmail;
}
public function setLoginEmail(?string $loginEmail): self
{
$this->loginEmail = $loginEmail;
return $this;
}
public function hasLogin(): bool
{
return null !== $this->loginEmail && null !== $this->password;
}
public function getUserIdentifier(): string
{
return (string) $this->loginEmail;
}
/** @return string[] */
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_values(array_unique($roles));
}
/** @param string[] $roles */
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
public function getPassword(): ?string
{
return $this->password;
}
public function setPassword(?string $password): self
{
$this->password = $password;
return $this;
}
public function eraseCredentials(): void
{
}
/** @return Collection<int, ContactLink> */
public function getContactLinks(): Collection
{
return $this->contactLinks;
}
public function addContactLink(ContactLink $link): self
{
if (!$this->contactLinks->contains($link)) {
$this->contactLinks->add($link);
$link->setEmployee($this);
}
return $this;
}
public function removeContactLink(ContactLink $link): self
{
$this->contactLinks->removeElement($link);
return $this;
}
public function getReseller(): ?Reseller
{
return $this->company->getReseller();
}
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updatedAt;
}
public function touch(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
}