cms.c2sgmbh/src/collections/Products.ts
Martin Porwoll da735cab46 feat: add Products and ProductCategories collections with CI/CD pipeline
- Add Products collection with comprehensive fields (pricing, inventory, SEO, CTA)
- Add ProductCategories collection with hierarchical structure
- Implement CI/CD pipeline with GitHub Actions (lint, typecheck, test, build, e2e)
- Add access control test utilities and unit tests
- Fix Posts API to include category field for backwards compatibility
- Update ESLint config with ignores for migrations and admin components
- Add centralized access control functions in src/lib/access
- Add db-direct.sh utility script for database access

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 21:36:26 +00:00

497 lines
13 KiB
TypeScript

import type { CollectionConfig } from 'payload'
import { tenantScopedPublicRead, authenticatedOnly } from '../lib/tenantAccess'
export const Products: CollectionConfig = {
slug: 'products',
admin: {
useAsTitle: 'title',
group: 'Produkte',
defaultColumns: ['title', 'category', 'status', 'price', 'isFeatured', 'updatedAt'],
description: 'Produkte und Artikel',
},
access: {
read: tenantScopedPublicRead,
create: authenticatedOnly,
update: authenticatedOnly,
delete: authenticatedOnly,
},
fields: [
// =========================================================================
// Grundinformationen
// =========================================================================
{
name: 'title',
type: 'text',
required: true,
label: 'Produktname',
localized: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: false, // Uniqueness per tenant
label: 'URL-Slug',
admin: {
description: 'URL-freundlicher Name (z.B. "premium-widget")',
},
},
{
name: 'sku',
type: 'text',
label: 'Artikelnummer (SKU)',
admin: {
description: 'Eindeutige Artikelnummer für interne Verwaltung',
},
},
{
name: 'shortDescription',
type: 'textarea',
label: 'Kurzbeschreibung',
localized: true,
admin: {
description: 'Kurze Beschreibung für Produktlisten (max. 200 Zeichen)',
},
},
{
name: 'description',
type: 'richText',
label: 'Produktbeschreibung',
localized: true,
},
// =========================================================================
// Kategorisierung
// =========================================================================
{
name: 'category',
type: 'relationship',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'product-categories' as any,
label: 'Kategorie',
admin: {
description: 'Hauptkategorie des Produkts',
},
},
{
name: 'tags',
type: 'array',
label: 'Tags',
admin: {
description: 'Zusätzliche Schlagworte für Filterung',
},
fields: [
{
name: 'tag',
type: 'text',
required: true,
},
],
},
// =========================================================================
// Medien
// =========================================================================
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
label: 'Hauptbild',
required: true,
},
{
name: 'gallery',
type: 'array',
label: 'Bildergalerie',
admin: {
description: 'Zusätzliche Produktbilder',
},
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'caption',
type: 'text',
label: 'Bildunterschrift',
localized: true,
},
],
},
// =========================================================================
// Preisgestaltung
// =========================================================================
{
name: 'pricing',
type: 'group',
label: 'Preisgestaltung',
fields: [
{
type: 'row',
fields: [
{
name: 'price',
type: 'number',
label: 'Preis',
admin: {
width: '33%',
description: 'Regulärer Preis in Euro',
},
},
{
name: 'salePrice',
type: 'number',
label: 'Angebotspreis',
admin: {
width: '33%',
description: 'Reduzierter Preis (optional)',
},
},
{
name: 'currency',
type: 'select',
label: 'Währung',
defaultValue: 'EUR',
options: [
{ label: 'Euro (EUR)', value: 'EUR' },
{ label: 'US-Dollar (USD)', value: 'USD' },
{ label: 'Schweizer Franken (CHF)', value: 'CHF' },
],
admin: {
width: '34%',
},
},
],
},
{
name: 'priceType',
type: 'select',
label: 'Preistyp',
defaultValue: 'fixed',
options: [
{ label: 'Festpreis', value: 'fixed' },
{ label: 'Ab Preis', value: 'from' },
{ label: 'Auf Anfrage', value: 'on_request' },
{ label: 'Kostenlos', value: 'free' },
],
},
{
name: 'priceNote',
type: 'text',
label: 'Preishinweis',
localized: true,
admin: {
description: 'z.B. "zzgl. MwSt.", "pro Monat", "Einmalzahlung"',
},
},
],
},
// =========================================================================
// Produktdetails
// =========================================================================
{
name: 'details',
type: 'group',
label: 'Produktdetails',
admin: {
description: 'Technische Daten und Spezifikationen',
},
fields: [
{
name: 'specifications',
type: 'array',
label: 'Spezifikationen',
admin: {
description: 'Technische Daten als Schlüssel-Wert-Paare',
},
fields: [
{
type: 'row',
fields: [
{
name: 'key',
type: 'text',
label: 'Eigenschaft',
required: true,
localized: true,
admin: {
width: '50%',
placeholder: 'z.B. Gewicht, Maße, Material',
},
},
{
name: 'value',
type: 'text',
label: 'Wert',
required: true,
localized: true,
admin: {
width: '50%',
placeholder: 'z.B. 500g, 10x20cm, Aluminium',
},
},
],
},
],
},
{
name: 'features',
type: 'array',
label: 'Highlights / Features',
admin: {
description: 'Wichtigste Produktvorteile',
},
fields: [
{
name: 'feature',
type: 'text',
required: true,
localized: true,
},
{
name: 'icon',
type: 'text',
label: 'Icon',
admin: {
description: 'Optional: Lucide-Icon Name',
},
},
],
},
],
},
// =========================================================================
// Lager & Verfügbarkeit
// =========================================================================
{
name: 'inventory',
type: 'group',
label: 'Lager & Verfügbarkeit',
fields: [
{
type: 'row',
fields: [
{
name: 'stockStatus',
type: 'select',
label: 'Verfügbarkeit',
defaultValue: 'in_stock',
options: [
{ label: 'Auf Lager', value: 'in_stock' },
{ label: 'Nur noch wenige', value: 'low_stock' },
{ label: 'Nicht auf Lager', value: 'out_of_stock' },
{ label: 'Auf Bestellung', value: 'on_order' },
{ label: 'Vorbestellung', value: 'preorder' },
],
admin: {
width: '50%',
},
},
{
name: 'stockQuantity',
type: 'number',
label: 'Lagerbestand',
admin: {
width: '50%',
description: 'Aktuelle Stückzahl (optional)',
},
},
],
},
{
name: 'deliveryTime',
type: 'text',
label: 'Lieferzeit',
localized: true,
admin: {
description: 'z.B. "1-3 Werktage", "Sofort lieferbar"',
},
},
],
},
// =========================================================================
// Verknüpfungen
// =========================================================================
{
name: 'relatedProducts',
type: 'relationship',
relationTo: 'products',
hasMany: true,
label: 'Ähnliche Produkte',
admin: {
description: 'Produktempfehlungen für Cross-Selling',
},
},
{
name: 'downloadFiles',
type: 'array',
label: 'Downloads',
admin: {
description: 'Produktdatenblätter, Anleitungen, etc.',
},
fields: [
{
name: 'file',
type: 'upload',
relationTo: 'media',
required: true,
},
{
name: 'title',
type: 'text',
label: 'Titel',
localized: true,
},
],
},
// =========================================================================
// Call-to-Action
// =========================================================================
{
name: 'cta',
type: 'group',
label: 'Call-to-Action',
admin: {
description: 'Handlungsaufforderung für das Produkt',
},
fields: [
{
name: 'type',
type: 'select',
label: 'CTA-Typ',
defaultValue: 'contact',
options: [
{ label: 'Kontakt aufnehmen', value: 'contact' },
{ label: 'Angebot anfordern', value: 'quote' },
{ label: 'In den Warenkorb', value: 'cart' },
{ label: 'Externer Link', value: 'external' },
{ label: 'Download', value: 'download' },
],
},
{
name: 'buttonText',
type: 'text',
label: 'Button-Text',
localized: true,
admin: {
description: 'z.B. "Jetzt anfragen", "Kaufen", "Herunterladen"',
},
},
{
name: 'externalUrl',
type: 'text',
label: 'Externer Link',
admin: {
condition: (_, siblingData) => siblingData?.type === 'external',
description: 'URL für externen Shop oder Bestellseite',
},
},
],
},
// =========================================================================
// SEO
// =========================================================================
{
name: 'seo',
type: 'group',
label: 'SEO',
fields: [
{
name: 'metaTitle',
type: 'text',
label: 'Meta-Titel',
localized: true,
admin: {
description: 'Überschreibt den Produktnamen für Suchmaschinen',
},
},
{
name: 'metaDescription',
type: 'textarea',
label: 'Meta-Beschreibung',
localized: true,
admin: {
description: 'Kurze Beschreibung für Suchergebnisse (max. 160 Zeichen)',
},
},
{
name: 'ogImage',
type: 'upload',
relationTo: 'media',
label: 'Social Media Bild',
admin: {
description: 'Bild für Social Media Shares (1200x630px empfohlen)',
},
},
],
},
// =========================================================================
// Status & Sortierung (Sidebar)
// =========================================================================
{
name: 'status',
type: 'select',
label: 'Status',
defaultValue: 'draft',
options: [
{ label: 'Entwurf', value: 'draft' },
{ label: 'Veröffentlicht', value: 'published' },
{ label: 'Archiviert', value: 'archived' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'isFeatured',
type: 'checkbox',
label: 'Hervorgehoben',
defaultValue: false,
admin: {
position: 'sidebar',
description: 'Auf Startseite oder in Highlights anzeigen',
},
},
{
name: 'isNew',
type: 'checkbox',
label: 'Neu',
defaultValue: false,
admin: {
position: 'sidebar',
description: '"Neu"-Badge anzeigen',
},
},
{
name: 'order',
type: 'number',
label: 'Sortierung',
defaultValue: 0,
admin: {
position: 'sidebar',
description: 'Kleinere Zahlen erscheinen zuerst',
},
},
{
name: 'publishedAt',
type: 'date',
label: 'Veröffentlichungsdatum',
admin: {
position: 'sidebar',
date: {
pickerAppearance: 'dayAndTime',
},
},
},
],
}