299 lines
8.3 KiB
PHP
299 lines
8.3 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/chore_helpers.php';
|
|
require_once __DIR__ . '/grocery_helpers.php';
|
|
|
|
const MEAL_SLOT_BREAKFAST = 'breakfast';
|
|
const MEAL_SLOT_LUNCH = 'lunch';
|
|
const MEAL_SLOT_DINNER = 'dinner';
|
|
|
|
/** @return array<int, string> */
|
|
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<string, array<string, string|null>>
|
|
*/
|
|
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<string, array<string, string|null>>}
|
|
*/
|
|
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<int, array<string, mixed>>
|
|
*/
|
|
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<string, mixed> $m
|
|
* @return array<string, mixed>
|
|
*/
|
|
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<int, array<string, mixed>> $meals
|
|
*/
|
|
function findMealById(array $meals, string $id): ?array {
|
|
foreach ($meals as $m) {
|
|
if (($m['id'] ?? '') === $id) {
|
|
return $m;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $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<int, array<string, mixed>> $meals
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
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<int, array<string, mixed>> $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);
|
|
}
|