498 lines
28 KiB
PHP
498 lines
28 KiB
PHP
<?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 = [];
|
||
}
|
||
|
||
$mealEditorNotFound = $editMealId !== '' && $editMealId !== 'new' && $prefillMeal === null && $canManageMeals;
|
||
$mealEditorAllowed = ($editMealId === 'new' && $canManageMeals)
|
||
|| ($prefillMeal !== null && $canEditMeal($prefillMeal));
|
||
$showMealEditorPage = $editMealId !== '' && ($mealEditorAllowed || $mealEditorNotFound);
|
||
$mealEditorForbidden = $editMealId !== '' && !$showMealEditorPage;
|
||
?>
|
||
|
||
<?php if ($detailMeal !== null && $editMealId === ''): ?>
|
||
<?php
|
||
$detailDesc = trim((string) ($detailMeal['description'] ?? ''));
|
||
$detailImageOk = !empty($detailMeal['image']) && preg_match('#^https?://#i', (string) $detailMeal['image']);
|
||
$hasRecipeLists = false;
|
||
foreach (($detailMeal['lists'] ?? []) as $block) {
|
||
if (is_array($block) && !empty($block['items'])) {
|
||
$hasRecipeLists = true;
|
||
break;
|
||
}
|
||
}
|
||
?>
|
||
<div id="meals" class="tab-content meal-detail">
|
||
<p class="mb-3"><a href="?tab=meals" class="btn btn-sm btn-outline-secondary">← Back to meal plan</a></p>
|
||
|
||
<header class="meal-detail-header d-flex flex-wrap align-items-start justify-content-between gap-3 pb-3 mb-0">
|
||
<div class="min-w-0 flex-grow-1">
|
||
<h2 class="h3 mb-2"><?= sanitizeInput($detailMeal['title'] ?? '') ?></h2>
|
||
<?php if (count($detailMeal['tags'] ?? []) > 0): ?>
|
||
<p class="meal-detail-tags mb-0"><?php foreach ($detailMeal['tags'] as $t): ?><span class="badge text-bg-secondary me-1"><?= sanitizeInput((string) $t) ?></span><?php endforeach; ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="d-flex flex-wrap gap-2 flex-shrink-0 align-items-start meal-detail-actions">
|
||
<?php if ($activePerson !== null && $detailId !== ''): ?>
|
||
<button type="button" class="btn btn-outline-secondary btn-meal-export-json"
|
||
data-meal-id="<?= htmlspecialchars($detailId, ENT_QUOTES, 'UTF-8') ?>">Export recipe JSON</button>
|
||
<?php endif; ?>
|
||
<?php if ($canEditMeal($detailMeal)): ?>
|
||
<a class="btn btn-outline-primary" href="?tab=meals&edit=<?= htmlspecialchars($detailId, ENT_QUOTES, 'UTF-8') ?>">Edit meal</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</header>
|
||
|
||
<?php if ($detailImageOk || $detailDesc !== ''): ?>
|
||
<section class="meal-detail-intro card border-0 shadow-sm mb-4" aria-label="Summary">
|
||
<div class="card-body">
|
||
<?php if ($detailImageOk): ?>
|
||
<img src="<?= sanitizeInput((string) $detailMeal['image']) ?>" class="img-fluid rounded mb-3 meal-hero" alt="">
|
||
<?php endif; ?>
|
||
<?php if ($detailDesc !== ''): ?>
|
||
<div class="meal-detail-lead text-body-secondary"><?= nl2br(sanitizeInput($detailDesc)) ?></div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<section class="meal-detail-section" aria-labelledby="meal-shopping-heading">
|
||
<h3 id="meal-shopping-heading" class="h5 meal-detail-section-heading">
|
||
<i class="fa fa-cart-shopping fa-fw" aria-hidden="true"></i> Shopping list
|
||
</h3>
|
||
<p class="text-muted small meal-detail-section-lead">Items to buy for this recipe. Choose a store and add each line to the grocery tab as <strong>pending review</strong>.</p>
|
||
<?php if (count($detailMeal['items'] ?? []) === 0): ?>
|
||
<p class="text-muted small mb-0">None listed for this meal.</p>
|
||
<?php else: ?>
|
||
<ul class="list-group meal-detail-list">
|
||
<?php foreach ($detailMeal['items'] as $shopIdx => $it): ?>
|
||
<?php
|
||
$shopName = (string) ($it['name'] ?? '');
|
||
$shopStorePref = (string) ($it['storeId'] ?? '');
|
||
?>
|
||
<li class="list-group-item d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||
<span>
|
||
<?= sanitizeInput($shopName) ?>
|
||
<?php if (!empty($it['quantity'])): ?> · <?= sanitizeInput((string) $it['quantity']) ?><?php endif; ?>
|
||
<?php if (!empty($it['size'])): ?> <span class="text-muted">(<?= sanitizeInput((string) $it['size']) ?>)</span><?php endif; ?>
|
||
</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 meal-shopping-store" style="width:auto; min-width:8rem;" aria-label="Store for grocery">
|
||
<?php foreach ($stores as $st): ?>
|
||
<?php $stid = (string) ($st['id'] ?? ''); ?>
|
||
<option value="<?= htmlspecialchars($stid, ENT_QUOTES, 'UTF-8') ?>" <?= ($shopStorePref !== '' && $shopStorePref === $stid) ? 'selected' : '' ?>><?= sanitizeInput($st['name'] ?? '') ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="button" class="btn btn-sm btn-outline-primary btn-meal-shopping-to-grocery"
|
||
data-meal-id="<?= htmlspecialchars($detailId, ENT_QUOTES, 'UTF-8') ?>"
|
||
data-item-index="<?= (int) $shopIdx ?>"
|
||
data-item-name="<?= htmlspecialchars($shopName, ENT_QUOTES, 'UTF-8') ?>"
|
||
>Add to grocery list</button>
|
||
</span>
|
||
<?php endif; ?>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
<div class="alert d-none mt-3 mb-0" id="mealShoppingGroceryFeedback" role="status"></div>
|
||
</section>
|
||
|
||
<section class="meal-detail-section meal-detail-pantry" aria-labelledby="meal-pantry-heading">
|
||
<h3 id="meal-pantry-heading" class="h5 meal-detail-section-heading">
|
||
<i class="fa fa-seedling fa-fw" aria-hidden="true"></i> Pantry / staples
|
||
</h3>
|
||
<p class="text-muted small meal-detail-section-lead">Ingredients you may already have. Add one line to the grocery list if you need to restock.</p>
|
||
<?php if (count($detailMeal['ingredients'] ?? []) === 0): ?>
|
||
<p class="text-muted small mb-0">None listed.</p>
|
||
<?php else: ?>
|
||
<ul class="list-group meal-detail-list">
|
||
<?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; ?>
|
||
<div class="alert d-none mt-3 mb-0" id="mealPantryGroceryFeedback" role="status"></div>
|
||
</section>
|
||
|
||
<?php if ($hasRecipeLists): ?>
|
||
<section class="meal-detail-section" aria-labelledby="meal-prep-heading">
|
||
<h3 id="meal-prep-heading" class="h5 meal-detail-section-heading">
|
||
<i class="fa fa-list-check fa-fw" aria-hidden="true"></i> Recipe checklist
|
||
</h3>
|
||
<p class="text-muted small meal-detail-section-lead">Quick reminders while you cook (not interactive).</p>
|
||
<?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 class="meal-detail-checklist"><?php foreach ($block['items'] as $it): ?><li><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ol>
|
||
<?php elseif ($t === 'unordered'): ?>
|
||
<ul class="meal-detail-checklist"><?php foreach ($block['items'] as $it): ?><li><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ul>
|
||
<?php else: ?>
|
||
<ul class="list-unstyled meal-detail-checklist"><?php foreach ($block['items'] as $it): ?><li><i class="fa-regular fa-square me-2 text-muted" aria-hidden="true"></i><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ul>
|
||
<?php endif; ?>
|
||
<?php endforeach; ?>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if (trim((string) ($detailMeal['directions'] ?? '')) !== ''): ?>
|
||
<section class="meal-detail-section" aria-labelledby="meal-directions-heading">
|
||
<h3 id="meal-directions-heading" class="h5 meal-detail-section-heading">
|
||
<i class="fa fa-book-open fa-fw" aria-hidden="true"></i> Directions
|
||
</h3>
|
||
<div class="meal-detail-directions"><?= nl2br(sanitizeInput((string) ($detailMeal['directions'] ?? ''))) ?></div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
</div>
|
||
|
||
<?php elseif ($showMealEditorPage): ?>
|
||
<div id="meals" class="tab-content meal-editor-page">
|
||
<p class="mb-3"><a href="?tab=meals" class="btn btn-sm btn-outline-secondary">← Back to meal plan</a></p>
|
||
<h2 class="h3 mb-3"><?= $editMealId === 'new' ? 'New meal' : 'Edit meal' ?></h2>
|
||
<div class="card border-primary shadow-sm" id="mealEditorCard">
|
||
<div class="card-header"><?= $editMealId === 'new' ? 'Recipe details' : 'Update recipe' ?></div>
|
||
<div class="card-body">
|
||
<?php if ($mealEditorNotFound): ?>
|
||
<div class="alert alert-warning mb-0">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 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>
|
||
</div>
|
||
|
||
<?php elseif ($mealEditorForbidden): ?>
|
||
<div id="meals" class="tab-content meal-editor-page">
|
||
<p class="mb-3"><a href="?tab=meals" class="btn btn-sm btn-outline-secondary">← Back to meal plan</a></p>
|
||
<div class="alert alert-warning mb-0" role="alert">You can’t open this editor. You may need to switch to a profile that is allowed to create or edit this meal.</div>
|
||
</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&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): ?>
|
||
<div class="d-flex flex-wrap gap-2 align-items-center mb-3 meals-recipe-actions">
|
||
<a href="?tab=meals&edit=new" class="btn btn-success btn-sm">New meal</a>
|
||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#mealImportModal">Import recipe JSON</button>
|
||
</div>
|
||
<?php else: ?>
|
||
<p class="text-muted small">Only a verified Head of household can add or import 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&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 class="d-flex flex-wrap gap-1 align-items-center">
|
||
<?php if ($activePerson !== null && $mid !== ''): ?>
|
||
<button type="button" class="btn btn-sm btn-outline-secondary btn-meal-export-json" data-meal-id="<?= $mid ?>">Export</button>
|
||
<?php endif; ?>
|
||
<?php if ($canEditMeal($m)): ?>
|
||
<a class="btn btn-sm btn-outline-primary" href="?tab=meals&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; ?>
|
||
|
||
</div>
|
||
|
||
<div class="modal fade" id="mealSlotModal" tabindex="-1" aria-labelledby="mealSlotModalLabel" aria-hidden="true" data-bs-backdrop="false">
|
||
<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 meal’s 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>
|
||
|
||
<?php if ($canManageMeals): ?>
|
||
<div class="modal fade" id="mealImportModal" tabindex="-1" aria-labelledby="mealImportModalLabel" aria-hidden="true" data-bs-backdrop="false">
|
||
<div class="modal-dialog modal-dialog-scrollable">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title h5" id="mealImportModalLabel">Import recipe JSON</h2>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body meal-import-modal-body">
|
||
<p class="text-muted small">Choose a JSON file (export from this hub or a shared recipe package). Only slots that reference the imported meal id(s) are applied to the <strong>current</strong> planning week.</p>
|
||
<div class="mb-3">
|
||
<label class="form-label" for="mealImportFile">Recipe file</label>
|
||
<input type="file" class="form-control" id="mealImportFile" accept="application/json,.json">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">Preview</label>
|
||
<pre class="small bg-light border rounded p-2 mb-0 meal-import-preview" id="mealImportPreview" style="max-height: 200px; overflow: auto;">—</pre>
|
||
</div>
|
||
<div class="alert d-none" id="mealImportFeedback" role="status"></div>
|
||
</div>
|
||
<div class="modal-footer flex-wrap gap-2">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||
<button type="button" class="btn btn-primary" id="btnMealImportSubmit" disabled>Import</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<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 | familyHubJsonEncodeShellFlags()) ?></script>
|
||
|
||
<?php endif; ?>
|