Shumiland/src/collections/Tariffs.ts
Vadym Samoilenko 8439941afa
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat(cms): standardize Lexical editor across all richText fields, add image upload to blog
- Add src/fields/richText.ts: standardEditor (all defaultFeatures) and blogEditor
  (defaultFeatures + UploadFeature for media + EXPERIMENTAL_TableFeature)
- Apply standardEditor to ThankYouPage, CheckoutPage, GroupVisitsPage, Tariffs, Locations
- Apply blogEditor to BlogPosts — editors can now insert images inline via Upload button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 14:09:27 +01:00

178 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { CollectionConfig, FieldAccess } from 'payload'
import { standardEditor } from '@/fields/richText'
import { isAdmin } from '@/access/isAdmin'
import { revalidateAfterChange } from '@/hooks/revalidatePath'
import { getTariffs } from '@/lib/ezy'
const adminFieldAccess: FieldAccess = ({ req: { user } }) => user?.role === 'admin'
export const Tariffs: CollectionConfig = {
slug: 'tariffs',
labels: { singular: 'Квиток', plural: 'Квитки (тарифи)' },
admin: {
useAsTitle: 'last_synced_name',
defaultColumns: [
'display_name',
'last_synced_name',
'category_tag',
'last_synced_price',
'visible',
],
group: 'Каталог',
description:
'Керування квитками: показ/приховування, значки, описи, фото. Зніміть «Показувати на сайті», щоб приховати квиток (напр. якщо його ще немає в ezy).',
},
hooks: {
afterChange: [revalidateAfterChange],
beforeChange: [
async ({ data, originalDoc, req }) => {
const newEzyId = data.ezy_id as number | null | undefined
const oldEzyId = (originalDoc as Record<string, unknown> | undefined)?.ezy_id as
| number
| null
| undefined
// Only validate when admin user manually changes ezy_id (skip automated sync)
if (newEzyId && newEzyId !== oldEzyId && req.user) {
const all = await getTariffs()
const match = all.find((t) => t.id === newEzyId)
if (!match) throw new Error(`ezy_id ${newEzyId} не знайдено в ezy API`)
data.last_synced_name = match.name
data.last_synced_price = match.price
data.last_synced_at = new Date().toISOString()
}
return data
},
],
},
access: {
read: () => true,
create: isAdmin,
update: isAdmin,
delete: isAdmin,
},
fields: [
{
name: 'ezy_id',
type: 'number',
unique: true,
index: true,
admin: {
description:
'Встановлюється автоматично при синхронізації з ezy API. Порожнє — для квитків, доданих вручну.',
},
access: { update: adminFieldAccess },
},
{ name: 'display_name', type: 'text', label: 'Назва для сайту (перевизначення)' },
{
name: 'description',
type: 'richText',
editor: standardEditor,
label: 'Опис квитка',
},
{ name: 'image', type: 'upload', relationTo: 'media', label: 'Фото квитка' },
{ name: 'icon', type: 'text', label: 'Emoji або назва іконки' },
{
name: 'category_tag',
type: 'select',
required: true,
label: 'Категорія (вкладка каталогу)',
options: [
{ label: 'Основні зони', value: 'zone' },
{ label: 'Атракціони', value: 'attraction' },
{ label: 'Програми та екскурсії', value: 'program' },
{ label: 'Комбо', value: 'combo' },
// legacy-значення з ранніх синхронізацій (мапляться у вкладки в каталозі)
{ label: 'Dyno (legacy)', value: 'dyno' },
{ label: 'Dyvolis (legacy)', value: 'dyvolis' },
{ label: 'Maze (legacy)', value: 'maze' },
{ label: 'Family (legacy)', value: 'family' },
],
},
{
name: 'badgeLabel',
type: 'text',
label: 'Назва бейджа (перевизначення)',
admin: {
description: 'Якщо порожнє — береться з назви категорії (Зона / Атракціон / Екскурсія).',
},
},
{
name: 'infoChips',
type: 'array',
label: 'Значки (характеристики квитка)',
labels: { singular: 'Значок', plural: 'Значки' },
admin: {
description:
'Маленькі значки під назвою: тривалість, вартість за людину, вік тощо. Іконка + текст.',
},
fields: [
{
name: 'icon',
type: 'select',
required: true,
label: 'Іконка',
defaultValue: 'clock',
options: [
{ label: '⏱ Тривалість (годинник)', value: 'clock' },
{ label: '👤 Вартість за людину', value: 'person' },
{ label: '👨‍👩‍👧 Група / для родини', value: 'group' },
{ label: '♿ Вік / доступність', value: 'age' },
{ label: '∞ Необмежено', value: 'infinity' },
{ label: '🎯 Влучність / постріли', value: 'target' },
{ label: '🎟 Сеанс / квиток', value: 'session' },
{ label: '📏 Зріст / розмір', value: 'height' },
{ label: '🔧 Інтерактив', value: 'wrench' },
{ label: '🎓 Навчальний', value: 'graduation' },
{ label: '🌈 Спокійно', value: 'rainbow' },
{ label: '🚀 Драйв', value: 'send' },
{ label: '🥽 VR досвід', value: 'vr' },
],
},
{ name: 'label', type: 'text', required: true, label: 'Текст' },
],
},
{ name: 'sort', type: 'number', defaultValue: 0, admin: { position: 'sidebar' } },
{
name: 'visible',
type: 'checkbox',
label: 'Показувати на сайті',
defaultValue: true,
admin: {
position: 'sidebar',
description:
'Зніміть галочку, щоб приховати квиток із сайту (напр. якщо його ще немає в ezy).',
},
},
{
name: 'sync_ezy_ui',
type: 'ui',
label: 'Ручна синхронізація',
admin: {
components: {
Field: '/src/components/admin/SyncEzyButton',
},
},
},
{
name: 'last_synced_name',
type: 'text',
label: 'Назва з ezy API',
admin: { readOnly: true, description: 'Оновлюється автоматично при синхронізації' },
access: { update: adminFieldAccess },
},
{
name: 'last_synced_price',
type: 'number',
label: 'Ціна з ezy API (грн)',
admin: { readOnly: true },
access: { update: adminFieldAccess },
},
{
name: 'last_synced_at',
type: 'date',
label: 'Дата синхронізації',
admin: { readOnly: true, date: { pickerAppearance: 'dayAndTime' } },
access: { update: adminFieldAccess },
},
],
}