/** * 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); } 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(), timezone: $('#timezone').val(), week_starts_on: parseInt($('#week_starts_on').val(), 10) }; 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'); }); }); } 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, 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'); }); }); } 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(); $('#btnPushMealGrocery').on('click', function () { var id = $(this).data('meal-id'); if (!id) { 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'); }) .catch(function (err) { showAlert($('#pushMealFeedback'), 'danger', err.message || 'Could not push'); $('#pushMealFeedback').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 $li = $btn.closest('li'); var storeId = $li.find('.ingredient-store').val() || ''; postJson('/meal_ingredient_to_grocery.php', { mealId: mealId, ingredient: ing, storeId: storeId }) .then(function () { window.alert('Added to grocery list.'); }) .catch(function (err) { window.alert(err.message || 'Could not add'); }); }); $('#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); } 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'); }); }); } 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'); }); }); } $(function () { bindPersonaSwitcher(); bindFirstHeadForm(); bindAddPersonForm(); bindFamilySettingsForm(); bindPeopleTable(); bindChoresPage(); bindCurrencyPage(); bindStoreManagement(); bindGroceryPage(); bindMealsPage(); }); })(jQuery);