*/ function mealSlotTypes(): array { return [MEAL_SLOT_BREAKFAST, MEAL_SLOT_LUNCH, MEAL_SLOT_DINNER]; } function mealCurrentWeekStart(): string { try { $d = new DateTimeImmutable('monday this week'); return $d->format('Y-m-d'); } catch (Exception $e) { return gmdate('Y-m-d'); } } /** * @return array> */ function mealDefaultEmptySlots(): array { $slots = []; for ($i = 0; $i < 7; $i++) { $key = (string) $i; $slots[$key] = [ MEAL_SLOT_BREAKFAST => null, MEAL_SLOT_LUNCH => null, MEAL_SLOT_DINNER => null, ]; } return $slots; } /** * @param mixed $raw * @return array{weekStart: string, slots: array>} */ function normalizeMealPlan($raw): array { $base = [ 'weekStart' => mealCurrentWeekStart(), 'slots' => mealDefaultEmptySlots(), ]; if (!is_array($raw)) { return $base; } $ws = isset($raw['weekStart']) ? trim((string) $raw['weekStart']) : ''; if ($ws !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $ws)) { $base['weekStart'] = $ws; } $slotsIn = $raw['slots'] ?? []; if (is_array($slotsIn)) { for ($i = 0; $i < 7; $i++) { $key = (string) $i; $day = $slotsIn[$key] ?? $slotsIn[$i] ?? []; if (!is_array($day)) { continue; } foreach (mealSlotTypes() as $mt) { $mid = $day[$mt] ?? null; if ($mid === null || $mid === '') { $base['slots'][$key][$mt] = null; } else { $base['slots'][$key][$mt] = is_string($mid) ? $mid : null; } } } } return $base; } function loadMealPlan(): array { return normalizeMealPlan(readJsonFile('meal_plans.json')); } /** * @param mixed $raw * @return array> */ function normalizeMealsList($raw): array { if (!is_array($raw) || !array_is_list($raw)) { return []; } $out = []; foreach ($raw as $row) { if (is_array($row) && !empty($row['id']) && is_string($row['id'])) { $out[] = $row; } } return $out; } /** * @param array $m * @return array */ function normalizeMealRow(array $m): array { if (isset($m['name']) && !isset($m['title'])) { $m['title'] = trim((string) $m['name']); } $m['title'] = trim((string) ($m['title'] ?? '')); $m['image'] = trim((string) ($m['image'] ?? '')); $m['description'] = trim((string) ($m['description'] ?? '')); $m['directions'] = trim((string) ($m['directions'] ?? '')); if (!isset($m['lists']) || !is_array($m['lists'])) { $m['lists'] = []; } $m['lists'] = normalizeChoreLists($m['lists']); $tags = $m['tags'] ?? []; if (!is_array($tags)) { $tags = []; } $cleanTags = []; foreach ($tags as $t) { $t = strtolower(trim((string) $t)); if (in_array($t, ['breakfast', 'lunch', 'dinner'], true)) { $cleanTags[] = $t; } } if (isset($m['type']) && is_string($m['type']) && count($cleanTags) === 0) { $tt = strtolower(trim($m['type'])); if (in_array($tt, ['breakfast', 'lunch', 'dinner'], true)) { $cleanTags[] = $tt; } } $m['tags'] = array_values(array_unique($cleanTags)); $ing = $m['ingredients'] ?? []; if (!is_array($ing)) { $ing = []; } $ingOut = []; foreach ($ing as $line) { $s = trim((string) $line); if ($s !== '') { $ingOut[] = $s; } } $m['ingredients'] = $ingOut; $items = $m['items'] ?? []; if (!is_array($items)) { $items = []; } $itemOut = []; foreach ($items as $it) { if (!is_array($it)) { continue; } $nm = trim((string) ($it['name'] ?? '')); if ($nm === '') { continue; } $itemOut[] = [ 'name' => $nm, 'storeId' => trim((string) ($it['storeId'] ?? '')), 'description' => trim((string) ($it['description'] ?? '')), 'size' => trim((string) ($it['size'] ?? '')), 'quantity' => trim((string) ($it['quantity'] ?? '1')) ?: '1', 'price' => trim((string) ($it['price'] ?? '')), 'image' => trim((string) ($it['image'] ?? '')), ]; } $m['items'] = $itemOut; if (!isset($m['author_id'])) { $m['author_id'] = ''; } return $m; } /** * @param array> $meals */ function findMealById(array $meals, string $id): ?array { foreach ($meals as $m) { if (($m['id'] ?? '') === $id) { return $m; } } return null; } /** * @param array> $meals */ function findMealIndexById(array $meals, string $id): ?int { foreach ($meals as $i => $m) { if (($m['id'] ?? '') === $id) { return $i; } } return null; } /** * Migrate legacy meals (name, date, type) into library format. * * @param array> $meals * @return array> */ function migrateLegacyMealsList(array $meals): array { $out = []; foreach ($meals as $m) { if (!is_array($m)) { continue; } if (isset($m['title']) && isset($m['tags'])) { $out[] = normalizeMealRow($m); continue; } $title = trim((string) ($m['title'] ?? $m['name'] ?? '')); if ($title === '') { continue; } $tags = []; if (!empty($m['type'])) { $tt = strtolower(trim((string) $m['type'])); if (in_array($tt, ['breakfast', 'lunch', 'dinner'], true)) { $tags[] = $tt; } } $out[] = normalizeMealRow([ 'id' => !empty($m['id']) && is_string($m['id']) ? $m['id'] : bin2hex(random_bytes(8)), 'title' => $title, 'tags' => $tags, 'description' => isset($m['date']) ? 'Legacy date: ' . (string) $m['date'] : '', 'image' => '', 'directions' => '', 'lists' => [], 'ingredients' => [], 'items' => [], 'author_id' => '', ]); } return $out; } /** * Push meal shopping items to grocery lists as pending_review. * * @param array> $stores * @return int number of lines added */ function pushMealItemsToGrocery(array $meal, array $stores): int { $items = $meal['items'] ?? []; if (!is_array($items) || count($items) === 0) { return 0; } $mealId = (string) ($meal['id'] ?? ''); $mealTitle = (string) ($meal['title'] ?? ''); $defaultStore = groceryFirstStoreId($stores); if ($defaultStore === '') { return 0; } $added = 0; foreach ($items as $row) { if (!is_array($row)) { continue; } $name = trim((string) ($row['name'] ?? '')); if ($name === '') { continue; } $sid = trim((string) ($row['storeId'] ?? '')); if ($sid === '' || findStoreById($stores, $sid) === null) { $sid = $defaultStore; } $res = groceryAppendShoppingLine( $stores, $sid, $name, trim((string) ($row['description'] ?? '')), trim((string) ($row['size'] ?? '')), trim((string) ($row['quantity'] ?? '1')) ?: '1', trim((string) ($row['price'] ?? '')), trim((string) ($row['image'] ?? '')), 'meal_plan', 0, $mealId !== '' ? $mealId : null, $mealTitle !== '' ? $mealTitle : null ); if ($res['ok']) { $added++; } } return $added; } function mealDayShortLabel(string $weekStart, int $offset): string { $ts = strtotime($weekStart . ' UTC +' . $offset . ' days'); if ($ts === false) { return (string) $offset; } return gmdate('D n/j', $ts); }