feat: initial setup, collections, caregiver frontend
This commit is contained in:
166
src/app/(app)/caregiver/dashboard/page.tsx
Normal file
166
src/app/(app)/caregiver/dashboard/page.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
name?: string
|
||||
email: string
|
||||
tenants?: Array<{
|
||||
tenant: { id: number; name: string } | number
|
||||
roles?: string[]
|
||||
}>
|
||||
}
|
||||
|
||||
interface OrderStats {
|
||||
pending: number
|
||||
preparing: number
|
||||
prepared: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export default function CaregiverDashboardPage() {
|
||||
const router = useRouter()
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [stats, setStats] = useState<OrderStats>({ pending: 0, preparing: 0, prepared: 0, total: 0 })
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Check auth
|
||||
const userRes = await fetch('/api/users/me', { credentials: 'include' })
|
||||
if (!userRes.ok) {
|
||||
router.push('/caregiver/login')
|
||||
return
|
||||
}
|
||||
const userData = await userRes.json()
|
||||
if (!userData.user) {
|
||||
router.push('/caregiver/login')
|
||||
return
|
||||
}
|
||||
setUser(userData.user)
|
||||
|
||||
// Fetch today's orders stats
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const ordersRes = await fetch(`/api/meal-orders?where[date][equals]=${today}&limit=1000`, {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (ordersRes.ok) {
|
||||
const ordersData = await ordersRes.json()
|
||||
const orders = ordersData.docs || []
|
||||
setStats({
|
||||
pending: orders.filter((o: { status: string }) => o.status === 'pending').length,
|
||||
preparing: orders.filter((o: { status: string }) => o.status === 'preparing').length,
|
||||
prepared: orders.filter((o: { status: string }) => o.status === 'prepared').length,
|
||||
total: orders.length,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [router])
|
||||
|
||||
const handleLogout = async () => {
|
||||
await fetch('/api/users/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
router.push('/caregiver/login')
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const tenantName =
|
||||
user?.tenants?.[0]?.tenant && typeof user.tenants[0].tenant === 'object'
|
||||
? user.tenants[0].tenant.name
|
||||
: 'Care Home'
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<h1 className="header__title">{tenantName}</h1>
|
||||
<div className="header__user">
|
||||
<span className="header__user-name">{user?.name || user?.email}</span>
|
||||
<button onClick={handleLogout} className="btn btn--secondary">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<div className="page-title">
|
||||
<h1>Dashboard</h1>
|
||||
<p>Today's overview</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.total}</div>
|
||||
<div className="stat-card__label">Total Orders Today</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.pending}</div>
|
||||
<div className="stat-card__label">Pending</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.preparing}</div>
|
||||
<div className="stat-card__label">Preparing</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.prepared}</div>
|
||||
<div className="stat-card__label">Prepared</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Quick Actions</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="quick-actions">
|
||||
<Link href="/caregiver/orders/new?mealType=breakfast" className="quick-action">
|
||||
<div className="quick-action__icon">🌅</div>
|
||||
<div className="quick-action__label">New Breakfast</div>
|
||||
</Link>
|
||||
<Link href="/caregiver/orders/new?mealType=lunch" className="quick-action">
|
||||
<div className="quick-action__icon">☀️</div>
|
||||
<div className="quick-action__label">New Lunch</div>
|
||||
</Link>
|
||||
<Link href="/caregiver/orders/new?mealType=dinner" className="quick-action">
|
||||
<div className="quick-action__icon">🌙</div>
|
||||
<div className="quick-action__label">New Dinner</div>
|
||||
</Link>
|
||||
<Link href="/caregiver/orders" className="quick-action">
|
||||
<div className="quick-action__icon">📋</div>
|
||||
<div className="quick-action__label">View Orders</div>
|
||||
</Link>
|
||||
<Link href="/caregiver/residents" className="quick-action">
|
||||
<div className="quick-action__icon">👥</div>
|
||||
<div className="quick-action__label">Residents</div>
|
||||
</Link>
|
||||
<Link href="/admin" className="quick-action">
|
||||
<div className="quick-action__icon">⚙️</div>
|
||||
<div className="quick-action__label">Admin Panel</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
141
src/app/(app)/caregiver/login/page.tsx
Normal file
141
src/app/(app)/caregiver/login/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function CaregiverLoginPage() {
|
||||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checking, setChecking] = useState(true)
|
||||
|
||||
// Check if already logged in
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/users/me', { credentials: 'include' })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
if (data.user) {
|
||||
router.push('/caregiver/dashboard')
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Not logged in
|
||||
}
|
||||
setChecking(false)
|
||||
}
|
||||
checkAuth()
|
||||
}, [router])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/users/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.errors?.[0]?.message || 'Login failed')
|
||||
}
|
||||
|
||||
// Check if user has caregiver or admin role
|
||||
const user = data.user
|
||||
const hasCaregiverRole =
|
||||
user?.roles?.includes('super-admin') ||
|
||||
user?.tenants?.some(
|
||||
(t: { roles?: string[] }) =>
|
||||
t.roles?.includes('caregiver') || t.roles?.includes('admin'),
|
||||
)
|
||||
|
||||
if (!hasCaregiverRole) {
|
||||
// Logout if not a caregiver
|
||||
await fetch('/api/users/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
throw new Error('You do not have caregiver access')
|
||||
}
|
||||
|
||||
router.push('/caregiver/dashboard')
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Login failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (checking) {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-page__card">
|
||||
<div className="login-page__logo">
|
||||
<h1>Meal Planner</h1>
|
||||
<p>Caregiver Portal</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card__body">
|
||||
{error && <div className="message message--error">{error}</div>}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
className="input"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className="input"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn--primary btn--block btn--large"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
768
src/app/(app)/caregiver/orders/new/page.tsx
Normal file
768
src/app/(app)/caregiver/orders/new/page.tsx
Normal file
@@ -0,0 +1,768 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Resident {
|
||||
id: number
|
||||
name: string
|
||||
room: string
|
||||
table?: string
|
||||
station?: string
|
||||
highCaloric?: boolean
|
||||
aversions?: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
type MealType = 'breakfast' | 'lunch' | 'dinner'
|
||||
|
||||
interface BreakfastOptions {
|
||||
accordingToPlan: boolean
|
||||
bread: {
|
||||
breadRoll: boolean
|
||||
wholeGrainRoll: boolean
|
||||
greyBread: boolean
|
||||
wholeGrainBread: boolean
|
||||
whiteBread: boolean
|
||||
crispbread: boolean
|
||||
}
|
||||
porridge: boolean
|
||||
preparation: { sliced: boolean; spread: boolean }
|
||||
spreads: {
|
||||
butter: boolean
|
||||
margarine: boolean
|
||||
jam: boolean
|
||||
diabeticJam: boolean
|
||||
honey: boolean
|
||||
cheese: boolean
|
||||
quark: boolean
|
||||
sausage: boolean
|
||||
}
|
||||
beverages: { coffee: boolean; tea: boolean; hotMilk: boolean; coldMilk: boolean }
|
||||
additions: { sugar: boolean; sweetener: boolean; coffeeCreamer: boolean }
|
||||
}
|
||||
|
||||
interface LunchOptions {
|
||||
portionSize: 'small' | 'large' | 'vegetarian'
|
||||
soup: boolean
|
||||
dessert: boolean
|
||||
specialPreparations: {
|
||||
pureedFood: boolean
|
||||
pureedMeat: boolean
|
||||
slicedMeat: boolean
|
||||
mashedPotatoes: boolean
|
||||
}
|
||||
restrictions: { noFish: boolean; fingerFood: boolean; onlySweet: boolean }
|
||||
}
|
||||
|
||||
interface DinnerOptions {
|
||||
accordingToPlan: boolean
|
||||
bread: { greyBread: boolean; wholeGrainBread: boolean; whiteBread: boolean; crispbread: boolean }
|
||||
preparation: { spread: boolean; sliced: boolean }
|
||||
spreads: { butter: boolean; margarine: boolean }
|
||||
soup: boolean
|
||||
porridge: boolean
|
||||
noFish: boolean
|
||||
beverages: { tea: boolean; cocoa: boolean; hotMilk: boolean; coldMilk: boolean }
|
||||
additions: { sugar: boolean; sweetener: boolean }
|
||||
}
|
||||
|
||||
const defaultBreakfast: BreakfastOptions = {
|
||||
accordingToPlan: false,
|
||||
bread: {
|
||||
breadRoll: false,
|
||||
wholeGrainRoll: false,
|
||||
greyBread: false,
|
||||
wholeGrainBread: false,
|
||||
whiteBread: false,
|
||||
crispbread: false,
|
||||
},
|
||||
porridge: false,
|
||||
preparation: { sliced: false, spread: false },
|
||||
spreads: {
|
||||
butter: false,
|
||||
margarine: false,
|
||||
jam: false,
|
||||
diabeticJam: false,
|
||||
honey: false,
|
||||
cheese: false,
|
||||
quark: false,
|
||||
sausage: false,
|
||||
},
|
||||
beverages: { coffee: false, tea: false, hotMilk: false, coldMilk: false },
|
||||
additions: { sugar: false, sweetener: false, coffeeCreamer: false },
|
||||
}
|
||||
|
||||
const defaultLunch: LunchOptions = {
|
||||
portionSize: 'large',
|
||||
soup: false,
|
||||
dessert: true,
|
||||
specialPreparations: {
|
||||
pureedFood: false,
|
||||
pureedMeat: false,
|
||||
slicedMeat: false,
|
||||
mashedPotatoes: false,
|
||||
},
|
||||
restrictions: { noFish: false, fingerFood: false, onlySweet: false },
|
||||
}
|
||||
|
||||
const defaultDinner: DinnerOptions = {
|
||||
accordingToPlan: false,
|
||||
bread: { greyBread: false, wholeGrainBread: false, whiteBread: false, crispbread: false },
|
||||
preparation: { spread: false, sliced: false },
|
||||
spreads: { butter: false, margarine: false },
|
||||
soup: false,
|
||||
porridge: false,
|
||||
noFish: false,
|
||||
beverages: { tea: false, cocoa: false, hotMilk: false, coldMilk: false },
|
||||
additions: { sugar: false, sweetener: false },
|
||||
}
|
||||
|
||||
function NewOrderContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const initialMealType = (searchParams.get('mealType') as MealType) || null
|
||||
|
||||
const [step, setStep] = useState(initialMealType ? 2 : 1)
|
||||
const [residents, setResidents] = useState<Resident[]>([])
|
||||
const [selectedResident, setSelectedResident] = useState<Resident | null>(null)
|
||||
const [mealType, setMealType] = useState<MealType | null>(initialMealType)
|
||||
const [date, setDate] = useState(() => new Date().toISOString().split('T')[0])
|
||||
const [breakfast, setBreakfast] = useState<BreakfastOptions>(defaultBreakfast)
|
||||
const [lunch, setLunch] = useState<LunchOptions>(defaultLunch)
|
||||
const [dinner, setDinner] = useState<DinnerOptions>(defaultDinner)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchResidents = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/residents?where[active][equals]=true&limit=100&sort=name', {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setResidents(data.docs || [])
|
||||
} else if (res.status === 401) {
|
||||
router.push('/caregiver/login')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching residents:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchResidents()
|
||||
}, [router])
|
||||
|
||||
const filteredResidents = residents.filter(
|
||||
(r) =>
|
||||
r.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.room.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedResident || !mealType || !date) return
|
||||
|
||||
setSubmitting(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const orderData: Record<string, unknown> = {
|
||||
resident: selectedResident.id,
|
||||
date,
|
||||
mealType,
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
if (mealType === 'breakfast') {
|
||||
orderData.breakfast = breakfast
|
||||
} else if (mealType === 'lunch') {
|
||||
orderData.lunch = lunch
|
||||
} else if (mealType === 'dinner') {
|
||||
orderData.dinner = dinner
|
||||
}
|
||||
|
||||
const res = await fetch('/api/meal-orders', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(orderData),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.errors?.[0]?.message || 'Failed to create order')
|
||||
}
|
||||
|
||||
router.push('/caregiver/dashboard')
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getMealTypeLabel = (type: MealType) => {
|
||||
switch (type) {
|
||||
case 'breakfast':
|
||||
return 'Breakfast (Frühstück)'
|
||||
case 'lunch':
|
||||
return 'Lunch (Mittagessen)'
|
||||
case 'dinner':
|
||||
return 'Dinner (Abendessen)'
|
||||
}
|
||||
}
|
||||
|
||||
const renderCheckbox = (
|
||||
label: string,
|
||||
checked: boolean,
|
||||
onChange: (checked: boolean) => void,
|
||||
) => (
|
||||
<label className={`checkbox-item ${checked ? 'checkbox-item--checked' : ''}`}>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<Link href="/caregiver/dashboard" className="btn btn--secondary">
|
||||
← Back
|
||||
</Link>
|
||||
<h1 className="header__title">New Meal Order</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
{/* Progress Steps */}
|
||||
<div className="steps">
|
||||
<div className={`steps__step ${step >= 1 ? 'steps__step--active' : ''} ${step > 1 ? 'steps__step--completed' : ''}`} />
|
||||
<div className={`steps__step ${step >= 2 ? 'steps__step--active' : ''} ${step > 2 ? 'steps__step--completed' : ''}`} />
|
||||
<div className={`steps__step ${step >= 3 ? 'steps__step--active' : ''} ${step > 3 ? 'steps__step--completed' : ''}`} />
|
||||
<div className={`steps__step ${step >= 4 ? 'steps__step--active' : ''}`} />
|
||||
</div>
|
||||
|
||||
{error && <div className="message message--error">{error}</div>}
|
||||
|
||||
{/* Step 1: Select Meal Type */}
|
||||
{step === 1 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 1: Select Meal Type</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="form-group">
|
||||
<label htmlFor="date">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
className="input"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="meal-type-grid">
|
||||
<button
|
||||
type="button"
|
||||
className={`meal-type-btn ${mealType === 'breakfast' ? 'meal-type-btn--selected' : ''}`}
|
||||
onClick={() => setMealType('breakfast')}
|
||||
>
|
||||
<div className="meal-type-btn__icon">🌅</div>
|
||||
<div className="meal-type-btn__label">Breakfast</div>
|
||||
<div className="meal-type-btn__sublabel">Frühstück</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`meal-type-btn ${mealType === 'lunch' ? 'meal-type-btn--selected' : ''}`}
|
||||
onClick={() => setMealType('lunch')}
|
||||
>
|
||||
<div className="meal-type-btn__icon">☀️</div>
|
||||
<div className="meal-type-btn__label">Lunch</div>
|
||||
<div className="meal-type-btn__sublabel">Mittagessen</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`meal-type-btn ${mealType === 'dinner' ? 'meal-type-btn--selected' : ''}`}
|
||||
onClick={() => setMealType('dinner')}
|
||||
>
|
||||
<div className="meal-type-btn__icon">🌙</div>
|
||||
<div className="meal-type-btn__label">Dinner</div>
|
||||
<div className="meal-type-btn__sublabel">Abendessen</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<button
|
||||
className="btn btn--primary btn--block btn--large"
|
||||
disabled={!mealType}
|
||||
onClick={() => setStep(2)}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Select Resident */}
|
||||
{step === 2 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 2: Select Resident</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="form-group">
|
||||
<div className="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or room..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="resident-list">
|
||||
{filteredResidents.map((resident) => (
|
||||
<div
|
||||
key={resident.id}
|
||||
className={`resident-card ${selectedResident?.id === resident.id ? 'resident-card--selected' : ''}`}
|
||||
onClick={() => setSelectedResident(resident)}
|
||||
>
|
||||
<div className="resident-card__name">{resident.name}</div>
|
||||
<div className="resident-card__details">
|
||||
<span>Room {resident.room}</span>
|
||||
{resident.table && <span>Table {resident.table}</span>}
|
||||
{resident.station && <span>{resident.station}</span>}
|
||||
</div>
|
||||
{resident.highCaloric && (
|
||||
<div className="resident-card__badge">High Caloric</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn btn--secondary" onClick={() => setStep(1)}>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--primary btn--block btn--large"
|
||||
disabled={!selectedResident}
|
||||
onClick={() => setStep(3)}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Meal Options */}
|
||||
{step === 3 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 3: {mealType && getMealTypeLabel(mealType)} Options</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
{/* Show resident notes if any */}
|
||||
{(selectedResident?.aversions || selectedResident?.notes) && (
|
||||
<div className="message message--warning">
|
||||
<strong>Notes for {selectedResident?.name}:</strong>
|
||||
{selectedResident?.aversions && <div>Aversions: {selectedResident.aversions}</div>}
|
||||
{selectedResident?.notes && <div>{selectedResident.notes}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* BREAKFAST OPTIONS */}
|
||||
{mealType === 'breakfast' && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3 className="section__title">General</h3>
|
||||
<div className="checkbox-group">
|
||||
{renderCheckbox('According to Plan (lt. Plan)', breakfast.accordingToPlan, (v) =>
|
||||
setBreakfast({ ...breakfast, accordingToPlan: v }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Bread (Brot)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Bread Roll (Brötchen)', breakfast.bread.breadRoll, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, breadRoll: v } }),
|
||||
)}
|
||||
{renderCheckbox('Whole Grain Roll (Vollkornbrötchen)', breakfast.bread.wholeGrainRoll, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainRoll: v } }),
|
||||
)}
|
||||
{renderCheckbox('Grey Bread (Graubrot)', breakfast.bread.greyBread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, greyBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Whole Grain Bread (Vollkornbrot)', breakfast.bread.wholeGrainBread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('White Bread (Weißbrot)', breakfast.bread.whiteBread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, whiteBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Crispbread (Knäckebrot)', breakfast.bread.crispbread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, crispbread: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Preparation</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Porridge (Brei)', breakfast.porridge, (v) =>
|
||||
setBreakfast({ ...breakfast, porridge: v }),
|
||||
)}
|
||||
{renderCheckbox('Sliced (geschnitten)', breakfast.preparation.sliced, (v) =>
|
||||
setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, sliced: v } }),
|
||||
)}
|
||||
{renderCheckbox('Spread (geschmiert)', breakfast.preparation.spread, (v) =>
|
||||
setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, spread: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Spreads (Aufstrich)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Butter', breakfast.spreads.butter, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, butter: v } }),
|
||||
)}
|
||||
{renderCheckbox('Margarine', breakfast.spreads.margarine, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, margarine: v } }),
|
||||
)}
|
||||
{renderCheckbox('Jam (Konfitüre)', breakfast.spreads.jam, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, jam: v } }),
|
||||
)}
|
||||
{renderCheckbox('Diabetic Jam (Diab. Konfitüre)', breakfast.spreads.diabeticJam, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, diabeticJam: v } }),
|
||||
)}
|
||||
{renderCheckbox('Honey (Honig)', breakfast.spreads.honey, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, honey: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cheese (Käse)', breakfast.spreads.cheese, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, cheese: v } }),
|
||||
)}
|
||||
{renderCheckbox('Quark', breakfast.spreads.quark, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, quark: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sausage (Wurst)', breakfast.spreads.sausage, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, sausage: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Beverages (Getränke)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Coffee (Kaffee)', breakfast.beverages.coffee, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coffee: v } }),
|
||||
)}
|
||||
{renderCheckbox('Tea (Tee)', breakfast.beverages.tea, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, tea: v } }),
|
||||
)}
|
||||
{renderCheckbox('Hot Milk (Milch heiß)', breakfast.beverages.hotMilk, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, hotMilk: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cold Milk (Milch kalt)', breakfast.beverages.coldMilk, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coldMilk: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Additions (Zusätze)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
{renderCheckbox('Sugar (Zucker)', breakfast.additions.sugar, (v) =>
|
||||
setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sugar: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sweetener (Süßstoff)', breakfast.additions.sweetener, (v) =>
|
||||
setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sweetener: v } }),
|
||||
)}
|
||||
{renderCheckbox('Coffee Creamer (Kaffeesahne)', breakfast.additions.coffeeCreamer, (v) =>
|
||||
setBreakfast({ ...breakfast, additions: { ...breakfast.additions, coffeeCreamer: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* LUNCH OPTIONS */}
|
||||
{mealType === 'lunch' && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3 className="section__title">Portion Size</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
<label
|
||||
className={`checkbox-item ${lunch.portionSize === 'small' ? 'checkbox-item--checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="portionSize"
|
||||
checked={lunch.portionSize === 'small'}
|
||||
onChange={() => setLunch({ ...lunch, portionSize: 'small' })}
|
||||
/>
|
||||
<span>Small (Kleine)</span>
|
||||
</label>
|
||||
<label
|
||||
className={`checkbox-item ${lunch.portionSize === 'large' ? 'checkbox-item--checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="portionSize"
|
||||
checked={lunch.portionSize === 'large'}
|
||||
onChange={() => setLunch({ ...lunch, portionSize: 'large' })}
|
||||
/>
|
||||
<span>Large (Große)</span>
|
||||
</label>
|
||||
<label
|
||||
className={`checkbox-item ${lunch.portionSize === 'vegetarian' ? 'checkbox-item--checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="portionSize"
|
||||
checked={lunch.portionSize === 'vegetarian'}
|
||||
onChange={() => setLunch({ ...lunch, portionSize: 'vegetarian' })}
|
||||
/>
|
||||
<span>Vegetarian</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Meal Options</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Soup (Suppe)', lunch.soup, (v) => setLunch({ ...lunch, soup: v }))}
|
||||
{renderCheckbox('Dessert', lunch.dessert, (v) => setLunch({ ...lunch, dessert: v }))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Special Preparations</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Pureed Food (passierte Kost)', lunch.specialPreparations.pureedFood, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedFood: v } }),
|
||||
)}
|
||||
{renderCheckbox('Pureed Meat (passiertes Fleisch)', lunch.specialPreparations.pureedMeat, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedMeat: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sliced Meat (geschnittenes Fleisch)', lunch.specialPreparations.slicedMeat, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, slicedMeat: v } }),
|
||||
)}
|
||||
{renderCheckbox('Mashed Potatoes (Kartoffelbrei)', lunch.specialPreparations.mashedPotatoes, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, mashedPotatoes: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Restrictions</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
{renderCheckbox('No Fish (ohne Fisch)', lunch.restrictions.noFish, (v) =>
|
||||
setLunch({ ...lunch, restrictions: { ...lunch.restrictions, noFish: v } }),
|
||||
)}
|
||||
{renderCheckbox('Finger Food', lunch.restrictions.fingerFood, (v) =>
|
||||
setLunch({ ...lunch, restrictions: { ...lunch.restrictions, fingerFood: v } }),
|
||||
)}
|
||||
{renderCheckbox('Only Sweet (nur süß)', lunch.restrictions.onlySweet, (v) =>
|
||||
setLunch({ ...lunch, restrictions: { ...lunch.restrictions, onlySweet: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* DINNER OPTIONS */}
|
||||
{mealType === 'dinner' && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3 className="section__title">General</h3>
|
||||
<div className="checkbox-group">
|
||||
{renderCheckbox('According to Plan (lt. Plan)', dinner.accordingToPlan, (v) =>
|
||||
setDinner({ ...dinner, accordingToPlan: v }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Bread (Brot)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Grey Bread (Graubrot)', dinner.bread.greyBread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, greyBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Whole Grain Bread (Vollkornbrot)', dinner.bread.wholeGrainBread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, wholeGrainBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('White Bread (Weißbrot)', dinner.bread.whiteBread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, whiteBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Crispbread (Knäckebrot)', dinner.bread.crispbread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, crispbread: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Preparation</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Spread (geschmiert)', dinner.preparation.spread, (v) =>
|
||||
setDinner({ ...dinner, preparation: { ...dinner.preparation, spread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sliced (geschnitten)', dinner.preparation.sliced, (v) =>
|
||||
setDinner({ ...dinner, preparation: { ...dinner.preparation, sliced: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Spreads (Aufstrich)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Butter', dinner.spreads.butter, (v) =>
|
||||
setDinner({ ...dinner, spreads: { ...dinner.spreads, butter: v } }),
|
||||
)}
|
||||
{renderCheckbox('Margarine', dinner.spreads.margarine, (v) =>
|
||||
setDinner({ ...dinner, spreads: { ...dinner.spreads, margarine: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Additional Items</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
{renderCheckbox('Soup (Suppe)', dinner.soup, (v) => setDinner({ ...dinner, soup: v }))}
|
||||
{renderCheckbox('Porridge (Brei)', dinner.porridge, (v) =>
|
||||
setDinner({ ...dinner, porridge: v }),
|
||||
)}
|
||||
{renderCheckbox('No Fish (ohne Fisch)', dinner.noFish, (v) =>
|
||||
setDinner({ ...dinner, noFish: v }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Beverages (Getränke)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Tea (Tee)', dinner.beverages.tea, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, tea: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cocoa (Kakao)', dinner.beverages.cocoa, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, cocoa: v } }),
|
||||
)}
|
||||
{renderCheckbox('Hot Milk (Milch heiß)', dinner.beverages.hotMilk, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, hotMilk: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cold Milk (Milch kalt)', dinner.beverages.coldMilk, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, coldMilk: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Additions (Zusätze)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Sugar (Zucker)', dinner.additions.sugar, (v) =>
|
||||
setDinner({ ...dinner, additions: { ...dinner.additions, sugar: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sweetener (Süßstoff)', dinner.additions.sweetener, (v) =>
|
||||
setDinner({ ...dinner, additions: { ...dinner.additions, sweetener: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn btn--secondary" onClick={() => setStep(2)}>
|
||||
Back
|
||||
</button>
|
||||
<button className="btn btn--primary btn--block btn--large" onClick={() => setStep(4)}>
|
||||
Review Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Review and Submit */}
|
||||
{step === 4 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 4: Review & Submit</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="order-summary">
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Resident</span>
|
||||
<span className="order-summary__value">{selectedResident?.name}</span>
|
||||
</div>
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Room</span>
|
||||
<span className="order-summary__value">{selectedResident?.room}</span>
|
||||
</div>
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Date</span>
|
||||
<span className="order-summary__value">{date}</span>
|
||||
</div>
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Meal Type</span>
|
||||
<span className="order-summary__value">{mealType && getMealTypeLabel(mealType)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedResident?.highCaloric && (
|
||||
<div className="message message--warning" style={{ marginTop: '1rem' }}>
|
||||
<strong>Note:</strong> This resident requires high caloric meals.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn btn--secondary" onClick={() => setStep(3)}>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--success btn--block btn--large"
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Creating...' : 'Create Order'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function NewOrderPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<NewOrderContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
168
src/app/(app)/caregiver/orders/page.tsx
Normal file
168
src/app/(app)/caregiver/orders/page.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Resident {
|
||||
id: number
|
||||
name: string
|
||||
room: string
|
||||
}
|
||||
|
||||
interface MealOrder {
|
||||
id: number
|
||||
title: string
|
||||
date: string
|
||||
mealType: 'breakfast' | 'lunch' | 'dinner'
|
||||
status: 'pending' | 'preparing' | 'prepared'
|
||||
resident: Resident | number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export default function OrdersListPage() {
|
||||
const router = useRouter()
|
||||
const [orders, setOrders] = useState<MealOrder[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dateFilter, setDateFilter] = useState(() => new Date().toISOString().split('T')[0])
|
||||
const [mealTypeFilter, setMealTypeFilter] = useState<string>('all')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
let url = `/api/meal-orders?sort=-createdAt&limit=100&depth=1`
|
||||
if (dateFilter) {
|
||||
url += `&where[date][equals]=${dateFilter}`
|
||||
}
|
||||
if (mealTypeFilter !== 'all') {
|
||||
url += `&where[mealType][equals]=${mealTypeFilter}`
|
||||
}
|
||||
|
||||
const res = await fetch(url, { credentials: 'include' })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setOrders(data.docs || [])
|
||||
} else if (res.status === 401) {
|
||||
router.push('/caregiver/login')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching orders:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchOrders()
|
||||
}, [router, dateFilter, mealTypeFilter])
|
||||
|
||||
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) => {
|
||||
return <span className={`badge badge--${status}`}>{status.charAt(0).toUpperCase() + status.slice(1)}</span>
|
||||
}
|
||||
|
||||
const getResidentName = (resident: Resident | number) => {
|
||||
if (typeof resident === 'object') {
|
||||
return resident.name
|
||||
}
|
||||
return `Resident #${resident}`
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<Link href="/caregiver/dashboard" className="btn btn--secondary">
|
||||
← Back
|
||||
</Link>
|
||||
<h1 className="header__title">Meal Orders</h1>
|
||||
<Link href="/caregiver/orders/new" className="btn btn--primary">
|
||||
+ New Order
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Filter Orders</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="grid grid--2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="date">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
className="input"
|
||||
value={dateFilter}
|
||||
onChange={(e) => setDateFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="mealType">Meal Type</label>
|
||||
<select
|
||||
id="mealType"
|
||||
className="select"
|
||||
value={mealTypeFilter}
|
||||
onChange={(e) => setMealTypeFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="breakfast">Breakfast</option>
|
||||
<option value="lunch">Lunch</option>
|
||||
<option value="dinner">Dinner</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ marginTop: '1rem' }}>
|
||||
<div className="card__body" style={{ padding: 0, overflowX: 'auto' }}>
|
||||
{loading ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<div className="spinner" style={{ margin: '0 auto' }} />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center', color: 'var(--gray-500)' }}>
|
||||
No orders found for the selected criteria.
|
||||
</div>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resident</th>
|
||||
<th>Date</th>
|
||||
<th>Meal</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.map((order) => (
|
||||
<tr key={order.id}>
|
||||
<td>{getResidentName(order.resident)}</td>
|
||||
<td>{order.date}</td>
|
||||
<td>{getMealTypeLabel(order.mealType)}</td>
|
||||
<td>{getStatusBadge(order.status)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
133
src/app/(app)/caregiver/residents/page.tsx
Normal file
133
src/app/(app)/caregiver/residents/page.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Resident {
|
||||
id: number
|
||||
name: string
|
||||
room: string
|
||||
table?: string
|
||||
station?: string
|
||||
highCaloric?: boolean
|
||||
aversions?: string
|
||||
notes?: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export default function ResidentsListPage() {
|
||||
const router = useRouter()
|
||||
const [residents, setResidents] = useState<Resident[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const fetchResidents = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/residents?where[active][equals]=true&limit=100&sort=name', {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setResidents(data.docs || [])
|
||||
} else if (res.status === 401) {
|
||||
router.push('/caregiver/login')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching residents:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchResidents()
|
||||
}, [router])
|
||||
|
||||
const filteredResidents = residents.filter(
|
||||
(r) =>
|
||||
r.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.room.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(r.station && r.station.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<Link href="/caregiver/dashboard" className="btn btn--secondary">
|
||||
← Back
|
||||
</Link>
|
||||
<h1 className="header__title">Residents</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<div className="page-title">
|
||||
<h1>Residents</h1>
|
||||
<p>View resident information and dietary requirements</p>
|
||||
</div>
|
||||
|
||||
<div className="actions-bar">
|
||||
<div className="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, room, or station..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<div className="spinner" style={{ margin: '0 auto' }} />
|
||||
</div>
|
||||
) : filteredResidents.length === 0 ? (
|
||||
<div className="card">
|
||||
<div className="card__body" style={{ textAlign: 'center', color: 'var(--gray-500)' }}>
|
||||
No residents found.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="resident-list">
|
||||
{filteredResidents.map((resident) => (
|
||||
<div key={resident.id} className="resident-card">
|
||||
<div className="resident-card__name">{resident.name}</div>
|
||||
<div className="resident-card__details">
|
||||
<span>Room {resident.room}</span>
|
||||
{resident.table && <span>Table {resident.table}</span>}
|
||||
{resident.station && <span>{resident.station}</span>}
|
||||
</div>
|
||||
{resident.highCaloric && (
|
||||
<div className="resident-card__badge">High Caloric</div>
|
||||
)}
|
||||
{(resident.aversions || resident.notes) && (
|
||||
<div style={{ marginTop: '0.75rem', fontSize: '0.875rem', color: 'var(--gray-600)' }}>
|
||||
{resident.aversions && (
|
||||
<div>
|
||||
<strong>Aversions:</strong> {resident.aversions}
|
||||
</div>
|
||||
)}
|
||||
{resident.notes && (
|
||||
<div>
|
||||
<strong>Notes:</strong> {resident.notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Link
|
||||
href={`/caregiver/orders/new?resident=${resident.id}`}
|
||||
className="btn btn--primary btn--block"
|
||||
>
|
||||
Create Order
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user