familyHub/tabs/meals.php

374 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/utils.php';
require_once __DIR__ . '/../includes/persona.php';
require_once __DIR__ . '/../includes/meal_helpers.php';
require_once __DIR__ . '/../includes/grocery_helpers.php';
if (!file_exists(DATA_PATH . '/meal_plans.json')) {
writeJsonFile('meal_plans.json', normalizeMealPlan([]));
}
$plan = normalizeMealPlan(readJsonFile('meal_plans.json'));
$meals = migrateLegacyMealsList(normalizeMealsList(readJsonFile('meals.json')));
migrateLegacyGroceriesIfNeeded();
ensureDefaultGroceryStore();
$stores = normalizeStoresList(readJsonFile('stores.json'));
$detailId = isset($_GET['meal']) ? trim((string) $_GET['meal']) : '';
$detailMeal = $detailId !== '' ? findMealById($meals, $detailId) : null;
$actorId = $activePerson['id'] ?? '';
$canManageMeals = $activePerson !== null
&& ($activePerson['role'] ?? '') === ROLE_HEAD
&& isHohVerified();
$canEditMeal = static function (array $m) use ($actorId, $canManageMeals): bool {
return $canManageMeals || (($m['author_id'] ?? '') === $actorId && $actorId !== '');
};
$weekStart = $plan['weekStart'];
$slots = $plan['slots'];
$mealTitleById = [];
foreach ($meals as $m) {
if (!empty($m['id'])) {
$mealTitleById[(string) $m['id']] = (string) ($m['title'] ?? '');
}
}
$prefillMeal = null;
$editMealId = isset($_GET['edit']) ? trim((string) $_GET['edit']) : '';
if ($editMealId !== '') {
$prefillMeal = findMealById($meals, $editMealId);
}
$listsPrefill = $prefillMeal['lists'] ?? [['type' => 'checkbox', 'items' => []]];
if (!is_array($listsPrefill) || count($listsPrefill) === 0) {
$listsPrefill = [['type' => 'checkbox', 'items' => []]];
}
$firstList = $listsPrefill[0];
$listType = in_array($firstList['type'] ?? '', ['ordered', 'unordered', 'checkbox'], true) ? $firstList['type'] : 'checkbox';
$listItems = $firstList['items'] ?? [];
if (!is_array($listItems)) {
$listItems = [];
}
?>
<?php if ($detailMeal !== null): ?>
<div id="meals" class="tab-content">
<p class="mb-2"><a href="?tab=meals" class="btn btn-sm btn-outline-secondary">&larr; Back to meal plan</a></p>
<h2 class="mb-2"><?= sanitizeInput($detailMeal['title'] ?? '') ?></h2>
<?php if (!empty($detailMeal['image']) && preg_match('#^https?://#i', (string) $detailMeal['image'])): ?>
<img src="<?= sanitizeInput((string) $detailMeal['image']) ?>" class="img-fluid rounded mb-3 meal-hero" alt="">
<?php endif; ?>
<p class="lead"><?= nl2br(sanitizeInput((string) ($detailMeal['description'] ?? ''))) ?></p>
<?php if (count($detailMeal['tags'] ?? []) > 0): ?>
<p><?php foreach ($detailMeal['tags'] as $t): ?><span class="badge text-bg-secondary me-1"><?= sanitizeInput((string) $t) ?></span><?php endforeach; ?></p>
<?php endif; ?>
<h3 class="h5">Directions</h3>
<div class="mb-4"><?= nl2br(sanitizeInput((string) ($detailMeal['directions'] ?? ''))) ?></div>
<?php foreach (($detailMeal['lists'] ?? []) as $block): ?>
<?php if (!is_array($block) || empty($block['items'])) { continue; } ?>
<?php $t = $block['type'] ?? 'checkbox'; ?>
<?php if ($t === 'ordered'): ?>
<ol><?php foreach ($block['items'] as $it): ?><li><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ol>
<?php elseif ($t === 'unordered'): ?>
<ul><?php foreach ($block['items'] as $it): ?><li><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ul>
<?php else: ?>
<ul class="list-unstyled"><?php foreach ($block['items'] as $it): ?><li><i class="fa-regular fa-square me-1"></i><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ul>
<?php endif; ?>
<?php endforeach; ?>
<h3 class="h5">Pantry / staples (ingredients)</h3>
<?php if (count($detailMeal['ingredients'] ?? []) === 0): ?>
<p class="text-muted small">None listed.</p>
<?php else: ?>
<ul class="list-group mb-3">
<?php foreach ($detailMeal['ingredients'] as $ing): ?>
<li class="list-group-item d-flex flex-wrap justify-content-between align-items-center gap-2">
<span><?= sanitizeInput((string) $ing) ?></span>
<?php if ($activePerson !== null && count($stores) > 0): ?>
<span class="d-flex flex-wrap gap-1 align-items-center">
<select class="form-select form-select-sm ingredient-store" style="width:auto; min-width:8rem;" aria-label="Store for grocery">
<?php foreach ($stores as $st): ?>
<option value="<?= htmlspecialchars((string) ($st['id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"><?= sanitizeInput($st['name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
<button type="button" class="btn btn-sm btn-outline-primary btn-ingredient-to-grocery" data-meal-id="<?= htmlspecialchars($detailId, ENT_QUOTES, 'UTF-8') ?>" data-ingredient="<?= htmlspecialchars((string) $ing, ENT_QUOTES, 'UTF-8') ?>">Add to grocery list</button>
</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<h3 class="h5">Shopping items</h3>
<?php if (count($detailMeal['items'] ?? []) === 0): ?>
<p class="text-muted small">None listed.</p>
<?php else: ?>
<ul class="list-group mb-3">
<?php foreach ($detailMeal['items'] as $it): ?>
<li class="list-group-item"><?= sanitizeInput((string) ($it['name'] ?? '')) ?>
<?php if (!empty($it['quantity'])): ?> · <?= sanitizeInput((string) $it['quantity']) ?><?php endif; ?>
<?php if (!empty($it['size'])): ?> (<?= sanitizeInput((string) $it['size']) ?>)<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($activePerson !== null && count($detailMeal['items'] ?? []) > 0): ?>
<button type="button" class="btn btn-primary mb-3" id="btnPushMealGrocery" data-meal-id="<?= htmlspecialchars($detailId, ENT_QUOTES, 'UTF-8') ?>">Send shopping items to grocery (pending review)</button>
<div class="alert d-none" id="pushMealFeedback" role="status"></div>
<?php endif; ?>
<?php if ($detailMeal && $canEditMeal($detailMeal)): ?>
<p><a class="btn btn-outline-primary" href="?tab=meals&amp;edit=<?= htmlspecialchars($detailId, ENT_QUOTES, 'UTF-8') ?>">Edit meal</a></p>
<?php endif; ?>
</div>
<?php else: ?>
<div id="meals" class="tab-content" data-week-start="<?= htmlspecialchars($weekStart, ENT_QUOTES, 'UTF-8') ?>">
<h2 class="mb-2">Meal plan</h2>
<p class="text-muted small">Week starts on <strong><?= sanitizeInput($weekStart) ?></strong> (day 0). Assign meals to each slot; shopping items go to the grocery list as <strong>pending review</strong>.</p>
<?php if ($activePerson === null): ?>
<div class="alert alert-warning">Choose who is using the hub to use the meal planner.</div>
<?php endif; ?>
<?php if ($canManageMeals): ?>
<div class="card mb-4">
<div class="card-body row g-2 align-items-end">
<div class="col-md-auto">
<label class="form-label" for="meal_week_start">Week starting (Monday)</label>
<input type="date" class="form-control" id="meal_week_start" value="<?= htmlspecialchars($weekStart, ENT_QUOTES, 'UTF-8') ?>">
</div>
<div class="col-md-auto">
<button type="button" class="btn btn-primary" id="btnMealWeekApply">Set planning week</button>
</div>
<div class="col-12">
<div class="alert d-none" id="mealWeekFeedback" role="status"></div>
</div>
</div>
</div>
<?php endif; ?>
<div class="table-responsive mb-4">
<table class="table table-bordered meal-grid-table">
<thead>
<tr>
<th></th>
<th>Breakfast</th>
<th>Lunch</th>
<th>Dinner</th>
</tr>
</thead>
<tbody>
<?php for ($d = 0; $d < 7; $d++): ?>
<tr>
<th scope="row" class="text-nowrap"><?= sanitizeInput(mealDayShortLabel($weekStart, $d)) ?></th>
<?php foreach (mealSlotTypes() as $mt): ?>
<?php
$mid = $slots[(string) $d][$mt] ?? null;
$label = $mid && isset($mealTitleById[$mid]) ? $mealTitleById[$mid] : '—';
?>
<td>
<?php if ($mid): ?>
<a href="?tab=meals&amp;meal=<?= htmlspecialchars($mid, ENT_QUOTES, 'UTF-8') ?>"><?= sanitizeInput($label) ?></a>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
<?php if ($activePerson !== null): ?>
<button type="button" class="btn btn-sm btn-outline-secondary ms-1 btn-open-slot-picker"
data-day="<?= (int) $d ?>"
data-meal-type="<?= htmlspecialchars($mt, ENT_QUOTES, 'UTF-8') ?>"
data-current-meal-id="<?= htmlspecialchars((string) ($mid ?? ''), ENT_QUOTES, 'UTF-8') ?>"
><?= $mid ? 'Change' : '+' ?></button>
<?php endif; ?>
</td>
<?php endforeach; ?>
</tr>
<?php endfor; ?>
</tbody>
</table>
</div>
<h3 class="h5">Meal library</h3>
<?php if ($canManageMeals): ?>
<p><a href="?tab=meals&amp;edit=new" class="btn btn-success btn-sm">New meal</a></p>
<?php else: ?>
<p class="text-muted small">Only a verified Head of household can add meals.</p>
<?php endif; ?>
<?php if (count($meals) === 0): ?>
<p class="text-muted">No meals in the library yet.</p>
<?php else: ?>
<ul class="list-group mb-4">
<?php foreach ($meals as $m): ?>
<?php $mid = htmlspecialchars((string) ($m['id'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<a href="?tab=meals&amp;meal=<?= $mid ?>"><strong><?= sanitizeInput($m['title'] ?? '') ?></strong></a>
<?php foreach (($m['tags'] ?? []) as $t): ?>
<span class="badge text-bg-light border ms-1"><?= sanitizeInput((string) $t) ?></span>
<?php endforeach; ?>
</div>
<span>
<?php if ($canEditMeal($m)): ?>
<a class="btn btn-sm btn-outline-primary" href="?tab=meals&amp;edit=<?= $mid ?>">Edit</a>
<button type="button" class="btn btn-sm btn-outline-danger btn-meal-delete" data-id="<?= $mid ?>">Delete</button>
<?php endif; ?>
</span>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($canManageMeals && ($editMealId === 'new' || $prefillMeal !== null)): ?>
<div class="card mb-4 border-primary" id="mealEditorCard">
<div class="card-header"><?= $editMealId === 'new' ? 'New meal' : 'Edit meal' ?></div>
<div class="card-body">
<?php if ($editMealId !== 'new' && $prefillMeal === null): ?>
<div class="alert alert-warning">Meal not found.</div>
<?php else: ?>
<form id="mealSaveForm" class="row g-3">
<input type="hidden" id="meal_id" value="<?= $prefillMeal ? htmlspecialchars((string) ($prefillMeal['id'] ?? ''), ENT_QUOTES, 'UTF-8') : '' ?>">
<div class="col-md-8">
<label class="form-label" for="meal_title">Title</label>
<input class="form-control" id="meal_title" required value="<?= $prefillMeal ? sanitizeInput($prefillMeal['title'] ?? '') : '' ?>">
</div>
<div class="col-md-4">
<label class="form-label">Tags</label>
<div class="d-flex flex-wrap gap-2">
<?php
$tagSet = $prefillMeal ? array_flip($prefillMeal['tags'] ?? []) : [];
foreach (['breakfast', 'lunch', 'dinner'] as $tg):
?>
<div class="form-check">
<input class="form-check-input meal-tag-cb" type="checkbox" value="<?= $tg ?>" id="tag_<?= $tg ?>" <?= isset($tagSet[$tg]) ? 'checked' : '' ?>>
<label class="form-check-label" for="tag_<?= $tg ?>"><?= ucfirst($tg) ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="col-12">
<label class="form-label" for="meal_description">Description</label>
<textarea class="form-control" id="meal_description" rows="2"><?= $prefillMeal ? sanitizeInput($prefillMeal['description'] ?? '') : '' ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label" for="meal_image">Image URL</label>
<input class="form-control" id="meal_image" type="url" value="<?= $prefillMeal ? sanitizeInput($prefillMeal['image'] ?? '') : '' ?>">
</div>
<div class="col-12">
<label class="form-label" for="meal_directions">Directions</label>
<textarea class="form-control" id="meal_directions" rows="4"><?= $prefillMeal ? sanitizeInput($prefillMeal['directions'] ?? '') : '' ?></textarea>
</div>
<div class="col-md-4">
<label class="form-label" for="meal_list_type">Recipe list type</label>
<select class="form-select" id="meal_list_type">
<option value="checkbox" <?= $listType === 'checkbox' ? 'selected' : '' ?>>Checkbox</option>
<option value="unordered" <?= $listType === 'unordered' ? 'selected' : '' ?>>Bullets</option>
<option value="ordered" <?= $listType === 'ordered' ? 'selected' : '' ?>>Numbered</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label" for="meal_list_items">Recipe list (one per line)</label>
<textarea class="form-control" id="meal_list_items" rows="3"><?= sanitizeInput(implode("\n", $listItems)) ?></textarea>
</div>
<div class="col-12">
<label class="form-label" for="meal_ingredients">Ingredients / staples (one per line)</label>
<textarea class="form-control" id="meal_ingredients" rows="3" placeholder="Salt&#10;Olive oil"><?= $prefillMeal ? sanitizeInput(implode("\n", $prefillMeal['ingredients'] ?? [])) : '' ?></textarea>
</div>
<div class="col-12">
<label class="form-label">Shopping items</label>
<p class="small text-muted">Per row: name, optional store, quantity, size, notes — store blank uses your first grocery store.</p>
<div id="mealGroceryRows" class="d-flex flex-column gap-2">
<?php
$shopItems = $prefillMeal['items'] ?? [];
if (!is_array($shopItems) || count($shopItems) === 0) {
$shopItems = [['name' => '', 'storeId' => '', 'quantity' => '1', 'size' => '', 'description' => '', 'price' => '', 'image' => '']];
}
foreach ($shopItems as $si):
?>
<div class="row g-2 align-items-end meal-grocery-row">
<div class="col-md-4">
<input class="form-control form-control-sm g-name" placeholder="Name" value="<?= sanitizeInput((string) ($si['name'] ?? '')) ?>">
</div>
<div class="col-md-3">
<select class="form-select form-select-sm g-store">
<option value="">Default store</option>
<?php foreach ($stores as $st): ?>
<option value="<?= htmlspecialchars((string) ($st['id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>" <?= (($si['storeId'] ?? '') === ($st['id'] ?? '')) ? 'selected' : '' ?>><?= sanitizeInput($st['name'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<input class="form-control form-control-sm g-qty" placeholder="Qty" value="<?= sanitizeInput((string) ($si['quantity'] ?? '1')) ?>">
</div>
<div class="col-md-3">
<input class="form-control form-control-sm g-size" placeholder="Size" value="<?= sanitizeInput((string) ($si['size'] ?? '')) ?>">
</div>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" id="btnAddGroceryRow">Add shopping row</button>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Save meal</button>
<a href="?tab=meals" class="btn btn-link">Cancel</a>
</div>
<div class="col-12">
<div class="alert d-none" id="mealSaveFeedback" role="status"></div>
</div>
</form>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<div class="modal fade" id="mealSlotModal" tabindex="-1" aria-labelledby="mealSlotModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title h5" id="mealSlotModalLabel">Choose a meal</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="slotPickerDay" value="">
<input type="hidden" id="slotPickerType" value="">
<div class="mb-2">
<label class="form-label">Filter by tag (optional)</label>
<select class="form-select" id="slotPickerFilter">
<option value="">All meals</option>
<option value="breakfast">Breakfast</option>
<option value="lunch">Lunch</option>
<option value="dinner">Dinner</option>
</select>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="slotPickerPushGrocery" checked>
<label class="form-check-label" for="slotPickerPushGrocery">Add this meals shopping items to grocery (pending review)</label>
</div>
<ul class="list-group" id="slotPickerList"></ul>
<button type="button" class="btn btn-outline-danger btn-sm mt-2" id="slotPickerClear">Clear slot</button>
</div>
</div>
</div>
</div>
<script type="application/json" id="mealsLibraryJson"><?= json_encode(array_map(static function ($m) {
return [
'id' => (string) ($m['id'] ?? ''),
'title' => (string) ($m['title'] ?? ''),
'tags' => array_values($m['tags'] ?? []),
];
}, $meals), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE) ?></script>
<?php endif; ?>