cms.c2sgmbh/src/collections/Services.ts
Martin Porwoll 8868a5be30 feat: add Services collection and block
- Add ServiceCategories collection for grouping services
- Add Services collection with comprehensive service profiles:
  - Title, slug, descriptions (short + full)
  - Icon (text or image) and image gallery
  - Category relationship for grouping
  - Features/benefits array
  - Flexible pricing (on-request default, fixed, hourly, range, etc.)
  - CTA buttons (primary + secondary)
  - Related services, team members, and FAQs relationships
  - Detail page sections with testimonials
  - SEO fields (meta title, description, OG image)
  - Status flags (active, featured, new badge)
- Add ServicesBlock with 8 layouts:
  - Grid, List, Tabs, Accordion, Featured+Grid, Slider, Compact, Masonry
- Multi-tenant enabled via plugin configuration
- Update documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 07:39:03 +00:00

488 lines
12 KiB
TypeScript

// 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',
},
},
],
}