Files
meal-planner/src/collections/MealOrders/index.ts

440 lines
13 KiB
TypeScript

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