vcard4reseller/docs/KONZEPT.md
Thomas Peterson b09931997b Bestellungen: PrintOrder/OrderItem + OrderController (Backend)
- PrintOrder (company, status-Workflow new/in_production/shipped/completed/
  cancelled, number, createdBy, items) + OrderItem (product, employee, quantity)
- OrderController: GET Liste (scoped: Firma eigene / Reseller alle seiner
  Firmen / Plattform alle), GET Detail (inkl. PDF-Link je Position),
  POST anlegen (Firmen-Admin), PATCH /status (Reseller wickelt ab / Firma
  storniert solange neu). Produkt-/Mitarbeiter-Sichtbarkeit geprüft.
- Migration + Demo-Bestellung (Muster: 100 Visitenkarten + 10 NFC). KONZEPT §13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:59:36 +02:00

33 KiB
Raw Blame History

vcard4reseller — Konzept & Datenmodell

White-Label-Plattform für digitale Visitenkarten (PrintShopCreator). Reseller (Druckereien/Agenturen) bieten ihren Firmenkunden ein zentrales Identity-Management an: ein Mitarbeiterprofil als Single Source of Truth, das in alle digitalen und gedruckten Ausgabekanäle synchronisiert wird.

Status: Konzeptphase — noch kein Code. Stack-Entscheidung: Symfony (API Platform, JSON-API) + eigenständige Vue 3 SPA (Vite), MariaDB. Architektur Variante A (SPA + getrennte API).


1. Ziele & Leitprinzipien

  1. Single Source of Truth: Eine Änderung am Mitarbeiterprofil propagiert in alle Kanäle (vCard, Wallet, Landingpage, NFC, QR, Druckdaten, E-Mail-Signatur).
  2. Mandantenfähigkeit (Multi-Tenancy): Strikte Datentrennung pro Reseller und pro Firmenkunde.
  3. White-Label: Jeder Reseller / Firmenkunde tritt unter eigener Domain & eigenem Branding auf.
  4. Wiederkehrender Umsatz: Reseller zahlt Plattformgebühr (Paket), Firmenkunde zahlt Reseller (eigene Preisgestaltung).
  5. Öffentliche Profilseiten müssen schnell & SEO-fähig sein → serverseitig gerendert (Twig), getrennt von den Dashboards (Vue-SPA).

2. Rollen & Berechtigungen

Rolle Symfony-Role Scope Kann
Plattform-Admin ROLE_PLATFORM_ADMIN global Reseller anlegen/verwalten, Plattform-Pakete, Abrechnung gegenüber Resellern, Demo-Leads
Reseller-Admin ROLE_RESELLER_ADMIN 1 Reseller Firmenkunden + Domains + Branding anlegen, Preise definieren, Druckaufträge, Reseller-Mitarbeiter
Firmen-Admin ROLE_COMPANY_ADMIN 1 Company Standorte, Mitarbeiter/Profile, Templates der Firma, NFC/QR ausrollen
Mitarbeiter ROLE_EMPLOYEE 1 Profil eigenes Profil pflegen (sofern Firma das erlaubt)
Öffentlicher Besucher Profilseite ansehen, vCard/Wallet herunterladen, NFC/QR scannen

Hierarchie der Mandanten: Plattform → Reseller → Company → Location → Employee/Profil

Mehrere Logins pro Ebene

Jede Ebene kann mehrere Benutzer haben (mehrere Plattform-, Reseller-, Firmen-Admins). Technisch: User ist die Login-Identität (1 Person = 1 Login), n User pro Reseller bzw. Company. Ein User ist optional mit einem Employee (Profil) verknüpft — so wird aus einem Mitarbeiter mit Profil per Rechtegruppen-Zuweisung zusätzlich ein eingeloggter Admin.

Rechtegruppen (zuweisbare Rollen)

Die Rollen werden im UI als wählbare Rechtegruppe angeboten — beim Anlegen/Bearbeiten eines Benutzers bzw. direkt in der Mitarbeiterliste („kein Login" | „Mitarbeiter" | „Firmen-Admin" | …). Start = die vier festen Rollen; später optional granulare Berechtigungs-Gruppen (eigene Rechte-Sets pro Mandant).

Delegierte Rechtevergabe (Kernregel)

Jede Ebene vergibt Rechte nur an oder unterhalb der eigenen Ebene und nur im eigenen Mandanten-Teilbaum — niemals nach oben, nie über Mandantengrenzen hinweg:

Akteur darf Rechtegruppe vergeben Geltungsbereich
Plattform-Admin Plattform-Admin, Reseller-Admin (+ darunter) alle Mandanten
Reseller-Admin Reseller-Admin (weitere im eigenen Reseller), Firmen-Admin, Mitarbeiter nur eigener Reseller + dessen Firmen
Firmen-Admin Firmen-Admin (weitere der eigenen Firma), Mitarbeiter nur eigene Firma
Mitarbeiter

D. h.: wir vergeben Reseller-Rechte, Reseller vergeben Firmen-Admin-Rechte, Firmen-Admins vergeben Mitarbeiter-/weitere Firmen-Admin-Rechte. Same-Level-Vergabe (weitere Admins der eigenen Ebene) ist erlaubt → ermöglicht mehrere Logins pro Ebene.

Durchsetzung über:

  • Doctrine-Filter / API-Platform-Query-Extension (automatisches Scoping nach reseller_id / company_id je nach eingeloggtem Kontext)
  • Security Voters (CompanyVoter, EmployeeVoter) für Aktion×Objekt
  • RoleAssignmentVoter/-Service: prüft bei jeder Rollen-/Rechtegruppen-Vergabe, dass die Zielrolle ≤ der höchsten Rolle des Akteurs ist und der Ziel-Benutzer im Mandanten-Teilbaum des Akteurs liegt (Schutz vor Privilege-Escalation)
  • API-Platform security-Attribute pro Resource & Operation

3. Architektur

┌──────────────────────────┐        ┌──────────────────────────────────┐
│  Vue 3 SPA (Vite)        │        │  Öffentliche Profilseiten         │
│  - Reseller-Dashboard    │        │  (Symfony + Twig, SSR, SEO)       │
│  - Firmen-Dashboard      │        │  /{slug}, /c/{company}/{slug}     │
│  - Mitarbeiter-Self-Svc  │        │  vCard-Download, Wallet, QR-Ziel  │
└───────────┬──────────────┘        └─────────────────┬────────────────┘
            │ JSON (JWT/Session)                        │
            ▼                                           ▼
┌──────────────────────────────────────────────────────────────────────┐
│  Symfony Backend                                                       │
│  ├─ API Platform (REST/JSON, OpenAPI)                                  │
│  ├─ Security (JWT/LexikJWT od. Session), Voters, Doctrine-Filter      │
│  ├─ Domain Services (ProfileSync, ChannelGenerator)                   │
│  ├─ Messenger (async): Wallet-Pass, Druckdaten-PDF, QR/NFC, E-Mail   │
│  └─ Doctrine ORM                                                       │
└───────────┬───────────────────────┬───────────────────┬──────────────┘
            ▼                        ▼                   ▼
      MariaDB                 Object-Storage         externe Dienste
   (Mandantendaten)        (Assets, PDFs, Pässe)   (Apple/Google Wallet,
                                                    Payment/Stripe, Mail)

Generierungs-Pipeline (ChannelGenerator): Bei jeder Profiländerung wird ein ProfileUpdated-Event ausgelöst → Messenger-Handler regenerieren die abgeleiteten Artefakte (vCard-String, Wallet-Pass, QR/Kurz-URL, Druck-PDF, E-Mail-Signatur-HTML) und legen sie versioniert ab. Öffentliche Endpunkte liefern immer den aktuellen Stand.


4. Datenmodell (Entitäten)

Kernentitäten

  • User — Auth-Identität (1 Login). Felder: email, password, roles[] (= Rechtegruppe), status, lastLogin. Verknüpft optional mit reseller_id, company_id, employee_id. Mehrere User pro Reseller/Company möglich; Rollenvergabe ist delegiert & scope-geprüft (siehe §2).
  • Reseller — oberster Mandant. Felder: name, slug, primaryDomain, Branding-Defaults, status, platformPlan_id. Hat Limits aus dem Plattform-Paket.
  • PlatformPlan — Reseller-Pakete (Starter/Professional/Business). Felder: name, pricePerMonth, maxProfiles, maxCompanies, features[].
  • ResellerSubscription — Abo des Resellers bei der Plattform. Felder: plan_id, status, startedAt, renewsAt, paymentRef (Stripe).
  • Company (Firmenkunde) — gehört zu Reseller. Felder: name, slug, Branding (Logo, Farben, Fonts), defaultLocation_id, status, selfEditEnabled (erlaubt Mitarbeiter-Self-Service generell). Domains ausgelagert in Domain.
  • Domain — gehört zu Company. Felder: hostname, type (subdomain|custom), status (pending|verified|failed), verificationCheckedAt, tlsStatus. Custom-Domains zeigen per A-Record auf unsere IP (siehe §11).
  • Location (Standort) — gehört zu Company. Felder: Adresse, Geo, Telefon, Öffnungszeiten, Standort-spezifisches Branding-Override.
  • Employee / Profile — gehört zu Company, optional Location. Single Source of Truth. Felder: Name, Titel, Position, Abteilung, Telefon(e), E-Mail, Foto, Bio, slug (öffentlich), status, user_id (optional, für Self-Service), selfEditAllowed (bool — Firmen-Admin gibt diesen Mitarbeiter frei), editableFields (optional: welche Felder der Mitarbeiter ändern darf).
  • ContactLink — Social-/Web-Links eines Profils (Typ + URL + Reihenfolge). 1:n zu Employee.

Branding & Vorlagen

  • Template — Vorlage, scoped auf Reseller oder Company. Felder: type (print_card | email_signature | landing_page | wallet_pass), name, config (JSON), assets.
  • Asset — hochgeladene Datei (Logo, Foto, Hintergrund). Felder: path, mimeType, owner (polymorph: reseller/company/employee).

Ausgabekanäle (generierte Artefakte)

  • NfcTag — physischer Tag. Felder: uid, shortUrl, employee_id (nullable bis Zuweisung), status (unassigned/active/disabled), lastScanAt.
  • QrCode — generierter QR (statisch oder dynamische Redirect-URL). Felder: target, imageAsset, employee_id.
  • WalletPass — Referenz auf Apple/Google-Pass. Felder: provider (apple|google), serial, authToken (Apple Web-Service-Auth), passUrl, employee_id, lastGeneratedAt. Siehe §12.
  • WalletDevice — registriertes Apple-Gerät für Push-Updates (Apple-spezifisch). Felder: deviceLibraryId, pushToken, serial (→ WalletPass), registeredAt. Google braucht das nicht (Server-Push über die API).
  • GeneratedArtifact (optional, generisch) — Cache abgeleiteter Outputs: type (vcard/print_pdf/signature_html), employee_id, payload/fileRef, generatedAt, version.

Vertrieb & Abrechnung

  • Invoice — Rechnung Plattform→Reseller. Felder: reseller_id, amount, period, status, pdfRef. (Reseller→Company-Rechnungen vorerst nicht Teil der Plattform, siehe §8.)
  • CompanySubscription / PricePlanzurückgestellt: Reseller↔Company-Abrechnung läuft außerhalb der Plattform.
  • PrintOrder — Druckauftrag. Felder: company_id, status (new/in_production/shipped), items[] (je Employee), printDataRef.

Marketing / Sonstiges

  • DemoRequest — Lead vom Marketing-Formular (/demo). Felder: Name, Firma, E-Mail, Telefon, Nachricht, status.
  • AuditLog — Nachvollziehbarkeit kritischer Aktionen (wer/was/wann).

5. ER-Diagramm

erDiagram
    PlatformPlan ||--o{ Reseller : "definiert Limits"
    Reseller ||--|| ResellerSubscription : hat
    Reseller ||--o{ Company : verwaltet
    Reseller ||--o{ User : "Reseller-Staff"
    Reseller ||--o{ Template : "Defaults"

    Company ||--o{ Location : hat
    Company ||--o{ Domain : "Sub-/Custom-Domain"
    Company ||--o{ Employee : beschäftigt
    Company ||--o{ User : "Firmen-Admins"
    Company ||--o{ Template : "eigene"

    Location ||--o{ Employee : "zugeordnet"

    Employee ||--o{ ContactLink : hat
    Employee ||--o| NfcTag : verknüpft
    Employee ||--o{ QrCode : hat
    Employee ||--o| WalletPass : hat
    WalletPass ||--o{ WalletDevice : "Apple-Push"
    Employee ||--o{ GeneratedArtifact : erzeugt
    Employee ||--o| User : "Self-Service"

    Company ||--o{ PrintOrder : bestellt
    Reseller ||--o{ Invoice : "Plattform→Reseller"

6. API-Struktur (API Platform, grob)

Alle unter /api, JWT-geschützt, mandantengescoped. Beispiele:

Resource Operationen Zugriff
/api/resellers CRUD Platform-Admin
/api/companies CRUD Reseller-Admin (eigene), Platform-Admin
/api/locations CRUD Company-/Reseller-Admin
/api/employees CRUD Company-Admin; GET self für Employee
/api/employees/{id}/regenerate POST löst Kanal-Neugenerierung aus
/api/templates CRUD Reseller-/Company-Admin
/api/nfc-tags CRUD + assign Company-Admin
/api/print-orders CRUD Company-Admin / Reseller
/api/demo-requests POST (öffentlich), GET (Admin) Marketing

Öffentliche, nicht-API Endpunkte (Symfony/Twig, SSR):

  • GET /{slug} oder https://{companyDomain}/{employeeSlug} → Landingpage
  • GET /{slug}/vcard.vcf → vCard-Download
  • GET /{slug}/wallet → Wallet-Pass
  • GET /t/{nfcShortCode} → NFC/QR-Redirect auf aktuelles Profil

7. Sicherheit & Mandantentrennung

  • Auth: LexikJWTAuthenticationBundle (Stateless API) oder Session für SPA-Komfort. Refresh-Tokens.
  • Scoping: Doctrine-Filter, der je nach eingeloggtem User automatisch reseller_id/company_id in alle Queries injiziert. Kein Datenleck zwischen Mandanten.
  • Voters: CompanyVoter, EmployeeVoter, TemplateVoter für Aktion×Objekt-Prüfung.
  • Domain-Auflösung: Middleware ermittelt aus Host-Header (Custom-Domain/Subdomain) den Mandantenkontext für öffentliche Seiten.
  • Datenschutz: DSGVO — Mitarbeiterdaten, Lösch-/Export-Funktion, AuditLog. Standort EU.

8. Billing-Modell

Entscheidung: Vorerst rechnet nur die Plattform gegenüber dem Reseller ab. Wie der Reseller seinerseits mit den Firmenkunden abrechnet, regelt er selbst — das ist nicht Teil der Plattform (Stand jetzt). Eine spätere optionale Reseller→Company-Fakturierung bleibt als Erweiterung vorgemerkt.

  1. Plattform → Reseller (umgesetzt): Reseller wählt PlatformPlan (Starter 99 € / Professional 249 € / Business 599 €). Stripe-Abo, Limits (maxProfiles, maxCompanies) werden durchgesetzt.
  2. Reseller → Company (außerhalb der Plattform): Reseller vereinbart Preise selbst. Plattform bietet höchstens einen Umsatzrechner/Reporting zur Orientierung, aber keine Rechnungsstellung. → Entität CompanySubscription/Invoice (Reseller→Company) wird zurückgestellt auf Phase „später, falls gewünscht".
  3. Umsatzrechner (aus Marketing): Profile × Preis Plattformgebühr = Marge (rein informativ).

9. Roadmap / Phasen

Phase 0 — Setup (nach Konzept-Freigabe)

  • Symfony-Projekt + API Platform + Doctrine + MariaDB
  • Vue 3 SPA (Vite) Grundgerüst, Auth-Flow
  • CI, Docker-Dev-Umgebung

Phase 1 — Kern-Domäne & Auth

  • Entitäten: User, Reseller, Company, Location, Employee, ContactLink
  • Rollen, Voters, Doctrine-Mandantenfilter
  • Reseller-/Firmen-Dashboards (CRUD)

Phase 2 — Öffentliche Profile

  • SSR-Landingpage, vCard-Download, QR-Code, Kurz-URL-Redirect
  • Branding/Template-Engine (landing_page)

Phase 3 — Ausgabekanäle

  • Wallet-Pässe (Apple/Google), E-Mail-Signaturen, Druckdaten-PDF
  • NFC-Tag-Verwaltung & Zuweisung
  • Messenger-Pipeline für async Generierung

Phase 4 — Billing & Vertrieb

  • Plattform-Pakete + Stripe, Limit-Durchsetzung
  • Reseller-Preisgestaltung, Umsatzrechner, PrintOrders, Invoices

Phase 5 — Marketing-Site & Politur

  • Öffentliche Landingpage (vcard4reseller.de-Nachbau), Demo-Formular → DemoRequest
  • Reporting, AuditLog, DSGVO-Tools

10. Geklärte Entscheidungen & offene Fragen

Geklärt (2026-05-30)

  1. Domains: Standard = Subdomains (firma.reseller.de). Firmenkunden können zusätzlich eine eigene Domain hinterlegen; sie müssen dann selbst per A-Record auf unsere IP zeigen. Plattform übernimmt Domain-Verifikation + TLS-Provisionierung (Let's Encrypt). Siehe §11.
  2. Self-Service: Möglich, aber muss vom Firmen-Admin freigegeben werden — zweistufig: Company.selfEditEnabled (Firma erlaubt es generell) + Employee.selfEditAllowed (pro Mitarbeiter). Mitarbeiter bearbeitet nur freigegebene Felder.
  3. Billing: Nur Plattform → Reseller (siehe §8). Reseller↔Company außerhalb der Plattform; spätere Erweiterung möglich.

Noch offen

  1. Wallet-Pässe: Apple Developer Account + Google Wallet API vorhanden? (Zertifikate erforderlich). White-Label-Frage: läuft der Apple-Pass unter einem zentralen Pass Type ID (Plattform) oder pro Reseller? Konzept dazu in §12.
  2. Druckdaten: Welches Format erwarten die Druckereien (PDF/X, bestimmte Maße, Beschnitt)? Gibt es Vorlagen?
  3. Bestehende Daten/Branding: Existieren Design-Assets/CI zur bestehenden vcard4reseller.de, die wir übernehmen?

11. Domain-Handling

Subdomain (Default): Jede Company bekommt {company.slug}.{reseller.primaryDomain}. Wildcard-DNS + Wildcard-TLS auf Reseller-Ebene → keine Aktion durch den Kunden nötig.

Eigene Domain (optional):

  1. Firmen-Admin trägt seine Domain (visitenkarte.firma.de) im Dashboard ein → Status pending.
  2. Plattform zeigt die einzutragende IP (A-Record) (+ optional CNAME) an.
  3. Kunde setzt den DNS-Eintrag bei seinem Provider.
  4. Plattform prüft periodisch (DNS-Lookup) → bei Treffer Status verified, dann automatisches TLS-Zertifikat (Let's Encrypt / ACME).
  5. Reverse-Proxy/Router löst den Host-Header → Mandantenkontext (Company) auf.

Datenmodell-Ergänzung an Company bzw. neue Entität Domain: hostname, type (subdomain|custom), status (pending|verified|failed), verificationCheckedAt, tlsStatus, company_id.


12. Wallet-Pässe & Kontakt-Synchronisation

Das Grundproblem

Eine heruntergeladene vCard (.vcf) ist ein Schnappschuss: Einmal in der Kontakte-App gespeichert, aktualisiert sie sich nicht mehr. Ändert ein Mitarbeiter seine Nummer, haben alle Empfänger veraltete Daten. Das ist eine OS-Einschränkung. Es gibt drei Wege zu „aktuellen Daten":

Weg Echter Auto-Sync? Wo landet es? Aufwand
Wallet-Pass (Apple/Google) Ja, over-the-air Wallet-App (nicht Kontakte) hoch (Zertifikate)
Link behalten (QR/NFC/Kurz-URL) Daten aktuell beim Öffnen, vCard wird live generiert Browser → Kontakt bei Bedarf neu umgesetzt (Phase 2)
CardDAV Ja, auf Kontakt-Ebene native Kontakte-App mittel, für Einzel-Empfänger unpraktisch (account-basiert)

Fazit: Den einzigen echten „Push in eine bereits gespeicherte Karte" liefert der Wallet-Pass. Der Link (unsere Profilseite) ist der pragmatische Standard: nicht die Daten, sondern der Link wird gespeichert; die vCard erzeugen wir bei jedem Abruf frisch.

Apple Wallet (PassKit)

  • .pkpass = ZIP aus pass.json + Bildern + manifest.json + PKCS#7-Signatur (Pass Type ID-Zertifikat + Apple-WWDR-Cert).
  • Pass-Stil generic (Visitenkarte): Name / Position / Firma / Telefon / E-Mail / Link.
  • PHP: z. B. includable/php-pkpass, gekapselt in einem WalletService.
  • Sync (Push): Pass enthält webServiceURL + authenticationToken.
    1. Nutzer fügt Pass hinzu → Gerät registriert sich (→ WalletDevice).
    2. Profil ändert sich → leerer APNs-Push an registrierte Geräte.
    3. Gerät holt aktualisierten Pass von unserer PassKit-Web-Service-API ab.
  • Nötige Endpunkte (PassKit Web Service): register, unregister, list serials, latest pass.

Google Wallet

  • REST-API + Service-Account (kostenlos). Generic-Pass-Klasse + pro Mitarbeiter ein Objekt.
  • Hinzufügen via „Add to Google Wallet"-Link (signiertes JWT).
  • Sync: Objekt server-seitig per API patchen → Google pusht selbst. Kein APNs, keine Geräte-Registrierung. → einfacher als Apple, daher als erster Wallet-Kanal empfohlen.

Einbettung in die Architektur

Employee geändert ──► ProfileUpdated-Event ──► Messenger (async)
   ├─ Apple:  Pass neu signieren  +  APNs-Push an WalletDevices
   └─ Google: Wallet-Objekt per API patchen (Google pusht selbst)

Neue Bausteine: WalletPass/WalletDevice (Datenmodell §4), WalletService (Pass bauen/signieren/patchen), ApplePassController (Web-Service-Endpunkte), Messenger-Handler am ProfileUpdated-Event (gleicher Trigger wie Employee::touch()).

White-Label-Überlegung

Apple Pass Type ID ist an einen Apple-Account gebunden. Optionen: (a) ein zentraler Pass Type ID der Plattform für alle Reseller (einfacher, Branding über Pass-Felder/Logo) oder (b) pro Reseller ein eigener (aufwändig, jeder Reseller braucht Apple-Account). Empfehlung: (a) zentral, Reseller-/Firmen-Branding über Logo & Farben im Pass. → zu klären (offene Frage #4).

Voraussetzungen (offen)

  • Apple Developer Account + Pass Type ID + Zertifikat (.p12) — für Apple Wallet.
  • Google-Cloud-Projekt + Wallet-API + Service-Account — für Google Wallet.

13. Produktkatalog (mehrere Produkttypen)

Die Plattform verkauft nicht nur Visitenkarten, sondern mehrere Produkttypen. Ein Produkt ist der Katalogeintrag, den eine Firma als druck-/ausrollbares Erzeugnis bestellt und gestaltet. Start mit drei Typen: Visitenkarte, Namensschild, NFC-Karte.

Eigentümer & Sichtbarkeit

Produkte definieren ausschließlich:

Definiert von reseller Sichtbar für
Plattform-Betreiber null (global) alle Reseller + deren Firmen
Reseller eigener Reseller nur eigener Mandant (Reseller + dessen Firmen)

Firmen(-Admins) wählen aus den für sie sichtbaren Produkten (globale + die des eigenen Resellers), legen aber keine Produkte an. Bearbeiten/Löschen darf jeweils nur der Eigentümer (Plattform globale, Reseller die eigenen). Globale Produkte sind für Reseller read-only.

kind (Produktart) — legt Fähigkeiten & Defaults fest

kind Standardformat Seiten Druck NFC
business_card (Visitenkarte) 85 × 55 mm, 2 mm Bleed V/R optional
name_tag (Namensschild) 90 × 55 mm, 0 mm Bleed nur V
nfc_card (NFC-Karte) 85,6 × 54 mm (ID-1), 2 mm Bleed V/R

kind ist ein festes Enum (Fähigkeiten = Druck-PDF und/oder NFC-Programmierung). Sowohl Plattform als auch Reseller legen Produkte dieser Arten an (z. B. ein Reseller-eigenes „Premium-Visitenkarte 90×50").

Datenmodell

Product (ResellerOwnedInterface): reseller_id (nullable = global), kind, name, description, widthMm, heightMm, bleedMm, safeMm, sides (1|2), nfcEnabled, printEnabled, active, sortOrder, createdAt.

CardTemplate referenziert künftig ein product_id: das konkrete Design einer Firma für ein Produkt (eine Firma kann je Produkt ein Design haben). Format (Maße/Bleed/Seiten) wird vom Produkt geerbt; der Renderer bleibt formatagnostisch.

Endpunkte

  • GET /api/products — sichtbare Produkte (global + eigener Reseller), API-Platform-scoped.
  • POST/PATCH/DELETE /api/products/{id} — nur Eigentümer (Voter).
  • Editor: GET/PUT /api/companies/{id}/card-template?product={productId} (Design je Produkt).

Renderer/NFC je Produktart

  • business_card / nfc_card / name_tag nutzen denselben CardPdfRenderer (Maße + Elementliste aus Produkt + Design). Beschnitt/Schnittmarken nur wenn bleedMm > 0.
  • nfc_card: zusätzlich NFC-Programmierung über die bestehende shortCode//t/-Infra (Tag schreibt die Kurz-URL des Profils) — Detail in §12/§14.

Bestellungen (PrintOrder)

Firmen-Admins bestellen Produkte für ihre Mitarbeiter; der Reseller (Druckshop) wickelt ab (produziert, versendet). Beispiel: 10 Visitenkarten + 5 NFC-Karten für verschiedene Mitarbeiter.

Datenmodell:

  • PrintOrder (ResellerOwnedInterface via company.reseller): number (kurze Bestellnr.), company, status, note, createdBy (Employee), createdAt, items[].
  • OrderItem: product, employee (für wen — personalisiert), quantity (Auflage).

Status-Workflow: newin_productionshippedcompleted; cancelled quer.

  • Firmen-Admin: legt Bestellung an (new), kann sie stornieren solange new.
  • Reseller-/Plattform-Admin: schiebt den Status vorwärts (Produktion/Versand/erledigt), kann jederzeit stornieren.

Druckdaten: je Position liefert das bestehende GET /api/employees/{id}/card.pdf?product={productId} das druckfertige PDF (Mitarbeiter × Produkt). Sammel-/Bogen-PDF später.

Endpunkte (OrderController, mandantengeprüft):

  • GET /api/orders — Liste (Firma: eigene; Reseller: alle seiner Firmen; Plattform: alle).
  • GET /api/orders/{id} — Detail inkl. Positionen + PDF-Links.
  • POST /api/orders — anlegen (Firmen-Admin; items[] = Produkt+Mitarbeiter+Menge).
  • PATCH /api/orders/{id}/status — Status setzen (Reseller wickelt ab / Firma storniert).

Sichtbarkeit: Produkt muss im Mandanten sichtbar sein (global oder eigener Reseller), Mitarbeiter muss zur bestellenden Firma gehören. Preise/Abrechnung = spätere Ausbaustufe.


14. Druckdaten: Visitenkarten-PDF (Kerngeschäft)

Reseller drucken für ihre Firmenkunden Visitenkarten und brauchen druckfertige PDFs. Das Layout variiert pro Firma. Später kommt Briefpapier (gleiches System, anderes Format) dazu.

Festgelegte Entscheidungen (2026-05-31)

  • Farbraum/Qualität: CMYK, Ziel PDF/X-1a, eingebettete Fonts.
  • Format: 85×55 mm, 2 mm Beschnitt (→ 89×59 mm Dokument), Schnittmarken, Sicherheitsabstand ~4 mm.
  • Seiten: Vorder- und Rückseite (2 PDF-Seiten).
  • Ausgabe: ein PDF pro Mitarbeiter (Druckbogen/Ausschießen später).
  • Layout-Definition: visueller Editor im Dashboard → strukturierte Element-Definition (JSON), nicht HTML.

Datenmodell

CardTemplate — gehört zu einer Company (company = null ⇒ globale Standardvorlage). Felder: name, widthMm (85), heightMm (55), bleedMm (2), safeMm (4), front (Element[]), back (Element[]), type (card | später letterhead), company_id.

Element (JSON): type (field | text | image | qr | rect | line), xMm, yMm, wMm, hMm, rotation, Typografie (fontFamily, fontSizePt, bold, italic, align, lineHeight), color (CMYK {c,m,y,k} 0100), fill (CMYK), binding (bei field: Mitarbeiter-Feld), text (bei text: statisch), src (bei image).

Bindings (Datenquelle = Mitarbeiterprofil): firstName, lastName, fullName, position, department, email, phone, mobile, company.name, profileUrl, shortUrl; Spezial: qr (Kurz-URL), logo (Firmen-Logo aus brandingConfig).

Rendering-Pipeline

CardTemplate (JSON) + Employee + Branding
        │
        ▼  TCPDF (Koordinaten in mm, CMYK, Fonts eingebettet)
   89×59 mm Seite (Trim 85×55 + 2 mm Bleed), Schnittmarken, V/R = 2 Seiten
        │
        ▼  (Finishing) Ghostscript → PDF/X-1a:2001 mit CMYK-Output-Intent (ICC, z. B. ISO Coated v2)
   druckfertiges PDF
  • TCPDF: exakte mm-Platzierung pro Element, setColor/setTextColor mit CMYK, cropMark() an den Trim-Ecken, Bilder/QR als eingebettete Grafik, Fonts eingebettet.
  • PDF/X-1a-Finishing (Ghostscript) als nachgelagerter Schritt — braucht ghostscript
    • ICC-Profil im Container. Bis dahin: valides CMYK-PDF mit Bleed + Schnittmarken.

Endpunkte

  • GET /api/employees/{id}/card.pdf — Einzelkarte (auth, Reseller/Firma; mandantengeprüft).
  • später: Sammel-PDF / ausgeschossener Bogen je Firma; Anbindung an PrintOrder.

Visueller Editor (SPA)

  • Canvas im mm-Maßstab mit sichtbarem Beschnitt-, Trim- und Sicherheitsrahmen.
  • Elemente per Drag&Drop; Eigenschaften-Panel (Position/Größe/Font/Farbe/Datenbindung).
  • Tabs Vorder-/Rückseite; Live-Vorschau mit echten oder Beispiel-Daten.
  • Speichern als CardTemplate (pro Firma); Default-Vorlage als Startpunkt.

Briefpapier (später)

Gleiches CardTemplate-Modell mit type = letterhead und Format A4; der Renderer ist format-agnostisch (Maße + Elementliste). Bindings identisch + ggf. Adressfeld/Faltmarken.

Hintergrund-PDF (Variable Data Printing) & eigene Schriften

Hintergrund-PDF: Der Kunde gestaltet die Karte vollständig in seinem DTP-Programm (InDesign/Illustrator: Logo, Farbflächen, Hintergrund, statische Texte) und exportiert ein druckfertiges PDF (CMYK, mit Beschnitt). Dieses wird hochgeladen und dient als Hintergrund; die Plattform legt darüber nur noch die dynamischen Felder (Name, Position, Kontakt, QR). Klassisches Variable Data Printing.

  • Technik: FPDI (setasign/fpdi) importiert die Seiten des Kunden-PDFs; der Renderer (FPDI-Tcpdf) platziert sie als Seitenhintergrund (useTemplate) und zeichnet die dynamischen Elemente darüber.
  • Erwartung: Hintergrund-PDF hat die Maße Endformat + Beschnitt (width+2·bleed × height+2·bleed). Seite 1 = Vorderseite, optional Seite 2 = Rückseite.
  • Bei Hintergrund-PDF: keine eigenen Schnittmarken/Beschnitt-Logik (der Kunde liefert das); Element-Koordinaten weiterhin im Trim-Raum, Offset = bleedMm.
  • Die Vorlage enthält dann nur noch dynamische Elemente (Felder/QR), die statischen entfallen.

Eigene Schriften: TTF/OTF werden hochgeladen und per TCPDF_FONTS::addTTFfont() eingebettet (Subset). CardTemplate führt eine Schriften-Liste (family → Pfad); Elemente referenzieren eine fontFamily (Custom oder Core helvetica/times/courier). Font-Embedding ist zugleich Voraussetzung für striktes PDF/X.

Uploads/Datenmodell: CardTemplate.backgroundPath (Pfad zum Kunden-PDF, nullable), CardTemplate.fonts ([{family, path}]). Dateien liegen außerhalb des Webroots unter var/storage/cards/{companyId}/. Upload-Endpunkte: POST /api/companies/{id}/card-template/background (PDF) und .../font (TTF/OTF).


15. Zeiterfassung (Modul „Kommen/Gehen")

Erweiterung über die digitale Visitenkarte hinaus: eine Arbeitszeiterfassung für die Firmenkunden der Reseller (vorzugsweise Druckshops). Der Mitarbeiter ist die gemeinsame Klammer — dieselbe Identität (Employee), dieselben Standorte, dieselbe NFC/QR-Infrastruktur. Positioniert die Plattform vom „Identity/Print-Tool" hin zur Mitarbeiter-Plattform (ein Datensatz, mehrere Module).

Markttreiber

EuGH 2019 + BAG 2022 → Pflicht zur Arbeitszeiterfassung in DE. SMB-Kunden suchen einfache Lösungen → starkes Verkaufsargument; NFC-Terminals/Badges sind zusätzlicher Hardware-Umsatz für den Druckshop.

Erfassungswege

  • Web/App (authentifizierter Mitarbeiter über sein User-Konto): Button „Kommen / Gehen / Pause" im Dashboard bzw. Handy-Browser. Quelle web/app.
  • Kiosk (geteiltes Standort-Gerät, per Geräte-Token): Mitarbeiter identifiziert sich per NFC-Tap (shortCode) oder PIN. Quelle kiosk.
  • NFC/QR nutzt die bestehende shortCode//t/-Infrastruktur. Hinweis: da der Profil-shortCode halb-öffentlich ist, am Kiosk zusätzlich PIN empfehlen.

Datenmodell

TimeEntry — append-only Stempel-Ereignis (revisionssicher, kein Update/Delete): employee, type (clock_in|clock_out|break_start|break_end), occurredAt (Stempelzeit), recordedAt (Speicherzeit, immutable), source (web|app|kiosk|nfc|qr|manual), location (nullable), createdBy (User), note, status (active|corrected|voided), correctsEntry (self-ref, nullable), reason (bei Korrektur/Storno).

KioskDevice — registriertes Standort-Terminal: name, token, location, status, lastSeenAt.

Employee-Ergänzung: clockPin (optional, für Kiosk/App-Identifikation).

Korrekturprinzip: Originale bleiben erhalten; eine Korrektur erzeugt einen neuen TimeEntry (type=correction-Bezug via correctsEntry) mit Grund + Bearbeiter, der alte wird als corrected markiert. Der „gültige" Stundenzettel wird aus den active-Einträgen berechnet. Jede Markierung ist protokolliert → nachvollziehbar/manipulationssicher.

Endpunkte (grob)

  • POST /api/time-clock/punch — eingeloggter Mitarbeiter stempelt (Typ ermittelt oder explizit), Quelle web/app.
  • POST /api/kiosk/punch — Geräte-Token + Mitarbeiter-Kennung (NFC-shortCode/PIN).
  • GET /api/time-entries — mandantengescoped (Mitarbeiter: eigene; Firmen-Admin: alle).
  • GET /api/timesheets?employee=&period= — Aggregation (Tag/Woche, Überstunden, je Standort).
  • POST /api/time-entries/{id}/correct — Firmen-Admin, hängt Korrektur an (Audit).

Rollen

  • Mitarbeiter: stempelt, sieht eigene Zeiten (Transparenzpflicht).
  • Firmen-Admin: Stundenzettel, Korrekturen (mit Grund/Audit), Export, Kiosk-Geräte.
  • Reseller/Plattform: white-label Einstellungen, Limits.

Compliance (bewusst zu behandeln)

  • Revisionssicherheit: append-only + protokollierte Korrekturen (s. o.).
  • ArbZG: Pausen/Höchstarbeits-/Ruhezeiten erfassbar bzw. warnend.
  • DSGVO + BetrVG §87 (Mitbestimmung): kein heimliches Tracking, Einsicht für Mitarbeiter, Standort EU, Lösch-/Exportkonzept.

Auswertung & Export

Stundenzettel (Tag/Woche/Monat), Überstunden, Pausensummen je Mitarbeiter/Standort; Export CSV/PDF — später Lohn-Schnittstelle (z. B. DATEV).

MVP-Abgrenzung

Drin: Kommen/Gehen/Pause über Web/App/Kiosk (NFC/PIN), Stundenzettel, revisionssichere Korrektur, CSV/PDF-Export. Draußen (Folgeschritte): Urlaub/ Krankmeldung, Schicht-/Dienstplanung, Projektzeiten, DATEV-Export, Geofencing.