diff --git a/src/app/(app)/caregiver/dashboard/page.tsx b/src/app/(app)/caregiver/dashboard/page.tsx index 912c663..a131db0 100644 --- a/src/app/(app)/caregiver/dashboard/page.tsx +++ b/src/app/(app)/caregiver/dashboard/page.tsx @@ -5,31 +5,24 @@ import { useRouter } from 'next/navigation' import Link from 'next/link' import { format, parseISO } from 'date-fns' import { - Loader2, LogOut, - Sun, - Moon, - Sunrise, ClipboardList, Users, Settings, Plus, Pencil, Eye, - Send, - Check, - ChefHat, AlertTriangle, ChevronLeft, ChevronRight, Calendar, UserCheck, UserX, + Check, } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Progress } from '@/components/ui/progress' @@ -64,6 +57,21 @@ import { } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' +import { + LoadingSpinner, + StatusBadge, + MealTypeIcon, + StatCard, + EmptyState, + MealTypeSelector, +} from '@/components/caregiver' +import { + ORDER_STATUSES, + getMealTypeLabel, + type MealType, + type OrderStatus, +} from '@/lib/constants/meal' + interface User { id: number name?: string @@ -78,8 +86,8 @@ interface MealOrder { id: number title: string date: string - mealType: 'breakfast' | 'lunch' | 'dinner' - status: 'draft' | 'submitted' | 'preparing' | 'completed' + mealType: MealType + status: OrderStatus mealCount: number createdAt: string } @@ -137,59 +145,58 @@ export default function CaregiverDashboardPage() { const [loading, setLoading] = useState(true) const [ordersLoading, setOrdersLoading] = useState(false) - // Filters const [dateFilter, setDateFilter] = useState('') const [statusFilter, setStatusFilter] = useState('all') - // Coverage dialog const [coverageDialogOpen, setCoverageDialogOpen] = useState(false) const [selectedOrder, setSelectedOrder] = useState(null) const [orderMeals, setOrderMeals] = useState([]) const [coverageLoading, setCoverageLoading] = useState(false) - // Create order dialog const [createDialogOpen, setCreateDialogOpen] = useState(false) const [newOrderDate, setNewOrderDate] = useState(() => format(new Date(), 'yyyy-MM-dd')) - const [newOrderMealType, setNewOrderMealType] = useState<'breakfast' | 'lunch' | 'dinner'>('breakfast') + const [newOrderMealType, setNewOrderMealType] = useState('breakfast') const [creating, setCreating] = useState(false) - const fetchOrders = useCallback(async (page: number = 1) => { - setOrdersLoading(true) - try { - let url = `/api/meal-orders?sort=-date,-createdAt&limit=${ITEMS_PER_PAGE}&page=${page}&depth=0` - if (dateFilter) { - url += `&where[date][equals]=${dateFilter}` - } - if (statusFilter !== 'all') { - url += `&where[status][equals]=${statusFilter}` - } + const fetchOrders = useCallback( + async (page: number = 1) => { + setOrdersLoading(true) + try { + let url = `/api/meal-orders?sort=-date,-createdAt&limit=${ITEMS_PER_PAGE}&page=${page}&depth=0` + if (dateFilter) { + url += `&where[date][equals]=${dateFilter}` + } + if (statusFilter !== 'all') { + url += `&where[status][equals]=${statusFilter}` + } - const res = await fetch(url, { credentials: 'include' }) - if (res.ok) { - const data = await res.json() - setOrders(data.docs || []) - setPagination({ - totalDocs: data.totalDocs || 0, - totalPages: data.totalPages || 1, - page: data.page || 1, - limit: data.limit || ITEMS_PER_PAGE, - hasNextPage: data.hasNextPage || false, - hasPrevPage: data.hasPrevPage || false, - }) - } else if (res.status === 401) { - router.push('/caregiver/login') + const res = await fetch(url, { credentials: 'include' }) + if (res.ok) { + const data = await res.json() + setOrders(data.docs || []) + setPagination({ + totalDocs: data.totalDocs || 0, + totalPages: data.totalPages || 1, + page: data.page || 1, + limit: data.limit || ITEMS_PER_PAGE, + hasNextPage: data.hasNextPage || false, + hasPrevPage: data.hasPrevPage || false, + }) + } else if (res.status === 401) { + router.push('/caregiver/login') + } + } catch (err) { + console.error('Error fetching orders:', err) + } finally { + setOrdersLoading(false) } - } catch (err) { - console.error('Error fetching orders:', err) - } finally { - setOrdersLoading(false) - } - }, [router, dateFilter, statusFilter]) + }, + [router, dateFilter, statusFilter], + ) useEffect(() => { const fetchInitialData = async () => { try { - // Fetch current user const userRes = await fetch('/api/users/me', { credentials: 'include' }) if (!userRes.ok) { router.push('/caregiver/login') @@ -202,7 +209,6 @@ export default function CaregiverDashboardPage() { } setUser(userData.user) - // Fetch residents const residentsRes = await fetch('/api/residents?where[active][equals]=true&limit=500', { credentials: 'include', }) @@ -211,12 +217,11 @@ export default function CaregiverDashboardPage() { setResidents(residentsData.docs || []) } - // Fetch stats (all orders from last 7 days) const weekAgo = new Date() weekAgo.setDate(weekAgo.getDate() - 7) const statsRes = await fetch( `/api/meal-orders?where[date][greater_than_equal]=${weekAgo.toISOString().split('T')[0]}&limit=1000&depth=0`, - { credentials: 'include' } + { credentials: 'include' }, ) if (statsRes.ok) { const statsData = await statsRes.json() @@ -284,10 +289,9 @@ export default function CaregiverDashboardPage() { setCoverageLoading(true) try { - const res = await fetch( - `/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`, - { credentials: 'include' } - ) + const res = await fetch(`/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`, { + credentials: 'include', + }) if (res.ok) { const data = await res.json() setOrderMeals(data.docs || []) @@ -299,67 +303,6 @@ export default function CaregiverDashboardPage() { } } - const getMealTypeLabel = (type: string) => { - switch (type) { - case 'breakfast': - return 'Breakfast' - case 'lunch': - return 'Lunch' - case 'dinner': - return 'Dinner' - default: - return type - } - } - - const getMealTypeIcon = (type: string) => { - switch (type) { - case 'breakfast': - return - case 'lunch': - return - case 'dinner': - return - default: - return null - } - } - - const getStatusBadge = (status: string) => { - switch (status) { - case 'draft': - return ( - - - Draft - - ) - case 'submitted': - return ( - - - Submitted - - ) - case 'preparing': - return ( - - - Preparing - - ) - case 'completed': - return ( - - - Completed - - ) - default: - return {status} - } - } - const getCoverageInfo = (order: MealOrder) => { const totalResidents = residents.length const coveredCount = order.mealCount @@ -379,11 +322,7 @@ export default function CaregiverDashboardPage() { } if (loading) { - return ( -
- -
- ) + return } const tenantName = @@ -391,9 +330,8 @@ export default function CaregiverDashboardPage() { ? user.tenants[0].tenant.name : 'Care Home' - // Calculate covered residents for coverage dialog const coveredResidentIds = new Set( - orderMeals.map((m) => (typeof m.resident === 'object' ? m.resident.id : m.resident)) + orderMeals.map((m) => (typeof m.resident === 'object' ? m.resident.id : m.resident)), ) const coveredResidents = residents.filter((r) => coveredResidentIds.has(r.id)) const uncoveredResidents = residents.filter((r) => !coveredResidentIds.has(r.id)) @@ -425,61 +363,24 @@ export default function CaregiverDashboardPage() { - {/* Stats Cards */}
- - - Total Orders - - - -
{stats.total}
-

Last 7 days

-
-
- - - Draft -
- - -
{stats.draft}
-

In progress

-
- - - - Submitted -
- - -
{stats.submitted}
-

Sent to kitchen

-
- - - - Preparing -
- - -
{stats.preparing}
-

Being prepared

-
- - - - Completed -
- - -
{stats.completed}
-

Finished

-
- + + {ORDER_STATUSES.map((status) => ( + + ))}
- {/* Quick Actions */} Quick Actions @@ -523,7 +424,6 @@ export default function CaregiverDashboardPage() { - {/* Meal Orders Table */}
@@ -534,29 +434,28 @@ export default function CaregiverDashboardPage() {
-
- - setDateFilter(e.target.value)} - className="w-40" - placeholder="Filter by date" - /> -
+ + setDateFilter(e.target.value)} + className="w-40" + placeholder="Filter by date" + /> {(dateFilter || statusFilter !== 'all') && ( @@ -576,21 +475,19 @@ export default function CaregiverDashboardPage() { {ordersLoading ? ( -
- -
+ ) : orders.length === 0 ? ( -
- -

No orders found.

-

- Create a new order to get started. -

- -
+ setCreateDialogOpen(true)}> + + Create Order + + } + /> ) : ( <> @@ -609,12 +506,7 @@ export default function CaregiverDashboardPage() { return ( -
- {getMealTypeIcon(order.mealType)} - - {getMealTypeLabel(order.mealType)} - -
+
@@ -631,10 +523,7 @@ export default function CaregiverDashboardPage() { onClick={() => handleViewCoverage(order)} >
- + {coverage.coveredCount}/{coverage.totalResidents} @@ -655,25 +544,25 @@ export default function CaregiverDashboardPage() { - {getStatusBadge(order.status)} + + + -
- {order.status === 'draft' ? ( - - ) : ( - - )} -
+ + )} + +
) @@ -681,7 +570,6 @@ export default function CaregiverDashboardPage() {
- {/* Pagination */} {pagination.totalPages > 1 && (
@@ -743,14 +631,11 @@ export default function CaregiverDashboardPage() { - {/* Create Order Dialog */} Create New Meal Order - - Select a date and meal type to create a new order. - + Select a date and meal type to create a new order.
@@ -764,28 +649,7 @@ export default function CaregiverDashboardPage() {
-
- {[ - { value: 'breakfast', label: 'Breakfast', icon: Sunrise, color: 'text-orange-500' }, - { value: 'lunch', label: 'Lunch', icon: Sun, color: 'text-yellow-500' }, - { value: 'dinner', label: 'Dinner', icon: Moon, color: 'text-indigo-500' }, - ].map(({ value, label, icon: Icon, color }) => ( - - ))} -
+
@@ -794,22 +658,16 @@ export default function CaregiverDashboardPage() {
- {/* Coverage Dialog */} @@ -827,12 +685,9 @@ export default function CaregiverDashboardPage() { {coverageLoading ? ( -
- -
+ ) : (
- {/* Coverage Summary */}
@@ -843,9 +698,7 @@ export default function CaregiverDashboardPage() {
0 - ? (coveredResidents.length / residents.length) * 100 - : 0 + residents.length > 0 ? (coveredResidents.length / residents.length) * 100 : 0 } className="h-3" /> @@ -856,8 +709,8 @@ export default function CaregiverDashboardPage() { getCoverageColor( residents.length > 0 ? (coveredResidents.length / residents.length) * 100 - : 0 - ) + : 0, + ), )} > {residents.length > 0 @@ -867,7 +720,6 @@ export default function CaregiverDashboardPage() {
- {/* Uncovered Residents */} {uncoveredResidents.length > 0 && (
@@ -893,7 +745,6 @@ export default function CaregiverDashboardPage() {
)} - {/* Covered Residents */} {coveredResidents.length > 0 && (
diff --git a/src/app/(app)/caregiver/orders/[id]/page.tsx b/src/app/(app)/caregiver/orders/[id]/page.tsx index 5916551..4a9dcca 100644 --- a/src/app/(app)/caregiver/orders/[id]/page.tsx +++ b/src/app/(app)/caregiver/orders/[id]/page.tsx @@ -5,8 +5,6 @@ import { useRouter, useParams } from 'next/navigation' import Link from 'next/link' import { formatISO, format, parseISO } from 'date-fns' import { - ArrowLeft, - Loader2, Search, Plus, Check, @@ -23,6 +21,7 @@ import { ImageIcon, Sparkles, Camera, + Loader2, } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -30,7 +29,6 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' -import { Checkbox } from '@/components/ui/checkbox' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Separator } from '@/components/ui/separator' @@ -69,6 +67,27 @@ import { } from '@/components/ui/alert-dialog' import { cn } from '@/lib/utils' +import { + PageHeader, + LoadingSpinner, + StatusBadge, + StatCard, + CheckboxOption, +} from '@/components/caregiver' +import { getMealTypeLabel, type MealType, type OrderStatus } from '@/lib/constants/meal' +import { + type BreakfastOptions, + type LunchOptions, + type DinnerOptions, + DEFAULT_BREAKFAST, + DEFAULT_LUNCH, + DEFAULT_DINNER, + BREAKFAST_CONFIG, + LUNCH_CONFIG, + DINNER_CONFIG, + getGridColsClass, +} from '@/lib/constants/meal-options' + interface Resident { id: number name: string @@ -91,7 +110,7 @@ interface MediaFile { interface Meal { id: number resident: Resident | number - mealType: 'breakfast' | 'lunch' | 'dinner' + mealType: MealType status: 'pending' | 'preparing' | 'prepared' formImage?: MediaFile | number breakfast?: BreakfastOptions @@ -103,329 +122,12 @@ interface MealOrder { id: number title: string date: string - mealType: 'breakfast' | 'lunch' | 'dinner' - status: 'draft' | 'submitted' | 'preparing' | 'completed' + mealType: MealType + status: OrderStatus mealCount: number notes?: string } -interface BreakfastOptions { - accordingToPlan: boolean - bread: { - 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 } -} - -interface LunchOptions { - portionSize: 'small' | 'large' | 'vegetarian' - soup: boolean - dessert: boolean - specialPreparations: { - pureedFood: boolean - pureedMeat: boolean - slicedMeat: boolean - mashedPotatoes: boolean - } - restrictions: { noFish: boolean; fingerFood: boolean; onlySweet: boolean } -} - -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 } -} - -const defaultBreakfast: BreakfastOptions = { - accordingToPlan: false, - bread: { - breadRoll: false, - wholeGrainRoll: false, - greyBread: false, - wholeGrainBread: false, - whiteBread: false, - crispbread: false, - }, - porridge: false, - preparation: { sliced: false, spread: false }, - spreads: { - butter: false, - margarine: false, - jam: false, - diabeticJam: false, - honey: false, - cheese: false, - quark: false, - sausage: false, - }, - beverages: { coffee: false, tea: false, hotMilk: false, coldMilk: false }, - additions: { sugar: false, sweetener: false, coffeeCreamer: false }, -} - -const defaultLunch: LunchOptions = { - portionSize: 'large', - soup: false, - dessert: true, - specialPreparations: { - pureedFood: false, - pureedMeat: false, - slicedMeat: false, - mashedPotatoes: false, - }, - restrictions: { noFish: false, fingerFood: false, onlySweet: false }, -} - -const defaultDinner: DinnerOptions = { - accordingToPlan: false, - bread: { greyBread: false, wholeGrainBread: false, whiteBread: false, crispbread: false }, - preparation: { spread: false, sliced: false }, - spreads: { butter: false, margarine: false }, - soup: false, - porridge: false, - noFish: false, - beverages: { tea: false, cocoa: false, hotMilk: false, coldMilk: false }, - additions: { sugar: false, sweetener: false }, -} - -// ============================================ -// MEAL OPTIONS CONFIGURATION -// ============================================ - -interface OptionItem { - key: string - label: string -} - -interface OptionSection { - title: string - columns: 1 | 2 | 3 - options: OptionItem[] -} - -// Breakfast configuration -const breakfastConfig: Record = { - bread: { - 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' }, - ], - }, - preparation: { - title: 'Preparation', - columns: 3, - options: [ - { key: 'porridge', label: 'Porridge' }, - { key: 'sliced', label: 'Sliced' }, - { key: 'spread', label: 'Spread' }, - ], - }, - 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' }, - ], - }, - beverages: { - title: 'Beverages', - columns: 2, - options: [ - { key: 'coffee', label: 'Coffee' }, - { key: 'tea', label: 'Tea' }, - { key: 'hotMilk', label: 'Hot Milk' }, - { key: 'coldMilk', label: 'Cold Milk' }, - ], - }, - additions: { - title: 'Additions', - columns: 3, - options: [ - { key: 'sugar', label: 'Sugar' }, - { key: 'sweetener', label: 'Sweetener' }, - { key: 'coffeeCreamer', label: 'Creamer' }, - ], - }, -} - -// Lunch configuration -const lunchConfig = { - portionSizes: [ - { value: 'small', label: 'Small' }, - { value: 'large', label: 'Large' }, - { value: 'vegetarian', label: 'Vegetarian' }, - ] as const, - mealOptions: { - title: 'Meal Options', - columns: 2 as const, - options: [ - { key: 'soup', label: 'Soup' }, - { key: 'dessert', label: 'Dessert' }, - ], - }, - specialPreparations: { - 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' }, - ], - }, - restrictions: { - title: 'Restrictions', - columns: 3 as const, - options: [ - { key: 'noFish', label: 'No Fish' }, - { key: 'fingerFood', label: 'Finger Food' }, - { key: 'onlySweet', label: 'Only Sweet' }, - ], - }, -} - -// Dinner configuration -const dinnerConfig: Record = { - 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' }, - ], - }, - preparation: { - title: 'Preparation', - columns: 2, - options: [ - { key: 'spread', label: 'Spread' }, - { key: 'sliced', label: 'Sliced' }, - ], - }, - spreads: { - title: 'Spreads', - columns: 2, - options: [ - { key: 'butter', label: 'Butter' }, - { key: 'margarine', label: 'Margarine' }, - ], - }, - additionalItems: { - title: 'Additional Items', - columns: 3, - options: [ - { key: 'soup', label: 'Soup' }, - { key: 'porridge', label: 'Porridge' }, - { key: 'noFish', label: 'No Fish' }, - ], - }, - beverages: { - title: 'Beverages', - columns: 2, - options: [ - { key: 'tea', label: 'Tea' }, - { key: 'cocoa', label: 'Cocoa' }, - { key: 'hotMilk', label: 'Hot Milk' }, - { key: 'coldMilk', label: 'Cold Milk' }, - ], - }, - additions: { - title: 'Additions', - columns: 2, - options: [ - { key: 'sugar', label: 'Sugar' }, - { key: 'sweetener', label: 'Sweetener' }, - ], - }, -} - -// Helper to get grid columns class -const getGridCols = (cols: 1 | 2 | 3) => { - switch (cols) { - case 1: return 'grid-cols-1' - case 2: return 'grid-cols-1 sm:grid-cols-2' - case 3: return 'grid-cols-1 sm:grid-cols-3' - } -} - -function CheckboxOption({ - id, - label, - checked, - onCheckedChange, -}: { - id: string - label: string - checked: boolean - onCheckedChange: (checked: boolean) => void -}) { - return ( -
onCheckedChange(!checked)} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onCheckedChange(!checked) - } - }} - > - e.stopPropagation()} - /> - -
- ) -} - export default function OrderDetailPage() { const router = useRouter() const params = useParams() @@ -439,16 +141,14 @@ export default function OrderDetailPage() { const [showSummary, setShowSummary] = useState(false) const [submitting, setSubmitting] = useState(false) - // Meal form state const [editingMeal, setEditingMeal] = useState(null) const [selectedResident, setSelectedResident] = useState(null) const [showMealForm, setShowMealForm] = useState(false) - const [breakfast, setBreakfast] = useState(defaultBreakfast) - const [lunch, setLunch] = useState(defaultLunch) - const [dinner, setDinner] = useState(defaultDinner) + const [breakfast, setBreakfast] = useState(DEFAULT_BREAKFAST) + const [lunch, setLunch] = useState(DEFAULT_LUNCH) + const [dinner, setDinner] = useState(DEFAULT_DINNER) const [savingMeal, setSavingMeal] = useState(false) - // Image upload state const [formImageFile, setFormImageFile] = useState(null) const [formImagePreview, setFormImagePreview] = useState(null) const [formImageId, setFormImageId] = useState(null) @@ -457,7 +157,6 @@ export default function OrderDetailPage() { const [analyzingImage, setAnalyzingImage] = useState(false) const [analysisError, setAnalysisError] = useState(null) - // Delete confirmation state const [mealToDelete, setMealToDelete] = useState(null) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [deletingMeal, setDeletingMeal] = useState(false) @@ -465,10 +164,7 @@ export default function OrderDetailPage() { const fetchData = useCallback(async () => { setLoading(true) try { - // Fetch order details - const orderRes = await fetch(`/api/meal-orders/${orderId}`, { - credentials: 'include', - }) + const orderRes = await fetch(`/api/meal-orders/${orderId}`, { credentials: 'include' }) if (!orderRes.ok) { if (orderRes.status === 401) { router.push('/caregiver/login') @@ -479,17 +175,14 @@ export default function OrderDetailPage() { const orderData = await orderRes.json() setOrder(orderData) - // Fetch meals for this order - const mealsRes = await fetch( - `/api/meals?where[order][equals]=${orderId}&depth=1&limit=100`, - { credentials: 'include' }, - ) + const mealsRes = await fetch(`/api/meals?where[order][equals]=${orderId}&depth=1&limit=100`, { + credentials: 'include', + }) if (mealsRes.ok) { const mealsData = await mealsRes.json() setMeals(mealsData.docs || []) } - // Fetch all active residents const residentsRes = await fetch( '/api/residents?where[active][equals]=true&limit=100&sort=name', { credentials: 'include' }, @@ -511,8 +204,7 @@ export default function OrderDetailPage() { const getResidentMeal = (residentId: number): Meal | undefined => { return meals.find((meal) => { - const mealResidentId = - typeof meal.resident === 'object' ? meal.resident.id : meal.resident + const mealResidentId = typeof meal.resident === 'object' ? meal.resident.id : meal.resident return mealResidentId === residentId }) } @@ -525,31 +217,17 @@ export default function OrderDetailPage() { const coveredResidents = residents.filter((r) => getResidentMeal(r.id)) const uncoveredResidents = residents.filter((r) => !getResidentMeal(r.id)) - const coveragePercent = residents.length > 0 ? Math.round((coveredResidents.length / residents.length) * 100) : 0 - - const getMealTypeLabel = (type: string) => { - switch (type) { - case 'breakfast': - return 'Breakfast' - case 'lunch': - return 'Lunch' - case 'dinner': - return 'Dinner' - default: - return type - } - } + const coveragePercent = + residents.length > 0 ? Math.round((coveredResidents.length / residents.length) * 100) : 0 const openMealForm = (resident: Resident, existingMeal?: Meal) => { setSelectedResident(resident) setEditingMeal(existingMeal || null) if (existingMeal) { - // Load existing meal data if (existingMeal.breakfast) setBreakfast(existingMeal.breakfast) if (existingMeal.lunch) setLunch(existingMeal.lunch) if (existingMeal.dinner) setDinner(existingMeal.dinner) - // Load existing form image if (existingMeal.formImage && typeof existingMeal.formImage === 'object') { setExistingFormImage(existingMeal.formImage) setFormImageId(existingMeal.formImage.id) @@ -557,10 +235,9 @@ export default function OrderDetailPage() { setFormImageId(existingMeal.formImage) } } else { - // Reset to defaults - setBreakfast(defaultBreakfast) - setLunch(defaultLunch) - setDinner(defaultDinner) + setBreakfast(DEFAULT_BREAKFAST) + setLunch(DEFAULT_LUNCH) + setDinner(DEFAULT_DINNER) } setShowMealForm(true) @@ -570,7 +247,6 @@ export default function OrderDetailPage() { setShowMealForm(false) setSelectedResident(null) setEditingMeal(null) - // Reset image state setFormImageFile(null) setFormImagePreview(null) setFormImageId(null) @@ -586,7 +262,6 @@ export default function OrderDetailPage() { setExistingFormImage(null) setAnalysisError(null) - // Create preview const reader = new FileReader() reader.onload = (event) => { setFormImagePreview(event.target?.result as string) @@ -621,10 +296,8 @@ export default function OrderDetailPage() { const data = await res.json() setFormImageId(data.doc.id) return data.doc.id - } else { - console.error('Failed to upload image') - return null } + return null } catch (err) { console.error('Error uploading image:', err) return null @@ -643,7 +316,6 @@ export default function OrderDetailPage() { const imageData: { imageBase64?: string; imageUrl?: string } = {} if (formImagePreview) { - // Extract base64 data from the data URL const base64Match = formImagePreview.match(/^data:image\/[^;]+;base64,(.+)$/) if (base64Match) { imageData.imageBase64 = base64Match[1] @@ -665,7 +337,6 @@ export default function OrderDetailPage() { if (res.ok) { const analysis = await res.json() - // Apply the analysis results to the form if (analysis.confidence > 50) { if (order?.mealType === 'breakfast' && analysis.breakfast) { setBreakfast((prev) => ({ @@ -681,7 +352,10 @@ export default function OrderDetailPage() { setLunch((prev) => ({ ...prev, ...analysis.lunch, - specialPreparations: { ...prev.specialPreparations, ...analysis.lunch?.specialPreparations }, + specialPreparations: { + ...prev.specialPreparations, + ...analysis.lunch?.specialPreparations, + }, restrictions: { ...prev.restrictions, ...analysis.lunch?.restrictions }, })) } else if (order?.mealType === 'dinner' && analysis.dinner) { @@ -696,7 +370,9 @@ export default function OrderDetailPage() { })) } } else { - setAnalysisError(`Low confidence (${analysis.confidence}%). Please check the options manually.`) + setAnalysisError( + `Low confidence (${analysis.confidence}%). Please check the options manually.`, + ) } } else { const error = await res.json() @@ -715,7 +391,6 @@ export default function OrderDetailPage() { setSavingMeal(true) try { - // Upload image first if there's a new one let imageId = formImageId if (formImageFile) { imageId = await handleUploadImage() @@ -740,7 +415,6 @@ export default function OrderDetailPage() { let res if (editingMeal) { - // Update existing meal res = await fetch(`/api/meals/${editingMeal.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, @@ -748,7 +422,6 @@ export default function OrderDetailPage() { credentials: 'include', }) } else { - // Create new meal res = await fetch('/api/meals', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -758,7 +431,6 @@ export default function OrderDetailPage() { } if (res.ok) { - // Update order meal count await fetch(`/api/meal-orders/${order.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, @@ -768,9 +440,6 @@ export default function OrderDetailPage() { closeMealForm() fetchData() - } else { - const data = await res.json() - console.error('Error saving meal:', data) } } catch (err) { console.error('Error saving meal:', err) @@ -795,14 +464,12 @@ export default function OrderDetailPage() { }) if (res.ok) { - // Update order meal count await fetch(`/api/meal-orders/${order.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mealCount: meals.length - 1 }), credentials: 'include', }) - fetchData() } } catch (err) { @@ -880,11 +547,7 @@ export default function OrderDetailPage() { } if (loading) { - return ( -
- -
- ) + return } if (!order) { @@ -910,96 +573,41 @@ export default function OrderDetailPage() { return (
-
-
-
- -
-

{order.title}

-

- {format(parseISO(order.date), 'EEEE, MMM d, yyyy')} -

-
-
-
- - {order.status.charAt(0).toUpperCase() + order.status.slice(1)} - -
-
-
+ } + />
- {/* Coverage Stats */}
- - -
-
- -
-
-

Total Residents

-

{residents.length}

-
-
-
-
- - -
-
- -
-
-

Meals Created

-

{coveredResidents.length}

-
-
-
-
- - -
-
- -
-
-

Missing Meals

-

{uncoveredResidents.length}

-
-
-
-
- - -
-
- -
-
-

Coverage

-

{coveragePercent}%

-
-
-
-
+ + + +
- {/* Progress Bar */}
@@ -1020,7 +628,6 @@ export default function OrderDetailPage() { - {/* Residents Table */}
@@ -1151,10 +758,9 @@ export default function OrderDetailPage() {
- {/* Meal Form Sheet */} - - + + {editingMeal ? 'Edit' : 'Add'} {getMealTypeLabel(order.mealType)} Meal @@ -1163,8 +769,7 @@ export default function OrderDetailPage() { -
- {/* Image Upload Section */} +
{isDraft && (
@@ -1253,7 +858,6 @@ export default function OrderDetailPage() {
)} - {/* Show existing image in view mode */} {!isDraft && existingFormImage && (

@@ -1273,7 +877,9 @@ export default function OrderDetailPage() { - {(selectedResident?.aversions || selectedResident?.notes || selectedResident?.highCaloric) && ( + {(selectedResident?.aversions || + selectedResident?.notes || + selectedResident?.highCaloric) && ( Notes for {selectedResident?.name} @@ -1281,13 +887,14 @@ export default function OrderDetailPage() { {selectedResident?.highCaloric && (
High caloric requirement
)} - {selectedResident?.aversions &&
Aversions: {selectedResident.aversions}
} + {selectedResident?.aversions && ( +
Aversions: {selectedResident.aversions}
+ )} {selectedResident?.notes &&
{selectedResident.notes}
}
)} - {/* BREAKFAST OPTIONS */} {order.mealType === 'breakfast' && ( <>
@@ -1300,138 +907,89 @@ export default function OrderDetailPage() { />
- {/* Bread */} - -
-

{breakfastConfig.bread.title}

-
- {breakfastConfig.bread.options.map(({ key, label }) => ( - setBreakfast({ ...breakfast, bread: { ...breakfast.bread, [key]: v } })} - /> - ))} -
-
- - {/* Preparation */} - -
-

{breakfastConfig.preparation.title}

-
- {breakfastConfig.preparation.options.map(({ key, label }) => { - const isTopLevel = key === 'porridge' - const checked = isTopLevel - ? breakfast.porridge - : breakfast.preparation[key as keyof typeof breakfast.preparation] - return ( - { - if (isTopLevel) { - setBreakfast({ ...breakfast, porridge: v }) - } else { - setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, [key]: v } }) - } - }} - /> - ) - })} -
-
- - {/* Spreads */} - -
-

{breakfastConfig.spreads.title}

-
- {breakfastConfig.spreads.options.map(({ key, label }) => ( - setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, [key]: v } })} - /> - ))} -
-
- - {/* Beverages */} - -
-

{breakfastConfig.beverages.title}

-
- {breakfastConfig.beverages.options.map(({ key, label }) => ( - setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, [key]: v } })} - /> - ))} -
-
- - {/* Additions */} - -
-

{breakfastConfig.additions.title}

-
- {breakfastConfig.additions.options.map(({ key, label }) => ( - setBreakfast({ ...breakfast, additions: { ...breakfast.additions, [key]: v } })} - /> - ))} -
-
+ {Object.entries(BREAKFAST_CONFIG).map(([key, section]) => ( + + +
+

{section.title}

+
+ {section.options.map(({ key: optKey, label }) => { + const isTopLevel = optKey === 'porridge' + const checked = isTopLevel + ? breakfast.porridge + : key === 'preparation' && (optKey === 'sliced' || optKey === 'spread') + ? breakfast.preparation[optKey as keyof typeof breakfast.preparation] + : (breakfast[key as keyof BreakfastOptions] as Record)?.[optKey] + return ( + { + if (isTopLevel) { + setBreakfast({ ...breakfast, porridge: v }) + } else if (key === 'preparation' && (optKey === 'sliced' || optKey === 'spread')) { + setBreakfast({ + ...breakfast, + preparation: { ...breakfast.preparation, [optKey]: v }, + }) + } else { + setBreakfast({ + ...breakfast, + [key]: { + ...(breakfast[key as keyof BreakfastOptions] as Record), + [optKey]: v, + }, + }) + } + }} + /> + ) + })} +
+
+
+ ))} )} - {/* LUNCH OPTIONS */} {order.mealType === 'lunch' && ( <> - {/* Portion Size */}

Portion Size

setLunch({ ...lunch, portionSize: v as 'small' | 'large' | 'vegetarian' })} + onValueChange={(v) => + setLunch({ ...lunch, portionSize: v as 'small' | 'large' | 'vegetarian' }) + } className="grid gap-2 grid-cols-1 sm:grid-cols-3" > - {lunchConfig.portionSizes.map(({ value, label }) => ( + {LUNCH_CONFIG.portionSizes.map(({ value, label }) => (
setLunch({ ...lunch, portionSize: value })} > - +
))}
- {/* Meal Options */}
-

{lunchConfig.mealOptions.title}

-
- {lunchConfig.mealOptions.options.map(({ key, label }) => ( +

{LUNCH_CONFIG.mealOptions.title}

+
+ {LUNCH_CONFIG.mealOptions.options.map(({ key, label }) => (
- {/* Special Preparations */}
-

{lunchConfig.specialPreparations.title}

-
- {lunchConfig.specialPreparations.options.map(({ key, label }) => ( +

{LUNCH_CONFIG.specialPreparations.title}

+
+ {LUNCH_CONFIG.specialPreparations.options.map(({ key, label }) => ( setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, [key]: v } })} + onCheckedChange={(v) => + setLunch({ + ...lunch, + specialPreparations: { ...lunch.specialPreparations, [key]: v }, + }) + } /> ))}
- {/* Restrictions */}
-

{lunchConfig.restrictions.title}

-
- {lunchConfig.restrictions.options.map(({ key, label }) => ( +

{LUNCH_CONFIG.restrictions.title}

+
+ {LUNCH_CONFIG.restrictions.options.map(({ key, label }) => ( setLunch({ ...lunch, restrictions: { ...lunch.restrictions, [key]: v } })} + onCheckedChange={(v) => + setLunch({ ...lunch, restrictions: { ...lunch.restrictions, [key]: v } }) + } /> ))}
@@ -1479,7 +1042,6 @@ export default function OrderDetailPage() { )} - {/* DINNER OPTIONS */} {order.mealType === 'dinner' && ( <>
@@ -1492,114 +1054,49 @@ export default function OrderDetailPage() { />
- {/* Bread */} - -
-

{dinnerConfig.bread.title}

-
- {dinnerConfig.bread.options.map(({ key, label }) => ( - setDinner({ ...dinner, bread: { ...dinner.bread, [key]: v } })} - /> - ))} -
-
- - {/* Preparation */} - -
-

{dinnerConfig.preparation.title}

-
- {dinnerConfig.preparation.options.map(({ key, label }) => ( - setDinner({ ...dinner, preparation: { ...dinner.preparation, [key]: v } })} - /> - ))} -
-
- - {/* Spreads */} - -
-

{dinnerConfig.spreads.title}

-
- {dinnerConfig.spreads.options.map(({ key, label }) => ( - setDinner({ ...dinner, spreads: { ...dinner.spreads, [key]: v } })} - /> - ))} -
-
- - {/* Additional Items */} - -
-

{dinnerConfig.additionalItems.title}

-
- {dinnerConfig.additionalItems.options.map(({ key, label }) => ( - ]} - onCheckedChange={(v) => setDinner({ ...dinner, [key]: v })} - /> - ))} -
-
- - {/* Beverages */} - -
-

{dinnerConfig.beverages.title}

-
- {dinnerConfig.beverages.options.map(({ key, label }) => ( - setDinner({ ...dinner, beverages: { ...dinner.beverages, [key]: v } })} - /> - ))} -
-
- - {/* Additions */} - -
-

{dinnerConfig.additions.title}

-
- {dinnerConfig.additions.options.map(({ key, label }) => ( - setDinner({ ...dinner, additions: { ...dinner.additions, [key]: v } })} - /> - ))} -
-
+ {Object.entries(DINNER_CONFIG).map(([key, section]) => ( + + +
+

{section.title}

+
+ {section.options.map(({ key: optKey, label }) => { + const isTopLevel = ['soup', 'porridge', 'noFish'].includes(optKey) + const checked = isTopLevel + ? dinner[optKey as keyof Pick] + : (dinner[key as keyof DinnerOptions] as Record)?.[optKey] + return ( + { + if (isTopLevel) { + setDinner({ ...dinner, [optKey]: v }) + } else { + setDinner({ + ...dinner, + [key]: { + ...(dinner[key as keyof DinnerOptions] as Record), + [optKey]: v, + }, + }) + } + }} + /> + ) + })} +
+
+
+ ))} )} -
{isDraft && ( -
+
@@ -1621,7 +1118,6 @@ export default function OrderDetailPage() { - {/* Summary Dialog */} @@ -1648,8 +1144,7 @@ export default function OrderDetailPage() {
{meals.map((meal) => { - const resident = - typeof meal.resident === 'object' ? meal.resident : null + const resident = typeof meal.resident === 'object' ? meal.resident : null return (
- {/* Delete Meal Confirmation Dialog */} diff --git a/src/app/(app)/caregiver/orders/page.tsx b/src/app/(app)/caregiver/orders/page.tsx index a8309b8..e1f1974 100644 --- a/src/app/(app)/caregiver/orders/page.tsx +++ b/src/app/(app)/caregiver/orders/page.tsx @@ -4,13 +4,12 @@ import React, { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import Link from 'next/link' import { format, parseISO } from 'date-fns' -import { ArrowLeft, Plus, Loader2, Send, Eye, Pencil, Check, ChefHat } from 'lucide-react' +import { Plus, Pencil, Eye } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { Badge } from '@/components/ui/badge' import { Select, SelectContent, @@ -27,12 +26,26 @@ import { TableRow, } from '@/components/ui/table' +import { + PageHeader, + LoadingSpinner, + StatusBadge, + MealTypeIcon, + EmptyState, +} from '@/components/caregiver' +import { + ORDER_STATUSES, + getMealTypeLabel, + type MealType, + type OrderStatus, +} from '@/lib/constants/meal' + interface MealOrder { id: number title: string date: string - mealType: 'breakfast' | 'lunch' | 'dinner' - status: 'draft' | 'submitted' | 'preparing' | 'completed' + mealType: MealType + status: OrderStatus mealCount: number createdAt: string } @@ -72,55 +85,7 @@ export default function OrdersListPage() { fetchOrders() }, [router, dateFilter, statusFilter]) - const getMealTypeLabel = (type: string) => { - switch (type) { - case 'breakfast': - return 'Breakfast' - case 'lunch': - return 'Lunch' - case 'dinner': - return 'Dinner' - default: - return type - } - } - - const getStatusBadge = (status: string) => { - switch (status) { - case 'draft': - return ( - - - Draft - - ) - case 'submitted': - return ( - - - Submitted - - ) - case 'preparing': - return ( - - - Preparing - - ) - case 'completed': - return ( - - - Completed - - ) - default: - return {status} - } - } - - const handleCreateOrder = async (mealType: 'breakfast' | 'lunch' | 'dinner') => { + const handleCreateOrder = async (mealType: MealType) => { try { const res = await fetch('/api/meal-orders', { method: 'POST', @@ -144,19 +109,7 @@ export default function OrdersListPage() { return (
-
-
-
- -

Meal Orders

-
-
-
+
@@ -182,43 +135,29 @@ export default function OrdersListPage() { All Statuses - Draft - Submitted - Preparing - Completed + {ORDER_STATUSES.map((status) => ( + + {status.label} + + ))}
- - - + {(['breakfast', 'lunch', 'dinner'] as const).map((type) => ( + + ))}
@@ -228,16 +167,12 @@ export default function OrdersListPage() { {loading ? ( -
- -
+ ) : orders.length === 0 ? ( -
-

No orders found for the selected date.

-

- Create a new order using the buttons above. -

-
+ ) : ( @@ -252,12 +187,14 @@ export default function OrdersListPage() { {orders.map((order) => ( - - {getMealTypeLabel(order.mealType)} + + {format(parseISO(order.date), 'MMM d, yyyy')} {order.mealCount} residents - {getStatusBadge(order.status)} + + + + ))} + + ) + } + + return ( +
+ {MEAL_TYPES.map(({ value: type, label, sublabel, icon: Icon, color }) => ( + onChange(type)} + > + + + {label} + {showSublabel && sublabel && ( + {sublabel} + )} + + + ))} +
+ ) +} diff --git a/src/components/caregiver/PageHeader.tsx b/src/components/caregiver/PageHeader.tsx new file mode 100644 index 0000000..abade4b --- /dev/null +++ b/src/components/caregiver/PageHeader.tsx @@ -0,0 +1,43 @@ +'use client' + +import Link from 'next/link' +import { ArrowLeft } from 'lucide-react' +import { Button } from '@/components/ui/button' + +interface PageHeaderProps { + title: string + subtitle?: string + backHref?: string + backLabel?: string + rightContent?: React.ReactNode +} + +export function PageHeader({ + title, + subtitle, + backHref, + backLabel = 'Back', + rightContent, +}: PageHeaderProps) { + return ( +
+
+
+ {backHref && ( + + )} +
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {rightContent &&
{rightContent}
} +
+
+ ) +} diff --git a/src/components/caregiver/StatCard.tsx b/src/components/caregiver/StatCard.tsx new file mode 100644 index 0000000..5cb3c52 --- /dev/null +++ b/src/components/caregiver/StatCard.tsx @@ -0,0 +1,39 @@ +'use client' + +import { type LucideIcon } from 'lucide-react' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { cn } from '@/lib/utils' + +interface StatCardProps { + title: string + value: string | number + description?: string + icon?: LucideIcon + iconColor?: string + dotColor?: string + className?: string +} + +export function StatCard({ + title, + value, + description, + icon: Icon, + iconColor, + dotColor, + className, +}: StatCardProps) { + return ( + + + {title} + {Icon && } + {!Icon && dotColor &&
} + + +
{value}
+ {description &&

{description}

} +
+ + ) +} diff --git a/src/components/caregiver/StatusBadge.tsx b/src/components/caregiver/StatusBadge.tsx new file mode 100644 index 0000000..56cf834 --- /dev/null +++ b/src/components/caregiver/StatusBadge.tsx @@ -0,0 +1,31 @@ +'use client' + +import { Badge } from '@/components/ui/badge' +import { cn } from '@/lib/utils' +import { getStatusConfig, type OrderStatus } from '@/lib/constants/meal' + +interface StatusBadgeProps { + status: OrderStatus + showIcon?: boolean + className?: string +} + +export function StatusBadge({ status, showIcon = true, className }: StatusBadgeProps) { + const config = getStatusConfig(status) + + if (!config) { + return {status} + } + + const Icon = config.icon + + return ( + + {showIcon && } + {config.label} + + ) +} diff --git a/src/components/caregiver/index.ts b/src/components/caregiver/index.ts new file mode 100644 index 0000000..db5b671 --- /dev/null +++ b/src/components/caregiver/index.ts @@ -0,0 +1,8 @@ +export { PageHeader } from './PageHeader' +export { LoadingSpinner } from './LoadingSpinner' +export { StatusBadge } from './StatusBadge' +export { MealTypeIcon } from './MealTypeIcon' +export { StatCard } from './StatCard' +export { CheckboxOption } from './CheckboxOption' +export { EmptyState } from './EmptyState' +export { MealTypeSelector } from './MealTypeSelector' diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 84649ad..5652e05 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -60,9 +60,9 @@ function SheetContent({ className={cn( "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", side === "right" && - "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", + "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full border-l", side === "left" && - "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", + "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full border-r", side === "top" && "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", side === "bottom" && diff --git a/src/lib/constants/meal-options.ts b/src/lib/constants/meal-options.ts new file mode 100644 index 0000000..5c34399 --- /dev/null +++ b/src/lib/constants/meal-options.ts @@ -0,0 +1,271 @@ +export interface OptionItem { + key: string + label: string +} + +export interface OptionSection { + title: string + columns: 1 | 2 | 3 + options: OptionItem[] +} + +export interface BreakfastOptions { + accordingToPlan: boolean + bread: { + 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 } +} + +export interface LunchOptions { + portionSize: 'small' | 'large' | 'vegetarian' + soup: boolean + dessert: boolean + specialPreparations: { + 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 } +} + +export const DEFAULT_BREAKFAST: BreakfastOptions = { + accordingToPlan: false, + bread: { + breadRoll: false, + wholeGrainRoll: false, + greyBread: false, + wholeGrainBread: false, + whiteBread: false, + crispbread: false, + }, + porridge: false, + preparation: { sliced: false, spread: false }, + spreads: { + butter: false, + margarine: false, + jam: false, + diabeticJam: false, + honey: false, + cheese: false, + quark: false, + sausage: false, + }, + beverages: { coffee: false, tea: false, hotMilk: false, coldMilk: false }, + additions: { sugar: false, sweetener: false, coffeeCreamer: false }, +} + +export const DEFAULT_LUNCH: LunchOptions = { + portionSize: 'large', + soup: false, + dessert: true, + specialPreparations: { + pureedFood: false, + pureedMeat: false, + slicedMeat: false, + 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 }, + preparation: { spread: false, sliced: false }, + spreads: { butter: false, margarine: false }, + soup: false, + porridge: false, + noFish: false, + beverages: { tea: false, cocoa: false, hotMilk: false, coldMilk: false }, + additions: { sugar: false, sweetener: false }, +} + +export const BREAKFAST_CONFIG: Record = { + bread: { + 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' }, + ], + }, + preparation: { + title: 'Preparation', + columns: 3, + options: [ + { key: 'porridge', label: 'Porridge' }, + { key: 'sliced', label: 'Sliced' }, + { key: 'spread', label: 'Spread' }, + ], + }, + 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' }, + ], + }, + beverages: { + title: 'Beverages', + columns: 2, + options: [ + { key: 'coffee', label: 'Coffee' }, + { key: 'tea', label: 'Tea' }, + { key: 'hotMilk', label: 'Hot Milk' }, + { key: 'coldMilk', label: 'Cold Milk' }, + ], + }, + additions: { + title: 'Additions', + columns: 3, + options: [ + { 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' }, + ] as const, + mealOptions: { + title: 'Meal Options', + columns: 2 as const, + options: [ + { key: 'soup', label: 'Soup' }, + { key: 'dessert', label: 'Dessert' }, + ], + }, + specialPreparations: { + 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' }, + ], + }, + restrictions: { + title: 'Restrictions', + columns: 3 as const, + options: [ + { key: 'noFish', label: 'No Fish' }, + { key: 'fingerFood', label: 'Finger Food' }, + { key: 'onlySweet', label: 'Only Sweet' }, + ], + }, +} + +export const DINNER_CONFIG: Record = { + 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' }, + ], + }, + preparation: { + title: 'Preparation', + columns: 2, + options: [ + { key: 'spread', label: 'Spread' }, + { key: 'sliced', label: 'Sliced' }, + ], + }, + spreads: { + title: 'Spreads', + columns: 2, + options: [ + { key: 'butter', label: 'Butter' }, + { key: 'margarine', label: 'Margarine' }, + ], + }, + additionalItems: { + title: 'Additional Items', + columns: 3, + options: [ + { key: 'soup', label: 'Soup' }, + { key: 'porridge', label: 'Porridge' }, + { key: 'noFish', label: 'No Fish' }, + ], + }, + beverages: { + title: 'Beverages', + columns: 2, + options: [ + { key: 'tea', label: 'Tea' }, + { key: 'cocoa', label: 'Cocoa' }, + { key: 'hotMilk', label: 'Hot Milk' }, + { key: 'coldMilk', label: 'Cold Milk' }, + ], + }, + additions: { + title: 'Additions', + columns: 2, + options: [ + { key: 'sugar', label: 'Sugar' }, + { key: 'sweetener', label: 'Sweetener' }, + ], + }, +} + +export const getGridColsClass = (cols: 1 | 2 | 3): string => { + switch (cols) { + case 1: + return 'grid-cols-1' + case 2: + return 'grid-cols-1 sm:grid-cols-2' + case 3: + return 'grid-cols-1 sm:grid-cols-3' + } +} diff --git a/src/lib/constants/meal.ts b/src/lib/constants/meal.ts new file mode 100644 index 0000000..b16f389 --- /dev/null +++ b/src/lib/constants/meal.ts @@ -0,0 +1,88 @@ +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 interface MealTypeConfig { + 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 +} + +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' }, +] + +export const ORDER_STATUSES: StatusConfig[] = [ + { + 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', + }, + { + 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', + }, + { + 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', + }, + { + value: 'completed', + label: 'Completed', + icon: Check, + 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 getStatusConfig = (status: OrderStatus): StatusConfig | undefined => { + return ORDER_STATUSES.find((s) => s.value === status) +} + +export const getMealTypeLabel = (type: MealType): string => { + return getMealTypeConfig(type)?.label ?? type +} + +export const getStatusLabel = (status: OrderStatus): string => { + return getStatusConfig(status)?.label ?? status +}