feat: redesign meal ordering system

This commit is contained in:
2025-12-02 12:57:42 +01:00
parent 0aada37286
commit 0cf6b405f1
24 changed files with 4823 additions and 639 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import React, { useState, useEffect } from 'react'
import React, { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import {
@@ -12,10 +12,56 @@ import {
ClipboardList,
Users,
Settings,
Plus,
Pencil,
Eye,
Send,
Check,
ChefHat,
AlertTriangle,
ChevronLeft,
ChevronRight,
Calendar,
UserCheck,
UserX,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
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'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
interface User {
id: number
@@ -27,22 +73,122 @@ interface User {
}>
}
interface MealOrder {
id: number
title: string
date: string
mealType: 'breakfast' | 'lunch' | 'dinner'
status: 'draft' | 'submitted' | 'preparing' | 'completed'
mealCount: number
createdAt: string
}
interface Resident {
id: number
name: string
room: string
}
interface Meal {
id: number
resident: number | { id: number; name: string }
}
interface OrderStats {
pending: number
draft: number
submitted: number
preparing: number
prepared: number
completed: number
total: number
}
interface PaginationInfo {
totalDocs: number
totalPages: number
page: number
limit: number
hasNextPage: boolean
hasPrevPage: boolean
}
const ITEMS_PER_PAGE = 10
export default function CaregiverDashboardPage() {
const router = useRouter()
const [user, setUser] = useState<User | null>(null)
const [stats, setStats] = useState<OrderStats>({ pending: 0, preparing: 0, prepared: 0, total: 0 })
const [orders, setOrders] = useState<MealOrder[]>([])
const [residents, setResidents] = useState<Resident[]>([])
const [stats, setStats] = useState<OrderStats>({
draft: 0,
submitted: 0,
preparing: 0,
completed: 0,
total: 0,
})
const [pagination, setPagination] = useState<PaginationInfo>({
totalDocs: 0,
totalPages: 1,
page: 1,
limit: ITEMS_PER_PAGE,
hasNextPage: false,
hasPrevPage: false,
})
const [loading, setLoading] = useState(true)
const [ordersLoading, setOrdersLoading] = useState(false)
// Filters
const [dateFilter, setDateFilter] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('all')
// Coverage dialog
const [coverageDialogOpen, setCoverageDialogOpen] = useState(false)
const [selectedOrder, setSelectedOrder] = useState<MealOrder | null>(null)
const [orderMeals, setOrderMeals] = useState<Meal[]>([])
const [coverageLoading, setCoverageLoading] = useState(false)
// Create order dialog
const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [newOrderDate, setNewOrderDate] = useState(() => new Date().toISOString().split('T')[0])
const [newOrderMealType, setNewOrderMealType] = useState<'breakfast' | 'lunch' | 'dinner'>('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 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)
}
}, [router, dateFilter, statusFilter])
useEffect(() => {
const fetchData = async () => {
const fetchInitialData = async () => {
try {
// Fetch current user
const userRes = await fetch('/api/users/me', { credentials: 'include' })
if (!userRes.ok) {
router.push('/caregiver/login')
@@ -55,18 +201,31 @@ export default function CaregiverDashboardPage() {
}
setUser(userData.user)
const today = new Date().toISOString().split('T')[0]
const ordersRes = await fetch(`/api/meal-orders?where[date][equals]=${today}&limit=1000`, {
// Fetch residents
const residentsRes = await fetch('/api/residents?where[active][equals]=true&limit=500', {
credentials: 'include',
})
if (ordersRes.ok) {
const ordersData = await ordersRes.json()
const orders = ordersData.docs || []
if (residentsRes.ok) {
const residentsData = await residentsRes.json()
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' }
)
if (statsRes.ok) {
const statsData = await statsRes.json()
const allOrders = statsData.docs || []
setStats({
pending: orders.filter((o: { status: string }) => o.status === 'pending').length,
preparing: orders.filter((o: { status: string }) => o.status === 'preparing').length,
prepared: orders.filter((o: { status: string }) => o.status === 'prepared').length,
total: orders.length,
draft: allOrders.filter((o: MealOrder) => o.status === 'draft').length,
submitted: allOrders.filter((o: MealOrder) => o.status === 'submitted').length,
preparing: allOrders.filter((o: MealOrder) => o.status === 'preparing').length,
completed: allOrders.filter((o: MealOrder) => o.status === 'completed').length,
total: allOrders.length,
})
}
} catch (error) {
@@ -75,9 +234,15 @@ export default function CaregiverDashboardPage() {
setLoading(false)
}
}
fetchData()
fetchInitialData()
}, [router])
useEffect(() => {
if (!loading) {
fetchOrders(1)
}
}, [loading, fetchOrders])
const handleLogout = async () => {
await fetch('/api/users/logout', {
method: 'POST',
@@ -86,6 +251,137 @@ export default function CaregiverDashboardPage() {
router.push('/caregiver/login')
}
const handleCreateOrder = async () => {
setCreating(true)
try {
const res = await fetch('/api/meal-orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: newOrderDate,
mealType: newOrderMealType,
status: 'draft',
}),
credentials: 'include',
})
if (res.ok) {
const data = await res.json()
setCreateDialogOpen(false)
router.push(`/caregiver/orders/${data.doc.id}`)
}
} catch (err) {
console.error('Error creating order:', err)
} finally {
setCreating(false)
}
}
const handleViewCoverage = async (order: MealOrder) => {
setSelectedOrder(order)
setCoverageDialogOpen(true)
setCoverageLoading(true)
try {
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 || [])
}
} catch (err) {
console.error('Error fetching meals:', err)
} finally {
setCoverageLoading(false)
}
}
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 <Sunrise className="h-4 w-4 text-orange-500" />
case 'lunch':
return <Sun className="h-4 w-4 text-yellow-500" />
case 'dinner':
return <Moon className="h-4 w-4 text-indigo-500" />
default:
return null
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'draft':
return (
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-200">
<Pencil className="mr-1 h-3 w-3" />
Draft
</Badge>
)
case 'submitted':
return (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
<Send className="mr-1 h-3 w-3" />
Submitted
</Badge>
)
case 'preparing':
return (
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">
<ChefHat className="mr-1 h-3 w-3" />
Preparing
</Badge>
)
case 'completed':
return (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
<Check className="mr-1 h-3 w-3" />
Completed
</Badge>
)
default:
return <Badge variant="outline">{status}</Badge>
}
}
const getCoverageInfo = (order: MealOrder) => {
const totalResidents = residents.length
const coveredCount = order.mealCount
const percentage = totalResidents > 0 ? Math.round((coveredCount / totalResidents) * 100) : 0
return { coveredCount, totalResidents, percentage }
}
const getCoverageColor = (percentage: number) => {
if (percentage === 100) return 'bg-green-500'
if (percentage >= 75) return 'bg-blue-500'
if (percentage >= 50) return 'bg-yellow-500'
return 'bg-red-500'
}
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
return date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
})
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-muted/50">
@@ -99,6 +395,13 @@ 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))
)
const coveredResidents = residents.filter((r) => coveredResidentIds.has(r.id))
const uncoveredResidents = residents.filter((r) => !coveredResidentIds.has(r.id))
return (
<div className="min-h-screen bg-muted/50">
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
@@ -115,12 +418,19 @@ export default function CaregiverDashboardPage() {
</header>
<main className="container py-6">
<div className="mb-6">
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">Today&apos;s overview</p>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">Manage meal orders for your care home</p>
</div>
<Button onClick={() => setCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
New Meal Order
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8">
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
@@ -128,76 +438,63 @@ export default function CaregiverDashboardPage() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground">Today</p>
<p className="text-xs text-muted-foreground">Last 7 days</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Pending</CardTitle>
<div className="h-2 w-2 rounded-full bg-yellow-500" />
<CardTitle className="text-sm font-medium">Draft</CardTitle>
<div className="h-2 w-2 rounded-full bg-gray-400" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.pending}</div>
<p className="text-xs text-muted-foreground">Awaiting preparation</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Preparing</CardTitle>
<div className="h-2 w-2 rounded-full bg-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.preparing}</div>
<div className="text-2xl font-bold">{stats.draft}</div>
<p className="text-xs text-muted-foreground">In progress</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Prepared</CardTitle>
<CardTitle className="text-sm font-medium">Submitted</CardTitle>
<div className="h-2 w-2 rounded-full bg-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.submitted}</div>
<p className="text-xs text-muted-foreground">Sent to kitchen</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Preparing</CardTitle>
<div className="h-2 w-2 rounded-full bg-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.preparing}</div>
<p className="text-xs text-muted-foreground">Being prepared</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Completed</CardTitle>
<div className="h-2 w-2 rounded-full bg-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.prepared}</div>
<p className="text-xs text-muted-foreground">Ready to serve</p>
<div className="text-2xl font-bold">{stats.completed}</div>
<p className="text-xs text-muted-foreground">Finished</p>
</CardContent>
</Card>
</div>
<Card>
{/* Quick Actions */}
<Card className="mb-6">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<Link href="/caregiver/orders/new?mealType=breakfast">
<Card className="cursor-pointer transition-colors hover:bg-accent">
<CardContent className="flex flex-col items-center justify-center p-6">
<Sunrise className="h-8 w-8 mb-2 text-orange-500" />
<span className="font-medium">New Breakfast</span>
</CardContent>
</Card>
</Link>
<Link href="/caregiver/orders/new?mealType=lunch">
<Card className="cursor-pointer transition-colors hover:bg-accent">
<CardContent className="flex flex-col items-center justify-center p-6">
<Sun className="h-8 w-8 mb-2 text-yellow-500" />
<span className="font-medium">New Lunch</span>
</CardContent>
</Card>
</Link>
<Link href="/caregiver/orders/new?mealType=dinner">
<Card className="cursor-pointer transition-colors hover:bg-accent">
<CardContent className="flex flex-col items-center justify-center p-6">
<Moon className="h-8 w-8 mb-2 text-indigo-500" />
<span className="font-medium">New Dinner</span>
</CardContent>
</Card>
</Link>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Link href="/caregiver/orders">
<Card className="cursor-pointer transition-colors hover:bg-accent">
<CardContent className="flex flex-col items-center justify-center p-6">
<ClipboardList className="h-8 w-8 mb-2 text-muted-foreground" />
<span className="font-medium">View Orders</span>
<span className="font-medium">All Orders</span>
</CardContent>
</Card>
</Link>
@@ -217,10 +514,432 @@ export default function CaregiverDashboardPage() {
</CardContent>
</Card>
</Link>
<Card
className="cursor-pointer transition-colors hover:bg-accent"
onClick={() => setCreateDialogOpen(true)}
>
<CardContent className="flex flex-col items-center justify-center p-6">
<Plus className="h-8 w-8 mb-2 text-primary" />
<span className="font-medium">New Order</span>
</CardContent>
</Card>
</div>
</CardContent>
</Card>
{/* Meal Orders Table */}
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>Meal Orders</CardTitle>
<CardDescription>
View and manage all meal orders. Click on coverage to see resident details.
</CardDescription>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="dateFilter" className="sr-only">
Filter by date
</Label>
<Input
type="date"
id="dateFilter"
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)}
className="w-40"
placeholder="Filter by date"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-36">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="submitted">Submitted</SelectItem>
<SelectItem value="preparing">Preparing</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
{(dateFilter || statusFilter !== 'all') && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setDateFilter('')
setStatusFilter('all')
}}
>
Clear
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{ordersLoading ? (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : orders.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 text-center">
<ClipboardList className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">No orders found.</p>
<p className="text-sm text-muted-foreground mt-2">
Create a new order to get started.
</p>
<Button className="mt-4" onClick={() => setCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create Order
</Button>
</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Meal</TableHead>
<TableHead>Date</TableHead>
<TableHead>Coverage</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order) => {
const coverage = getCoverageInfo(order)
return (
<TableRow key={order.id}>
<TableCell>
<div className="flex items-center gap-2">
{getMealTypeIcon(order.mealType)}
<span className="font-medium">
{getMealTypeLabel(order.mealType)}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
{formatDate(order.date)}
</div>
</TableCell>
<TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
className="w-full max-w-32 cursor-pointer"
onClick={() => handleViewCoverage(order)}
>
<div className="flex items-center gap-2">
<Progress
value={coverage.percentage}
className="h-2 flex-1"
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{coverage.coveredCount}/{coverage.totalResidents}
</span>
</div>
{coverage.percentage < 100 && (
<div className="flex items-center gap-1 mt-1">
<AlertTriangle className="h-3 w-3 text-yellow-500" />
<span className="text-xs text-yellow-600">
{coverage.totalResidents - coverage.coveredCount} missing
</span>
</div>
)}
</button>
</TooltipTrigger>
<TooltipContent>
<p>Click to view coverage details</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell>{getStatusBadge(order.status)}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
{order.status === 'draft' ? (
<Button variant="outline" size="sm" asChild>
<Link href={`/caregiver/orders/${order.id}`}>
<Pencil className="mr-1 h-3 w-3" />
Edit
</Link>
</Button>
) : (
<Button variant="outline" size="sm" asChild>
<Link href={`/caregiver/orders/${order.id}`}>
<Eye className="mr-1 h-3 w-3" />
View
</Link>
</Button>
)}
</div>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
{/* Pagination */}
{pagination.totalPages > 1 && (
<div className="flex items-center justify-between px-4 py-4 border-t">
<div className="text-sm text-muted-foreground">
Showing {(pagination.page - 1) * pagination.limit + 1} to{' '}
{Math.min(pagination.page * pagination.limit, pagination.totalDocs)} of{' '}
{pagination.totalDocs} orders
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => fetchOrders(pagination.page - 1)}
disabled={!pagination.hasPrevPage || ordersLoading}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
let pageNum: number
if (pagination.totalPages <= 5) {
pageNum = i + 1
} else if (pagination.page <= 3) {
pageNum = i + 1
} else if (pagination.page >= pagination.totalPages - 2) {
pageNum = pagination.totalPages - 4 + i
} else {
pageNum = pagination.page - 2 + i
}
return (
<Button
key={pageNum}
variant={pagination.page === pageNum ? 'default' : 'outline'}
size="sm"
className="w-8 h-8 p-0"
onClick={() => fetchOrders(pageNum)}
disabled={ordersLoading}
>
{pageNum}
</Button>
)
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => fetchOrders(pagination.page + 1)}
disabled={!pagination.hasNextPage || ordersLoading}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</>
)}
</CardContent>
</Card>
</main>
{/* Create Order Dialog */}
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Meal Order</DialogTitle>
<DialogDescription>
Select a date and meal type to create a new order.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="newOrderDate">Date</Label>
<Input
type="date"
id="newOrderDate"
value={newOrderDate}
onChange={(e) => setNewOrderDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Meal Type</Label>
<div className="grid grid-cols-3 gap-2">
{[
{ 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 }) => (
<button
key={value}
type="button"
className={cn(
'flex flex-col items-center justify-center p-4 rounded-lg border-2 transition-colors',
newOrderMealType === value
? 'border-primary bg-primary/5'
: 'border-border hover:bg-muted'
)}
onClick={() => setNewOrderMealType(value as 'breakfast' | 'lunch' | 'dinner')}
>
<Icon className={cn('h-6 w-6 mb-1', color)} />
<span className="text-sm font-medium">{label}</span>
</button>
))}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleCreateOrder} disabled={creating}>
{creating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="mr-2 h-4 w-4" />
Create Order
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Coverage Dialog */}
<Dialog open={coverageDialogOpen} onOpenChange={setCoverageDialogOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Resident Coverage
</DialogTitle>
<DialogDescription>
{selectedOrder && (
<>
{getMealTypeLabel(selectedOrder.mealType)} - {formatDate(selectedOrder.date)}
</>
)}
</DialogDescription>
</DialogHeader>
{coverageLoading ? (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-6">
{/* Coverage Summary */}
<div className="flex items-center gap-4 p-4 bg-muted rounded-lg">
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Coverage Progress</span>
<span className="text-sm text-muted-foreground">
{coveredResidents.length}/{residents.length} residents
</span>
</div>
<Progress
value={
residents.length > 0
? (coveredResidents.length / residents.length) * 100
: 0
}
className="h-3"
/>
</div>
<div
className={cn(
'flex items-center justify-center w-16 h-16 rounded-full text-white font-bold',
getCoverageColor(
residents.length > 0
? (coveredResidents.length / residents.length) * 100
: 0
)
)}
>
{residents.length > 0
? Math.round((coveredResidents.length / residents.length) * 100)
: 0}
%
</div>
</div>
{/* Uncovered Residents */}
{uncoveredResidents.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<UserX className="h-4 w-4 text-red-500" />
<h4 className="font-medium text-red-700">
Missing Meals ({uncoveredResidents.length})
</h4>
</div>
<div className="grid gap-2 sm:grid-cols-2">
{uncoveredResidents.map((resident) => (
<div
key={resident.id}
className="flex items-center justify-between p-3 bg-red-50 border border-red-200 rounded-lg"
>
<div>
<div className="font-medium text-red-900">{resident.name}</div>
<div className="text-sm text-red-600">Room {resident.room}</div>
</div>
<AlertTriangle className="h-4 w-4 text-red-500" />
</div>
))}
</div>
</div>
)}
{/* Covered Residents */}
{coveredResidents.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-3">
<UserCheck className="h-4 w-4 text-green-500" />
<h4 className="font-medium text-green-700">
Covered Residents ({coveredResidents.length})
</h4>
</div>
<div className="grid gap-2 sm:grid-cols-2">
{coveredResidents.map((resident) => (
<div
key={resident.id}
className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg"
>
<div>
<div className="font-medium text-green-900">{resident.name}</div>
<div className="text-sm text-green-600">Room {resident.room}</div>
</div>
<Check className="h-4 w-4 text-green-500" />
</div>
))}
</div>
</div>
)}
</div>
)}
<DialogFooter>
{selectedOrder?.status === 'draft' && (
<Button asChild>
<Link href={`/caregiver/orders/${selectedOrder.id}`}>
<Pencil className="mr-2 h-4 w-4" />
Edit Order
</Link>
</Button>
)}
<Button variant="outline" onClick={() => setCoverageDialogOpen(false)}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { ArrowLeft, Plus, Loader2 } from 'lucide-react'
import { ArrowLeft, Plus, Loader2, Send, Eye, Pencil, Check, ChefHat } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -26,19 +26,13 @@ import {
TableRow,
} from '@/components/ui/table'
interface Resident {
id: number
name: string
room: string
}
interface MealOrder {
id: number
title: string
date: string
mealType: 'breakfast' | 'lunch' | 'dinner'
status: 'pending' | 'preparing' | 'prepared'
resident: Resident | number
status: 'draft' | 'submitted' | 'preparing' | 'completed'
mealCount: number
createdAt: string
}
@@ -47,18 +41,18 @@ export default function OrdersListPage() {
const [orders, setOrders] = useState<MealOrder[]>([])
const [loading, setLoading] = useState(true)
const [dateFilter, setDateFilter] = useState(() => new Date().toISOString().split('T')[0])
const [mealTypeFilter, setMealTypeFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('all')
useEffect(() => {
const fetchOrders = async () => {
setLoading(true)
try {
let url = `/api/meal-orders?sort=-createdAt&limit=100&depth=1`
let url = `/api/meal-orders?sort=-date&limit=100&depth=0`
if (dateFilter) {
url += `&where[date][equals]=${dateFilter}`
}
if (mealTypeFilter !== 'all') {
url += `&where[mealType][equals]=${mealTypeFilter}`
if (statusFilter !== 'all') {
url += `&where[status][equals]=${statusFilter}`
}
const res = await fetch(url, { credentials: 'include' })
@@ -75,7 +69,7 @@ export default function OrdersListPage() {
}
}
fetchOrders()
}, [router, dateFilter, mealTypeFilter])
}, [router, dateFilter, statusFilter])
const getMealTypeLabel = (type: string) => {
switch (type) {
@@ -92,22 +86,59 @@ export default function OrdersListPage() {
const getStatusBadge = (status: string) => {
switch (status) {
case 'pending':
return <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">Pending</Badge>
case 'draft':
return (
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-200">
<Pencil className="mr-1 h-3 w-3" />
Draft
</Badge>
)
case 'submitted':
return (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
<Send className="mr-1 h-3 w-3" />
Submitted
</Badge>
)
case 'preparing':
return <Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">Preparing</Badge>
case 'prepared':
return <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">Prepared</Badge>
return (
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">
<ChefHat className="mr-1 h-3 w-3" />
Preparing
</Badge>
)
case 'completed':
return (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">
<Check className="mr-1 h-3 w-3" />
Completed
</Badge>
)
default:
return <Badge variant="outline">{status}</Badge>
}
}
const getResidentName = (resident: Resident | number) => {
if (typeof resident === 'object') {
return resident.name
const handleCreateOrder = async (mealType: 'breakfast' | 'lunch' | 'dinner') => {
try {
const res = await fetch('/api/meal-orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: dateFilter,
mealType,
status: 'draft',
}),
credentials: 'include',
})
if (res.ok) {
const data = await res.json()
router.push(`/caregiver/orders/${data.doc.id}`)
}
} catch (err) {
console.error('Error creating order:', err)
}
return `Resident #${resident}`
}
return (
@@ -123,22 +154,16 @@ export default function OrdersListPage() {
</Button>
<h1 className="text-xl font-semibold">Meal Orders</h1>
</div>
<Button asChild>
<Link href="/caregiver/orders/new">
<Plus className="mr-2 h-4 w-4" />
New Order
</Link>
</Button>
</div>
</header>
<main className="container py-6">
<Card className="mb-6">
<CardHeader>
<CardTitle>Filter Orders</CardTitle>
<CardTitle>Filter & Create Orders</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-4 sm:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="date">Date</Label>
<Input
@@ -149,19 +174,52 @@ export default function OrdersListPage() {
/>
</div>
<div className="space-y-2">
<Label htmlFor="mealType">Meal Type</Label>
<Select value={mealTypeFilter} onValueChange={setMealTypeFilter}>
<Label htmlFor="status">Status</Label>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<SelectValue placeholder="Select meal type" />
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="breakfast">Breakfast</SelectItem>
<SelectItem value="lunch">Lunch</SelectItem>
<SelectItem value="dinner">Dinner</SelectItem>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="draft">Draft</SelectItem>
<SelectItem value="submitted">Submitted</SelectItem>
<SelectItem value="preparing">Preparing</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Create New Order</Label>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleCreateOrder('breakfast')}
className="flex-1"
>
<Plus className="mr-1 h-3 w-3" />
Breakfast
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleCreateOrder('lunch')}
className="flex-1"
>
<Plus className="mr-1 h-3 w-3" />
Lunch
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleCreateOrder('dinner')}
className="flex-1"
>
<Plus className="mr-1 h-3 w-3" />
Dinner
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
@@ -174,28 +232,48 @@ export default function OrdersListPage() {
</div>
) : orders.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 text-center">
<p className="text-muted-foreground">No orders found for the selected criteria.</p>
<Button asChild className="mt-4">
<Link href="/caregiver/orders/new">Create New Order</Link>
</Button>
<p className="text-muted-foreground">No orders found for the selected date.</p>
<p className="text-sm text-muted-foreground mt-2">
Create a new order using the buttons above.
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Resident</TableHead>
<TableHead>Meal Type</TableHead>
<TableHead>Date</TableHead>
<TableHead>Meal</TableHead>
<TableHead>Meals</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order) => (
<TableRow key={order.id}>
<TableCell className="font-medium">{getResidentName(order.resident)}</TableCell>
<TableCell className="font-medium">
{getMealTypeLabel(order.mealType)}
</TableCell>
<TableCell>{order.date}</TableCell>
<TableCell>{getMealTypeLabel(order.mealType)}</TableCell>
<TableCell>{order.mealCount} residents</TableCell>
<TableCell>{getStatusBadge(order.status)}</TableCell>
<TableCell className="text-right">
<Button variant="outline" size="sm" asChild>
<Link href={`/caregiver/orders/${order.id}`}>
{order.status === 'draft' ? (
<>
<Pencil className="mr-1 h-3 w-3" />
Edit
</>
) : (
<>
<Eye className="mr-1 h-3 w-3" />
View
</>
)}
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>