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 { format, parseISO } from 'date-fns'
|
||||
import {
|
||||
Loader2,
|
||||
LogOut,
|
||||
Sun,
|
||||
Moon,
|
||||
Sunrise,
|
||||
ClipboardList,
|
||||
Users,
|
||||
Settings,
|
||||
Plus,
|
||||
Pencil,
|
||||
Eye,
|
||||
Send,
|
||||
Check,
|
||||
ChefHat,
|
||||
AlertTriangle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Calendar,
|
||||
UserCheck,
|
||||
UserX,
|
||||
Check,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
@@ -64,6 +57,21 @@ import {
|
||||
} from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import {
|
||||
LoadingSpinner,
|
||||
StatusBadge,
|
||||
MealTypeIcon,
|
||||
StatCard,
|
||||
EmptyState,
|
||||
MealTypeSelector,
|
||||
} from '@/components/caregiver'
|
||||
import {
|
||||
ORDER_STATUSES,
|
||||
getMealTypeLabel,
|
||||
type MealType,
|
||||
type OrderStatus,
|
||||
} from '@/lib/constants/meal'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
name?: string
|
||||
@@ -78,8 +86,8 @@ interface MealOrder {
|
||||
id: number
|
||||
title: string
|
||||
date: string
|
||||
mealType: 'breakfast' | 'lunch' | 'dinner'
|
||||
status: 'draft' | 'submitted' | 'preparing' | 'completed'
|
||||
mealType: MealType
|
||||
status: OrderStatus
|
||||
mealCount: number
|
||||
createdAt: string
|
||||
}
|
||||
@@ -137,23 +145,21 @@ export default function CaregiverDashboardPage() {
|
||||
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(() => 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 fetchOrders = useCallback(async (page: number = 1) => {
|
||||
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`
|
||||
@@ -184,12 +190,13 @@ export default function CaregiverDashboardPage() {
|
||||
} finally {
|
||||
setOrdersLoading(false)
|
||||
}
|
||||
}, [router, dateFilter, statusFilter])
|
||||
},
|
||||
[router, dateFilter, statusFilter],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
// Fetch current user
|
||||
const userRes = await fetch('/api/users/me', { credentials: 'include' })
|
||||
if (!userRes.ok) {
|
||||
router.push('/caregiver/login')
|
||||
@@ -202,7 +209,6 @@ export default function CaregiverDashboardPage() {
|
||||
}
|
||||
setUser(userData.user)
|
||||
|
||||
// Fetch residents
|
||||
const residentsRes = await fetch('/api/residents?where[active][equals]=true&limit=500', {
|
||||
credentials: 'include',
|
||||
})
|
||||
@@ -211,12 +217,11 @@ export default function CaregiverDashboardPage() {
|
||||
setResidents(residentsData.docs || [])
|
||||
}
|
||||
|
||||
// Fetch stats (all orders from last 7 days)
|
||||
const weekAgo = new Date()
|
||||
weekAgo.setDate(weekAgo.getDate() - 7)
|
||||
const statsRes = await fetch(
|
||||
`/api/meal-orders?where[date][greater_than_equal]=${weekAgo.toISOString().split('T')[0]}&limit=1000&depth=0`,
|
||||
{ credentials: 'include' }
|
||||
{ credentials: 'include' },
|
||||
)
|
||||
if (statsRes.ok) {
|
||||
const statsData = await statsRes.json()
|
||||
@@ -284,10 +289,9 @@ export default function CaregiverDashboardPage() {
|
||||
setCoverageLoading(true)
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`,
|
||||
{ credentials: 'include' }
|
||||
)
|
||||
const res = await fetch(`/api/meals?where[order][equals]=${order.id}&depth=1&limit=500`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setOrderMeals(data.docs || [])
|
||||
@@ -299,67 +303,6 @@ export default function CaregiverDashboardPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const getMealTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'breakfast':
|
||||
return 'Breakfast'
|
||||
case 'lunch':
|
||||
return 'Lunch'
|
||||
case 'dinner':
|
||||
return 'Dinner'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const getMealTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'breakfast':
|
||||
return <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
|
||||
@@ -379,11 +322,7 @@ export default function CaregiverDashboardPage() {
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)
|
||||
return <LoadingSpinner fullPage />
|
||||
}
|
||||
|
||||
const tenantName =
|
||||
@@ -391,9 +330,8 @@ export default function CaregiverDashboardPage() {
|
||||
? user.tenants[0].tenant.name
|
||||
: 'Care Home'
|
||||
|
||||
// Calculate covered residents for coverage dialog
|
||||
const coveredResidentIds = new Set(
|
||||
orderMeals.map((m) => (typeof m.resident === 'object' ? m.resident.id : m.resident))
|
||||
orderMeals.map((m) => (typeof m.resident === 'object' ? m.resident.id : m.resident)),
|
||||
)
|
||||
const coveredResidents = residents.filter((r) => coveredResidentIds.has(r.id))
|
||||
const uncoveredResidents = residents.filter((r) => !coveredResidentIds.has(r.id))
|
||||
@@ -425,61 +363,24 @@ export default function CaregiverDashboardPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<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">Draft</CardTitle>
|
||||
<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>
|
||||
<StatCard
|
||||
title="Total Orders"
|
||||
value={stats.total}
|
||||
description="Last 7 days"
|
||||
icon={ClipboardList}
|
||||
/>
|
||||
{ORDER_STATUSES.map((status) => (
|
||||
<StatCard
|
||||
key={status.value}
|
||||
title={status.label}
|
||||
value={stats[status.value as keyof OrderStats]}
|
||||
description={status.description}
|
||||
dotColor={status.dotColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
@@ -523,7 +424,6 @@ export default function CaregiverDashboardPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Meal Orders Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<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.
|
||||
</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
|
||||
@@ -546,17 +445,17 @@ export default function CaregiverDashboardPage() {
|
||||
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>
|
||||
{ORDER_STATUSES.map((status) => (
|
||||
<SelectItem key={status.value} value={status.value}>
|
||||
{status.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(dateFilter || statusFilter !== 'all') && (
|
||||
@@ -576,21 +475,19 @@ export default function CaregiverDashboardPage() {
|
||||
</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>
|
||||
<LoadingSpinner />
|
||||
) : 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)}>
|
||||
<EmptyState
|
||||
icon={ClipboardList}
|
||||
title="No orders found."
|
||||
description="Create a new order to get started."
|
||||
action={
|
||||
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Order
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
@@ -609,12 +506,7 @@ export default function CaregiverDashboardPage() {
|
||||
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>
|
||||
<MealTypeIcon type={order.mealType} showLabel />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -631,10 +523,7 @@ export default function CaregiverDashboardPage() {
|
||||
onClick={() => handleViewCoverage(order)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress
|
||||
value={coverage.percentage}
|
||||
className="h-2 flex-1"
|
||||
/>
|
||||
<Progress value={coverage.percentage} className="h-2 flex-1" />
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{coverage.coveredCount}/{coverage.totalResidents}
|
||||
</span>
|
||||
@@ -655,25 +544,25 @@ export default function CaregiverDashboardPage() {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(order.status)}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={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}`}>
|
||||
{order.status === 'draft' ? (
|
||||
<>
|
||||
<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>
|
||||
)
|
||||
@@ -681,7 +570,6 @@ export default function CaregiverDashboardPage() {
|
||||
</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">
|
||||
@@ -743,14 +631,11 @@ export default function CaregiverDashboardPage() {
|
||||
</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>
|
||||
<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">
|
||||
@@ -764,28 +649,7 @@ export default function CaregiverDashboardPage() {
|
||||
</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>
|
||||
<MealTypeSelector value={newOrderMealType} onChange={setNewOrderMealType} variant="button" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
@@ -794,22 +658,16 @@ export default function CaregiverDashboardPage() {
|
||||
</Button>
|
||||
<Button onClick={handleCreateOrder} disabled={creating}>
|
||||
{creating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
<LoadingSpinner size="sm" className="mr-2" />
|
||||
) : (
|
||||
<>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Order
|
||||
</>
|
||||
)}
|
||||
{creating ? 'Creating...' : '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>
|
||||
@@ -827,12 +685,9 @@ export default function CaregiverDashboardPage() {
|
||||
</DialogHeader>
|
||||
|
||||
{coverageLoading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<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">
|
||||
@@ -843,9 +698,7 @@ export default function CaregiverDashboardPage() {
|
||||
</div>
|
||||
<Progress
|
||||
value={
|
||||
residents.length > 0
|
||||
? (coveredResidents.length / residents.length) * 100
|
||||
: 0
|
||||
residents.length > 0 ? (coveredResidents.length / residents.length) * 100 : 0
|
||||
}
|
||||
className="h-3"
|
||||
/>
|
||||
@@ -856,8 +709,8 @@ export default function CaregiverDashboardPage() {
|
||||
getCoverageColor(
|
||||
residents.length > 0
|
||||
? (coveredResidents.length / residents.length) * 100
|
||||
: 0
|
||||
)
|
||||
: 0,
|
||||
),
|
||||
)}
|
||||
>
|
||||
{residents.length > 0
|
||||
@@ -867,7 +720,6 @@ export default function CaregiverDashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Uncovered Residents */}
|
||||
{uncoveredResidents.length > 0 && (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -893,7 +745,6 @@ export default function CaregiverDashboardPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Covered Residents */}
|
||||
{coveredResidents.length > 0 && (
|
||||
<div>
|
||||
<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 Link from 'next/link'
|
||||
import { format, parseISO } from 'date-fns'
|
||||
import { ArrowLeft, Plus, Loader2, Send, Eye, Pencil, Check, ChefHat } from 'lucide-react'
|
||||
import { Plus, Pencil, Eye } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -27,12 +26,26 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
import {
|
||||
PageHeader,
|
||||
LoadingSpinner,
|
||||
StatusBadge,
|
||||
MealTypeIcon,
|
||||
EmptyState,
|
||||
} from '@/components/caregiver'
|
||||
import {
|
||||
ORDER_STATUSES,
|
||||
getMealTypeLabel,
|
||||
type MealType,
|
||||
type OrderStatus,
|
||||
} from '@/lib/constants/meal'
|
||||
|
||||
interface MealOrder {
|
||||
id: number
|
||||
title: string
|
||||
date: string
|
||||
mealType: 'breakfast' | 'lunch' | 'dinner'
|
||||
status: 'draft' | 'submitted' | 'preparing' | 'completed'
|
||||
mealType: MealType
|
||||
status: OrderStatus
|
||||
mealCount: number
|
||||
createdAt: string
|
||||
}
|
||||
@@ -72,55 +85,7 @@ export default function OrdersListPage() {
|
||||
fetchOrders()
|
||||
}, [router, dateFilter, statusFilter])
|
||||
|
||||
const getMealTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'breakfast':
|
||||
return 'Breakfast'
|
||||
case 'lunch':
|
||||
return 'Lunch'
|
||||
case 'dinner':
|
||||
return 'Dinner'
|
||||
default:
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return (
|
||||
<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') => {
|
||||
const handleCreateOrder = async (mealType: MealType) => {
|
||||
try {
|
||||
const res = await fetch('/api/meal-orders', {
|
||||
method: 'POST',
|
||||
@@ -144,19 +109,7 @@ export default function OrdersListPage() {
|
||||
|
||||
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">
|
||||
<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>
|
||||
<PageHeader title="Meal Orders" backHref="/caregiver/dashboard" />
|
||||
|
||||
<main className="container py-6">
|
||||
<Card className="mb-6">
|
||||
@@ -182,43 +135,29 @@ export default function OrdersListPage() {
|
||||
</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>
|
||||
{ORDER_STATUSES.map((status) => (
|
||||
<SelectItem key={status.value} value={status.value}>
|
||||
{status.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Create New Order</Label>
|
||||
<div className="flex gap-2">
|
||||
{(['breakfast', 'lunch', 'dinner'] as const).map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleCreateOrder('breakfast')}
|
||||
onClick={() => handleCreateOrder(type)}
|
||||
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
|
||||
{getMealTypeLabel(type)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,16 +167,12 @@ export default function OrdersListPage() {
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
<LoadingSpinner />
|
||||
) : 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 date.</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
Create a new order using the buttons above.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
title="No orders found for the selected date."
|
||||
description="Create a new order using the buttons above."
|
||||
/>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -252,12 +187,14 @@ export default function OrdersListPage() {
|
||||
<TableBody>
|
||||
{orders.map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-medium">
|
||||
{getMealTypeLabel(order.mealType)}
|
||||
<TableCell>
|
||||
<MealTypeIcon type={order.mealType} showLabel />
|
||||
</TableCell>
|
||||
<TableCell>{format(parseISO(order.date), 'MMM d, yyyy')}</TableCell>
|
||||
<TableCell>{order.mealCount} residents</TableCell>
|
||||
<TableCell>{getStatusBadge(order.status)}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={order.status} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/caregiver/orders/${order.id}`}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import type { Viewport } from 'next'
|
||||
|
||||
import '@/app/globals.css'
|
||||
|
||||
@@ -7,6 +8,12 @@ export const metadata = {
|
||||
title: 'Meal Planner - Caregiver',
|
||||
}
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
viewportFit: 'cover',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -166,4 +166,13 @@
|
||||
.container {
|
||||
@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(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full border-l",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full border-r",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
|
||||
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