feat: initial setup, collections, caregiver frontend
This commit is contained in:
241
src/collections/MealOrders/endpoints/kitchenReport.ts
Normal file
241
src/collections/MealOrders/endpoints/kitchenReport.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import type { Endpoint } from 'payload'
|
||||
import { isSuperAdmin } from '@/access/isSuperAdmin'
|
||||
import { canAccessKitchen } from '@/access/roles'
|
||||
|
||||
/**
|
||||
* Field mappings for aggregation
|
||||
* Maps meal type to their respective boolean fields that should be counted
|
||||
*/
|
||||
const breakfastFields = {
|
||||
'breakfast.accordingToPlan': 'According to Plan',
|
||||
'breakfast.bread.breadRoll': 'Bread Roll (Brötchen)',
|
||||
'breakfast.bread.wholeGrainRoll': 'Whole Grain Roll (Vollkornbrötchen)',
|
||||
'breakfast.bread.greyBread': 'Grey Bread (Graubrot)',
|
||||
'breakfast.bread.wholeGrainBread': 'Whole Grain Bread (Vollkornbrot)',
|
||||
'breakfast.bread.whiteBread': 'White Bread (Weißbrot)',
|
||||
'breakfast.bread.crispbread': 'Crispbread (Knäckebrot)',
|
||||
'breakfast.porridge': 'Porridge (Brei)',
|
||||
'breakfast.preparation.sliced': 'Sliced (geschnitten)',
|
||||
'breakfast.preparation.spread': 'Spread (geschmiert)',
|
||||
'breakfast.spreads.butter': 'Butter',
|
||||
'breakfast.spreads.margarine': 'Margarine',
|
||||
'breakfast.spreads.jam': 'Jam (Konfitüre)',
|
||||
'breakfast.spreads.diabeticJam': 'Diabetic Jam (Diab. Konfitüre)',
|
||||
'breakfast.spreads.honey': 'Honey (Honig)',
|
||||
'breakfast.spreads.cheese': 'Cheese (Käse)',
|
||||
'breakfast.spreads.quark': 'Quark',
|
||||
'breakfast.spreads.sausage': 'Sausage (Wurst)',
|
||||
'breakfast.beverages.coffee': 'Coffee (Kaffee)',
|
||||
'breakfast.beverages.tea': 'Tea (Tee)',
|
||||
'breakfast.beverages.hotMilk': 'Hot Milk (Milch heiß)',
|
||||
'breakfast.beverages.coldMilk': 'Cold Milk (Milch kalt)',
|
||||
'breakfast.additions.sugar': 'Sugar (Zucker)',
|
||||
'breakfast.additions.sweetener': 'Sweetener (Süßstoff)',
|
||||
'breakfast.additions.coffeeCreamer': 'Coffee Creamer (Kaffeesahne)',
|
||||
}
|
||||
|
||||
const lunchFields = {
|
||||
'lunch.soup': 'Soup (Suppe)',
|
||||
'lunch.dessert': 'Dessert',
|
||||
'lunch.specialPreparations.pureedFood': 'Pureed Food (passierte Kost)',
|
||||
'lunch.specialPreparations.pureedMeat': 'Pureed Meat (passiertes Fleisch)',
|
||||
'lunch.specialPreparations.slicedMeat': 'Sliced Meat (geschnittenes Fleisch)',
|
||||
'lunch.specialPreparations.mashedPotatoes': 'Mashed Potatoes (Kartoffelbrei)',
|
||||
'lunch.restrictions.noFish': 'No Fish (ohne Fisch)',
|
||||
'lunch.restrictions.fingerFood': 'Finger Food',
|
||||
'lunch.restrictions.onlySweet': 'Only Sweet (nur süß)',
|
||||
}
|
||||
|
||||
const dinnerFields = {
|
||||
'dinner.accordingToPlan': 'According to Plan',
|
||||
'dinner.bread.greyBread': 'Grey Bread (Graubrot)',
|
||||
'dinner.bread.wholeGrainBread': 'Whole Grain Bread (Vollkornbrot)',
|
||||
'dinner.bread.whiteBread': 'White Bread (Weißbrot)',
|
||||
'dinner.bread.crispbread': 'Crispbread (Knäckebrot)',
|
||||
'dinner.preparation.spread': 'Spread (geschmiert)',
|
||||
'dinner.preparation.sliced': 'Sliced (geschnitten)',
|
||||
'dinner.spreads.butter': 'Butter',
|
||||
'dinner.spreads.margarine': 'Margarine',
|
||||
'dinner.soup': 'Soup (Suppe)',
|
||||
'dinner.porridge': 'Porridge (Brei)',
|
||||
'dinner.noFish': 'No Fish (ohne Fisch)',
|
||||
'dinner.beverages.tea': 'Tea (Tee)',
|
||||
'dinner.beverages.cocoa': 'Cocoa (Kakao)',
|
||||
'dinner.beverages.hotMilk': 'Hot Milk (Milch heiß)',
|
||||
'dinner.beverages.coldMilk': 'Cold Milk (Milch kalt)',
|
||||
'dinner.additions.sugar': 'Sugar (Zucker)',
|
||||
'dinner.additions.sweetener': 'Sweetener (Süßstoff)',
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested value from object using dot notation path
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
||||
return path.split('.').reduce((current: unknown, key) => {
|
||||
if (current && typeof current === 'object' && key in (current as Record<string, unknown>)) {
|
||||
return (current as Record<string, unknown>)[key]
|
||||
}
|
||||
return undefined
|
||||
}, obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kitchen Report API Endpoint
|
||||
*
|
||||
* GET /api/meal-orders/kitchen-report
|
||||
*
|
||||
* Query Parameters:
|
||||
* - date (required): YYYY-MM-DD format
|
||||
* - mealType (required): breakfast | lunch | dinner
|
||||
*
|
||||
* Returns aggregated ingredient counts for the specified date and meal type.
|
||||
* Only accessible by users with admin or kitchen role.
|
||||
*/
|
||||
export const kitchenReportEndpoint: Endpoint = {
|
||||
path: '/kitchen-report',
|
||||
method: 'get',
|
||||
handler: async (req) => {
|
||||
const { payload, user } = req
|
||||
|
||||
// Check authentication
|
||||
if (!user) {
|
||||
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Check authorization - must be super admin, tenant admin, or kitchen staff
|
||||
if (!isSuperAdmin(user) && !canAccessKitchen(user)) {
|
||||
return Response.json(
|
||||
{ error: 'Forbidden - Kitchen or Admin role required' },
|
||||
{ status: 403 },
|
||||
)
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
const url = new URL(req.url || '', 'http://localhost')
|
||||
const date = url.searchParams.get('date')
|
||||
const mealType = url.searchParams.get('mealType')
|
||||
|
||||
// Validate parameters
|
||||
if (!date) {
|
||||
return Response.json({ error: 'Missing required parameter: date' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!mealType || !['breakfast', 'lunch', 'dinner'].includes(mealType)) {
|
||||
return Response.json(
|
||||
{ error: 'Invalid or missing mealType. Must be: breakfast, lunch, or dinner' },
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
|
||||
if (!dateRegex.test(date)) {
|
||||
return Response.json({ error: 'Invalid date format. Use YYYY-MM-DD' }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
// Query meal orders for the specified date and meal type
|
||||
const orders = await payload.find({
|
||||
collection: 'meal-orders',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
date: {
|
||||
equals: date,
|
||||
},
|
||||
},
|
||||
{
|
||||
mealType: {
|
||||
equals: mealType,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
limit: 1000, // Get all orders for the day
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
// Select the appropriate field mapping
|
||||
const fieldMapping =
|
||||
mealType === 'breakfast'
|
||||
? breakfastFields
|
||||
: mealType === 'lunch'
|
||||
? lunchFields
|
||||
: dinnerFields
|
||||
|
||||
// Aggregate counts
|
||||
const ingredients: Record<string, { count: number; label: string }> = {}
|
||||
|
||||
// Initialize all fields with 0
|
||||
for (const [fieldPath, label] of Object.entries(fieldMapping)) {
|
||||
ingredients[fieldPath] = { count: 0, label }
|
||||
}
|
||||
|
||||
// Count lunch portion sizes separately
|
||||
const portionSizes: Record<string, number> = {
|
||||
small: 0,
|
||||
large: 0,
|
||||
vegetarian: 0,
|
||||
}
|
||||
|
||||
// Count occurrences
|
||||
for (const order of orders.docs) {
|
||||
// Count boolean fields
|
||||
for (const fieldPath of Object.keys(fieldMapping)) {
|
||||
const value = getNestedValue(order as unknown as Record<string, unknown>, fieldPath)
|
||||
if (value === true) {
|
||||
ingredients[fieldPath].count++
|
||||
}
|
||||
}
|
||||
|
||||
// Count lunch portion sizes
|
||||
if (mealType === 'lunch' && order.lunch?.portionSize) {
|
||||
const size = order.lunch.portionSize as string
|
||||
if (size in portionSizes) {
|
||||
portionSizes[size]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build response with non-zero items
|
||||
const ingredientCounts: Record<string, number> = {}
|
||||
const ingredientLabels: Record<string, string> = {}
|
||||
|
||||
for (const [fieldPath, { count, label }] of Object.entries(ingredients)) {
|
||||
if (count > 0) {
|
||||
// Use a cleaner key name (last part of the path)
|
||||
const key = fieldPath.split('.').pop() || fieldPath
|
||||
ingredientCounts[key] = count
|
||||
ingredientLabels[key] = label
|
||||
}
|
||||
}
|
||||
|
||||
// Build the response
|
||||
const response: Record<string, unknown> = {
|
||||
date,
|
||||
mealType,
|
||||
totalOrders: orders.totalDocs,
|
||||
ingredients: ingredientCounts,
|
||||
labels: ingredientLabels,
|
||||
}
|
||||
|
||||
// Add portion sizes for lunch
|
||||
if (mealType === 'lunch') {
|
||||
const nonZeroPortions: Record<string, number> = {}
|
||||
for (const [size, count] of Object.entries(portionSizes)) {
|
||||
if (count > 0) {
|
||||
nonZeroPortions[size] = count
|
||||
}
|
||||
}
|
||||
if (Object.keys(nonZeroPortions).length > 0) {
|
||||
response.portionSizes = nonZeroPortions
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json(response, { status: 200 })
|
||||
} catch (error) {
|
||||
console.error('Kitchen report error:', error)
|
||||
return Response.json({ error: 'Failed to generate report' }, { status: 500 })
|
||||
}
|
||||
},
|
||||
}
|
||||
52
src/collections/MealOrders/hooks/generateTitle.ts
Normal file
52
src/collections/MealOrders/hooks/generateTitle.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { CollectionBeforeValidateHook } from 'payload'
|
||||
|
||||
/**
|
||||
* Hook to auto-generate the title field from date, meal type, and resident name
|
||||
* Format: "Breakfast - 2024-01-15 - John Doe"
|
||||
*/
|
||||
export const generateTitle: CollectionBeforeValidateHook = async ({ data, req, operation }) => {
|
||||
if (!data) return data
|
||||
|
||||
const mealType = data.mealType
|
||||
const date = data.date
|
||||
|
||||
// Format meal type with first letter capitalized
|
||||
const mealTypeLabel =
|
||||
mealType === 'breakfast' ? 'Breakfast' : mealType === 'lunch' ? 'Lunch' : 'Dinner'
|
||||
|
||||
// Format date as YYYY-MM-DD
|
||||
let dateStr = ''
|
||||
if (date) {
|
||||
const dateObj = typeof date === 'string' ? new Date(date) : date
|
||||
dateStr = dateObj.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
// Get resident name if we have the resident ID
|
||||
let residentName = ''
|
||||
if (data.resident && req.payload) {
|
||||
try {
|
||||
const residentId = typeof data.resident === 'object' ? data.resident.id : data.resident
|
||||
if (residentId) {
|
||||
const resident = await req.payload.findByID({
|
||||
collection: 'residents',
|
||||
id: residentId,
|
||||
depth: 0,
|
||||
})
|
||||
if (resident?.name) {
|
||||
residentName = resident.name
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If we can't fetch the resident, just skip the name
|
||||
}
|
||||
}
|
||||
|
||||
// Compose title
|
||||
const parts = [mealTypeLabel, dateStr, residentName].filter(Boolean)
|
||||
const title = parts.join(' - ')
|
||||
|
||||
return {
|
||||
...data,
|
||||
title: title || 'New Meal Order',
|
||||
}
|
||||
}
|
||||
14
src/collections/MealOrders/hooks/setCreatedBy.ts
Normal file
14
src/collections/MealOrders/hooks/setCreatedBy.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { CollectionBeforeChangeHook } from 'payload'
|
||||
|
||||
/**
|
||||
* Hook to automatically set the createdBy field to the current user on creation
|
||||
*/
|
||||
export const setCreatedBy: CollectionBeforeChangeHook = async ({ data, req, operation }) => {
|
||||
if (operation === 'create' && req.user) {
|
||||
return {
|
||||
...data,
|
||||
createdBy: req.user.id,
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
439
src/collections/MealOrders/index.ts
Normal file
439
src/collections/MealOrders/index.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isSuperAdmin } from '@/access/isSuperAdmin'
|
||||
import { hasTenantRole } from '@/access/roles'
|
||||
import { setCreatedBy } from './hooks/setCreatedBy'
|
||||
import { generateTitle } from './hooks/generateTitle'
|
||||
import { kitchenReportEndpoint } from './endpoints/kitchenReport'
|
||||
|
||||
/**
|
||||
* Meal Orders Collection
|
||||
*
|
||||
* Represents a single meal order for a resident, including:
|
||||
* - Date and meal type (breakfast, lunch, dinner)
|
||||
* - Status tracking (pending, preparing, prepared)
|
||||
* - Meal-specific options from the paper forms
|
||||
*
|
||||
* Multi-tenant: each order belongs to a specific care home.
|
||||
*/
|
||||
export const MealOrders: CollectionConfig = {
|
||||
slug: 'meal-orders',
|
||||
labels: {
|
||||
singular: 'Meal Order',
|
||||
plural: 'Meal Orders',
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
description: 'Manage meal orders for residents',
|
||||
defaultColumns: ['title', 'resident', 'date', 'mealType', 'status'],
|
||||
group: 'Meal Planning',
|
||||
},
|
||||
endpoints: [kitchenReportEndpoint],
|
||||
hooks: {
|
||||
beforeChange: [setCreatedBy],
|
||||
beforeValidate: [generateTitle],
|
||||
},
|
||||
access: {
|
||||
// Admin and caregiver can create orders
|
||||
create: ({ req }) => {
|
||||
if (!req.user) return false
|
||||
if (isSuperAdmin(req.user)) return true
|
||||
return hasTenantRole(req.user, 'admin') || hasTenantRole(req.user, 'caregiver')
|
||||
},
|
||||
// All authenticated users within the tenant can read orders
|
||||
read: ({ req }) => {
|
||||
if (!req.user) return false
|
||||
return true // Multi-tenant plugin will filter by tenant
|
||||
},
|
||||
// Admin can update all, caregiver can update own pending orders, kitchen can update status
|
||||
update: ({ req }) => {
|
||||
if (!req.user) return false
|
||||
if (isSuperAdmin(req.user)) return true
|
||||
// All tenant roles can update (with field-level restrictions)
|
||||
return (
|
||||
hasTenantRole(req.user, 'admin') ||
|
||||
hasTenantRole(req.user, 'caregiver') ||
|
||||
hasTenantRole(req.user, 'kitchen')
|
||||
)
|
||||
},
|
||||
// Only admin can delete orders
|
||||
delete: ({ req }) => {
|
||||
if (!req.user) return false
|
||||
if (isSuperAdmin(req.user)) return true
|
||||
return hasTenantRole(req.user, 'admin')
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
// Core Fields
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
description: 'Auto-generated title',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'resident',
|
||||
type: 'relationship',
|
||||
relationTo: 'residents',
|
||||
required: true,
|
||||
index: true,
|
||||
admin: {
|
||||
description: 'Select the resident for this meal order',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'date',
|
||||
type: 'date',
|
||||
required: true,
|
||||
index: true,
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayOnly',
|
||||
displayFormat: 'yyyy-MM-dd',
|
||||
},
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mealType',
|
||||
type: 'select',
|
||||
required: true,
|
||||
index: true,
|
||||
options: [
|
||||
{ label: 'Breakfast (Frühstück)', value: 'breakfast' },
|
||||
{ label: 'Lunch (Mittagessen)', value: 'lunch' },
|
||||
{ label: 'Dinner (Abendessen)', value: 'dinner' },
|
||||
],
|
||||
admin: {
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'pending',
|
||||
index: true,
|
||||
options: [
|
||||
{ label: 'Pending', value: 'pending' },
|
||||
{ label: 'Preparing', value: 'preparing' },
|
||||
{ label: 'Prepared', value: 'prepared' },
|
||||
],
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Order status for kitchen tracking',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'createdBy',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
readOnly: true,
|
||||
description: 'User who created this order',
|
||||
},
|
||||
},
|
||||
|
||||
// Override Fields (optional per-order overrides)
|
||||
{
|
||||
type: 'collapsible',
|
||||
label: 'Order Overrides',
|
||||
admin: {
|
||||
initCollapsed: true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'highCaloric',
|
||||
type: 'checkbox',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description: 'Override: high-caloric requirement for this order',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'aversions',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
description: 'Override: specific aversions for this order',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'notes',
|
||||
type: 'textarea',
|
||||
admin: {
|
||||
description: 'Special notes for this order',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// BREAKFAST FIELDS GROUP
|
||||
// ============================================
|
||||
{
|
||||
type: 'group',
|
||||
name: 'breakfast',
|
||||
label: 'Breakfast Options (Frühstück)',
|
||||
admin: {
|
||||
condition: (data) => data?.mealType === 'breakfast',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'accordingToPlan',
|
||||
type: 'checkbox',
|
||||
label: 'According to Plan (Frühstück lt. Plan)',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'bread',
|
||||
label: 'Bread Selection',
|
||||
fields: [
|
||||
{ name: 'breadRoll', type: 'checkbox', label: 'Bread Roll (Brötchen)' },
|
||||
{
|
||||
name: 'wholeGrainRoll',
|
||||
type: 'checkbox',
|
||||
label: 'Whole Grain Roll (Vollkornbrötchen)',
|
||||
},
|
||||
{ name: 'greyBread', type: 'checkbox', label: 'Grey Bread (Graubrot)' },
|
||||
{
|
||||
name: 'wholeGrainBread',
|
||||
type: 'checkbox',
|
||||
label: 'Whole Grain Bread (Vollkornbrot)',
|
||||
},
|
||||
{ name: 'whiteBread', type: 'checkbox', label: 'White Bread (Weißbrot)' },
|
||||
{ name: 'crispbread', type: 'checkbox', label: 'Crispbread (Knäckebrot)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'porridge',
|
||||
type: 'checkbox',
|
||||
label: 'Porridge/Puree (Brei)',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'preparation',
|
||||
label: 'Bread Preparation',
|
||||
fields: [
|
||||
{ name: 'sliced', type: 'checkbox', label: 'Sliced (geschnitten)' },
|
||||
{ name: 'spread', type: 'checkbox', label: 'Spread (geschmiert)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'spreads',
|
||||
label: 'Spreads',
|
||||
fields: [
|
||||
{ name: 'butter', type: 'checkbox', label: 'Butter' },
|
||||
{ name: 'margarine', type: 'checkbox', label: 'Margarine' },
|
||||
{ name: 'jam', type: 'checkbox', label: 'Jam (Konfitüre)' },
|
||||
{ name: 'diabeticJam', type: 'checkbox', label: 'Diabetic Jam (Diab. Konfitüre)' },
|
||||
{ name: 'honey', type: 'checkbox', label: 'Honey (Honig)' },
|
||||
{ name: 'cheese', type: 'checkbox', label: 'Cheese (Käse)' },
|
||||
{ name: 'quark', type: 'checkbox', label: 'Quark' },
|
||||
{ name: 'sausage', type: 'checkbox', label: 'Sausage (Wurst)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'beverages',
|
||||
label: 'Beverages',
|
||||
fields: [
|
||||
{ name: 'coffee', type: 'checkbox', label: 'Coffee (Kaffee)' },
|
||||
{ name: 'tea', type: 'checkbox', label: 'Tea (Tee)' },
|
||||
{ name: 'hotMilk', type: 'checkbox', label: 'Hot Milk (Milch heiß)' },
|
||||
{ name: 'coldMilk', type: 'checkbox', label: 'Cold Milk (Milch kalt)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'additions',
|
||||
label: 'Additions',
|
||||
fields: [
|
||||
{ name: 'sugar', type: 'checkbox', label: 'Sugar (Zucker)' },
|
||||
{ name: 'sweetener', type: 'checkbox', label: 'Sweetener (Süßstoff)' },
|
||||
{ name: 'coffeeCreamer', type: 'checkbox', label: 'Coffee Creamer (Kaffeesahne)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// LUNCH FIELDS GROUP
|
||||
// ============================================
|
||||
{
|
||||
type: 'group',
|
||||
name: 'lunch',
|
||||
label: 'Lunch Options (Mittagessen)',
|
||||
admin: {
|
||||
condition: (data) => data?.mealType === 'lunch',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'portionSize',
|
||||
type: 'select',
|
||||
label: 'Portion Size',
|
||||
options: [
|
||||
{ label: 'Small Portion (Kleine Portion)', value: 'small' },
|
||||
{ label: 'Large Portion (Große Portion)', value: 'large' },
|
||||
{
|
||||
label: 'Vegetarian Whole-Food (Vollwertkost vegetarisch)',
|
||||
value: 'vegetarian',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{ name: 'soup', type: 'checkbox', label: 'Soup (Suppe)', admin: { width: '50%' } },
|
||||
{ name: 'dessert', type: 'checkbox', label: 'Dessert', admin: { width: '50%' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'specialPreparations',
|
||||
label: 'Special Preparations',
|
||||
fields: [
|
||||
{ name: 'pureedFood', type: 'checkbox', label: 'Pureed Food (passierte Kost)' },
|
||||
{ name: 'pureedMeat', type: 'checkbox', label: 'Pureed Meat (passiertes Fleisch)' },
|
||||
{ name: 'slicedMeat', type: 'checkbox', label: 'Sliced Meat (geschnittenes Fleisch)' },
|
||||
{ name: 'mashedPotatoes', type: 'checkbox', label: 'Mashed Potatoes (Kartoffelbrei)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'restrictions',
|
||||
label: 'Restrictions',
|
||||
fields: [
|
||||
{ name: 'noFish', type: 'checkbox', label: 'No Fish (ohne Fisch)' },
|
||||
{ name: 'fingerFood', type: 'checkbox', label: 'Finger Food' },
|
||||
{ name: 'onlySweet', type: 'checkbox', label: 'Only Sweet (nur süß)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// ============================================
|
||||
// DINNER FIELDS GROUP
|
||||
// ============================================
|
||||
{
|
||||
type: 'group',
|
||||
name: 'dinner',
|
||||
label: 'Dinner Options (Abendessen)',
|
||||
admin: {
|
||||
condition: (data) => data?.mealType === 'dinner',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'accordingToPlan',
|
||||
type: 'checkbox',
|
||||
label: 'According to Plan (Abendessen lt. Plan)',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'bread',
|
||||
label: 'Bread Selection',
|
||||
fields: [
|
||||
{ name: 'greyBread', type: 'checkbox', label: 'Grey Bread (Graubrot)' },
|
||||
{
|
||||
name: 'wholeGrainBread',
|
||||
type: 'checkbox',
|
||||
label: 'Whole Grain Bread (Vollkornbrot)',
|
||||
},
|
||||
{ name: 'whiteBread', type: 'checkbox', label: 'White Bread (Weißbrot)' },
|
||||
{ name: 'crispbread', type: 'checkbox', label: 'Crispbread (Knäckebrot)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'preparation',
|
||||
label: 'Bread Preparation',
|
||||
fields: [
|
||||
{ name: 'spread', type: 'checkbox', label: 'Spread (geschmiert)' },
|
||||
{ name: 'sliced', type: 'checkbox', label: 'Sliced (geschnitten)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'spreads',
|
||||
label: 'Spreads',
|
||||
fields: [
|
||||
{ name: 'butter', type: 'checkbox', label: 'Butter' },
|
||||
{ name: 'margarine', type: 'checkbox', label: 'Margarine' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{ name: 'soup', type: 'checkbox', label: 'Soup (Suppe)', admin: { width: '33%' } },
|
||||
{
|
||||
name: 'porridge',
|
||||
type: 'checkbox',
|
||||
label: 'Porridge (Brei)',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'noFish',
|
||||
type: 'checkbox',
|
||||
label: 'No Fish (ohne Fisch)',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'beverages',
|
||||
label: 'Beverages',
|
||||
fields: [
|
||||
{ name: 'tea', type: 'checkbox', label: 'Tea (Tee)' },
|
||||
{ name: 'cocoa', type: 'checkbox', label: 'Cocoa (Kakao)' },
|
||||
{ name: 'hotMilk', type: 'checkbox', label: 'Hot Milk (Milch heiß)' },
|
||||
{ name: 'coldMilk', type: 'checkbox', label: 'Cold Milk (Milch kalt)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'additions',
|
||||
label: 'Additions',
|
||||
fields: [
|
||||
{ name: 'sugar', type: 'checkbox', label: 'Sugar (Zucker)' },
|
||||
{ name: 'sweetener', type: 'checkbox', label: 'Sweetener (Süßstoff)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user