familyHub/tabs/meals.php
2026-03-30 21:05:12 -05:00

498 lines
28 KiB
PHP
Raw Permalink 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 = [];
}
$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">&larr; 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&amp;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">&larr; 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&#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>
</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">&larr; Back to meal plan</a></p>
<div class="alert alert-warning mb-0" role="alert">You cant 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&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): ?>
<div class="d-flex flex-wrap gap-2 align-items-center mb-3 meals-recipe-actions">
<a href="?tab=meals&amp;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&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 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&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; ?>
</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 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>
<?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 | JSON_UNESCAPED_UNICODE) ?></script>
<?php endif; ?>