feat: add kitchen dashboard, format codebase

This commit is contained in:
2025-12-02 17:00:17 +01:00
parent 2a9956c3e6
commit 5ce1b4728b
82 changed files with 5206 additions and 3134 deletions

82
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,82 @@
export type UserRole = "caregiver" | "kitchen" | "admin" | "super-admin";
export interface TenantRole {
tenant: { id: number; name: string } | number;
roles?: string[];
}
export interface User {
id: number;
email: string;
name?: string;
roles?: string[];
tenants?: TenantRole[];
}
export function getUserRoles(user: User): UserRole[] {
const roles: UserRole[] = [];
if (user.roles?.includes("super-admin")) {
roles.push("super-admin", "admin", "caregiver", "kitchen");
return roles;
}
const tenantRoles = user.tenants?.flatMap((t) => t.roles || []) || [];
if (tenantRoles.includes("admin")) {
roles.push("admin", "caregiver", "kitchen");
}
if (tenantRoles.includes("caregiver") && !roles.includes("caregiver")) {
roles.push("caregiver");
}
if (tenantRoles.includes("kitchen") && !roles.includes("kitchen")) {
roles.push("kitchen");
}
return roles;
}
export function hasRole(user: User, role: UserRole): boolean {
return getUserRoles(user).includes(role);
}
export function getPrimaryRole(user: User): UserRole | null {
if (user.roles?.includes("super-admin")) {
return "admin";
}
const tenantRoles = user.tenants?.flatMap((t) => t.roles || []) || [];
if (tenantRoles.includes("admin")) {
return "admin";
}
if (tenantRoles.includes("kitchen")) {
return "kitchen";
}
if (tenantRoles.includes("caregiver")) {
return "caregiver";
}
return null;
}
export function getRedirectPath(role: UserRole | null): string {
switch (role) {
case "kitchen":
return "/kitchen/dashboard";
case "caregiver":
case "admin":
case "super-admin":
return "/caregiver/dashboard";
default:
return "/login";
}
}
export function getTenantName(user: User): string {
const firstTenant = user.tenants?.[0]?.tenant;
if (firstTenant && typeof firstTenant === "object") {
return firstTenant.name;
}
return "Care Home";
}

View File

@@ -1,63 +1,78 @@
export interface OptionItem {
key: string
label: string
key: string;
label: string;
}
export interface OptionSection {
title: string
columns: 1 | 2 | 3
options: OptionItem[]
title: string;
columns: 1 | 2 | 3;
options: OptionItem[];
}
export interface BreakfastOptions {
accordingToPlan: boolean
accordingToPlan: boolean;
bread: {
breadRoll: boolean
wholeGrainRoll: boolean
greyBread: boolean
wholeGrainBread: boolean
whiteBread: boolean
crispbread: boolean
}
porridge: boolean
preparation: { sliced: boolean; spread: boolean }
breadRoll: boolean;
wholeGrainRoll: boolean;
greyBread: boolean;
wholeGrainBread: boolean;
whiteBread: boolean;
crispbread: boolean;
};
porridge: boolean;
preparation: { sliced: boolean; spread: boolean };
spreads: {
butter: boolean
margarine: boolean
jam: boolean
diabeticJam: boolean
honey: boolean
cheese: boolean
quark: boolean
sausage: boolean
}
beverages: { coffee: boolean; tea: boolean; hotMilk: boolean; coldMilk: boolean }
additions: { sugar: boolean; sweetener: boolean; coffeeCreamer: boolean }
butter: boolean;
margarine: boolean;
jam: boolean;
diabeticJam: boolean;
honey: boolean;
cheese: boolean;
quark: boolean;
sausage: boolean;
};
beverages: {
coffee: boolean;
tea: boolean;
hotMilk: boolean;
coldMilk: boolean;
};
additions: { sugar: boolean; sweetener: boolean; coffeeCreamer: boolean };
}
export interface LunchOptions {
portionSize: 'small' | 'large' | 'vegetarian'
soup: boolean
dessert: boolean
portionSize: "small" | "large" | "vegetarian";
soup: boolean;
dessert: boolean;
specialPreparations: {
pureedFood: boolean
pureedMeat: boolean
slicedMeat: boolean
mashedPotatoes: boolean
}
restrictions: { noFish: boolean; fingerFood: boolean; onlySweet: boolean }
pureedFood: boolean;
pureedMeat: boolean;
slicedMeat: boolean;
mashedPotatoes: boolean;
};
restrictions: { noFish: boolean; fingerFood: boolean; onlySweet: boolean };
}
export interface DinnerOptions {
accordingToPlan: boolean
bread: { greyBread: boolean; wholeGrainBread: boolean; whiteBread: boolean; crispbread: boolean }
preparation: { spread: boolean; sliced: boolean }
spreads: { butter: boolean; margarine: boolean }
soup: boolean
porridge: boolean
noFish: boolean
beverages: { tea: boolean; cocoa: boolean; hotMilk: boolean; coldMilk: boolean }
additions: { sugar: boolean; sweetener: boolean }
accordingToPlan: boolean;
bread: {
greyBread: boolean;
wholeGrainBread: boolean;
whiteBread: boolean;
crispbread: boolean;
};
preparation: { spread: boolean; sliced: boolean };
spreads: { butter: boolean; margarine: boolean };
soup: boolean;
porridge: boolean;
noFish: boolean;
beverages: {
tea: boolean;
cocoa: boolean;
hotMilk: boolean;
coldMilk: boolean;
};
additions: { sugar: boolean; sweetener: boolean };
}
export const DEFAULT_BREAKFAST: BreakfastOptions = {
@@ -84,10 +99,10 @@ export const DEFAULT_BREAKFAST: BreakfastOptions = {
},
beverages: { coffee: false, tea: false, hotMilk: false, coldMilk: false },
additions: { sugar: false, sweetener: false, coffeeCreamer: false },
}
};
export const DEFAULT_LUNCH: LunchOptions = {
portionSize: 'large',
portionSize: "large",
soup: false,
dessert: true,
specialPreparations: {
@@ -97,11 +112,16 @@ export const DEFAULT_LUNCH: LunchOptions = {
mashedPotatoes: false,
},
restrictions: { noFish: false, fingerFood: false, onlySweet: false },
}
};
export const DEFAULT_DINNER: DinnerOptions = {
accordingToPlan: false,
bread: { greyBread: false, wholeGrainBread: false, whiteBread: false, crispbread: false },
bread: {
greyBread: false,
wholeGrainBread: false,
whiteBread: false,
crispbread: false,
},
preparation: { spread: false, sliced: false },
spreads: { butter: false, margarine: false },
soup: false,
@@ -109,163 +129,163 @@ export const DEFAULT_DINNER: DinnerOptions = {
noFish: false,
beverages: { tea: false, cocoa: false, hotMilk: false, coldMilk: false },
additions: { sugar: false, sweetener: false },
}
};
export const BREAKFAST_CONFIG: Record<string, OptionSection> = {
bread: {
title: 'Bread (Brot)',
title: "Bread (Brot)",
columns: 2,
options: [
{ key: 'breadRoll', label: 'Bread Roll' },
{ key: 'wholeGrainRoll', label: 'Whole Grain Roll' },
{ key: 'greyBread', label: 'Grey Bread' },
{ key: 'wholeGrainBread', label: 'Whole Grain' },
{ key: 'whiteBread', label: 'White Bread' },
{ key: 'crispbread', label: 'Crispbread' },
{ key: "breadRoll", label: "Bread Roll" },
{ key: "wholeGrainRoll", label: "Whole Grain Roll" },
{ key: "greyBread", label: "Grey Bread" },
{ key: "wholeGrainBread", label: "Whole Grain" },
{ key: "whiteBread", label: "White Bread" },
{ key: "crispbread", label: "Crispbread" },
],
},
preparation: {
title: 'Preparation',
title: "Preparation",
columns: 3,
options: [
{ key: 'porridge', label: 'Porridge' },
{ key: 'sliced', label: 'Sliced' },
{ key: 'spread', label: 'Spread' },
{ key: "porridge", label: "Porridge" },
{ key: "sliced", label: "Sliced" },
{ key: "spread", label: "Spread" },
],
},
spreads: {
title: 'Spreads',
title: "Spreads",
columns: 2,
options: [
{ key: 'butter', label: 'Butter' },
{ key: 'margarine', label: 'Margarine' },
{ key: 'jam', label: 'Jam' },
{ key: 'diabeticJam', label: 'Diabetic Jam' },
{ key: 'honey', label: 'Honey' },
{ key: 'cheese', label: 'Cheese' },
{ key: 'quark', label: 'Quark' },
{ key: 'sausage', label: 'Sausage' },
{ key: "butter", label: "Butter" },
{ key: "margarine", label: "Margarine" },
{ key: "jam", label: "Jam" },
{ key: "diabeticJam", label: "Diabetic Jam" },
{ key: "honey", label: "Honey" },
{ key: "cheese", label: "Cheese" },
{ key: "quark", label: "Quark" },
{ key: "sausage", label: "Sausage" },
],
},
beverages: {
title: 'Beverages',
title: "Beverages",
columns: 2,
options: [
{ key: 'coffee', label: 'Coffee' },
{ key: 'tea', label: 'Tea' },
{ key: 'hotMilk', label: 'Hot Milk' },
{ key: 'coldMilk', label: 'Cold Milk' },
{ key: "coffee", label: "Coffee" },
{ key: "tea", label: "Tea" },
{ key: "hotMilk", label: "Hot Milk" },
{ key: "coldMilk", label: "Cold Milk" },
],
},
additions: {
title: 'Additions',
title: "Additions",
columns: 3,
options: [
{ key: 'sugar', label: 'Sugar' },
{ key: 'sweetener', label: 'Sweetener' },
{ key: 'coffeeCreamer', label: 'Creamer' },
{ key: "sugar", label: "Sugar" },
{ key: "sweetener", label: "Sweetener" },
{ key: "coffeeCreamer", label: "Creamer" },
],
},
}
};
export const LUNCH_CONFIG = {
portionSizes: [
{ value: 'small', label: 'Small' },
{ value: 'large', label: 'Large' },
{ value: 'vegetarian', label: 'Vegetarian' },
{ value: "small", label: "Small" },
{ value: "large", label: "Large" },
{ value: "vegetarian", label: "Vegetarian" },
] as const,
mealOptions: {
title: 'Meal Options',
title: "Meal Options",
columns: 2 as const,
options: [
{ key: 'soup', label: 'Soup' },
{ key: 'dessert', label: 'Dessert' },
{ key: "soup", label: "Soup" },
{ key: "dessert", label: "Dessert" },
],
},
specialPreparations: {
title: 'Special Preparations',
title: "Special Preparations",
columns: 2 as const,
options: [
{ key: 'pureedFood', label: 'Pureed Food' },
{ key: 'pureedMeat', label: 'Pureed Meat' },
{ key: 'slicedMeat', label: 'Sliced Meat' },
{ key: 'mashedPotatoes', label: 'Mashed Potatoes' },
{ key: "pureedFood", label: "Pureed Food" },
{ key: "pureedMeat", label: "Pureed Meat" },
{ key: "slicedMeat", label: "Sliced Meat" },
{ key: "mashedPotatoes", label: "Mashed Potatoes" },
],
},
restrictions: {
title: 'Restrictions',
title: "Restrictions",
columns: 3 as const,
options: [
{ key: 'noFish', label: 'No Fish' },
{ key: 'fingerFood', label: 'Finger Food' },
{ key: 'onlySweet', label: 'Only Sweet' },
{ key: "noFish", label: "No Fish" },
{ key: "fingerFood", label: "Finger Food" },
{ key: "onlySweet", label: "Only Sweet" },
],
},
}
};
export const DINNER_CONFIG: Record<string, OptionSection> = {
bread: {
title: 'Bread',
title: "Bread",
columns: 2,
options: [
{ key: 'greyBread', label: 'Grey Bread' },
{ key: 'wholeGrainBread', label: 'Whole Grain' },
{ key: 'whiteBread', label: 'White Bread' },
{ key: 'crispbread', label: 'Crispbread' },
{ key: "greyBread", label: "Grey Bread" },
{ key: "wholeGrainBread", label: "Whole Grain" },
{ key: "whiteBread", label: "White Bread" },
{ key: "crispbread", label: "Crispbread" },
],
},
preparation: {
title: 'Preparation',
title: "Preparation",
columns: 2,
options: [
{ key: 'spread', label: 'Spread' },
{ key: 'sliced', label: 'Sliced' },
{ key: "spread", label: "Spread" },
{ key: "sliced", label: "Sliced" },
],
},
spreads: {
title: 'Spreads',
title: "Spreads",
columns: 2,
options: [
{ key: 'butter', label: 'Butter' },
{ key: 'margarine', label: 'Margarine' },
{ key: "butter", label: "Butter" },
{ key: "margarine", label: "Margarine" },
],
},
additionalItems: {
title: 'Additional Items',
title: "Additional Items",
columns: 3,
options: [
{ key: 'soup', label: 'Soup' },
{ key: 'porridge', label: 'Porridge' },
{ key: 'noFish', label: 'No Fish' },
{ key: "soup", label: "Soup" },
{ key: "porridge", label: "Porridge" },
{ key: "noFish", label: "No Fish" },
],
},
beverages: {
title: 'Beverages',
title: "Beverages",
columns: 2,
options: [
{ key: 'tea', label: 'Tea' },
{ key: 'cocoa', label: 'Cocoa' },
{ key: 'hotMilk', label: 'Hot Milk' },
{ key: 'coldMilk', label: 'Cold Milk' },
{ key: "tea", label: "Tea" },
{ key: "cocoa", label: "Cocoa" },
{ key: "hotMilk", label: "Hot Milk" },
{ key: "coldMilk", label: "Cold Milk" },
],
},
additions: {
title: 'Additions',
title: "Additions",
columns: 2,
options: [
{ key: 'sugar', label: 'Sugar' },
{ key: 'sweetener', label: 'Sweetener' },
{ key: "sugar", label: "Sugar" },
{ key: "sweetener", label: "Sweetener" },
],
},
}
};
export const getGridColsClass = (cols: 1 | 2 | 3): string => {
switch (cols) {
case 1:
return 'grid-cols-1'
return "grid-cols-1";
case 2:
return 'grid-cols-1 sm:grid-cols-2'
return "grid-cols-1 sm:grid-cols-2";
case 3:
return 'grid-cols-1 sm:grid-cols-3'
return "grid-cols-1 sm:grid-cols-3";
}
}
};

View File

@@ -1,88 +1,119 @@
import { Sunrise, Sun, Moon, Pencil, Send, ChefHat, Check, type LucideIcon } from 'lucide-react'
import {
Sunrise,
Sun,
Moon,
Pencil,
Send,
ChefHat,
Check,
type LucideIcon,
} from "lucide-react";
export type MealType = 'breakfast' | 'lunch' | 'dinner'
export type OrderStatus = 'draft' | 'submitted' | 'preparing' | 'completed'
export type MealType = "breakfast" | "lunch" | "dinner";
export type OrderStatus = "draft" | "submitted" | "preparing" | "completed";
export interface MealTypeConfig {
value: MealType
label: string
sublabel?: string
icon: LucideIcon
color: string
value: MealType;
label: string;
sublabel?: string;
icon: LucideIcon;
color: string;
}
export interface StatusConfig {
value: OrderStatus
label: string
icon: LucideIcon
bgColor: string
textColor: string
borderColor: string
dotColor: string
description: string
value: OrderStatus;
label: string;
icon: LucideIcon;
bgColor: string;
textColor: string;
borderColor: string;
dotColor: string;
description: string;
}
export const MEAL_TYPES: MealTypeConfig[] = [
{ value: 'breakfast', label: 'Breakfast', sublabel: 'Frühstück', icon: Sunrise, color: 'text-orange-500' },
{ value: 'lunch', label: 'Lunch', sublabel: 'Mittagessen', icon: Sun, color: 'text-yellow-500' },
{ value: 'dinner', label: 'Dinner', sublabel: 'Abendessen', icon: Moon, color: 'text-indigo-500' },
]
{
value: "breakfast",
label: "Breakfast",
sublabel: "Frühstück",
icon: Sunrise,
color: "text-orange-500",
},
{
value: "lunch",
label: "Lunch",
sublabel: "Mittagessen",
icon: Sun,
color: "text-yellow-500",
},
{
value: "dinner",
label: "Dinner",
sublabel: "Abendessen",
icon: Moon,
color: "text-indigo-500",
},
];
export const ORDER_STATUSES: StatusConfig[] = [
{
value: 'draft',
label: 'Draft',
value: "draft",
label: "Draft",
icon: Pencil,
bgColor: 'bg-gray-50',
textColor: 'text-gray-700',
borderColor: 'border-gray-200',
dotColor: 'bg-gray-400',
description: 'In progress',
bgColor: "bg-gray-50",
textColor: "text-gray-700",
borderColor: "border-gray-200",
dotColor: "bg-gray-400",
description: "In progress",
},
{
value: 'submitted',
label: 'Submitted',
value: "submitted",
label: "Submitted",
icon: Send,
bgColor: 'bg-blue-50',
textColor: 'text-blue-700',
borderColor: 'border-blue-200',
dotColor: 'bg-blue-500',
description: 'Sent to kitchen',
bgColor: "bg-blue-50",
textColor: "text-blue-700",
borderColor: "border-blue-200",
dotColor: "bg-blue-500",
description: "Sent to kitchen",
},
{
value: 'preparing',
label: 'Preparing',
value: "preparing",
label: "Preparing",
icon: ChefHat,
bgColor: 'bg-yellow-50',
textColor: 'text-yellow-700',
borderColor: 'border-yellow-200',
dotColor: 'bg-yellow-500',
description: 'Being prepared',
bgColor: "bg-yellow-50",
textColor: "text-yellow-700",
borderColor: "border-yellow-200",
dotColor: "bg-yellow-500",
description: "Being prepared",
},
{
value: 'completed',
label: 'Completed',
value: "completed",
label: "Completed",
icon: Check,
bgColor: 'bg-green-50',
textColor: 'text-green-700',
borderColor: 'border-green-200',
dotColor: 'bg-green-500',
description: 'Finished',
bgColor: "bg-green-50",
textColor: "text-green-700",
borderColor: "border-green-200",
dotColor: "bg-green-500",
description: "Finished",
},
]
];
export const getMealTypeConfig = (type: MealType): MealTypeConfig | undefined => {
return MEAL_TYPES.find((m) => m.value === type)
}
export const getMealTypeConfig = (
type: MealType,
): MealTypeConfig | undefined => {
return MEAL_TYPES.find((m) => m.value === type);
};
export const getStatusConfig = (status: OrderStatus): StatusConfig | undefined => {
return ORDER_STATUSES.find((s) => s.value === status)
}
export const getStatusConfig = (
status: OrderStatus,
): StatusConfig | undefined => {
return ORDER_STATUSES.find((s) => s.value === status);
};
export const getMealTypeLabel = (type: MealType): string => {
return getMealTypeConfig(type)?.label ?? type
}
return getMealTypeConfig(type)?.label ?? type;
};
export const getStatusLabel = (status: OrderStatus): string => {
return getStatusConfig(status)?.label ?? status
}
return getStatusConfig(status)?.label ?? status;
};

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}