/** * Tabs use full page navigation (?tab=) from index.php; active state is set server-side. * Do not programmatically .click() links on load — that re-follows href and reloads in a tight loop. */ (function ($) { 'use strict'; var apiBase = typeof window.familyHubApiBase === 'string' ? window.familyHubApiBase : '/api'; function postJson(path, body) { return fetch(apiBase + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(body || {}) }).then(function (res) { return res.json().then(function (data) { if (!res.ok) { var err = new Error(data.error || res.statusText || 'Request failed'); err.payload = data; err.status = res.status; throw err; } return data; }); }); } function showAlert($el, kind, message) { if (!$el || !$el.length) { return; } $el.removeClass('d-none alert-success alert-danger alert-warning'); $el.addClass('alert-' + kind); $el.text(message); } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } var pendingPersonId = null; var pinModal = null; function switchPerson(personId, pin) { return postJson('/switch_person.php', { personId: personId, pin: pin || '' }); } function bindPersonaSwitcher() { $(document).on('click', '.persona-chip', function () { var $btn = $(this); var id = $btn.data('person-id'); var needsPin = String($btn.data('needs-pin')) === '1'; var name = $btn.data('person-name') || 'this profile'; if (!id) { return; } if (needsPin) { pendingPersonId = id; $('#hohPinModalPrompt').text('Enter PIN for ' + name + '.'); $('#hohPinInput').val(''); $('#hohPinError').addClass('d-none').text(''); if (!pinModal && typeof bootstrap !== 'undefined') { pinModal = new bootstrap.Modal(document.getElementById('hohPinModal')); } if (pinModal) { pinModal.show(); } setTimeout(function () { document.getElementById('hohPinInput').focus(); }, 200); return; } switchPerson(id, '') .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not switch profile'); }); }); $('#hohPinSubmit').on('click', function () { var pin = $('#hohPinInput').val() || ''; if (!pendingPersonId) { return; } $('#hohPinError').addClass('d-none').text(''); switchPerson(pendingPersonId, pin) .then(function () { if (pinModal) { pinModal.hide(); } pendingPersonId = null; window.location.reload(); }) .catch(function (err) { $('#hohPinError').removeClass('d-none').text(err.message || 'PIN incorrect'); }); }); } function bindFirstHeadForm() { var $form = $('#firstHeadForm'); if (!$form.length) { return; } $form.on('submit', function (e) { e.preventDefault(); var payload = { name: $('#first_name').val(), pin: $('#first_pin').val(), favoriteColor: $('#first_favoriteColor').val() }; postJson('/people_create_first.php', payload) .then(function () { showAlert($('#firstHeadFeedback'), 'success', 'Profile created. Reloading…'); window.location.reload(); }) .catch(function (err) { showAlert($('#firstHeadFeedback'), 'danger', err.message || 'Could not create profile'); $('#firstHeadFeedback').removeClass('d-none'); }); }); } function toggleAddPinVisibility() { var role = $('#add_role').val(); var $wrap = $('#add_pin_wrap'); var $pin = $('#add_pin'); if (role === 'head_of_household') { $wrap.show(); $pin.prop('required', true); } else { $wrap.hide(); $pin.prop('required', false); $pin.val(''); } } function bindAddPersonForm() { var $form = $('#addPersonForm'); if (!$form.length) { return; } $('#add_role').on('change', toggleAddPinVisibility); toggleAddPinVisibility(); $form.on('submit', function (e) { e.preventDefault(); var role = $('#add_role').val(); var payload = { name: $('#add_name').val(), role: role, pin: role === 'head_of_household' ? $('#add_pin').val() : '', favoriteColor: $('#add_favoriteColor').val() }; postJson('/people_create.php', payload) .then(function () { showAlert($('#addPersonFeedback'), 'success', 'Person added. Reloading…'); $('#addPersonFeedback').removeClass('d-none'); window.location.reload(); }) .catch(function (err) { showAlert($('#addPersonFeedback'), 'danger', err.message || 'Could not add person'); $('#addPersonFeedback').removeClass('d-none'); }); }); } function bindFamilySettingsForm() { var $form = $('#familySettingsForm'); if (!$form.length) { return; } $form.on('submit', function (e) { e.preventDefault(); var payload = { currency_symbol: $('#currency_symbol').val(), currency_name: $('#currency_name').val(), currency_permanence: $('#currency_permanence').val(), week_starts_on: parseInt($('#week_starts_on').val(), 10), nfc_base_url: ($('#nfc_base_url').val() || '').trim(), nfc_show_confirmation: $('#nfc_show_confirmation').is(':checked'), nfc_scan_cooldown_seconds: parseInt($('#nfc_scan_cooldown_seconds').val(), 10) || 0 }; postJson('/family_settings_save.php', payload) .then(function () { showAlert($('#familySettingsFeedback'), 'success', 'Saved.'); $('#familySettingsFeedback').removeClass('d-none'); }) .catch(function (err) { showAlert($('#familySettingsFeedback'), 'danger', err.message || 'Could not save'); $('#familySettingsFeedback').removeClass('d-none'); }); }); $('#btnDisableAllChoreNfc').on('click', function () { if (!window.confirm('Disable NFC links for all chores?')) { return; } postJson('/family_nfc_admin.php', { action: 'disable_all_chore_nfc' }) .then(function () { showAlert($('#familySettingsFeedback'), 'warning', 'All chore NFC links disabled.'); $('#familySettingsFeedback').removeClass('d-none'); }) .catch(function (err) { showAlert($('#familySettingsFeedback'), 'danger', err.message || 'Could not disable NFC links'); $('#familySettingsFeedback').removeClass('d-none'); }); }); $('#btnRotateAllPersonNfcTokens').on('click', function () { if (!window.confirm('Rotate NFC submitter tokens for all people? Existing person-token links will stop working.')) { return; } postJson('/family_nfc_admin.php', { action: 'rotate_all_person_tokens' }) .then(function () { showAlert($('#familySettingsFeedback'), 'success', 'All person NFC tokens rotated.'); $('#familySettingsFeedback').removeClass('d-none'); }) .catch(function (err) { showAlert($('#familySettingsFeedback'), 'danger', err.message || 'Could not rotate tokens'); $('#familySettingsFeedback').removeClass('d-none'); }); }); } function collectBillDaysFromDom() { var out = []; $('#billDaysRows .bill-day-row').each(function () { var $row = $(this); var day = parseInt($row.find('.bill-day-dom').val(), 10); var title = ($row.find('.bill-day-title').val() || '').trim(); if (title !== '' && day >= 1 && day <= 31) { out.push({ dayOfMonth: day, title: title }); } }); return out; } function bindCalendarSettingsForm() { var $form = $('#calendarSettingsForm'); if (!$form.length) { return; } $('#btnAddBillDayRow').on('click', function () { var $first = $('#billDaysRows .bill-day-row').first(); if (!$first.length) { return; } var $clone = $first.clone(); $clone.find('.bill-day-dom').val('1'); $clone.find('.bill-day-title').val(''); $('#billDaysRows').append($clone); }); $form.on('submit', function (e) { e.preventDefault(); var payload = { timezone: $('#family_timezone').val(), calendar_two_way_google: $('#calendar_two_way_google').is(':checked'), calendar_bill_days: collectBillDaysFromDom() }; postJson('/family_settings_save.php', payload) .then(function () { showAlert($('#calendarSettingsFeedback'), 'success', 'Saved.'); $('#calendarSettingsFeedback').removeClass('d-none'); }) .catch(function (err) { showAlert($('#calendarSettingsFeedback'), 'danger', err.message || 'Could not save'); $('#calendarSettingsFeedback').removeClass('d-none'); }); }); } function choreListPayloadFromForm() { var type = $('#chore_list_type').val() || 'checkbox'; var raw = $('#chore_list_items').val() || ''; var lines = raw.split(/\r?\n/).map(function (l) { return l.trim(); }).filter(Boolean); if (lines.length === 0) { return []; } return [{ type: type, items: lines }]; } function toggleChoreRecurrence() { var sched = $('#chore_schedule').val(); if (sched === 'recurring') { $('#chore_recurrence_wrap').show(); } else { $('#chore_recurrence_wrap').hide(); } } function bindChoresPage() { var $form = $('#choreForm'); if (!$form.length) { return; } $('#chore_schedule').on('change', toggleChoreRecurrence); toggleChoreRecurrence(); $form.on('submit', function (e) { e.preventDefault(); var assignees = []; $('.chore-assignee:checked').each(function () { assignees.push($(this).val()); }); var due = $('#chore_due_date').val() || ''; var payload = { id: ($('#chore_id').val() || '').trim(), title: $('#chore_title').val(), description: $('#chore_description').val(), image: $('#chore_image').val(), value: Number($('#chore_value').val()) || 0, due_date: due, schedule: $('#chore_schedule').val(), recurrence_days: parseInt($('#chore_recurrence_days').val(), 10) || 7, assignee_ids: assignees, anyone_can_complete: $('#chore_anyone_can_complete').is(':checked'), lists: choreListPayloadFromForm() }; postJson('/chore_save.php', payload) .then(function () { window.location.href = window.location.pathname + '?tab=chores'; }) .catch(function (err) { var $fb = $('#choreFormFeedback'); showAlert($fb, 'danger', err.message || 'Could not save chore'); $fb.removeClass('d-none'); }); }); $('#choreDeleteBtn').on('click', function () { var id = $(this).data('id'); if (!id || !window.confirm('Delete this chore?')) { return; } postJson('/chore_delete.php', { id: id }) .then(function () { window.location.href = window.location.pathname + '?tab=chores'; }) .catch(function (err) { window.alert(err.message || 'Could not delete'); }); }); $(document).on('click', '.btn-chore-submit', function () { var id = $(this).data('id'); if (!id || !window.confirm('Submit this chore as done? A Head of household will approve it before the reward is paid.')) { return; } postJson('/chore_submit.php', { id: id, note: '' }) .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not submit'); }); }); $(document).on('click', '.btn-chore-approve', function () { var id = $(this).data('id'); if (!id || !window.confirm('Approve and pay the reward?')) { return; } postJson('/chore_review.php', { id: id, decision: 'approve' }) .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not approve'); }); }); $(document).on('click', '.btn-chore-reject', function () { var id = $(this).data('id'); if (!id || !window.confirm('Reject this submission?')) { return; } postJson('/chore_review.php', { id: id, decision: 'reject' }) .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not reject'); }); }); $(document).on('click', '.btn-chore-nfc-generate', function () { var id = $(this).data('id'); if (!id) { return; } postJson('/chore_nfc_rotate.php', { id: id }) .then(function (data) { var links = Array.isArray(data.links) ? data.links : []; var html = '
NFC links (copy per person)
'; if (!links.length) { html += '
No links were returned.
'; } else { links.forEach(function (row) { html += ''; html += '
'; html += ''; html += ''; html += '
'; }); } var $links = $('#chore_nfc_links_' + id); $links.html(html).removeClass('d-none'); showAlert($('#chore_nfc_feedback_' + id), 'success', 'Generated new NFC links.'); $('#chore_nfc_feedback_' + id).removeClass('d-none'); }) .catch(function (err) { showAlert($('#chore_nfc_feedback_' + id), 'danger', err.message || 'Could not generate links'); $('#chore_nfc_feedback_' + id).removeClass('d-none'); }); }); $(document).on('click', '.btn-copy-nfc-link', function () { var $input = $(this).closest('.input-group').find('.chore-nfc-link-input'); var val = $input.val() || ''; if (!val) { return; } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(String(val)); } else { $input.trigger('focus').trigger('select'); document.execCommand('copy'); } }); $(document).on('click', '.btn-chore-nfc-toggle', function () { var id = $(this).data('id'); var enable = String($(this).data('enable')) === '1'; if (!id) { return; } postJson('/chore_nfc_toggle.php', { id: id, enabled: enable }) .then(function () { window.location.reload(); }) .catch(function (err) { showAlert($('#chore_nfc_feedback_' + id), 'danger', err.message || 'Could not update NFC status'); $('#chore_nfc_feedback_' + id).removeClass('d-none'); }); }); } function groceryReloadKeepParams() { var $tab = $('#groceries'); var store = $tab.data('store-id') || $('#grocery_store_id').val(); var params = new URLSearchParams(window.location.search); var filter = params.get('filter') || 'active'; var q = '?tab=groceries&store=' + encodeURIComponent(store || '') + '&filter=' + encodeURIComponent(filter); window.location.href = window.location.pathname + q; } function bindStoreManagement() { $('#storeAddForm').on('submit', function (e) { e.preventDefault(); var name = ($('#new_store_name').val() || '').trim(); if (!name) { return; } postJson('/store_create.php', { name: name }) .then(function () { window.location.reload(); }) .catch(function (err) { var $fb = $('#storeAddFeedback'); showAlert($fb, 'danger', err.message || 'Could not add store'); $fb.removeClass('d-none'); }); }); $(document).on('click', '.btn-store-delete', function () { var id = $(this).data('id'); if (!id || !window.confirm('Delete this store? Lists must be empty.')) { return; } postJson('/store_delete.php', { id: id }) .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not delete'); }); }); $(document).on('click', '.btn-store-rename', function () { var id = $(this).data('id'); var current = $(this).data('name') || ''; var name = window.prompt('Store name', current); if (name === null) { return; } name = String(name).trim(); if (!name) { return; } postJson('/store_update.php', { id: id, name: name }) .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not rename'); }); }); } function bindGroceryPage() { var $tab = $('#groceries'); if (!$tab.length) { return; } $('#grocery_catalog_pick').on('change', function () { var v = $(this).val(); if (!v) { return; } var $o = $(this).find('option:selected'); $('#grocery_name').val($o.data('name') || ''); $('#grocery_description').val($o.data('description') || ''); $('#grocery_size').val($o.data('size') || ''); $('#grocery_image').val($o.data('image') || ''); }); $('#groceryAddForm').on('submit', function (e) { e.preventDefault(); var storeId = ($('#grocery_store_id').val() || $tab.data('store-id') || '').trim(); var payload = { storeId: storeId, catalogPickId: $('#grocery_catalog_pick').val() || '', name: $('#grocery_name').val(), description: $('#grocery_description').val(), size: $('#grocery_size').val(), quantity: $('#grocery_quantity').val(), price: $('#grocery_price').val(), image: $('#grocery_image').val(), recurringIntervalDays: parseInt($('#grocery_recurring').val(), 10) || 0, source: 'manual' }; postJson('/grocery_item_create.php', payload) .then(function () { groceryReloadKeepParams(); }) .catch(function (err) { var $fb = $('#groceryFormFeedback'); showAlert($fb, 'danger', err.message || 'Could not add item'); $fb.removeClass('d-none'); }); }); $(document).on('click', '.btn-grocery-purchase', function () { var storeId = $tab.data('store-id'); var itemId = $(this).data('item-id'); var purchased = String($(this).data('purchased')) === '1'; postJson('/grocery_item_purchase.php', { storeId: storeId, itemId: itemId, purchased: purchased }) .then(function () { groceryReloadKeepParams(); }) .catch(function (err) { window.alert(err.message || 'Could not update'); }); }); $(document).on('click', '.btn-grocery-delete', function () { var storeId = $tab.data('store-id'); var itemId = $(this).data('item-id'); if (!itemId || !window.confirm('Remove this item?')) { return; } postJson('/grocery_item_delete.php', { storeId: storeId, itemId: itemId }) .then(function () { groceryReloadKeepParams(); }) .catch(function (err) { window.alert(err.message || 'Could not remove'); }); }); $(document).on('click', '.btn-grocery-approve', function () { var storeId = $tab.data('store-id'); var itemId = $(this).data('item-id'); if (!itemId) { return; } postJson('/grocery_item_review.php', { storeId: storeId, itemId: itemId }) .then(function () { groceryReloadKeepParams(); }) .catch(function (err) { window.alert(err.message || 'Could not approve'); }); }); } function mealListPayloadFromForm() { var type = $('#meal_list_type').val() || 'checkbox'; var raw = $('#meal_list_items').val() || ''; var lines = raw.split(/\r?\n/).map(function (l) { return l.trim(); }).filter(Boolean); if (lines.length === 0) { return []; } return [{ type: type, items: lines }]; } function mealGroceryRowsPayload() { var out = []; $('#mealGroceryRows .meal-grocery-row').each(function () { var $r = $(this); var name = ($r.find('.g-name').val() || '').trim(); if (!name) { return; } out.push({ name: name, storeId: ($r.find('.g-store').val() || '').trim(), quantity: ($r.find('.g-qty').val() || '1').trim() || '1', size: ($r.find('.g-size').val() || '').trim(), description: '', price: '', image: '' }); }); return out; } function readMealsLibrary() { var el = document.getElementById('mealsLibraryJson'); if (!el || !el.textContent) { return []; } try { return JSON.parse(el.textContent); } catch (e) { return []; } } function hideMealSlotModal() { var el = document.getElementById('mealSlotModal'); if (el && typeof bootstrap !== 'undefined') { var inst = bootstrap.Modal.getInstance(el); if (inst) { inst.hide(); } } } function bindMealsPage() { var $tab = $('#meals'); if (!$tab.length) { return; } var weekStart = ($tab.attr('data-week-start') || '').trim(); var mealImportParsed = null; $(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_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) { 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.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 () { 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) { if ($fb.length) { showAlert($fb, 'danger', (err.message || 'Could not add') + ' — “' + label + '”'); $fb.removeClass('d-none'); } }); }); $('#btnMealWeekApply').on('click', function () { var ws = ($('#meal_week_start').val() || '').trim(); if (!ws) { return; } if (!window.confirm('Change the planning week? This clears the grid for the new week.')) { return; } postJson('/meal_plan_set_week.php', { weekStart: ws }) .then(function () { window.location.reload(); }) .catch(function (err) { showAlert($('#mealWeekFeedback'), 'danger', err.message || 'Could not set week'); $('#mealWeekFeedback').removeClass('d-none'); }); }); var slotModalEl = document.getElementById('mealSlotModal'); var slotModal = null; if (slotModalEl && typeof bootstrap !== 'undefined') { slotModal = new bootstrap.Modal(slotModalEl, { backdrop: false }); } function renderSlotPickerList() { var lib = readMealsLibrary(); var filter = ($('#slotPickerFilter').val() || '').toLowerCase(); var $ul = $('#slotPickerList'); $ul.empty(); lib.forEach(function (m) { if (filter) { var tags = (m.tags || []).map(function (t) { return String(t).toLowerCase(); }); if (tags.indexOf(filter) === -1) { return; } } var $li = $('
  • ') .text(m.title || m.id) .attr('data-meal-id', m.id); $ul.append($li); }); if ($ul.children().length === 0) { $ul.append('
  • No meals match this filter.
  • '); } } $(document).on('click', '.btn-open-slot-picker', function () { var day = $(this).data('day'); var mealType = $(this).data('meal-type'); $('#slotPickerDay').val(String(day)); $('#slotPickerType').val(String(mealType)); if (mealType) { $('#slotPickerFilter').val(String(mealType)); } renderSlotPickerList(); if (slotModal) { slotModal.show(); } }); $('#slotPickerFilter').on('change', renderSlotPickerList); $(document).on('click', '#slotPickerList li[data-meal-id]', function () { var mealId = $(this).attr('data-meal-id'); if (!mealId || !weekStart) { return; } var day = parseInt($('#slotPickerDay').val(), 10); var mealType = $('#slotPickerType').val(); var pushGrocery = $('#slotPickerPushGrocery').is(':checked'); postJson('/meal_plan_set_slot.php', { weekStart: weekStart, day: day, mealType: mealType, mealId: String(mealId), pushGrocery: pushGrocery }) .then(function () { hideMealSlotModal(); window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not assign meal'); }); }); $('#slotPickerClear').on('click', function () { if (!weekStart) { return; } var day = parseInt($('#slotPickerDay').val(), 10); var mealType = $('#slotPickerType').val(); postJson('/meal_plan_set_slot.php', { weekStart: weekStart, day: day, mealType: mealType, mealId: '', pushGrocery: false }) .then(function () { hideMealSlotModal(); window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not clear slot'); }); }); $('#mealSaveForm').on('submit', function (e) { e.preventDefault(); var tags = []; $('.meal-tag-cb:checked').each(function () { tags.push($(this).val()); }); var ingRaw = $('#meal_ingredients').val() || ''; var ingredients = ingRaw.split(/\r?\n/).map(function (l) { return l.trim(); }).filter(Boolean); var payload = { id: ($('#meal_id').val() || '').trim(), title: ($('#meal_title').val() || '').trim(), image: $('#meal_image').val(), description: $('#meal_description').val(), directions: $('#meal_directions').val(), lists: mealListPayloadFromForm(), tags: tags, ingredients: ingredients, items: mealGroceryRowsPayload() }; postJson('/meal_save.php', payload) .then(function () { window.location.href = window.location.pathname + '?tab=meals'; }) .catch(function (err) { showAlert($('#mealSaveFeedback'), 'danger', err.message || 'Could not save'); $('#mealSaveFeedback').removeClass('d-none'); }); }); $('#btnAddGroceryRow').on('click', function () { var $first = $('#mealGroceryRows .meal-grocery-row').first(); if (!$first.length) { return; } var $clone = $first.clone(); $clone.find('input').val(''); $clone.find('.g-qty').val('1'); $clone.find('select').prop('selectedIndex', 0); $('#mealGroceryRows').append($clone); }); $(document).on('click', '.btn-meal-delete', function () { var id = $(this).data('id'); if (!id || !window.confirm('Delete this meal from the library?')) { return; } postJson('/meal_delete.php', { id: id }) .then(function () { window.location.href = window.location.pathname + '?tab=meals'; }) .catch(function (err) { 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() { var $form = $('#expenseForm'); if (!$form.length) { return; } $form.on('submit', function (e) { e.preventDefault(); var payload = { title: $('#expense_title').val(), description: $('#expense_description').val(), date: $('#expense_date').val(), value: Number($('#expense_value').val()), assignee_id: $('#expense_assignee').val() }; postJson('/expense_create.php', payload) .then(function () { var $fb = $('#expenseFormFeedback'); showAlert($fb, 'success', 'Expense recorded. Reloading…'); $fb.removeClass('d-none'); window.location.reload(); }) .catch(function (err) { var $fb = $('#expenseFormFeedback'); showAlert($fb, 'danger', err.message || 'Could not record expense'); $fb.removeClass('d-none'); }); }); } function bindPeopleTable() { $(document).on('click', '.btn-delete-person', function () { var id = $(this).data('id'); if (!id || !window.confirm('Remove this person from the hub?')) { return; } postJson('/people_delete.php', { id: id }) .then(function () { window.location.reload(); }) .catch(function (err) { window.alert(err.message || 'Could not remove'); }); }); $(document).on('click', '.btn-reset-pin', function () { var id = $(this).data('id'); var pin = window.prompt('Enter a new PIN (4+ characters) for this Head of household.') || ''; if (pin.length < 4) { return; } postJson('/people_reset_pin.php', { targetPersonId: id, newPin: pin }) .then(function () { window.alert('PIN updated.'); }) .catch(function (err) { window.alert(err.message || 'Could not reset PIN'); }); }); $(document).on('click', '.btn-rotate-person-nfc-token', function () { var id = $(this).data('id'); var name = $(this).data('name') || 'this person'; if (!id || !window.confirm('Rotate NFC submitter token for ' + name + '? Existing personal NFC links will stop working.')) { return; } postJson('/person_nfc_token_rotate.php', { person_id: id }) .then(function () { window.alert('Token rotated for ' + name + '. Generate fresh chore NFC links as needed.'); }) .catch(function (err) { window.alert(err.message || 'Could not rotate NFC token'); }); }); } $(function () { bindPersonaSwitcher(); bindFirstHeadForm(); bindAddPersonForm(); bindFamilySettingsForm(); bindCalendarSettingsForm(); bindPeopleTable(); bindChoresPage(); bindCurrencyPage(); bindStoreManagement(); bindGroceryPage(); bindMealsPage(); }); })(jQuery);