Import Export Grocery List
And misc improvements
This commit is contained in:
+157
-14
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user