440 lines
13 KiB
TypeScript
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)' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|