Import Export Grocery List

And misc improvements
This commit is contained in:
2026-03-30 21:05:12 -05:00
parent 5e0b096082
commit 9a78c4564e
9 changed files with 919 additions and 225 deletions
+157 -14
View File
@@ -569,36 +569,62 @@
}
var weekStart = ($tab.attr('data-week-start') || '').trim();
var mealImportParsed = null;
$('#btnPushMealGrocery').on('click', function () {
var id = $(this).data('meal-id');
if (!id) {
$(document).on('click', '.btn-meal-shopping-to-grocery', function () {
var $btn = $(this);
var mealId = $btn.data('meal-id');
var itemIndex = parseInt($btn.attr('data-item-index'), 10);
var label = ($btn.attr('data-item-name') || $btn.data('itemName') || '').trim() || 'Item';
var $li = $btn.closest('li');
var storeId = $li.find('.meal-shopping-store').val() || '';
var $fb = $('#mealShoppingGroceryFeedback');
if (!mealId || itemIndex < 0 || isNaN(itemIndex)) {
return;
}
postJson('/meal_push_grocery.php', { id: id })
.then(function (data) {
var n = data.groceryLinesAdded != null ? data.groceryLinesAdded : 0;
showAlert($('#pushMealFeedback'), 'success', 'Added ' + n + ' line(s) to grocery (pending review).');
$('#pushMealFeedback').removeClass('d-none');
postJson('/meal_shopping_item_to_grocery.php', { mealId: mealId, itemIndex: itemIndex, storeId: storeId })
.then(function () {
if ($fb.length) {
showAlert($fb, 'success', 'Added ' + label + '” to the grocery list (pending review).');
$fb.removeClass('d-none');
var el = $fb.get(0);
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
})
.catch(function (err) {
showAlert($('#pushMealFeedback'), 'danger', err.message || 'Could not push');
$('#pushMealFeedback').removeClass('d-none');
if ($fb.length) {
showAlert($fb, 'danger', (err.message || 'Could not add') + ' — “' + label + '”');
$fb.removeClass('d-none');
}
});
});
$(document).on('click', '.btn-ingredient-to-grocery', function () {
var $btn = $(this);
var mealId = $btn.data('meal-id');
var ing = $btn.data('ingredient');
var ing = $btn.attr('data-ingredient') || $btn.data('ingredient') || '';
var label = String(ing).trim() || 'Item';
var $li = $btn.closest('li');
var storeId = $li.find('.ingredient-store').val() || '';
var $fb = $('#mealPantryGroceryFeedback');
postJson('/meal_ingredient_to_grocery.php', { mealId: mealId, ingredient: ing, storeId: storeId })
.then(function () {
window.alert('Added to grocery list.');
if ($fb.length) {
showAlert($fb, 'success', 'Added “' + label + '” to the grocery list (pending review).');
$fb.removeClass('d-none');
var el = $fb.get(0);
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
})
.catch(function (err) {
window.alert(err.message || 'Could not add');
if ($fb.length) {
showAlert($fb, 'danger', (err.message || 'Could not add') + ' — “' + label + '”');
$fb.removeClass('d-none');
}
});
});
@@ -623,7 +649,7 @@
var slotModalEl = document.getElementById('mealSlotModal');
var slotModal = null;
if (slotModalEl && typeof bootstrap !== 'undefined') {
slotModal = new bootstrap.Modal(slotModalEl);
slotModal = new bootstrap.Modal(slotModalEl, { backdrop: false });
}
function renderSlotPickerList() {
@@ -764,6 +790,123 @@
window.alert(err.message || 'Could not delete');
});
});
$(document).on('click', '.btn-meal-export-json', function () {
var mealId = $(this).data('meal-id');
if (!mealId) {
return;
}
var safeId = String(mealId);
fetch(apiBase + '/meal_export.php?mealId=' + encodeURIComponent(safeId), { credentials: 'same-origin' })
.then(function (res) {
return res.json().then(function (data) {
return { res: res, data: data };
});
})
.then(function (r) {
if (!r.res.ok || !r.data || !r.data.success) {
var msg = (r.data && r.data.error) ? r.data.error : 'Export failed';
throw new Error(msg);
}
var pkg = {
version: r.data.version || '1',
meals: r.data.meals,
meal_plans: r.data.meal_plans
};
var json = JSON.stringify(pkg, null, 2);
var blob = new Blob([json], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
var ymd = new Date().toISOString().slice(0, 10);
var fileStem = safeId.replace(/[^a-zA-Z0-9_-]+/g, '_').slice(0, 80);
a.download = 'meal_export_' + fileStem + '_' + ymd + '.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
})
.catch(function (err) {
window.alert(err.message || 'Could not export recipe');
});
});
$('#mealImportFile').on('change', function () {
mealImportParsed = null;
var f = this.files && this.files[0];
var $prev = $('#mealImportPreview');
var $fb = $('#mealImportFeedback');
if ($fb.length) {
$fb.addClass('d-none').removeClass('alert-success alert-danger alert-warning');
}
$('#btnMealImportSubmit').prop('disabled', true);
if (!$prev.length) {
return;
}
if (!f) {
$prev.text('—');
return;
}
var reader = new FileReader();
reader.onload = function () {
try {
mealImportParsed = JSON.parse(String(reader.result || ''));
$prev.text(JSON.stringify(mealImportParsed, null, 2));
$('#btnMealImportSubmit').prop('disabled', false);
if ($fb.length) {
$fb.addClass('d-none');
}
} catch (e) {
mealImportParsed = null;
$prev.text('Invalid JSON: ' + (e.message || String(e)));
if ($fb.length) {
showAlert($fb, 'danger', 'Could not parse JSON from this file.');
$fb.removeClass('d-none');
}
$('#btnMealImportSubmit').prop('disabled', true);
}
};
reader.readAsText(f);
});
$('#btnMealImportSubmit').on('click', function () {
if (!mealImportParsed) {
return;
}
var $fb = $('#mealImportFeedback');
var $btn = $(this);
$btn.prop('disabled', true);
postJson('/meal_import.php', mealImportParsed)
.then(function (data) {
var n = typeof data.mealsUpserted === 'number' ? data.mealsUpserted : 0;
var s = typeof data.slotsApplied === 'number' ? data.slotsApplied : 0;
if ($fb.length) {
showAlert($fb, 'success', 'Imported ' + n + ' meal(s), ' + s + ' slot(s). Reloading…');
$fb.removeClass('d-none');
}
window.setTimeout(function () {
window.location.href = window.location.pathname + '?tab=meals';
}, 600);
})
.catch(function (err) {
if ($fb.length) {
showAlert($fb, 'danger', err.message || 'Import failed');
$fb.removeClass('d-none');
}
$btn.prop('disabled', false);
});
});
$('#mealImportModal').on('hidden.bs.modal', function () {
mealImportParsed = null;
$('#mealImportFile').val('');
var $prev = $('#mealImportPreview');
if ($prev.length) {
$prev.text('—');
}
$('#mealImportFeedback').addClass('d-none');
$('#btnMealImportSubmit').prop('disabled', true);
});
}
function bindCurrencyPage() {