- WalletService (dependency-frei): Google signierter RS256-„Save"-JWT-Link;
Apple .pkpass (pass.json + GD-Icons + manifest + PKCS#7 via openssl + zip).
Konfigurationsgesteuert (env), ohne Zugangsdaten deaktiviert.
- WalletController: /w/{code} Landing (Geräteerkennung + Buttons),
/w/{code}/qr.png, /apple.pkpass, /google (302). Adressierung via shortCode.
- Öffentliche Profilseite: QR-Bereich „Zur Wallet hinzufügen" (nur wenn
Provider konfiguriert + shortCode vorhanden).
- .env Wallet-Block (leer=aus), KONZEPT §12 + deploy/README dokumentiert.
Verifiziert: not-configured → ausgeblendet/404; mit Test-Zertifikaten valides
signiertes .pkpass + Google-Save-JWT. Produktiv: echte Apple-/Google-Creds nötig.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
34 KiB
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
- Single Source of Truth: Eine Änderung am Mitarbeiterprofil propagiert in alle Kanäle (vCard, Wallet, Landingpage, NFC, QR, Druckdaten, E-Mail-Signatur).
- Mandantenfähigkeit (Multi-Tenancy): Strikte Datentrennung pro Reseller und pro Firmenkunde.
- White-Label: Jeder Reseller / Firmenkunde tritt unter eigener Domain & eigenem Branding auf.
- Wiederkehrender Umsatz: Reseller zahlt Plattformgebühr (Paket), Firmenkunde zahlt Reseller (eigene Preisgestaltung).
- Ö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_idje 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 mitreseller_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 inDomain. - 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 / PricePlan— zurü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}oderhttps://{companyDomain}/{employeeSlug}→ LandingpageGET /{slug}/vcard.vcf→ vCard-DownloadGET /{slug}/wallet→ Wallet-PassGET /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 automatischreseller_id/company_idin alle Queries injiziert. Kein Datenleck zwischen Mandanten. - Voters:
CompanyVoter,EmployeeVoter,TemplateVoterfü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.
- Plattform → Reseller (umgesetzt): Reseller wählt
PlatformPlan(Starter 99 € / Professional 249 € / Business 599 €). Stripe-Abo, Limits (maxProfiles,maxCompanies) werden durchgesetzt. - 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". - 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)
- 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. - 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. - Billing: Nur Plattform → Reseller (siehe §8). Reseller↔Company außerhalb der Plattform; spätere Erweiterung möglich.
Noch offen
- 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.
- Druckdaten: Welches Format erwarten die Druckereien (PDF/X, bestimmte Maße, Beschnitt)? Gibt es Vorlagen?
- 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):
- Firmen-Admin trägt seine Domain (
visitenkarte.firma.de) im Dashboard ein → Statuspending. - Plattform zeigt die einzutragende IP (A-Record) (+ optional CNAME) an.
- Kunde setzt den DNS-Eintrag bei seinem Provider.
- Plattform prüft periodisch (DNS-Lookup) → bei Treffer Status
verified, dann automatisches TLS-Zertifikat (Let's Encrypt / ACME). - 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 auspass.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 einemWalletService. - Sync (Push): Pass enthält
webServiceURL+authenticationToken.- Nutzer fügt Pass hinzu → Gerät registriert sich (→
WalletDevice). - Profil ändert sich → leerer APNs-Push an registrierte Geräte.
- Gerät holt aktualisierten Pass von unserer PassKit-Web-Service-API ab.
- Nutzer fügt Pass hinzu → Gerät registriert sich (→
- 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.
Umsetzung (implementiert)
QR-Code auf der öffentlichen Profilseite („Zur Wallet hinzufügen") → Landing
/w/{shortCode} mit Geräteerkennung und Apple-/Google-Buttons. Beides
konfigurationsgesteuert (env); ohne Zugangsdaten ausgeblendet/deaktiviert.
WalletService(dependency-frei): Google = signierter RS256-„Save"-JWT-Link (pay.google.com/gp/v/save/{jwt}, fat genericClasses+Objects); Apple =.pkpass(pass.json + GD-Icons + manifest + PKCS#7-Signatur viaopenssl_pkcs7_sign, gezippt).WalletController:GET /w/{code}(Landing),/w/{code}/qr.png,/w/{code}/apple.pkpass,/w/{code}/google(302). Adressierung übershortCode.- Konfig (env): Apple =
APPLE_WALLET_PASS_TYPE_ID,_TEAM_ID,_CERT_PATH,_KEY_PATH,_KEY_PASSWORD,_WWDR_PATH,_ORG_NAME; Google =GOOGLE_WALLET_ISSUER_ID,GOOGLE_WALLET_SERVICE_ACCOUNT(Pfad zur service-account.json),GOOGLE_WALLET_CLASS_SUFFIX. - Offen (Sync/Push): Apple PassKit-Web-Service (
register/unregister/latest)- APNs +
WalletDevice; Google Objekt-patch. Bisher nur Pass-Erstellung.
- APNs +
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_tagnutzen denselbenCardPdfRenderer(Maße + Elementliste aus Produkt + Design). Beschnitt/Schnittmarken nur wennbleedMm > 0.nfc_card: zusätzlich NFC-Programmierung über die bestehendeshortCode//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(ResellerOwnedInterfaceviacompany.reseller):number(kurze Bestellnr.),company,status,note,createdBy(Employee),createdAt,items[].OrderItem:product,employee(für wen — personalisiert),quantity(Auflage).
Status-Workflow: new → in_production → shipped → completed; cancelled quer.
- Firmen-Admin: legt Bestellung an (
new), kann sie stornieren solangenew. - 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} 0–100), 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/setTextColormit 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. Quelleweb/app. - Kiosk (geteiltes Standort-Gerät, per Geräte-Token): Mitarbeiter identifiziert
sich per NFC-Tap (
shortCode) oder PIN. Quellekiosk. - NFC/QR nutzt die bestehende
shortCode//t/-Infrastruktur. Hinweis: da der Profil-shortCodehalb-ö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.