9566e19cd0
userNutritionGoals are daily targets (the nutrition diary already compares a single day's totals against them directly) — but the weekly meal-plan route summed an entire week's entries and compared that raw total straight against the same daily number, so coverage read roughly 7x too high. Now computes a daily average (across days that actually have planned meals) and compares that against the goal instead, adds a per-day breakdown (byDay) for a future day-view, and tracks unknownCount for entries whose recipe has no nutrition data yet, matching the diary route. Also fixed a real bug this surfaced: meal-plan/page.tsx and print/meal-plan parsed the ?week= date via new Date(dateStr) (UTC midnight) then read .getDay() (local time) — in positive-UTC-offset timezones this resolves to the wrong Monday, and the reverse (Date -> string via toISOString()) has the same mismatch in the other direction. Both now do local y/m/d math throughout. Verified locally: correct daily-average math end to end (recipe entry + batch-dish entry, which already attributed nutrition correctly via its required parent recipeId — no separate fix needed there), and confirmed the meal-plan page now resolves the right week and renders real coverage bars against actual planned entries.
138 lines
5.0 KiB
TypeScript
138 lines
5.0 KiB
TypeScript
import { headers } from "next/headers";
|
||
import { auth } from "@/lib/auth/server";
|
||
import { db, mealPlans, eq, and } from "@epicure/db";
|
||
import { PrintTrigger } from "@/components/recipe/print-trigger";
|
||
import { getMessages, formatMessage } from "@/lib/i18n/server";
|
||
|
||
const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
|
||
const MEAL_ORDER = ["breakfast", "lunch", "dinner", "snack"] as const;
|
||
|
||
function getMonday(dateStr?: string): Date {
|
||
// Parse y/m/d directly into local time — new Date("YYYY-MM-DD") parses as
|
||
// UTC midnight, which getDay() (local time) can read as the wrong weekday
|
||
// depending on the server's timezone offset.
|
||
const d = dateStr
|
||
? (() => {
|
||
const [y, m, day] = dateStr.split("-").map(Number);
|
||
return new Date(y!, m! - 1, day!);
|
||
})()
|
||
: new Date();
|
||
const dow = d.getDay();
|
||
const diff = dow === 0 ? -6 : 1 - dow;
|
||
d.setDate(d.getDate() + diff);
|
||
d.setHours(0, 0, 0, 0);
|
||
return d;
|
||
}
|
||
|
||
function toDateStr(d: Date): string {
|
||
const y = d.getFullYear();
|
||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||
const day = String(d.getDate()).padStart(2, "0");
|
||
return `${y}-${m}-${day}`;
|
||
}
|
||
|
||
export default async function MealPlanPrintPage({
|
||
searchParams,
|
||
}: {
|
||
searchParams: Promise<{ week?: string }>;
|
||
}) {
|
||
const { week } = await searchParams;
|
||
const session = await auth.api.getSession({ headers: await headers() });
|
||
if (!session) return null;
|
||
|
||
const m = getMessages((session.user as { locale?: string }).locale);
|
||
|
||
const monday = getMonday(week);
|
||
const weekStart = toDateStr(monday);
|
||
const sunday = new Date(monday);
|
||
sunday.setDate(sunday.getDate() + 6);
|
||
|
||
const plan = await db.query.mealPlans.findFirst({
|
||
where: and(eq(mealPlans.userId, session.user.id), eq(mealPlans.weekStart, weekStart)),
|
||
with: { entries: { with: { recipe: true } } },
|
||
});
|
||
|
||
const label = `${monday.toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${sunday.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}`;
|
||
|
||
const entries = plan?.entries ?? [];
|
||
|
||
return (
|
||
<>
|
||
<style>{`
|
||
@media print {
|
||
body { margin: 0; }
|
||
.no-print { display: none !important; }
|
||
}
|
||
body {
|
||
font-family: system-ui, sans-serif;
|
||
color: #1a1a1a;
|
||
max-width: 900px;
|
||
margin: 40px auto;
|
||
padding: 0 24px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
h1 { font-size: 1.8em; margin: 0 0 2px; }
|
||
.subtitle { color: #666; font-size: 0.9em; margin-bottom: 24px; }
|
||
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||
th { text-align: left; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.06em; color: #888; padding: 6px 8px; border-bottom: 2px solid #ccc; }
|
||
td { padding: 6px 8px; border-bottom: 1px solid #eee; border-right: 1px solid #eee; vertical-align: top; min-height: 40px; }
|
||
td:last-child { border-right: none; }
|
||
.meal-label { font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.05em; color: #aaa; display: block; margin-bottom: 2px; }
|
||
.recipe-title { font-weight: 500; }
|
||
.servings { font-size: 0.8em; color: #888; }
|
||
.empty { color: #ccc; font-size: 0.85em; }
|
||
footer { margin-top: 40px; font-size: 0.75em; color: #aaa; text-align: center; }
|
||
.print-btn {
|
||
position: fixed; top: 16px; right: 16px; padding: 8px 16px;
|
||
background: #18181b; color: white; border: none; border-radius: 6px;
|
||
cursor: pointer; font-size: 13px;
|
||
}
|
||
.print-btn:hover { background: #3f3f46; }
|
||
`}</style>
|
||
|
||
<PrintTrigger />
|
||
|
||
<h1>{m.mealPlan.title}</h1>
|
||
<p className="subtitle">{label}</p>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style={{ width: "100px" }}>{m.print.day}</th>
|
||
{MEAL_ORDER.map((meal) => (
|
||
<th key={meal}>{m.mealPlan.meals[meal]}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{DAYS.map((day) => (
|
||
<tr key={day}>
|
||
<td style={{ fontWeight: 600, color: "#555" }}>{m.print.days[day]}</td>
|
||
{MEAL_ORDER.map((mealType) => {
|
||
const entry = entries.find((e) => e.day === day && e.mealType === mealType);
|
||
return (
|
||
<td key={mealType}>
|
||
{entry ? (
|
||
<>
|
||
<span className="recipe-title">{entry.recipe?.title ?? entry.note ?? "—"}</span>
|
||
{entry.servings && (
|
||
<span className="servings">{formatMessage(m.print.srv, { count: entry.servings })}</span>
|
||
)}
|
||
</>
|
||
) : (
|
||
<span className="empty">—</span>
|
||
)}
|
||
</td>
|
||
);
|
||
})}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
|
||
<footer>{m.print.footer}</footer>
|
||
</>
|
||
);
|
||
}
|