refactor: codebase and some fixes
This commit is contained in:
@@ -5,31 +5,24 @@ import { useRouter } from 'next/navigation'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { format, parseISO } from 'date-fns'
|
import { format, parseISO } from 'date-fns'
|
||||||
import {
|
import {
|
||||||
Loader2,
|
|
||||||
LogOut,
|
LogOut,
|
||||||
Sun,
|
|
||||||
Moon,
|
|
||||||
Sunrise,
|
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
Users,
|
Users,
|
||||||
Settings,
|
Settings,
|
||||||
Plus,
|
Plus,
|
||||||
Pencil,
|
Pencil,
|
||||||
Eye,
|
Eye,
|
||||||
Send,
|
|
||||||
Check,
|
|
||||||
ChefHat,
|
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Calendar,
|
Calendar,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
UserX,
|
UserX,
|
||||||
|
Check,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } 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 { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
@@ -64,6 +57,21 @@ import {
|
|||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { cn } from '@/lib/utils'
|
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 {
|
interface User {
|
||||||
id: number
|
id: number
|
||||||
name?: string
|
name?: string
|
||||||
@@ -78,8 +86,8 @@ interface MealOrder {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
date: string
|
date: string
|
||||||
mealType: 'breakfast' | 'lunch' | 'dinner'
|
mealType: MealType
|
||||||
status: 'draft' | 'submitted' | 'preparing' | 'completed'
|
status: OrderStatus
|
||||||
mealCount: number
|
mealCount: number
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
@@ -137,23 +145,21 @@ export default function CaregiverDashboardPage() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [ordersLoading, setOrdersLoading] = useState(false)
|
const [ordersLoading, setOrdersLoading] = useState(false)
|
||||||
|
|
||||||
// Filters
|
|
||||||
const [dateFilter, setDateFilter] = useState('')
|
const [dateFilter, setDateFilter] = useState('')
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||||
|
|
||||||
// Coverage dialog
|
|
||||||
const [coverageDialogOpen, setCoverageDialogOpen] = useState(false)
|
const [coverageDialogOpen, setCoverageDialogOpen] = useState(false)
|
||||||
const [selectedOrder, setSelectedOrder] = useState<MealOrder | null>(null)
|
const [selectedOrder, setSelectedOrder] = useState<MealOrder | null>(null)
|
||||||
const [orderMeals, setOrderMeals] = useState<Meal[]>([])
|
const [orderMeals, setOrderMeals] = useState<Meal[]>([])
|
||||||
const [coverageLoading, setCoverageLoading] = useState(false)
|
const [coverageLoading, setCoverageLoading] = useState(false)
|
||||||
|
|
||||||
// Create order dialog
|
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
||||||
const [newOrderDate, setNewOrderDate] = useState(() => format(new Date(), 'yyyy-MM-dd'))
|
const [newOrderDate, setNewOrderDate] = useState(() => format(new Date(), 'yyyy-MM-dd'))
|
||||||
const [newOrderMealType, setNewOrderMealType] = useState<'breakfast' | 'lunch' | 'dinner'>('breakfast')
|
const [newOrderMealType, setNewOrderMealType] = useState<MealType>('breakfast')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
|
|
||||||
const fetchOrders = useCallback(async (page: number = 1) => {
|
const fetchOrders = useCallback(
|
||||||
|
async (page: number = 1) => {
|
||||||
setOrdersLoading(true)
|
setOrdersLoading(true)
|
||||||
try {
|
try {
|
||||||
let url = `/api/meal-orders?sort=-date,-createdAt&limit=${ITEMS_PER_PAGE}&page=${page}&depth=0`
|
let url = `/api/meal-orders?sort=-date,-createdAt&limit=${ITEMS_PER_PAGE}&page=${page}&depth=0`
|
||||||
@@ -184,12 +190,13 @@ export default function CaregiverDashboardPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setOrdersLoading(false)
|
setOrdersLoading(false)
|
||||||
}
|
}
|
||||||
}, [router, dateFilter, statusFilter])
|
},
|
||||||
|
[router, dateFilter, statusFilter],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchInitialData = async () => {
|
const fetchInitialData = async () => {
|
||||||
try {
|
try {
|
||||||
// Fetch current user
|
|
||||||
const userRes = await fetch('/api/users/me', { credentials: 'include' })
|
const userRes = await fetch('/api/users/me', { credentials: 'include' })
|
||||||
if (!userRes.ok) {
|
if (!userRes.ok) {
|
||||||
router.push('/caregiver/login')
|
router.push('/caregiver/login')
|
||||||
@@ -202,7 +209,6 @@ export default function CaregiverDashboardPage() {
|
|||||||
}
|
}
|
||||||
setUser(userData.user)
|
setUser(userData.user)
|
||||||
|
|
||||||
// Fetch residents
|
|
||||||
const residentsRes = await fetch('/api/residents?where[active][equals]=true&limit=500', {
|
const residentsRes = await fetch('/api/residents?where[active][equals]=true&limit=500', {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
@@ -211,12 +217,11 @@ export default function CaregiverDashboardPage() {
|
|||||||
setResidents(residentsData.docs || [])
|
setResidents(residentsData.docs || [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch stats (all orders from last 7 days)
|
|
||||||
const weekAgo = new Date()
|
const weekAgo = new Date()
|
||||||
weekAgo.setDate(weekAgo.getDate() - 7)
|
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||||
const statsRes = await fetch(
|
const statsRes = await fetch(
|
||||||
`/api/meal-orders?where[date][greater_than_equal]=${weekAgo.toISOString().split('T')[0]}&limit=1000&depth=0`,
|
`/api/meal-orders?where[date][greater_than_equal]=${weekAgo.toISOString().split('T')[0]}&limit=1000&depth=0`,
|
||||||
{ credentials: 'include' }
|
{ credentials: 'include' },
|
||||||
)
|
)
|
||||||
if (statsRes.ok) {
|
if (statsRes.ok) {
|
||||||
const statsData = await statsRes.json()
|
const statsData = await statsRes.json()
|
||||||
@@ -284,10 +289,9 @@ export default function CaregiverDashboardPage() {
|
|||||||
setCoverageLoading(true)
|
setCoverageLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`, {
|
||||||
`/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`,
|
credentials: 'include',
|
||||||
{ credentials: 'include' }
|
})
|
||||||
)
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setOrderMeals(data.docs || [])
|
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 <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 getCoverageInfo = (order: MealOrder) => {
|
||||||
const totalResidents = residents.length
|
const totalResidents = residents.length
|
||||||
const coveredCount = order.mealCount
|
const coveredCount = order.mealCount
|
||||||
@@ -379,11 +322,7 @@ export default function CaregiverDashboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <LoadingSpinner fullPage />
|
||||||
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenantName =
|
const tenantName =
|
||||||
@@ -391,9 +330,8 @@ export default function CaregiverDashboardPage() {
|
|||||||
? user.tenants[0].tenant.name
|
? user.tenants[0].tenant.name
|
||||||
: 'Care Home'
|
: 'Care Home'
|
||||||
|
|
||||||
// Calculate covered residents for coverage dialog
|
|
||||||
const coveredResidentIds = new Set(
|
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 coveredResidents = residents.filter((r) => coveredResidentIds.has(r.id))
|
||||||
const uncoveredResidents = residents.filter((r) => !coveredResidentIds.has(r.id))
|
const uncoveredResidents = residents.filter((r) => !coveredResidentIds.has(r.id))
|
||||||
@@ -425,61 +363,24 @@ export default function CaregiverDashboardPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
|
||||||
<Card>
|
<StatCard
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
title="Total Orders"
|
||||||
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
|
value={stats.total}
|
||||||
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
description="Last 7 days"
|
||||||
</CardHeader>
|
icon={ClipboardList}
|
||||||
<CardContent>
|
/>
|
||||||
<div className="text-2xl font-bold">{stats.total}</div>
|
{ORDER_STATUSES.map((status) => (
|
||||||
<p className="text-xs text-muted-foreground">Last 7 days</p>
|
<StatCard
|
||||||
</CardContent>
|
key={status.value}
|
||||||
</Card>
|
title={status.label}
|
||||||
<Card>
|
value={stats[status.value as keyof OrderStats]}
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
description={status.description}
|
||||||
<CardTitle className="text-sm font-medium">Draft</CardTitle>
|
dotColor={status.dotColor}
|
||||||
<div className="h-2 w-2 rounded-full bg-gray-400" />
|
/>
|
||||||
</CardHeader>
|
))}
|
||||||
<CardContent>
|
|
||||||
<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">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.completed}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Finished</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Quick Actions</CardTitle>
|
<CardTitle>Quick Actions</CardTitle>
|
||||||
@@ -523,7 +424,6 @@ export default function CaregiverDashboardPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Meal Orders Table */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
@@ -533,7 +433,6 @@ export default function CaregiverDashboardPage() {
|
|||||||
View and manage all meal orders. Click on coverage to see resident details.
|
View and manage all meal orders. Click on coverage to see resident details.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label htmlFor="dateFilter" className="sr-only">
|
<Label htmlFor="dateFilter" className="sr-only">
|
||||||
Filter by date
|
Filter by date
|
||||||
@@ -546,17 +445,17 @@ export default function CaregiverDashboardPage() {
|
|||||||
className="w-40"
|
className="w-40"
|
||||||
placeholder="Filter by date"
|
placeholder="Filter by date"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
<SelectTrigger className="w-36">
|
<SelectTrigger className="w-36">
|
||||||
<SelectValue placeholder="Status" />
|
<SelectValue placeholder="Status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Statuses</SelectItem>
|
<SelectItem value="all">All Statuses</SelectItem>
|
||||||
<SelectItem value="draft">Draft</SelectItem>
|
{ORDER_STATUSES.map((status) => (
|
||||||
<SelectItem value="submitted">Submitted</SelectItem>
|
<SelectItem key={status.value} value={status.value}>
|
||||||
<SelectItem value="preparing">Preparing</SelectItem>
|
{status.label}
|
||||||
<SelectItem value="completed">Completed</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{(dateFilter || statusFilter !== 'all') && (
|
{(dateFilter || statusFilter !== 'all') && (
|
||||||
@@ -576,21 +475,19 @@ export default function CaregiverDashboardPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{ordersLoading ? (
|
{ordersLoading ? (
|
||||||
<div className="flex items-center justify-center p-8">
|
<LoadingSpinner />
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
) : orders.length === 0 ? (
|
) : orders.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
<EmptyState
|
||||||
<ClipboardList className="h-12 w-12 text-muted-foreground mb-4" />
|
icon={ClipboardList}
|
||||||
<p className="text-muted-foreground">No orders found.</p>
|
title="No orders found."
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
description="Create a new order to get started."
|
||||||
Create a new order to get started.
|
action={
|
||||||
</p>
|
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||||
<Button className="mt-4" onClick={() => setCreateDialogOpen(true)}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Create Order
|
Create Order
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -609,12 +506,7 @@ export default function CaregiverDashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<TableRow key={order.id}>
|
<TableRow key={order.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<MealTypeIcon type={order.mealType} showLabel />
|
||||||
{getMealTypeIcon(order.mealType)}
|
|
||||||
<span className="font-medium">
|
|
||||||
{getMealTypeLabel(order.mealType)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -631,10 +523,7 @@ export default function CaregiverDashboardPage() {
|
|||||||
onClick={() => handleViewCoverage(order)}
|
onClick={() => handleViewCoverage(order)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Progress
|
<Progress value={coverage.percentage} className="h-2 flex-1" />
|
||||||
value={coverage.percentage}
|
|
||||||
className="h-2 flex-1"
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{coverage.coveredCount}/{coverage.totalResidents}
|
{coverage.coveredCount}/{coverage.totalResidents}
|
||||||
</span>
|
</span>
|
||||||
@@ -655,25 +544,25 @@ export default function CaregiverDashboardPage() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{getStatusBadge(order.status)}</TableCell>
|
<TableCell>
|
||||||
|
<StatusBadge status={order.status} />
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-2">
|
|
||||||
{order.status === 'draft' ? (
|
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link href={`/caregiver/orders/${order.id}`}>
|
<Link href={`/caregiver/orders/${order.id}`}>
|
||||||
|
{order.status === 'draft' ? (
|
||||||
|
<>
|
||||||
<Pencil className="mr-1 h-3 w-3" />
|
<Pencil className="mr-1 h-3 w-3" />
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</>
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button variant="outline" size="sm" asChild>
|
<>
|
||||||
<Link href={`/caregiver/orders/${order.id}`}>
|
|
||||||
<Eye className="mr-1 h-3 w-3" />
|
<Eye className="mr-1 h-3 w-3" />
|
||||||
View
|
View
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
@@ -681,7 +570,6 @@ export default function CaregiverDashboardPage() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{pagination.totalPages > 1 && (
|
{pagination.totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between px-4 py-4 border-t">
|
<div className="flex items-center justify-between px-4 py-4 border-t">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
@@ -743,14 +631,11 @@ export default function CaregiverDashboardPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Create Order Dialog */}
|
|
||||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create New Meal Order</DialogTitle>
|
<DialogTitle>Create New Meal Order</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>Select a date and meal type to create a new order.</DialogDescription>
|
||||||
Select a date and meal type to create a new order.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -764,28 +649,7 @@ export default function CaregiverDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Meal Type</Label>
|
<Label>Meal Type</Label>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<MealTypeSelector value={newOrderMealType} onChange={setNewOrderMealType} variant="button" />
|
||||||
{[
|
|
||||||
{ 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>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -794,22 +658,16 @@ export default function CaregiverDashboardPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleCreateOrder} disabled={creating}>
|
<Button onClick={handleCreateOrder} disabled={creating}>
|
||||||
{creating ? (
|
{creating ? (
|
||||||
<>
|
<LoadingSpinner size="sm" className="mr-2" />
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Creating...
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Create Order
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
{creating ? 'Creating...' : 'Create Order'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Coverage Dialog */}
|
|
||||||
<Dialog open={coverageDialogOpen} onOpenChange={setCoverageDialogOpen}>
|
<Dialog open={coverageDialogOpen} onOpenChange={setCoverageDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -827,12 +685,9 @@ export default function CaregiverDashboardPage() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{coverageLoading ? (
|
{coverageLoading ? (
|
||||||
<div className="flex items-center justify-center p-8">
|
<LoadingSpinner />
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Coverage Summary */}
|
|
||||||
<div className="flex items-center gap-4 p-4 bg-muted rounded-lg">
|
<div className="flex items-center gap-4 p-4 bg-muted rounded-lg">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
@@ -843,9 +698,7 @@ export default function CaregiverDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={
|
value={
|
||||||
residents.length > 0
|
residents.length > 0 ? (coveredResidents.length / residents.length) * 100 : 0
|
||||||
? (coveredResidents.length / residents.length) * 100
|
|
||||||
: 0
|
|
||||||
}
|
}
|
||||||
className="h-3"
|
className="h-3"
|
||||||
/>
|
/>
|
||||||
@@ -856,8 +709,8 @@ export default function CaregiverDashboardPage() {
|
|||||||
getCoverageColor(
|
getCoverageColor(
|
||||||
residents.length > 0
|
residents.length > 0
|
||||||
? (coveredResidents.length / residents.length) * 100
|
? (coveredResidents.length / residents.length) * 100
|
||||||
: 0
|
: 0,
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{residents.length > 0
|
{residents.length > 0
|
||||||
@@ -867,7 +720,6 @@ export default function CaregiverDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Uncovered Residents */}
|
|
||||||
{uncoveredResidents.length > 0 && (
|
{uncoveredResidents.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
@@ -893,7 +745,6 @@ export default function CaregiverDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Covered Residents */}
|
|
||||||
{coveredResidents.length > 0 && (
|
{coveredResidents.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,12 @@ import React, { useState, useEffect } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { format, parseISO } from 'date-fns'
|
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 { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -27,12 +26,26 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} 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 {
|
interface MealOrder {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
date: string
|
date: string
|
||||||
mealType: 'breakfast' | 'lunch' | 'dinner'
|
mealType: MealType
|
||||||
status: 'draft' | 'submitted' | 'preparing' | 'completed'
|
status: OrderStatus
|
||||||
mealCount: number
|
mealCount: number
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
@@ -72,55 +85,7 @@ export default function OrdersListPage() {
|
|||||||
fetchOrders()
|
fetchOrders()
|
||||||
}, [router, dateFilter, statusFilter])
|
}, [router, dateFilter, statusFilter])
|
||||||
|
|
||||||
const getMealTypeLabel = (type: string) => {
|
const handleCreateOrder = async (mealType: MealType) => {
|
||||||
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 (
|
|
||||||
<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 handleCreateOrder = async (mealType: 'breakfast' | 'lunch' | 'dinner') => {
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/meal-orders', {
|
const res = await fetch('/api/meal-orders', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -144,19 +109,7 @@ export default function OrdersListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-muted/50">
|
<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">
|
<PageHeader title="Meal Orders" backHref="/caregiver/dashboard" />
|
||||||
<div className="container flex h-16 items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Button variant="ghost" size="sm" asChild>
|
|
||||||
<Link href="/caregiver/dashboard">
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Back
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-xl font-semibold">Meal Orders</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="container py-6">
|
<main className="container py-6">
|
||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
@@ -182,43 +135,29 @@ export default function OrdersListPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Statuses</SelectItem>
|
<SelectItem value="all">All Statuses</SelectItem>
|
||||||
<SelectItem value="draft">Draft</SelectItem>
|
{ORDER_STATUSES.map((status) => (
|
||||||
<SelectItem value="submitted">Submitted</SelectItem>
|
<SelectItem key={status.value} value={status.value}>
|
||||||
<SelectItem value="preparing">Preparing</SelectItem>
|
{status.label}
|
||||||
<SelectItem value="completed">Completed</SelectItem>
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Create New Order</Label>
|
<Label>Create New Order</Label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
{(['breakfast', 'lunch', 'dinner'] as const).map((type) => (
|
||||||
<Button
|
<Button
|
||||||
|
key={type}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleCreateOrder('breakfast')}
|
onClick={() => handleCreateOrder(type)}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 h-3 w-3" />
|
<Plus className="mr-1 h-3 w-3" />
|
||||||
Breakfast
|
{getMealTypeLabel(type)}
|
||||||
</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>
|
</Button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,16 +167,12 @@ export default function OrdersListPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center p-8">
|
<LoadingSpinner />
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
) : orders.length === 0 ? (
|
) : orders.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
<EmptyState
|
||||||
<p className="text-muted-foreground">No orders found for the selected date.</p>
|
title="No orders found for the selected date."
|
||||||
<p className="text-sm text-muted-foreground mt-2">
|
description="Create a new order using the buttons above."
|
||||||
Create a new order using the buttons above.
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -252,12 +187,14 @@ export default function OrdersListPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<TableRow key={order.id}>
|
<TableRow key={order.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell>
|
||||||
{getMealTypeLabel(order.mealType)}
|
<MealTypeIcon type={order.mealType} showLabel />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{format(parseISO(order.date), 'MMM d, yyyy')}</TableCell>
|
<TableCell>{format(parseISO(order.date), 'MMM d, yyyy')}</TableCell>
|
||||||
<TableCell>{order.mealCount} residents</TableCell>
|
<TableCell>{order.mealCount} residents</TableCell>
|
||||||
<TableCell>{getStatusBadge(order.status)}</TableCell>
|
<TableCell>
|
||||||
|
<StatusBadge status={order.status} />
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button variant="outline" size="sm" asChild>
|
<Button variant="outline" size="sm" asChild>
|
||||||
<Link href={`/caregiver/orders/${order.id}`}>
|
<Link href={`/caregiver/orders/${order.id}`}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import type { Viewport } from 'next'
|
||||||
|
|
||||||
import '@/app/globals.css'
|
import '@/app/globals.css'
|
||||||
|
|
||||||
@@ -7,6 +8,12 @@ export const metadata = {
|
|||||||
title: 'Meal Planner - Caregiver',
|
title: 'Meal Planner - Caregiver',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: 'device-width',
|
||||||
|
initialScale: 1,
|
||||||
|
viewportFit: 'cover',
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-exports
|
// eslint-disable-next-line no-restricted-exports
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -166,4 +166,13 @@
|
|||||||
.container {
|
.container {
|
||||||
@apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8;
|
@apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Safe area padding for mobile devices with notches/home indicators */
|
||||||
|
.safe-area-bottom {
|
||||||
|
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-top {
|
||||||
|
padding-top: max(1rem, env(safe-area-inset-top));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/components/caregiver/CheckboxOption.tsx
Normal file
58
src/components/caregiver/CheckboxOption.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface CheckboxOptionProps {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
checked: boolean
|
||||||
|
onCheckedChange: (checked: boolean) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckboxOption({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
onCheckedChange,
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
}: CheckboxOptionProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center space-x-3 rounded-lg border p-3 cursor-pointer transition-colors select-none',
|
||||||
|
checked ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50',
|
||||||
|
disabled && 'opacity-50 cursor-not-allowed',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={() => !disabled && onCheckedChange(!checked)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (!disabled && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
|
e.preventDefault()
|
||||||
|
onCheckedChange(!checked)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id={id}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className={cn('cursor-pointer flex-1 text-sm', disabled && 'cursor-not-allowed')}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
src/components/caregiver/EmptyState.tsx
Normal file
23
src/components/caregiver/EmptyState.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { type LucideIcon } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon?: LucideIcon
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
action?: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex flex-col items-center justify-center p-8 text-center', className)}>
|
||||||
|
{Icon && <Icon className="h-12 w-12 text-muted-foreground mb-4" />}
|
||||||
|
<p className="text-muted-foreground">{title}</p>
|
||||||
|
{description && <p className="text-sm text-muted-foreground mt-2">{description}</p>}
|
||||||
|
{action && <div className="mt-4">{action}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
src/components/caregiver/LoadingSpinner.tsx
Normal file
36
src/components/caregiver/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface LoadingSpinnerProps {
|
||||||
|
fullPage?: boolean
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-8 w-8',
|
||||||
|
lg: 'h-12 w-12',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingSpinner({ fullPage = false, size = 'md', className }: LoadingSpinnerProps) {
|
||||||
|
const spinner = (
|
||||||
|
<Loader2 className={cn('animate-spin text-primary', sizeClasses[size], className)} />
|
||||||
|
)
|
||||||
|
|
||||||
|
if (fullPage) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
||||||
|
{spinner}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
{spinner}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
src/components/caregiver/MealTypeIcon.tsx
Normal file
38
src/components/caregiver/MealTypeIcon.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { getMealTypeConfig, getMealTypeLabel, type MealType } from '@/lib/constants/meal'
|
||||||
|
|
||||||
|
interface MealTypeIconProps {
|
||||||
|
type: MealType
|
||||||
|
showLabel?: boolean
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'h-4 w-4',
|
||||||
|
md: 'h-6 w-6',
|
||||||
|
lg: 'h-10 w-10',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MealTypeIcon({ type, showLabel = false, size = 'sm', className }: MealTypeIconProps) {
|
||||||
|
const config = getMealTypeConfig(type)
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return showLabel ? <span>{type}</span> : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = config.icon
|
||||||
|
|
||||||
|
if (showLabel) {
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-2', className)}>
|
||||||
|
<Icon className={cn(sizeClasses[size], config.color)} />
|
||||||
|
<span className="font-medium">{getMealTypeLabel(type)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Icon className={cn(sizeClasses[size], config.color, className)} />
|
||||||
|
}
|
||||||
67
src/components/caregiver/MealTypeSelector.tsx
Normal file
67
src/components/caregiver/MealTypeSelector.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { MEAL_TYPES, type MealType } from '@/lib/constants/meal'
|
||||||
|
|
||||||
|
interface MealTypeSelectorProps {
|
||||||
|
value: MealType | null
|
||||||
|
onChange: (type: MealType) => void
|
||||||
|
showSublabel?: boolean
|
||||||
|
variant?: 'card' | 'button'
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MealTypeSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
showSublabel = false,
|
||||||
|
variant = 'card',
|
||||||
|
className,
|
||||||
|
}: MealTypeSelectorProps) {
|
||||||
|
if (variant === 'button') {
|
||||||
|
return (
|
||||||
|
<div className={cn('grid grid-cols-3 gap-2', className)}>
|
||||||
|
{MEAL_TYPES.map(({ value: type, label, icon: Icon, color }) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center justify-center p-4 rounded-lg border-2 transition-colors',
|
||||||
|
value === type
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-border hover:bg-muted',
|
||||||
|
)}
|
||||||
|
onClick={() => onChange(type)}
|
||||||
|
>
|
||||||
|
<Icon className={cn('h-6 w-6 mb-1', color)} />
|
||||||
|
<span className="text-sm font-medium">{label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('grid gap-4 sm:grid-cols-3', className)}>
|
||||||
|
{MEAL_TYPES.map(({ value: type, label, sublabel, icon: Icon, color }) => (
|
||||||
|
<Card
|
||||||
|
key={type}
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer transition-colors',
|
||||||
|
value === type ? 'border-primary bg-primary/5' : 'hover:bg-muted/50',
|
||||||
|
)}
|
||||||
|
onClick={() => onChange(type)}
|
||||||
|
>
|
||||||
|
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||||
|
<Icon className={cn('h-10 w-10 mb-2', color)} />
|
||||||
|
<span className="font-semibold">{label}</span>
|
||||||
|
{showSublabel && sublabel && (
|
||||||
|
<span className="text-sm text-muted-foreground">{sublabel}</span>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
src/components/caregiver/PageHeader.tsx
Normal file
43
src/components/caregiver/PageHeader.tsx
Normal file
@@ -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 (
|
||||||
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||||
|
<div className="container flex h-16 items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{backHref && (
|
||||||
|
<Button variant="ghost" size="sm" asChild>
|
||||||
|
<Link href={backHref}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
{backLabel}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold">{title}</h1>
|
||||||
|
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{rightContent && <div className="flex items-center gap-2">{rightContent}</div>}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
src/components/caregiver/StatCard.tsx
Normal file
39
src/components/caregiver/StatCard.tsx
Normal file
@@ -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 (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||||
|
{Icon && <Icon className={cn('h-4 w-4 text-muted-foreground', iconColor)} />}
|
||||||
|
{!Icon && dotColor && <div className={cn('h-2 w-2 rounded-full', dotColor)} />}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/caregiver/StatusBadge.tsx
Normal file
31
src/components/caregiver/StatusBadge.tsx
Normal file
@@ -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 <Badge variant="outline">{status}</Badge>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = config.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(config.bgColor, config.textColor, config.borderColor, className)}
|
||||||
|
>
|
||||||
|
{showIcon && <Icon className="mr-1 h-3 w-3" />}
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
8
src/components/caregiver/index.ts
Normal file
8
src/components/caregiver/index.ts
Normal file
@@ -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'
|
||||||
@@ -60,9 +60,9 @@ function SheetContent({
|
|||||||
className={cn(
|
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",
|
"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" &&
|
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" &&
|
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" &&
|
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",
|
"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" &&
|
side === "bottom" &&
|
||||||
|
|||||||
271
src/lib/constants/meal-options.ts
Normal file
271
src/lib/constants/meal-options.ts
Normal file
@@ -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<string, OptionSection> = {
|
||||||
|
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<string, OptionSection> = {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/lib/constants/meal.ts
Normal file
88
src/lib/constants/meal.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user