diff --git a/README.md b/README.md index 761247a..6922254 100644 --- a/README.md +++ b/README.md @@ -103,5 +103,11 @@ reseller@demo.de / reseller). `GET /api/employees/{id}/card.pdf`. Layout via `CardTemplate` (Standardvorlage greift Firmen-Branding + QR ab). Siehe `docs/KONZEPT.md` §13. -Nächster Schritt: visueller Karten-Editor, PDF/X-1a-Finishing (Ghostscript), -dann Wallet-Pässe (§12). Siehe `docs/KONZEPT.md` §9. +**Visueller Karten-Editor** (SPA, Menü „Visitenkarten"): Canvas im mm-Maßstab +mit Beschnitt/Endformat/Sicherheits-Markierung, Elemente per Drag&Drop +(Feld/Text/QR/Logo/Fläche/Linie), Eigenschaften-Panel (Position/Größe/Schrift/ +Farbe/Datenbindung), Vorder-/Rückseite, Live-Vorschau mit echten Daten, +Speichern + PDF-Vorschau. Backend: `GET|PUT /api/companies/{id}/card-template`. + +Nächster Schritt: PDF/X-1a-Finishing (Ghostscript) + Font-Embedding, +Sammel-PDF/Druckbogen, dann Wallet-Pässe (§12). Siehe `docs/KONZEPT.md` §9. diff --git a/backend/src/Controller/CardTemplateEditorController.php b/backend/src/Controller/CardTemplateEditorController.php new file mode 100644 index 0000000..c17558e --- /dev/null +++ b/backend/src/Controller/CardTemplateEditorController.php @@ -0,0 +1,100 @@ +company($id); + $template = $this->templates->findCardForCompany($company); + + return new JsonResponse($this->serialize($template ?? $this->factory->default(), null === $template)); + } + + #[Route('/api/companies/{id}/card-template', name: 'company_card_template_put', methods: ['PUT'])] + public function put(string $id, Request $request): JsonResponse + { + $company = $this->company($id); + $data = json_decode($request->getContent(), true) ?? []; + + $template = $this->templates->findCardForCompany($company) ?? (new CardTemplate())->setCompany($company); + $template + ->setName((string) ($data['name'] ?? 'Standard')) + ->setWidthMm((float) ($data['widthMm'] ?? 85)) + ->setHeightMm((float) ($data['heightMm'] ?? 55)) + ->setBleedMm((float) ($data['bleedMm'] ?? 2)) + ->setSafeMm((float) ($data['safeMm'] ?? 4)) + ->setFront(is_array($data['front'] ?? null) ? $data['front'] : []) + ->setBack(is_array($data['back'] ?? null) ? $data['back'] : []); + + $this->em->persist($template); + $this->em->flush(); + + return new JsonResponse($this->serialize($template, false)); + } + + private function company(string $id): Company + { + $company = $this->em->getRepository(Company::class)->find(Uuid::fromString($id)); + if (!$company instanceof Company) { + throw new NotFoundHttpException('Firma nicht gefunden.'); + } + if ($this->tenant->isPlatformAdmin()) { + return $company; + } + $reseller = $this->tenant->getReseller(); + if (null === $reseller || $company->getReseller()?->getId()->equals($reseller->getId()) !== true) { + throw new AccessDeniedHttpException('Firma gehört nicht zum eigenen Mandanten.'); + } + $own = $this->tenant->getCompany(); + if (null !== $own && !$company->getId()->equals($own->getId())) { + throw new AccessDeniedHttpException('Nur die eigene Firma.'); + } + + return $company; + } + + private function serialize(CardTemplate $t, bool $isDefault): array + { + return [ + 'id' => $isDefault ? null : (string) $t->getId(), + 'isDefault' => $isDefault, + 'name' => $t->getName(), + 'widthMm' => $t->getWidthMm(), + 'heightMm' => $t->getHeightMm(), + 'bleedMm' => $t->getBleedMm(), + 'safeMm' => $t->getSafeMm(), + 'front' => $t->getFront(), + 'back' => $t->getBack(), + ]; + } +} diff --git a/backend/src/Service/CardPdfRenderer.php b/backend/src/Service/CardPdfRenderer.php index 1ab1ac0..cb369d6 100644 --- a/backend/src/Service/CardPdfRenderer.php +++ b/backend/src/Service/CardPdfRenderer.php @@ -137,6 +137,9 @@ final class CardPdfRenderer return $this->hexToCmyk($hex); } + if (is_array($color) && isset($color['hex'])) { + return $this->hexToCmyk((string) $color['hex']); + } if (is_array($color) && isset($color['c'])) { return [(float) $color['c'], (float) $color['m'], (float) $color['y'], (float) $color['k']]; } diff --git a/frontend/src/layouts/DashboardLayout.vue b/frontend/src/layouts/DashboardLayout.vue index 5f530d7..68dfb52 100644 --- a/frontend/src/layouts/DashboardLayout.vue +++ b/frontend/src/layouts/DashboardLayout.vue @@ -13,6 +13,7 @@ const nav = computed(() => [ { label: 'Reseller', to: '/app/resellers', icon: 'M3 21h18M5 21V7l8-4v18M19 21V11l-6-3', show: auth.isPlatformAdmin }, { label: 'Firmen', to: '/app/companies', icon: 'M3 7h18v13H3zM8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', show: auth.isResellerAdmin || auth.isPlatformAdmin }, { label: 'Mitarbeiter', to: '/app/employees', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z', show: true }, + { label: 'Visitenkarten', to: '/app/card-editor', icon: 'M3 5h18v14H3zM3 10h18M7 15h5', show: true }, { label: 'Standorte', to: '/app/locations', icon: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0zM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z', show: auth.isCompanyAdmin || auth.isResellerAdmin }, { label: 'Domains', to: '/app/domains', icon: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM3 12h18M12 3a15 15 0 0 1 0 18 15 15 0 0 1 0-18z', show: auth.isCompanyAdmin || auth.isResellerAdmin }, { label: 'Design', to: '/app/design', icon: 'M12 2l7 7a7 7 0 1 1-14 0z', show: true }, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index fd4b343..53bfef5 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -19,6 +19,7 @@ const router = createRouter({ { path: 'resellers', name: 'resellers', component: () => import('@/views/ResellersView.vue') }, { path: 'companies', name: 'companies', component: () => import('@/views/CompaniesView.vue') }, { path: 'employees', name: 'employees', component: () => import('@/views/EmployeesView.vue') }, + { path: 'card-editor', name: 'card-editor', component: () => import('@/views/CardEditorView.vue') }, { path: 'locations', name: 'locations', component: () => import('@/views/LocationsView.vue') }, { path: 'domains', name: 'domains', component: () => import('@/views/DomainsView.vue') }, { path: 'design', name: 'design', component: () => import('@/views/DesignView.vue') }, diff --git a/frontend/src/views/CardEditorView.vue b/frontend/src/views/CardEditorView.vue new file mode 100644 index 0000000..04ac6ec --- /dev/null +++ b/frontend/src/views/CardEditorView.vue @@ -0,0 +1,335 @@ + + + + +