familyHub/tabs/settings.php
2026-04-03 20:43:00 -04:00

471 lines
35 KiB
PHP
Raw 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/grocery_helpers.php';
require_once __DIR__ . '/../includes/family_settings.php';
$usTimezones = familyHubUsTimezoneIdentifiers();
$billDays = $familySettings['calendar_bill_days'] ?? [];
if (!is_array($billDays) || $billDays === []) {
$billDays = [['dayOfMonth' => 1, 'title' => '']];
}
$canManage = count($people) === 0
|| (
$activePerson !== null
&& ($activePerson['role'] ?? '') === ROLE_HEAD
&& isHohVerified()
);
$hasPeople = count($people) > 0;
$showSetup = !$hasPeople;
$permanenceOptions = [
'permanent' => 'Permanent',
'weekly' => 'Weekly',
'biweekly' => 'Bi-weekly',
'monthly' => 'Monthly',
'quarterly' => 'Quarterly',
'yearly' => 'Yearly',
];
?>
<div id="settings" class="tab-content">
<h2 class="mb-3">Family settings</h2>
<p class="text-muted">People and chore currency defaults apply across the whole hub. Heads of household verify with a PIN when switching profiles at the top of the page.</p>
<div class="row g-4 align-items-start">
<div class="col-lg-3">
<div class="nav nav-pills flex-lg-column gap-2" id="settingsSectionTabs" role="tablist" aria-orientation="vertical">
<?php if ($showSetup): ?>
<button class="nav-link active text-start" id="settings-tab-setup" data-bs-toggle="pill" data-bs-target="#settings-pane-setup" type="button" role="tab" aria-controls="settings-pane-setup" aria-selected="true">First-time setup</button>
<?php endif; ?>
<button class="nav-link text-start <?= !$showSetup ? 'active' : '' ?>" id="settings-tab-calendar" data-bs-toggle="pill" data-bs-target="#settings-pane-calendar" type="button" role="tab" aria-controls="settings-pane-calendar" aria-selected="<?= !$showSetup ? 'true' : 'false' ?>">Calendar and dates</button>
<button class="nav-link text-start" id="settings-tab-economy" data-bs-toggle="pill" data-bs-target="#settings-pane-economy" type="button" role="tab" aria-controls="settings-pane-economy" aria-selected="false">Chore economy</button>
<button class="nav-link text-start" id="settings-tab-stores" data-bs-toggle="pill" data-bs-target="#settings-pane-stores" type="button" role="tab" aria-controls="settings-pane-stores" aria-selected="false">Grocery stores</button>
<?php if ($hasPeople): ?>
<button class="nav-link text-start" id="settings-tab-people" data-bs-toggle="pill" data-bs-target="#settings-pane-people" type="button" role="tab" aria-controls="settings-pane-people" aria-selected="false">People</button>
<?php endif; ?>
</div>
</div>
<div class="col-lg-9">
<div class="tab-content" id="settingsSectionTabContent">
<?php if ($showSetup): ?>
<div class="tab-pane fade show active" id="settings-pane-setup" role="tabpanel" aria-labelledby="settings-tab-setup" tabindex="0">
<div class="card border-primary">
<div class="card-header bg-primary text-white">First-time setup</div>
<div class="card-body">
<p>Create the first <strong>Head of household</strong> profile so you can manage family members and settings.</p>
<form id="firstHeadForm" class="row g-3">
<div class="col-md-6">
<label class="form-label" for="first_name">Name</label>
<input class="form-control" id="first_name" name="name" required autocomplete="name">
</div>
<div class="col-md-6">
<label class="form-label" for="first_pin">PIN (4+ characters)</label>
<input type="password" class="form-control" id="first_pin" name="pin" required minlength="4" autocomplete="new-password">
</div>
<div class="col-md-4">
<label class="form-label" for="first_favoriteColor">Favorite color</label>
<input type="color" class="form-control form-control-color" id="first_favoriteColor" name="favoriteColor" value="#4a90e2">
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary btn-lg">Create profile</button>
</div>
<div class="col-12">
<div class="alert d-none" id="firstHeadFeedback" role="status"></div>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<div class="tab-pane fade <?= !$showSetup ? 'show active' : '' ?>" id="settings-pane-calendar" role="tabpanel" aria-labelledby="settings-tab-calendar" tabindex="0">
<div class="card">
<div class="card-header">Calendar and dates</div>
<div class="card-body">
<p class="text-muted small">Used for the <strong>Calendar</strong> tab agenda (today, due dates, and bill reminders). US timezones only.</p>
<?php if (!$canManage && $hasPeople): ?>
<p class="text-muted">Switch to a verified Head of household to edit these settings.</p>
<?php endif; ?>
<form id="calendarSettingsForm" class="row g-3">
<div class="col-md-6">
<label class="form-label" for="family_timezone">Family timezone</label>
<select class="form-select" id="family_timezone" name="timezone" <?= !$canManage ? 'disabled' : '' ?>>
<?php
$curTz = (string) ($familySettings['timezone'] ?? 'America/New_York');
foreach ($usTimezones as $zid):
?>
<option value="<?= htmlspecialchars($zid, ENT_QUOTES, 'UTF-8') ?>" <?= $curTz === $zid ? 'selected' : '' ?>>
<?= htmlspecialchars(str_replace('_', ' ', $zid), ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<div class="form-check mb-1">
<input class="form-check-input" type="checkbox" id="calendar_two_way_google" name="calendar_two_way_google" value="1"
<?= !empty($familySettings['calendar_two_way_google']) ? 'checked' : '' ?>
<?= !$canManage ? 'disabled' : '' ?>>
<label class="form-check-label" for="calendar_two_way_google">Notify me when two-way Google Calendar sync is available</label>
</div>
<p class="small text-muted mb-0">Family Hub does not call Googles API yet. Your calendar appears on the Calendar tab only via <code class="small">.env</code> (<code>GOOGLE_CALENDAR_ID</code> or <code>GOOGLE_CALENDAR_EMBED_CODE</code>). This checkbox only stores a preference; it does not enable sync today.</p>
</div>
<div class="col-12">
<label class="form-label">Monthly bill reminders</label>
<p class="small text-muted mb-2">Shown on the family agenda on the same day each month (e.g. rent). Leave a row blank or clear the title to skip.</p>
<div id="billDaysRows" class="d-flex flex-column gap-2">
<?php foreach ($billDays as $i => $bd): ?>
<?php
$dom = (int) ($bd['dayOfMonth'] ?? 1);
$bt = (string) ($bd['title'] ?? '');
?>
<div class="bill-day-row row g-2 align-items-end">
<div class="col-4 col-md-3">
<label class="form-label small text-muted">Day of month</label>
<select class="form-select bill-day-dom" aria-label="Bill day of month">
<?php for ($d = 1; $d <= 31; $d++): ?>
<option value="<?= $d ?>" <?= $dom === $d ? 'selected' : '' ?>><?= $d ?></option>
<?php endfor; ?>
</select>
</div>
<div class="col">
<label class="form-label small text-muted">Reminder title</label>
<input type="text" class="form-control bill-day-title" maxlength="120" placeholder="e.g. Rent" value="<?= htmlspecialchars($bt, ENT_QUOTES, 'UTF-8') ?>">
</div>
</div>
<?php endforeach; ?>
</div>
<?php if ($canManage): ?>
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" id="btnAddBillDayRow">Add reminder row</button>
<div class="col-12 mt-3">
<button type="submit" class="btn btn-primary">Save calendar settings</button>
</div>
<?php endif; ?>
<div class="col-12">
<div class="alert d-none" id="calendarSettingsFeedback" role="status"></div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-pane-economy" role="tabpanel" aria-labelledby="settings-tab-economy" tabindex="0">
<div class="card">
<div class="card-header">Chore economy</div>
<div class="card-body">
<?php if (!$canManage && $hasPeople): ?>
<p class="text-muted">Switch to a verified Head of household to edit these settings.</p>
<?php endif; ?>
<form id="familySettingsForm" class="row g-3">
<div class="col-md-4">
<label class="form-label" for="currency_symbol">Currency symbol</label>
<input class="form-control" id="currency_symbol" name="currency_symbol" maxlength="8"
value="<?= sanitizeInput($familySettings['currency_symbol'] ?? '') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-4">
<label class="form-label" for="currency_name">Currency name</label>
<input class="form-control" id="currency_name" name="currency_name"
value="<?= sanitizeInput($familySettings['currency_name'] ?? '') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-4">
<label class="form-label" for="currency_permanence">Balance reset</label>
<select class="form-select" id="currency_permanence" name="currency_permanence" <?= !$canManage ? 'disabled' : '' ?>>
<?php foreach ($permanenceOptions as $val => $label): ?>
<option value="<?= htmlspecialchars($val, ENT_QUOTES, 'UTF-8') ?>" <?= ($familySettings['currency_permanence'] ?? '') === $val ? 'selected' : '' ?>>
<?= htmlspecialchars($label, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="week_starts_on">Week starts on (0 = Sunday)</label>
<input type="number" class="form-control" id="week_starts_on" name="week_starts_on" min="0" max="6"
value="<?= (int) ($familySettings['week_starts_on'] ?? 0) ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-12"><hr class="my-2"></div>
<div class="col-12">
<h3 class="h6 mb-2">Banking system (optional)</h3>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="banking_enabled" name="banking_enabled" value="1"
<?= !empty($familySettings['banking_enabled']) ? 'checked' : '' ?>
<?= !$canManage ? 'disabled' : '' ?>>
<label class="form-check-label" for="banking_enabled">Enable checking/savings/charity accounts</label>
</div>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="banking_auto_split_enabled" name="banking_auto_split_enabled" value="1"
<?= !empty($familySettings['banking_auto_split_enabled']) ? 'checked' : '' ?>
<?= !$canManage ? 'disabled' : '' ?>>
<label class="form-check-label" for="banking_auto_split_enabled">Auto split all checking income</label>
</div>
</div>
<div class="col-md-4">
<label class="form-label" for="banking_roundup_destination">Round-up rule</label>
<select class="form-select" id="banking_roundup_destination" name="banking_roundup_destination" <?= !$canManage ? 'disabled' : '' ?>>
<?php $roundupDest = (string) ($familySettings['banking_roundup_destination'] ?? 'off'); ?>
<option value="off" <?= $roundupDest === 'off' ? 'selected' : '' ?>>Off</option>
<option value="savings" <?= $roundupDest === 'savings' ? 'selected' : '' ?>>Nearest whole to savings</option>
<option value="charity" <?= $roundupDest === 'charity' ? 'selected' : '' ?>>Nearest whole to charity</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="banking_auto_split_savings_pct">Auto split to savings (%)</label>
<input type="number" step="0.01" min="0" max="100" class="form-control" id="banking_auto_split_savings_pct" name="banking_auto_split_savings_pct"
value="<?= htmlspecialchars(number_format((float) ($familySettings['banking_auto_split_savings_pct'] ?? 0), 2, '.', ''), ENT_QUOTES, 'UTF-8') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-4">
<label class="form-label" for="banking_auto_split_charity_pct">Auto split to charity (%)</label>
<input type="number" step="0.01" min="0" max="100" class="form-control" id="banking_auto_split_charity_pct" name="banking_auto_split_charity_pct"
value="<?= htmlspecialchars(number_format((float) ($familySettings['banking_auto_split_charity_pct'] ?? 0), 2, '.', ''), ENT_QUOTES, 'UTF-8') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-4">
<label class="form-label" for="banking_savings_monthly_interest_rate">Savings monthly interest rate (%)</label>
<input type="number" step="0.000001" min="0" class="form-control" id="banking_savings_monthly_interest_rate" name="banking_savings_monthly_interest_rate"
value="<?= htmlspecialchars((string) ($familySettings['banking_savings_monthly_interest_rate'] ?? 0), ENT_QUOTES, 'UTF-8') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-6">
<label class="form-label" for="nfc_base_url">NFC base URL override (optional)</label>
<input class="form-control" id="nfc_base_url" name="nfc_base_url" placeholder="https://your-domain/familyHub"
value="<?= sanitizeInput($familySettings['nfc_base_url'] ?? '') ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-6">
<label class="form-label" for="nfc_scan_cooldown_seconds">NFC scan cooldown seconds</label>
<input type="number" class="form-control" id="nfc_scan_cooldown_seconds" name="nfc_scan_cooldown_seconds" min="0" max="600"
value="<?= (int) ($familySettings['nfc_scan_cooldown_seconds'] ?? 0) ?>"
<?= !$canManage ? 'disabled' : '' ?>>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="nfc_show_confirmation" name="nfc_show_confirmation" value="1"
<?= !empty($familySettings['nfc_show_confirmation']) ? 'checked' : '' ?>
<?= !$canManage ? 'disabled' : '' ?>>
<label class="form-check-label" for="nfc_show_confirmation">Show NFC confirmation page after scan</label>
</div>
</div>
<?php if ($canManage): ?>
<div class="col-12">
<button type="submit" class="btn btn-primary">Save chore economy</button>
</div>
<div class="col-12">
<div class="small text-muted mb-2">Emergency NFC actions</div>
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-warning btn-sm" id="btnDisableAllChoreNfc">Disable all chore NFC links</button>
<button type="button" class="btn btn-outline-danger btn-sm" id="btnRotateAllPersonNfcTokens">Rotate all person NFC tokens</button>
</div>
</div>
<?php endif; ?>
<div class="col-12">
<div class="alert d-none" id="familySettingsFeedback" role="status"></div>
</div>
</form>
</div>
</div>
</div>
<?php
$storesList = normalizeStoresList(readJsonFile('stores.json'));
?>
<div class="tab-pane fade" id="settings-pane-stores" role="tabpanel" aria-labelledby="settings-tab-stores" tabindex="0">
<div class="card">
<div class="card-header">Grocery stores</div>
<div class="card-body">
<p class="small text-muted">Each store has its own list in the <strong>Grocery list</strong> tab.</p>
<?php if ($canManage): ?>
<form id="storeAddForm" class="row g-2 align-items-end mb-3">
<div class="col-md-8">
<label class="form-label" for="new_store_name">New store name</label>
<input class="form-control" id="new_store_name" required maxlength="80" placeholder="e.g. Costco">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary">Add store</button>
</div>
<div class="col-12">
<div class="alert d-none" id="storeAddFeedback" role="status"></div>
</div>
</form>
<?php else: ?>
<p class="text-muted small">Only a verified Head of household can manage stores.</p>
<?php endif; ?>
<?php if (count($storesList) === 0): ?>
<p class="text-muted mb-0">No stores yet. Add one above (or open the Grocery list tab to get a default Home store).</p>
<?php else: ?>
<ul class="list-group">
<?php foreach ($storesList as $st): ?>
<?php $stid = htmlspecialchars((string) ($st['id'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>
<li class="list-group-item d-flex flex-wrap justify-content-between align-items-center gap-2">
<span><?= sanitizeInput($st['name'] ?? '') ?></span>
<?php if ($canManage): ?>
<span class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary btn-store-rename" data-id="<?= $stid ?>" data-name="<?= htmlspecialchars((string) ($st['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">Rename</button>
<button type="button" class="btn btn-sm btn-outline-danger btn-store-delete" data-id="<?= $stid ?>">Delete</button>
</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
<?php if ($hasPeople): ?>
<div class="tab-pane fade" id="settings-pane-people" role="tabpanel" aria-labelledby="settings-tab-people" tabindex="0">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>People</span>
</div>
<div class="card-body">
<?php if ($canManage): ?>
<form id="addPersonForm" class="row g-3 mb-4 pb-3 border-bottom">
<div class="col-md-4">
<label class="form-label" for="add_name">Name</label>
<input class="form-control" id="add_name" required>
</div>
<div class="col-md-4">
<label class="form-label" for="add_role">Role</label>
<select class="form-select" id="add_role">
<option value="<?= ROLE_CHILD ?>">Child</option>
<option value="<?= ROLE_ADULT ?>">Adult</option>
<option value="<?= ROLE_HEAD ?>">Head of household</option>
</select>
</div>
<div class="col-md-4" id="add_pin_wrap">
<label class="form-label" for="add_pin">PIN (required for Head of household)</label>
<input type="password" class="form-control" id="add_pin" minlength="4" autocomplete="new-password">
</div>
<div class="col-md-4">
<label class="form-label" for="add_favoriteColor">Favorite color</label>
<input type="color" class="form-control form-control-color" id="add_favoriteColor" value="#4a90e2">
</div>
<div class="col-12">
<button type="submit" class="btn btn-success">Add person</button>
</div>
<div class="col-12">
<div class="alert d-none" id="addPersonFeedback" role="status"></div>
</div>
</form>
<?php
$peopleEditPayload = [];
foreach ($people as $ep) {
$peopleEditPayload[] = [
'id' => (string) ($ep['id'] ?? ''),
'name' => (string) ($ep['name'] ?? ''),
'role' => (string) ($ep['role'] ?? ROLE_CHILD),
'favoriteColor' => (string) ($ep['favoriteColor'] ?? '#4a90e2'),
'birthday' => (string) ($ep['birthday'] ?? ''),
'description' => (string) ($ep['description'] ?? ''),
];
}
?>
<script type="application/json" id="settingsPeopleEditPayload"><?= json_encode($peopleEditPayload, JSON_HEX_TAG | JSON_HEX_AMP | familyHubJsonEncodeShellFlags()) ?></script>
<div class="modal fade" id="editPersonModal" tabindex="-1" aria-labelledby="editPersonModalLabel" aria-hidden="true" data-bs-backdrop="false">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<form id="editPersonForm">
<div class="modal-header">
<h3 class="modal-title h5" id="editPersonModalLabel">Edit person</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row g-3">
<input type="hidden" name="id" id="edit_person_id" value="">
<input type="hidden" id="edit_person_initial_role" value="">
<div class="col-12">
<label class="form-label" for="edit_name">Name</label>
<input class="form-control" id="edit_name" name="name" required maxlength="120" autocomplete="name">
</div>
<div class="col-md-6">
<label class="form-label" for="edit_role">Role</label>
<select class="form-select" id="edit_role" name="role">
<option value="<?= ROLE_CHILD ?>">Child</option>
<option value="<?= ROLE_ADULT ?>">Adult</option>
<option value="<?= ROLE_HEAD ?>">Head of household</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label" for="edit_favoriteColor">Favorite color</label>
<input type="color" class="form-control form-control-color" id="edit_favoriteColor" name="favoriteColor" value="#4a90e2">
</div>
<div class="col-12" id="edit_pin_wrap">
<label class="form-label" for="edit_pin">PIN for new Head of household (4+ characters)</label>
<input type="password" class="form-control" id="edit_pin" minlength="4" autocomplete="new-password">
</div>
<div class="col-md-6">
<label class="form-label" for="edit_birthday">Birthday (optional)</label>
<input type="date" class="form-control" id="edit_birthday" name="birthday" autocomplete="bday">
</div>
<div class="col-12">
<label class="form-label" for="edit_description">Notes (optional)</label>
<textarea class="form-control" id="edit_description" name="description" rows="2" maxlength="500" placeholder="Short note visible in household tools"></textarea>
</div>
<div class="col-12">
<div class="alert d-none" id="editPersonFeedback" role="status"></div>
</div>
</div>
<div class="modal-footer flex-wrap gap-2 justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<?php else: ?>
<p class="text-muted">Only a verified Head of household can add or edit people.</p>
<?php endif; ?>
<div class="table-responsive">
<table class="table align-middle" id="peopleTable">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th class="text-end">Balance</th>
<?php if ($canManage): ?><th>NFC submit token</th><?php endif; ?>
<?php if ($canManage): ?><th></th><?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($people as $p): ?>
<tr data-person-id="<?= htmlspecialchars($p['id'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<td><?= sanitizeInput($p['name'] ?? '') ?></td>
<td><?= sanitizeInput($p['role'] ?? '') ?></td>
<td class="text-end"><?= sanitizeInput((string) ($p['currency_balance'] ?? 0)) ?> <?= sanitizeInput($familySettings['currency_symbol'] ?? '') ?></td>
<?php if ($canManage): ?>
<td class="text-nowrap">
<button type="button" class="btn btn-sm btn-outline-secondary btn-rotate-person-nfc-token" data-id="<?= htmlspecialchars($p['id'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-name="<?= htmlspecialchars((string) ($p['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">Rotate token</button>
</td>
<td class="text-end text-nowrap">
<button type="button" class="btn btn-sm btn-outline-primary btn-edit-person" data-person-id="<?= htmlspecialchars($p['id'] ?? '', ENT_QUOTES, 'UTF-8') ?>">Edit</button>
<?php if (($p['role'] ?? '') === ROLE_HEAD): ?>
<button type="button" class="btn btn-sm btn-outline-secondary btn-reset-pin" data-id="<?= htmlspecialchars($p['id'] ?? '', ENT_QUOTES, 'UTF-8') ?>">Reset PIN</button>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-person" data-id="<?= htmlspecialchars($p['id'] ?? '', ENT_QUOTES, 'UTF-8') ?>">Remove</button>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>