// src/collections/Services.ts import type { CollectionConfig } from 'payload' import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess' /** * Services Collection * * Dienstleistungen und Leistungsangebote mit Detail-Seiten. * Unterstützt Kategorisierung, Preisangaben und Verknüpfungen. * Tenant-scoped für Multi-Tenant-Betrieb. */ export const Services: CollectionConfig = { slug: 'services', admin: { useAsTitle: 'title', group: 'Content', defaultColumns: ['title', 'category', 'pricingType', 'order', 'isActive'], description: 'Leistungen und Dienstleistungen', }, access: { read: tenantScopedPublicRead, create: authenticatedOnly, update: authenticatedOnly, delete: authenticatedOnly, }, fields: [ // Basis-Informationen { name: 'title', type: 'text', required: true, label: 'Titel', localized: true, admin: { description: 'Name der Leistung (z.B. "Intensivpflege", "Beratungsgespräch")', }, }, { name: 'slug', type: 'text', required: true, label: 'Slug', unique: false, admin: { description: 'URL-Pfad für Detail-Seite (z.B. "intensivpflege")', }, }, { name: 'subtitle', type: 'text', label: 'Untertitel', localized: true, admin: { description: 'Optionaler Untertitel oder Slogan', }, }, { name: 'shortDescription', type: 'textarea', required: true, label: 'Kurzbeschreibung', localized: true, admin: { description: 'Kurze Beschreibung für Übersichtsseiten (1-2 Sätze)', }, }, { name: 'description', type: 'richText', label: 'Ausführliche Beschreibung', localized: true, admin: { description: 'Detaillierte Beschreibung für die Detail-Seite', }, }, // Medien { type: 'row', fields: [ { name: 'icon', type: 'text', label: 'Icon', admin: { width: '50%', description: 'Icon-Name (z.B. "heart", "shield", "clock")', }, }, { name: 'iconImage', type: 'upload', relationTo: 'media', label: 'Icon als Bild', admin: { width: '50%', description: 'Alternativ: Icon als hochgeladenes Bild', }, }, ], }, { name: 'image', type: 'upload', relationTo: 'media', label: 'Hauptbild', admin: { description: 'Bild für Karten und Header der Detail-Seite', }, }, { name: 'gallery', type: 'array', label: 'Bildergalerie', admin: { description: 'Zusätzliche Bilder für die Detail-Seite', initCollapsed: true, }, fields: [ { name: 'image', type: 'upload', relationTo: 'media', required: true, label: 'Bild', }, { name: 'caption', type: 'text', label: 'Bildunterschrift', localized: true, }, ], }, // Kategorie { name: 'category', type: 'relationship', // eslint-disable-next-line @typescript-eslint/no-explicit-any relationTo: 'service-categories' as any, label: 'Kategorie', admin: { description: 'Kategorie zur Gruppierung der Leistung', }, }, // Features/Leistungsmerkmale { name: 'features', type: 'array', label: 'Leistungsmerkmale', admin: { description: 'Vorteile und Merkmale dieser Leistung', }, fields: [ { name: 'title', type: 'text', required: true, label: 'Merkmal', localized: true, }, { name: 'description', type: 'textarea', label: 'Beschreibung', localized: true, }, { name: 'icon', type: 'text', label: 'Icon', admin: { description: 'Optionales Icon für das Merkmal', }, }, ], }, // Preisgestaltung { type: 'collapsible', label: 'Preisgestaltung', admin: { initCollapsed: false, }, fields: [ { name: 'pricingType', type: 'select', defaultValue: 'on-request', label: 'Preistyp', options: [ { label: 'Auf Anfrage', value: 'on-request' }, { label: 'Festpreis', value: 'fixed' }, { label: 'Ab-Preis', value: 'from' }, { label: 'Preisspanne', value: 'range' }, { label: 'Stundensatz', value: 'hourly' }, { label: 'Monatlich', value: 'monthly' }, { label: 'Kostenlos', value: 'free' }, ], }, { name: 'price', type: 'number', label: 'Preis (€)', admin: { condition: (data, siblingData) => ['fixed', 'from', 'hourly', 'monthly'].includes(siblingData?.pricingType), description: 'Preis in Euro', }, }, { name: 'priceMax', type: 'number', label: 'Preis bis (€)', admin: { condition: (data, siblingData) => siblingData?.pricingType === 'range', description: 'Maximaler Preis bei Preisspanne', }, }, { name: 'priceUnit', type: 'text', label: 'Preiseinheit', localized: true, admin: { condition: (data, siblingData) => ['fixed', 'from', 'range', 'hourly', 'monthly'].includes(siblingData?.pricingType), description: 'z.B. "pro Stunde", "pro Monat", "pro Behandlung"', }, }, { name: 'priceNote', type: 'text', label: 'Preishinweis', localized: true, admin: { description: 'Zusätzliche Info (z.B. "zzgl. MwSt.", "inkl. Anfahrt")', }, }, { name: 'pricingDetails', type: 'richText', label: 'Preisdetails', localized: true, admin: { description: 'Ausführliche Preisinformationen für Detail-Seite', }, }, ], }, // Call-to-Action { type: 'collapsible', label: 'Call-to-Action', admin: { initCollapsed: true, }, fields: [ { name: 'ctaText', type: 'text', defaultValue: 'Jetzt anfragen', label: 'Button-Text', localized: true, }, { name: 'ctaLink', type: 'text', defaultValue: '/kontakt', label: 'Button-Link', admin: { description: 'Ziel-URL oder Pfad (z.B. "/kontakt", "#formular")', }, }, { name: 'ctaStyle', type: 'select', defaultValue: 'primary', label: 'Button-Stil', options: [ { label: 'Primär', value: 'primary' }, { label: 'Sekundär', value: 'secondary' }, { label: 'Outline', value: 'outline' }, ], }, { name: 'secondaryCta', type: 'group', label: 'Sekundärer Button', fields: [ { name: 'enabled', type: 'checkbox', defaultValue: false, label: 'Aktivieren', }, { name: 'text', type: 'text', label: 'Text', localized: true, admin: { condition: (data, siblingData) => siblingData?.enabled, }, }, { name: 'link', type: 'text', label: 'Link', admin: { condition: (data, siblingData) => siblingData?.enabled, }, }, ], }, ], }, // Verknüpfungen { type: 'collapsible', label: 'Verknüpfungen', admin: { initCollapsed: true, }, fields: [ { name: 'relatedServices', type: 'relationship', // eslint-disable-next-line @typescript-eslint/no-explicit-any relationTo: 'services' as any, hasMany: true, label: 'Verwandte Leistungen', admin: { description: 'Andere Leistungen die thematisch passen', }, }, { name: 'teamMembers', type: 'relationship', // eslint-disable-next-line @typescript-eslint/no-explicit-any relationTo: 'team' as any, hasMany: true, label: 'Zuständige Team-Mitglieder', admin: { description: 'Ansprechpartner für diese Leistung', }, }, { name: 'faqs', type: 'relationship', // eslint-disable-next-line @typescript-eslint/no-explicit-any relationTo: 'faqs' as any, hasMany: true, label: 'Häufige Fragen', admin: { description: 'FAQs zu dieser Leistung', }, }, ], }, // Detail-Seiten-Inhalte { type: 'collapsible', label: 'Detail-Seite', admin: { initCollapsed: true, description: 'Zusätzliche Inhalte für die Detail-Seite', }, fields: [ { name: 'detailSections', type: 'array', label: 'Zusätzliche Abschnitte', fields: [ { name: 'title', type: 'text', required: true, label: 'Überschrift', localized: true, }, { name: 'content', type: 'richText', required: true, label: 'Inhalt', localized: true, }, { name: 'icon', type: 'text', label: 'Icon', }, ], }, { name: 'testimonialQuote', type: 'textarea', label: 'Kundenzitat', localized: true, admin: { description: 'Optionales Zitat zur Leistung', }, }, { name: 'testimonialAuthor', type: 'text', label: 'Zitat-Autor', }, ], }, // SEO { type: 'collapsible', label: 'SEO', admin: { initCollapsed: true, }, fields: [ { name: 'metaTitle', type: 'text', label: 'Meta-Titel', localized: true, admin: { description: 'Titel für Suchmaschinen (falls anders als Titel)', }, }, { name: 'metaDescription', type: 'textarea', label: 'Meta-Beschreibung', localized: true, admin: { description: 'Beschreibung für Suchmaschinen', }, }, { name: 'ogImage', type: 'upload', relationTo: 'media', label: 'Social Media Bild', }, ], }, // Status & Sortierung { name: 'isActive', type: 'checkbox', defaultValue: true, label: 'Aktiv/Sichtbar', admin: { position: 'sidebar', description: 'Inaktive Leistungen werden nicht angezeigt', }, }, { name: 'isFeatured', type: 'checkbox', defaultValue: false, label: 'Hervorgehoben', admin: { position: 'sidebar', description: 'Für Startseiten-Anzeige oder besondere Hervorhebung', }, }, { name: 'isNew', type: 'checkbox', defaultValue: false, label: 'Neu', admin: { position: 'sidebar', description: 'Zeigt "Neu"-Badge an', }, }, { name: 'order', type: 'number', defaultValue: 0, label: 'Sortierung', admin: { position: 'sidebar', description: 'Niedrigere Zahlen werden zuerst angezeigt', }, }, ], }