# Migration: Strapi zu Payload CMS ## Projekt: zweitmeinu.ng **Erstellt:** 2026-02-20 **Strapi Version:** 5.x **Ziel:** Payload CMS 3.x --- ## Inhaltsverzeichnis 1. [Executive Summary](#1-executive-summary) 2. [Content-Types / Collections](#2-content-types--collections) 3. [Components / Blocks](#3-components--blocks) 4. [API-Struktur](#4-api-struktur) 5. [Plugins & Extensions](#5-plugins--extensions) 6. [Datenbank & Relationen](#6-datenbank--relationen) 7. [Migrationsplan](#7-migrationsplan) 8. [Payload CMS Schema-Entwürfe](#8-payload-cms-schema-entwürfe) --- ## 1. Executive Summary ### Projektumfang | Kategorie | Anzahl | |-----------|--------| | Content-Types | 16 | | Components | 43 | | API-Module | 18 | | Plugins | 16 | | Custom Controller Methods | 14 | | Custom Services | 10 | | Relationen | 25+ | ### Komplexitätsbewertung: **MITTEL-HOCH** Hauptkomplexität liegt im: - Termin-Buchungssystem (Appointment) - Multi-Site-Konfiguration - Dynamic Zone Page Builder - Verschachtelte Component-Hierarchien --- ## 2. Content-Types / Collections ### 2.1 Übersicht aller Content-Types | # | Content-Type | Draft/Publish | Felder | Relationen | Komplexität | |---|--------------|---------------|--------|------------|-------------| | 1 | appointment | Nein | 26 | 3 | Hoch | | 2 | appointment-exception | Nein | 15 | 2 | Mittel | | 3 | appointment-slot | Nein | 16 | 3 | Mittel | | 4 | blog-category | Ja | 11 | 2 | Niedrig | | 5 | blog-post | Ja | 15 | 2 | Mittel | | 6 | contact-message | Nein | 13 | 1 | Niedrig | | 7 | expert | Ja | 16 | 2 | Mittel | | 8 | faq | Ja | 17 | 5 | Mittel | | 9 | faq-category | Nein | 8 | 2 | Niedrig | | 10 | faq-feedback | Nein | 15 | 3 | Niedrig | | 11 | legal-page | Ja | 7 | 0 | Niedrig | | 12 | news-item | Ja | 15 | 2 | Mittel | | 13 | page | Ja | 5 | 1 | Hoch (Dynamic Zone) | | 14 | service | Ja | 14 | 2 | Mittel | | 15 | site-configuration | Ja | 25 | 5 | Hoch | | 16 | waiting-list | Nein | 20 | 3 | Mittel | --- ### 2.2 Detaillierte Content-Type Definitionen #### APPOINTMENT (Terminbuchungen) ``` Felder: ├── appointmentId (UUID, unique) ├── patientName (string, required, max 100) ├── patientEmail (email, required) ├── patientPhone (string, required, max 30) ├── patientDateOfBirth (date) ├── patientInsurance (string, max 100) ├── appointmentDate (date, required) ├── appointmentTime (time, required) ├── duration (integer, 15-240, default 30) ├── appointmentType (enum: consultation, examination, treatment, followup, vaccination, other) ├── status (enum: requested, confirmed, cancelled, completed, no-show) ├── confirmationToken (string, unique, private) ├── cancellationToken (string, unique, private) ├── cancellationReason (text, max 500) ├── notes (text, max 1000) ├── internalNotes (text, private) ├── reminderSent (boolean, private) ├── confirmationSent (boolean, private) ├── patientConsent (boolean, required) ├── requestedAt (datetime, private) ├── confirmedAt (datetime, private) ├── cancelledAt (datetime, private) ├── completedAt (datetime, private) ├── ipAddress (string, private) └── userAgent (string, private) Relationen: ├── expert → Expert (manyToOne) ├── service → Service (manyToOne) └── site → Site-Configuration (manyToOne) ``` #### APPOINTMENT-EXCEPTION (Ausnahmen/Urlaub) ``` Felder: ├── type (enum: holiday, vacation, sick, conference, training, blocked, other) ├── startDate (datetime, required) ├── endDate (datetime, required) ├── allDay (boolean, default false) ├── reason (string, required, max 200) ├── description (text, max 500) ├── affectsAllSites (boolean, default false) ├── recurring (boolean, default false) ├── recurringPattern (enum: daily, weekly, monthly, yearly) ├── recurringEndDate (date) ├── isActive (boolean, default true) ├── notifyPatients (boolean, default true) └── color (string, hex format) Relationen: ├── expert → Expert (manyToOne, required) └── site → Site-Configuration (manyToOne) ``` #### APPOINTMENT-SLOT (Verfügbare Zeitslots) ``` Felder: ├── dayOfWeek (enum: Mo-So, required) ├── startTime (time, required) ├── endTime (time, required) ├── slotDuration (integer, 15-240, default 30) ├── maxAppointments (integer, 1-10, default 1) ├── isActive (boolean, default true) ├── validFrom (date) ├── validUntil (date) ├── breakAfter (integer, 0-60, default 0) ├── bufferBefore (integer, 0-60, default 0) ├── bufferAfter (integer, 0-60, default 0) ├── color (string, hex) └── notes (text, max 500) Relationen: ├── expert → Expert (manyToOne, required) ├── site → Site-Configuration (manyToOne, required) └── services ↔ Service (manyToMany) ``` #### BLOG-CATEGORY ``` Felder: ├── name (string, required, unique, max 100) ├── slug (uid, required, targetField: name) ├── description (text, max 500) ├── color (string, hex, default #0070f3) ├── icon (string, max 50) ├── order (integer, default 0) ├── isActive (boolean, default true) └── seo (component: shared.seo) Relationen: ├── blogPosts ← Blog-Post (oneToMany) ├── parent → Blog-Category (manyToOne, self-referential) └── children ← Blog-Category (oneToMany, self-referential) ``` #### BLOG-POST ``` Felder: ├── title (string, required, max 200) ├── slug (uid, required, targetField: title) ├── excerpt (text, required, max 300) ├── content (richtext, required) ├── featuredImage (media, images only) ├── author (component: blog.author) ├── tags (json) ├── publishedAt (datetime) ├── readingTime (integer, min 1) ├── featured (boolean, default false) ├── relatedPosts (json) └── seo (component: shared.seo) Relationen: ├── category → Blog-Category (manyToOne) └── sites ↔ Site-Configuration (manyToMany) ``` #### CONTACT-MESSAGE ``` Felder: ├── messageId (UUID, unique) ├── name (string, required, max 100) ├── email (email, required) ├── phone (string, max 30) ├── subject (string, required, max 200) ├── message (text, required, max 5000) ├── status (enum: new, read, replied, spam) ├── consentGiven (boolean, required) ├── submittedAt (datetime, private) ├── ipAddress (string, private) └── userAgent (string, private) Relationen: └── site → Site-Configuration (manyToOne) ``` #### EXPERT (Ärzte/Spezialisten) ``` Felder: ├── expertId (UUID, unique) ├── firstName (string, required) ├── lastName (string, required) ├── slug (uid, required, targetField: lastName) ├── title (string) ├── specialties (json) ├── bio (richtext) ├── image (media, images only) ├── qualifications (component: expert.qualification, repeatable) ├── availability (component: expert.availability) ├── contactInfo (json) ├── isActive (boolean, default true) ├── order (integer, default 0) └── seo (component: shared.seo) Relationen: ├── services ↔ Service (manyToMany) └── sites ↔ Site-Configuration (manyToMany) ``` #### FAQ ``` Felder: ├── question (string, required, max 300) ├── slug (uid, required, targetField: question) ├── answer (richtext, required) ├── shortAnswer (text, max 500) ├── tags (json) ├── priority (enum: low, medium, high, featured) ├── viewCount (integer, private) ├── helpfulCount (integer) ├── notHelpfulCount (integer) ├── attachments (media, multiple) ├── videoUrl (string) ├── searchKeywords (text, private) ├── lastUpdated (datetime) └── seo (component: shared.seo) Relationen: ├── category → FAQ-Category (manyToOne) ├── relatedFaqs ↔ FAQ (manyToMany, self-referential) ├── relatedServices ↔ Service (manyToMany) ├── relatedExperts ↔ Expert (manyToMany) └── sites ↔ Site-Configuration (manyToMany) ``` #### FAQ-CATEGORY ``` Felder: ├── name (string, required, unique, max 100) ├── slug (uid, required, targetField: name) ├── description (text, max 300) ├── icon (string, default "question-circle") ├── color (string, hex, default #3B82F6) └── order (integer, default 0) Relationen: ├── faqs ← FAQ (oneToMany) └── sites ↔ Site-Configuration (manyToMany) ``` #### FAQ-FEEDBACK ``` Felder: ├── isHelpful (boolean, required) ├── comment (text, max 1000) ├── additionalFeedback (enum: too_technical, not_detailed_enough, outdated, confusing, perfect, needs_examples) ├── userEmail (email) ├── ipAddress (string, private) ├── userAgent (string, private) ├── sessionId (string, private) ├── followUpRequested (boolean, default false) ├── resolved (boolean, default false) ├── resolvedAt (datetime) └── internalNotes (text, private) Relationen: ├── faq → FAQ (manyToOne, required) ├── site → Site-Configuration (manyToOne) └── resolvedBy → Admin-User (manyToOne) ``` #### LEGAL-PAGE ``` Felder: ├── type (enum: impressum, datenschutz, agb, cookie-policy, required) ├── content (richtext, required) ├── country (enum: global, de, at, ch, nl, es, uk) ├── language (enum: de, en, nl, es, fr, it) ├── validFrom (date) ├── validUntil (date) └── version (string) Relationen: Keine ``` #### NEWS-ITEM ``` Felder: ├── title (string, required, max 150) ├── slug (uid, required, targetField: title) ├── summary (text, required, max 250) ├── content (richtext, required) ├── featuredImage (media, images only) ├── newsType (enum: announcement, update, event, press_release, medical_news) ├── priority (enum: normal, important, urgent) ├── publishDate (datetime, required) ├── expiryDate (datetime) ├── author (component: blog.author) ├── tags (json) ├── attachments (media, multiple) ├── externalLink (string) └── seo (component: shared.seo) Relationen: ├── sites ↔ Site-Configuration (manyToMany) └── relatedServices ↔ Service (manyToMany) ``` #### PAGE (Dynamischer Page Builder) ``` Felder: ├── title (string, required) ├── slug (uid, required, targetField: title) ├── isSharedContent (boolean, default false) ├── sections (dynamiczone) ← KRITISCH FÜR MIGRATION └── seo (component: shared.seo) Dynamic Zone Components (11): ├── sections.hero ├── sections.text-block ├── sections.cta-banner ├── sections.services-grid ├── sections.team-grid ├── sections.testimonials ├── sections.expert-grid ├── sections.faq ├── sections.contact-form ├── sections.blog-list └── sections.faq-reference Relationen: └── sites ↔ Site-Configuration (manyToMany) ``` #### SERVICE (Medizinische Dienstleistungen) ``` Felder: ├── name (string, required) ├── slug (uid, required, targetField: name) ├── description (richtext, required) ├── shortDescription (text, max 300) ├── icon (media, images only) ├── featuredImage (media, images only) ├── price (component: service.pricing) ├── features (component: service.feature, repeatable) ├── duration (string) ├── category (enum: diagnostic, therapeutic, preventive, surgical, emergency, consultation) ├── isActive (boolean, default true) ├── order (integer, default 0) └── seo (component: shared.seo) Relationen: ├── sites ↔ Site-Configuration (manyToMany) └── experts ↔ Expert (manyToMany) ``` #### SITE-CONFIGURATION (Zentrale Domain-Konfiguration) ``` Felder: ├── siteIdentifier (string, required, unique) ├── domain (string, required) ├── siteName (string, required, max 100) ├── tagline (string, max 200) ├── logo (media, images only) ├── favicon (media, images only) ├── aliases (json) ├── brand (enum: complexcare, zweitmeinung, portal, required) ├── specialty (enum: general, intensiv, kardiologie, onkologie, chirurgie, radiologie, nephrologie, orthopaedie, polypharmazie, pflege) ├── locales (json, default: de-DE) ├── navigation (json) ├── footer (json) ├── theme (component: site.theme) ├── contact (component: site.contact) ├── features (json) ├── socialMedia (component: site.social-media) ├── analytics (component: site.analytics) ├── seo (component: shared.seo) ├── portalSettings (component: site.portal-settings) ├── maintenanceMode (component: site.maintenance-mode) ├── emailSettings (component: site.email-settings, required) └── appointmentSettings (component: appointment.appointment-settings) Relationen: ├── newsItems ↔ News-Item (manyToMany) ├── blogPosts ↔ Blog-Post (manyToMany) ├── services ↔ Service (manyToMany) ├── experts ↔ Expert (manyToMany) └── pages ↔ Page (manyToMany) ``` #### WAITING-LIST ``` Felder: ├── waitingListId (UUID, unique) ├── patientName (string, required, max 100) ├── patientEmail (email, required) ├── patientPhone (string, required, max 30) ├── preferredDates (json, default []) ├── preferredTimeOfDay (enum: morning, afternoon, evening, any) ├── flexibleDates (boolean, default false) ├── priority (enum: normal, urgent, emergency) ├── status (enum: waiting, contacted, appointed, expired, cancelled) ├── notes (text, max 1000) ├── internalNotes (text, private) ├── addedAt (datetime, private) ├── contactedAt (datetime, private) ├── appointedAt (datetime, private) ├── expiresAt (datetime) ├── contactAttempts (integer, private) ├── lastContactAttempt (datetime, private) ├── patientConsent (boolean, required) └── ipAddress (string, private) Relationen: ├── expert → Expert (manyToOne) ├── service → Service (manyToOne) └── site → Site-Configuration (manyToOne) ``` --- ## 3. Components / Blocks ### 3.1 Component-Kategorien Übersicht | Kategorie | Anzahl | Verwendungszweck | |-----------|--------|------------------| | admin | 1 | Analytics Dashboard | | appointment | 5 | Terminverwaltung-Einstellungen | | blog | 2 | Blog-Autoren & Kategorien | | elements | 1 | Wiederverwendbare UI-Elemente | | expert | 2 | Qualifikationen & Verfügbarkeit | | layout | 3 | Navigation & Footer | | sections | 11 | Page Builder Blöcke | | service | 2 | Preise & Features | | shared | 3 | SEO & Meta-Daten | | site | 12 | Site-Konfiguration | **Gesamt: 43 Components** --- ### 3.2 Detaillierte Component-Definitionen #### SHARED (Wiederverwendbar in mehreren Content-Types) **shared.seo** (Verwendet in 7 Content-Types) ``` ├── metaTitle (string, max 60) ├── metaDescription (text, max 160) ├── keywords (text) ├── canonicalURL (string) ├── structuredData (json) ├── metaImage (media, images only) ├── openGraph (component: shared.open-graph) └── metaSocial (component: shared.meta-social, repeatable) ``` **shared.open-graph** ``` ├── type (enum: website, article, profile, book, music, video) ├── title (string, max 95) ├── description (text, max 200) ├── image (media, images only) ├── url (string) ├── siteName (string, max 50) └── locale (string, default "de_DE") ``` **shared.meta-social** ``` ├── socialNetwork (enum: Facebook, Twitter, LinkedIn, required) ├── title (string, max 60) ├── description (text, max 160) └── image (media, images only) ``` --- #### ELEMENTS (Basis-Bausteine) **elements.button** (Verwendet in 7 Components) ``` ├── label (string, required) ├── url (string) ├── style (enum: primary, secondary, outline) ├── icon (string) └── openInNewTab (boolean, default false) ``` --- #### SECTIONS (Page Builder Blöcke) **sections.hero** ``` ├── title (string) ├── subtitle (text) ├── backgroundImage (media) ├── alignment (enum: left, center, right) └── buttons (component: elements.button, repeatable) ``` **sections.text-block** ``` ├── content (richtext, required) ├── layout (enum: full-width, centered, two-columns) └── backgroundColor (enum: white, light, dark, primary) ``` **sections.cta-banner** ``` ├── title (string, required) ├── description (text) ├── button (component: elements.button) └── backgroundStyle (enum: gradient, solid, image) ``` **sections.services-grid** ``` ├── title (string) ├── subtitle (text) ├── serviceIds (json) ├── displayMode (enum: all, selected, category) ├── category (enum: diagnostic, therapeutic, preventive, surgical, emergency, consultation) ├── columns (integer, 2-4) ├── showPrices (boolean) └── cta (component: elements.button) ``` **sections.team-grid** ``` ├── title (string) ├── subtitle (text) ├── members (component: sections.team-member, repeatable) └── columns (integer, 2-4) ``` **sections.team-member** ``` ├── name (string, required) ├── role (string, required) ├── bio (text) ├── image (media, images only) └── socialLinks (json) ``` **sections.testimonials** ``` ├── title (string) ├── subtitle (text) └── testimonials (component: sections.testimonial, repeatable) ``` **sections.testimonial** ``` ├── content (text, required) ├── author (string, required) ├── role (string) ├── company (string) ├── rating (integer, 1-5) └── image (media, images only) ``` **sections.expert-grid** ``` ├── title (string) ├── subtitle (text) ├── expertIds (json) ├── displayMode (enum: all, selected, specialty) ├── specialty (string) ├── columns (integer, 2-4) ├── showAvailability (boolean) └── cta (component: elements.button) ``` **sections.faq** ``` ├── title (string) ├── subtitle (text) └── questions (component: sections.faq-item, repeatable) ``` **sections.faq-item** ``` ├── question (string, required) └── answer (richtext, required) ``` **sections.faq-reference** (Referenziert zentrales FAQ-System) ``` ├── title (string) ├── subtitle (text) ├── displayMode (enum: accordion, list, cards, compact) ├── source (enum: selected, category, featured, popular) ├── selectedFaqs (json) ├── categorySlug (string) ├── limit (integer, 1-50) ├── showSearch (boolean) ├── showCategories (boolean) └── cta (component: elements.button) ``` **sections.blog-list** ``` ├── title (string) ├── subtitle (text) ├── contentType (enum: blog_posts, news_items, both) ├── displayMode (enum: grid, list, featured) ├── itemsToShow (integer, 1-12) ├── filterByCategory (string) ├── showPagination (boolean) └── cta (component: elements.button) ``` **sections.contact-form** ``` ├── title (string) ├── subtitle (text) ├── formFields (json) ├── submitButtonText (string) ├── successMessage (text) ├── recipientEmail (email) └── consentText (text) ``` --- #### SITE (Konfiguration) **site.theme** ``` ├── primaryColor (string, hex, default #2563eb) ├── secondaryColor (string, hex, default #10b981) ├── accentColor (string, hex, default #f59e0b) ├── darkColor (string, hex, default #1f2937) ├── backgroundColor (string, hex, default #f3f4f6) ├── fontFamily (string, default "Inter, sans-serif") └── customCSS (text) ``` **site.contact** ``` ├── email (email, required) ├── phone (string) ├── fax (string) ├── address (text) ├── emergencyHotline (string) └── openingHours (json) ``` **site.social-media** ``` ├── facebook (string, regex validated) ├── twitter (string, regex validated) ├── linkedin (string, regex validated) ├── instagram (string, regex validated) ├── youtube (string, regex validated) ├── xing (string, regex validated) ├── tiktok (string, regex validated) ├── whatsapp (string) └── telegram (string) ``` **site.analytics** ``` ├── googleAnalyticsId (string, regex: UA-* or G-*) ├── googleTagManagerId (string, regex: GTM-*) ├── facebookPixelId (string) ├── matomo (json) ├── hotjar (json) ├── clarity (string) ├── cookieConsent (json) └── customScripts (json) ``` **site.email-settings** ``` ├── fromEmail (email, required) ├── fromName (string, required) ├── replyToEmail (email, required) ├── notificationEmail (email, required) ├── ccEmails (json) ├── emailSignature (richtext) ├── emailFooter (text) ├── notificationSettings (json) └── emailTemplateStyle (json) ``` **site.maintenance-mode** ``` ├── enabled (boolean, required) ├── startDate (datetime) ├── endDate (datetime) ├── title (string) ├── message (richtext, required) ├── allowedIPs (json, private) ├── allowedRoles (json) ├── showProgressBar (boolean) ├── progressPercentage (integer, 0-100) ├── contactInfo (json) ├── customCSS (text) └── redirectUrl (string) ``` **site.portal-settings** ``` ├── isPortal (boolean, required) ├── aggregatedDomains (component: site.domain-reference, repeatable) ├── showDomainGrid (boolean) ├── showLiveStatistics (boolean) ├── showExpertCarousel (boolean) ├── enableGlobalSearch (boolean) ├── showTrustIndicators (boolean) ├── statisticsConfig (component: site.statistics-config) ├── quickAccessCategories (json) ├── featuredExperts (json) └── highlightedServices (json) ``` **site.domain-reference** ``` ├── domain (string, required) ├── siteIdentifier (string, required) ├── displayName (string) ├── category (enum) ├── order (integer) └── isActive (boolean) ``` **site.statistics-config** ``` ├── showTotalOpinions (boolean) ├── showActiveExperts (boolean) ├── showSatisfactionRate (boolean) ├── showResponseTime (boolean) ├── refreshInterval (integer) ├── animateNumbers (boolean) └── displayFormat (enum: compact, detailed, minimal) ``` --- #### APPOINTMENT (Terminverwaltung) **appointment.appointment-settings** ``` ├── bookingAdvanceDays (integer, 1-365) ├── minBookingHours (integer, 0-168) ├── maxBookingDays (integer, 1-365) ├── cancellationHours (integer, 0-168) ├── defaultDuration (integer, 15-240) ├── workingHours (component: appointment.working-hours) ├── bookingRules (json) ├── reminderHours (integer, 0-168) ├── allowWaitingList (boolean) ├── requireConfirmation (boolean) ├── requirePayment (boolean) ├── autoConfirmHours (integer) ├── maxAppointmentsPerDay (integer) ├── bufferBetweenAppointments (integer) ├── allowOnlineBooking (boolean) ├── showPrices (boolean) ├── termsAndConditions (richtext) ├── emailTemplates (component: appointment.email-templates) ├── smsEnabled (boolean) └── notificationChannels (json) ``` **appointment.working-hours** ``` ├── monday (component: appointment.day-schedule) ├── tuesday (component: appointment.day-schedule) ├── wednesday (component: appointment.day-schedule) ├── thursday (component: appointment.day-schedule) ├── friday (component: appointment.day-schedule) ├── saturday (component: appointment.day-schedule) └── sunday (component: appointment.day-schedule) ``` **appointment.day-schedule** ``` ├── isOpen (boolean) ├── openTime (time) ├── closeTime (time) └── breaks (component: appointment.break-time, repeatable) ``` **appointment.break-time** ``` ├── startTime (time, required) ├── endTime (time, required) └── description (string) ``` **appointment.email-templates** ``` ├── bookingConfirmation (richtext) ├── appointmentReminder (richtext) ├── cancellationConfirmation (richtext) ├── waitingListNotification (richtext) ├── noShowFollowUp (richtext) ├── appointmentRescheduled (richtext) ├── senderName (string) ├── senderEmail (email, required) └── replyToEmail (email) ``` --- #### EXPERT **expert.qualification** ``` ├── title (string, required) ├── institution (string) ├── year (integer) ├── description (text) └── type (enum: degree, certification, specialization, award, membership) ``` **expert.availability** ``` ├── schedule (json) ├── consultationTypes (json) ├── locations (json) ├── bookingUrl (string) ├── responseTime (string) └── languages (json) ``` --- #### BLOG **blog.author** ``` ├── name (string, required) ├── bio (text, max 500) ├── avatar (media, images only) ├── role (string) └── socialLinks (json) ``` --- #### SERVICE **service.pricing** ``` ├── basePrice (decimal, required) ├── currency (string, default EUR) ├── priceNote (text) ├── isStartingPrice (boolean) └── paymentOptions (json) ``` **service.feature** ``` ├── title (string, required) ├── description (text) └── icon (string) ``` --- ## 4. API-Struktur ### 4.1 API-Endpoints Übersicht | Modul | Custom Logic | Endpoints | Authentifizierung | |-------|--------------|-----------|-------------------| | appointment | Ja (14 Methods) | 13 | Nein (Token-basiert) | | appointment-exception | Nein | 5 | Nein | | appointment-slot | Nein | 5 | Nein | | blog-category | Ja (1 Method) | 6 | Nein | | blog-post | Ja (2 Methods) | 7 | Optional | | contact-message | Ja (4 Methods) | 8 | Nein | | expert | Nein | 5 | Optional | | faq | Nein | 5 | Nein | | faq-category | Nein | 5 | Nein | | faq-feedback | Nein | 5 | Nein | | legal-page | Nein | 5 | Optional | | news-item | Nein | 5 | Optional | | page | Nein | 5 | Optional | | service | Nein | 5 | Optional | | site-configuration | Nein | 5 | Optional | | test-email | Ja (2 Methods) | 2 | Nein | | waiting-list | Nein | 5 | Nein | --- ### 4.2 Custom API-Logik (Migration erforderlich) #### Appointment Controller (14 Custom Methods) ```typescript // Kritische Business Logic für Payload Migration 1. getAvailability(expert, service, startDate, endDate) - Berechnet verfügbare Zeitslots - Berücksichtigt: Slots, Exceptions, bestehende Termine - Filtert nach Buchungsfenster (24h-90d) 2. bookAppointment(patientData, appointmentDetails) - Validiert alle Felder - Prüft Slot-Verfügbarkeit - Generiert Confirmation/Cancellation Tokens - Sendet Bestätigungs-Email 3. confirmAppointment(token) - Validiert Token - Setzt Status auf 'confirmed' - Sendet Bestätigungs-Email 4. cancelAppointment(token, reason) - Prüft Stornierungsrichtlinie (24h) - Setzt Status auf 'cancelled' - Verarbeitet Warteliste - Benachrichtigt nächsten Patienten 5. rescheduleAppointment(appointmentId, newSlot) - Validiert neuen Slot - Aktualisiert Termin - Sendet Benachrichtigung 6. addToWaitingList(patientData, preferences) - Erstellt Wartelisten-Eintrag - Sendet Bestätigung 7. getCalendarView(expert, view, date) - Erstellt Kalender-Übersicht - Gruppiert nach Tag/Woche/Monat 8. getStatistics(period, expert) - Berechnet Statistiken - Status-Verteilung, No-Show-Rate, etc. 9. sendReminders() - Daily Job für morgige Termine - Setzt reminderSent Flag 10. exportAppointments(format, filters) - CSV-Export - Gefiltert nach Datum/Expert/Status 11-14. Email Services - sendConfirmationEmail() - sendCancellationEmail() - sendReminderEmail() - sendWaitingListNotification() ``` #### Contact-Message Controller (4 Custom Methods) ```typescript 1. create(data) - mit Spam-Schutz - Honeypot-Feld Prüfung - Minimum 3 Sekunden Ausfüllzeit - Email-Benachrichtigung an Site-Admin 2. markAsRead(id) - Setzt Status auf 'read' 3. markAsSpam(id) - Setzt Status auf 'spam' 4. stats() - Statistiken der letzten 30 Tage ``` #### Blog-Post Controller (2 Custom Methods) ```typescript 1. findRelated(postId) - Findet ähnliche Posts nach Kategorie 2. findByCategory(categorySlug, pagination) - Filtert Posts nach Kategorie-Slug ``` --- ### 4.3 Middleware **Contact Rate Limiting** (Optional) ```typescript // /src/middlewares/contact-rate-limit.ts { limit: 5, // Anfragen interval: 60, // Sekunden tracking: 'ip+path' } ``` --- ## 5. Plugins & Extensions ### 5.1 Installierte Plugins | Plugin | Version | Payload Äquivalent | |--------|---------|-------------------| | @strapi/plugin-documentation | 5.18.0 | Built-in API Docs | | @strapi/plugin-graphql | 5.18.0 | @payloadcms/plugin-graphql | | @strapi/plugin-users-permissions | 5.18.0 | Built-in Auth | | @strapi/plugin-seo | 2.0.8 | @payloadcms/plugin-seo | | @strapi/provider-email-nodemailer | 5.18.0 | Nodemailer via Hooks | | @_sh/strapi-plugin-ckeditor | 6.0.2 | Lexical Editor (built-in) | | strapi-plugin-navigation | 3.0.16 | @payloadcms/plugin-nested-docs | | strapi-plugin-superfields | 5.8.2 | Custom Field Types | | strapi-plugin-multi-select | 2.1.1 | Select Field (built-in) | | strapi-plugin-country-select | 2.1.0 | Custom Select | | strapi-plugin-bold-title-editor | 2.0.0 | Custom RichText Config | | strapi-5-sitemap-plugin | 1.0.7 | afterChange Hook | | strapi-cache | 1.5.5 | Redis Plugin | | strapi-advanced-uuid | 2.1.1 | Custom ID Field | | strapi-5-plugin-responsive-backend | 0.0.4 | Built-in Responsive UI | | @chartbrew/plugin-strapi | 3.0.0 | External Integration | ### 5.2 Plugin-Konfigurationen **CKEditor 5 Toolbar:** ``` Heading | Bold, Italic, Underline, Strikethrough | BulletedList, NumberedList | Blockquote | Table | Code, CodeBlock | Link | Image ``` **Navigation Plugin:** ```typescript { contentTypes: ["page", "service", "expert", "blog-post", "faq"], allowedLevels: 3 } ``` **Cache Plugin:** ```typescript { type: "mem", maxAge: 3600000, cacheRoutes: ["/api/services", "/api/experts", "/api/faq", "/api/blog-posts"] } ``` --- ## 6. Datenbank & Relationen ### 6.1 Relationen-Diagramm ``` ┌─────────────────────────────────────────────────────────────────┐ │ SITE-CONFIGURATION │ │ (Zentrale Konfiguration) │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ Expert │ │Service │ │ Page │ │BlogPost│ │NewsItem│ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │BlogCategory │ │ │ └─────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────┐ │ APPOINTMENT │ │ (Termin-Buchungen) │ └─────────────────────────────────────────┘ │ ├──────────────────┐ ▼ ▼ ┌───────────────┐ ┌─────────────────────┐ │Appointment- │ │ Appointment- │ │Exception │ │ Slot │ └───────────────┘ └─────────────────────┘ ┌─────────────────────────────────────────┐ │ FAQ SYSTEM │ └─────────────────────────────────────────┘ ┌────────────┐ ┌────────┐ ┌─────────────┐ │FAQ-Category│ ──► │ FAQ │ ◄── │FAQ-Feedback │ └────────────┘ └────────┘ └─────────────┘ │ ▼ ┌─────────────────────┐ │ Related: Service, │ │ Expert, FAQ │ └─────────────────────┘ ┌─────────────────────────────────────────┐ │ CONTACT & WAITING │ └─────────────────────────────────────────┘ ┌────────────────┐ ┌─────────────┐ │Contact-Message │ │Waiting-List │ │ → Site │ │→Expert,Svc │ └────────────────┘ └─────────────┘ ``` ### 6.2 Bidirektionale Relationen (ManyToMany) | Relation | Seite A | Seite B | |----------|---------|---------| | 1 | Site-Configuration | Expert | | 2 | Site-Configuration | Service | | 3 | Site-Configuration | Page | | 4 | Site-Configuration | Blog-Post | | 5 | Site-Configuration | News-Item | | 6 | Site-Configuration | FAQ | | 7 | Site-Configuration | FAQ-Category | | 8 | Expert | Service | | 9 | FAQ | FAQ (self-referential) | | 10 | FAQ | Service | | 11 | FAQ | Expert | | 12 | Blog-Category | Blog-Category (self-referential) | --- ## 7. Migrationsplan ### 7.1 Phase 1: Payload CMS Setup (Woche 1) ```bash # Installation npx create-payload-app@latest zweitmeinu-ng-payload cd zweitmeinu-ng-payload # Plugins npm install @payloadcms/plugin-graphql npm install @payloadcms/plugin-seo npm install @payloadcms/plugin-nested-docs npm install @payloadcms/plugin-form-builder npm install @payloadcms/richtext-lexical ``` ### 7.2 Phase 2: Collections erstellen (Woche 2-3) **Priorität 1 - Basis Collections:** 1. Users (Admin) 2. Media 3. Site-Configuration → Sites 4. Legal-Page → LegalPages **Priorität 2 - Content Collections:** 5. Expert → Experts 6. Service → Services 7. FAQ-Category → FAQCategories 8. FAQ → FAQs 9. Blog-Category → BlogCategories 10. Blog-Post → BlogPosts 11. News-Item → NewsItems **Priorität 3 - Page Builder:** 12. Page → Pages (mit Blocks) **Priorität 4 - Appointment System:** 13. Appointment → Appointments 14. Appointment-Slot → AppointmentSlots 15. Appointment-Exception → AppointmentExceptions 16. Waiting-List → WaitingList **Priorität 5 - Formulare:** 17. Contact-Message → ContactMessages 18. FAQ-Feedback → FAQFeedback ### 7.3 Phase 3: Blocks erstellen (Woche 3) ```typescript // /src/blocks/index.ts export const blocks = [ HeroBlock, TextBlock, CTABanner, ServicesGrid, TeamGrid, Testimonials, ExpertGrid, FAQSection, FAQReference, BlogList, ContactForm, ]; ``` ### 7.4 Phase 4: Custom Endpoints (Woche 4) ```typescript // /src/endpoints/appointments.ts export const appointmentEndpoints = { getAvailability, bookAppointment, confirmAppointment, cancelAppointment, rescheduleAppointment, getCalendarView, getStatistics, sendReminders, exportAppointments, }; ``` ### 7.5 Phase 5: Daten-Migration (Woche 5) ```typescript // Migration Script Struktur const migrationOrder = [ 'site-configuration', 'expert', 'service', 'faq-category', 'faq', 'blog-category', 'blog-post', 'news-item', 'legal-page', 'page', 'appointment-slot', 'appointment-exception', 'appointment', 'waiting-list', 'contact-message', 'faq-feedback', ]; ``` ### 7.6 Phase 6: Testing & Go-Live (Woche 6) 1. API-Kompatibilitätstests 2. Frontend-Integration 3. Performance-Tests 4. Staging-Deployment 5. Production-Migration --- ## 8. Payload CMS Schema-Entwürfe ### 8.1 Beispiel: Appointments Collection ```typescript // /src/collections/Appointments.ts import { CollectionConfig } from 'payload/types'; export const Appointments: CollectionConfig = { slug: 'appointments', admin: { useAsTitle: 'patientName', defaultColumns: ['patientName', 'appointmentDate', 'status', 'expert'], group: 'Termine', }, access: { read: () => true, create: () => true, update: ({ req: { user } }) => Boolean(user), delete: ({ req: { user } }) => Boolean(user), }, fields: [ { name: 'appointmentId', type: 'text', unique: true, admin: { readOnly: true }, hooks: { beforeChange: [({ value }) => value || crypto.randomUUID()], }, }, { type: 'row', fields: [ { name: 'patientName', type: 'text', required: true, maxLength: 100 }, { name: 'patientEmail', type: 'email', required: true }, { name: 'patientPhone', type: 'text', required: true, maxLength: 30 }, ], }, { type: 'row', fields: [ { name: 'appointmentDate', type: 'date', required: true }, { name: 'appointmentTime', type: 'text', required: true }, // HH:MM format { name: 'duration', type: 'number', min: 15, max: 240, defaultValue: 30 }, ], }, { name: 'appointmentType', type: 'select', required: true, options: [ { label: 'Beratung', value: 'consultation' }, { label: 'Untersuchung', value: 'examination' }, { label: 'Behandlung', value: 'treatment' }, { label: 'Nachsorge', value: 'followup' }, { label: 'Impfung', value: 'vaccination' }, { label: 'Sonstiges', value: 'other' }, ], }, { name: 'status', type: 'select', required: true, defaultValue: 'requested', options: [ { label: 'Angefragt', value: 'requested' }, { label: 'Bestätigt', value: 'confirmed' }, { label: 'Storniert', value: 'cancelled' }, { label: 'Abgeschlossen', value: 'completed' }, { label: 'Nicht erschienen', value: 'no-show' }, ], }, { name: 'expert', type: 'relationship', relationTo: 'experts', }, { name: 'service', type: 'relationship', relationTo: 'services', }, { name: 'site', type: 'relationship', relationTo: 'sites', }, { name: 'tokens', type: 'group', admin: { condition: () => false }, // Hidden in admin fields: [ { name: 'confirmationToken', type: 'text' }, { name: 'cancellationToken', type: 'text' }, ], }, { name: 'notes', type: 'textarea', maxLength: 1000, }, { name: 'internalNotes', type: 'textarea', admin: { condition: ({ req: { user } }) => Boolean(user) }, }, { name: 'patientConsent', type: 'checkbox', required: true, }, { name: 'timestamps', type: 'group', admin: { readOnly: true }, fields: [ { name: 'requestedAt', type: 'date' }, { name: 'confirmedAt', type: 'date' }, { name: 'cancelledAt', type: 'date' }, { name: 'completedAt', type: 'date' }, ], }, ], hooks: { beforeChange: [ async ({ data, operation }) => { if (operation === 'create') { data.timestamps = { requestedAt: new Date() }; data.tokens = { confirmationToken: crypto.randomUUID(), cancellationToken: crypto.randomUUID(), }; } return data; }, ], afterChange: [ async ({ doc, operation }) => { if (operation === 'create') { // Send confirmation email await sendAppointmentConfirmationEmail(doc); } }, ], }, }; ``` ### 8.2 Beispiel: Page Collection mit Blocks ```typescript // /src/collections/Pages.ts import { CollectionConfig } from 'payload/types'; import { blocks } from '../blocks'; export const Pages: CollectionConfig = { slug: 'pages', admin: { useAsTitle: 'title', group: 'Content', }, versions: { drafts: true, }, fields: [ { name: 'title', type: 'text', required: true, }, { name: 'slug', type: 'text', required: true, unique: true, admin: { position: 'sidebar', }, hooks: { beforeValidate: [({ value, data }) => value || data?.title?.toLowerCase().replace(/\s+/g, '-') ], }, }, { name: 'isSharedContent', type: 'checkbox', defaultValue: false, admin: { position: 'sidebar' }, }, { name: 'sections', type: 'blocks', blocks, }, { name: 'sites', type: 'relationship', relationTo: 'sites', hasMany: true, admin: { position: 'sidebar' }, }, { name: 'seo', type: 'group', fields: [ { name: 'metaTitle', type: 'text', maxLength: 60 }, { name: 'metaDescription', type: 'textarea', maxLength: 160 }, { name: 'metaImage', type: 'upload', relationTo: 'media' }, ], }, ], }; ``` ### 8.3 Beispiel: Hero Block ```typescript // /src/blocks/Hero.ts import { Block } from 'payload/types'; export const HeroBlock: Block = { slug: 'hero', labels: { singular: 'Hero', plural: 'Heroes', }, fields: [ { name: 'title', type: 'text', }, { name: 'subtitle', type: 'textarea', }, { name: 'backgroundImage', type: 'upload', relationTo: 'media', }, { name: 'alignment', type: 'select', defaultValue: 'center', options: [ { label: 'Links', value: 'left' }, { label: 'Zentriert', value: 'center' }, { label: 'Rechts', value: 'right' }, ], }, { name: 'buttons', type: 'array', fields: [ { name: 'label', type: 'text', required: true }, { name: 'url', type: 'text' }, { name: 'style', type: 'select', options: ['primary', 'secondary', 'outline'], }, { name: 'openInNewTab', type: 'checkbox' }, ], }, ], }; ``` --- ## Anhang: Checkliste für Migration ### Content-Types → Collections - [ ] appointment → Appointments - [ ] appointment-exception → AppointmentExceptions - [ ] appointment-slot → AppointmentSlots - [ ] blog-category → BlogCategories - [ ] blog-post → BlogPosts - [ ] contact-message → ContactMessages - [ ] expert → Experts - [ ] faq → FAQs - [ ] faq-category → FAQCategories - [ ] faq-feedback → FAQFeedback - [ ] legal-page → LegalPages - [ ] news-item → NewsItems - [ ] page → Pages - [ ] service → Services - [ ] site-configuration → Sites - [ ] waiting-list → WaitingList ### Components → Blocks/Fields - [ ] shared.seo → SEO Group Field - [ ] elements.button → Button Array Field - [ ] sections.hero → HeroBlock - [ ] sections.text-block → TextBlock - [ ] sections.cta-banner → CTABannerBlock - [ ] sections.services-grid → ServicesGridBlock - [ ] sections.team-grid → TeamGridBlock - [ ] sections.testimonials → TestimonialsBlock - [ ] sections.expert-grid → ExpertGridBlock - [ ] sections.faq → FAQBlock - [ ] sections.faq-reference → FAQReferenceBlock - [ ] sections.blog-list → BlogListBlock - [ ] sections.contact-form → ContactFormBlock - [ ] site.theme → ThemeGroup - [ ] site.contact → ContactGroup - [ ] site.social-media → SocialMediaGroup - [ ] site.analytics → AnalyticsGroup - [ ] site.email-settings → EmailSettingsGroup - [ ] appointment.appointment-settings → AppointmentSettingsGroup - [ ] appointment.working-hours → WorkingHoursGroup - [ ] appointment.email-templates → EmailTemplatesGroup ### Custom API Logic → Hooks/Endpoints - [ ] Appointment Availability Calculation - [ ] Appointment Booking Flow - [ ] Appointment Confirmation (Token-based) - [ ] Appointment Cancellation - [ ] Waiting List Processing - [ ] Email Notifications - [ ] Statistics Calculation - [ ] CSV Export - [ ] Contact Form Spam Protection - [ ] Blog Related Posts ### Plugins → Payload Äquivalente - [ ] GraphQL Plugin - [ ] SEO Plugin - [ ] Nested Docs (Navigation) - [ ] Rich Text Editor Configuration - [ ] Caching Strategy - [ ] Sitemap Generation --- *Dokument generiert am 2026-02-20*