familyHub/tabs/chores.php
Louis Whittington f14de0b7e1 Add NFC chore submission feature and enhance family settings
- Introduced NFC support for chore submissions, allowing specific person credit after Head of Household approval.
- Updated family settings to include NFC base URL, scan cooldown, and confirmation page options.
- Enhanced chore management with options for anyone to complete chores and NFC link generation.
- Improved API endpoints for handling NFC tokens and chore submissions.
- Updated readme to reflect new NFC features and settings.
2026-03-31 10:28:27 -05:00

369 lines
22 KiB
PHP
Raw Permalink 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/chore_helpers.php';
$rawChores = readJsonFile('chores.json');
$chores = migrateAllChores(normalizeChoresList($rawChores), $people);
$nameById = [];
foreach ($people as $p) {
if (!empty($p['id'])) {
$nameById[(string) $p['id']] = (string) ($p['name'] ?? '');
}
}
$currencySymbol = $familySettings['currency_symbol'] ?? '★';
$canReview = $activePerson !== null
&& ($activePerson['role'] ?? '') === ROLE_HEAD
&& isHohVerified();
$actorId = $activePerson['id'] ?? '';
$editId = isset($_GET['edit']) ? trim((string) $_GET['edit']) : '';
$editChore = $editId !== '' ? findChoreById($chores, $editId) : null;
$editNotFound = $editId !== '' && $editChore === null;
$editChoreAllowed = $editChore !== null && $activePerson !== null && (
(string) ($editChore['author_id'] ?? '') === (string) $actorId
|| $canReview
);
$showChoreForm = $activePerson !== null
&& !$editNotFound
&& (
($editChore !== null && $editChoreAllowed)
|| ($editChore === null && $canReview)
);
$pendingForReview = [];
$activeChores = [];
$completedChores = [];
foreach ($chores as $c) {
$st = (string) ($c['status'] ?? 'active');
$pend = $c['pending_submission'] ?? null;
if ($st === 'active' && is_array($pend) && $canReview) {
$pendingForReview[] = $c;
}
if ($st === 'completed') {
$completedChores[] = $c;
} elseif ($st === 'active') {
$activeChores[] = $c;
}
}
$prefillList = ['type' => 'checkbox', 'items' => []];
if ($editChore) {
$lists = $editChore['lists'] ?? [];
if (is_array($lists) && count($lists) > 0) {
$b = $lists[0];
$t = $b['type'] ?? 'checkbox';
$prefillList['type'] = in_array($t, ['ordered', 'unordered', 'checkbox'], true) ? $t : 'checkbox';
$items = $b['items'] ?? [];
if (is_array($items)) {
foreach ($items as $it) {
$prefillList['items'][] = (string) $it;
}
}
}
}
?>
<div id="chores" class="tab-content">
<h2 class="mb-2">Chores</h2>
<?php if ($activePerson !== null): ?>
<div class="alert alert-info small mb-3 chore-howto">
<strong>How to finish a chore:</strong> The checklist bullets are reminders only (not clickable).
If this chore is assigned to you, use <strong>Submit for approval</strong> on the card when the work is done.
A Head of household then <strong>approves</strong> it so the reward is added to balances.
<span class="d-block mt-1 text-muted">Use the <strong>person chips at the top</strong> to switch to the right family member if you dont see that button.</span>
<span class="d-block mt-2"><strong>New chores:</strong> Only a verified <strong>Head of household</strong> can add them (switch profile and enter PIN if needed).</span>
</div>
<?php endif; ?>
<?php if ($activePerson === null): ?>
<div class="alert alert-warning">Choose who is using the hub (top of the page) to work with chores.</div>
<?php endif; ?>
<?php if ($canReview && count($pendingForReview) > 0): ?>
<div class="card border-warning mb-4">
<div class="card-header bg-warning-subtle">Waiting for your approval</div>
<ul class="list-group list-group-flush">
<?php foreach ($pendingForReview as $c): ?>
<?php
$cid = htmlspecialchars($c['id'] ?? '', ENT_QUOTES, 'UTF-8');
$sub = $c['pending_submission'] ?? [];
$byId = is_array($sub) ? (string) ($sub['submitted_by'] ?? '') : '';
$byName = $nameById[$byId] ?? $byId;
?>
<li class="list-group-item d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2">
<div>
<strong><?= sanitizeInput($c['title'] ?? '') ?></strong>
<span class="text-muted small ms-1">— submitted by <?= sanitizeInput($byName) ?></span>
<div class="small text-muted"><?= sanitizeInput((string) ($c['value'] ?? 0)) ?> <?= sanitizeInput($currencySymbol) ?> · assignees:
<?php
$ids = $c['assignee_ids'] ?? [];
$nm = [];
foreach ($ids as $i) {
$nm[] = $nameById[(string) $i] ?? (string) $i;
}
echo sanitizeInput(implode(', ', $nm));
?>
</div>
</div>
<div class="btn-group">
<button type="button" class="btn btn-success btn-sm btn-chore-approve" data-id="<?= $cid ?>">Approve</button>
<button type="button" class="btn btn-outline-secondary btn-sm btn-chore-reject" data-id="<?= $cid ?>">Reject</button>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ($editNotFound): ?>
<div class="alert alert-warning">That chore was not found. <a href="?tab=chores">Back to chores</a></div>
<?php endif; ?>
<?php if ($showChoreForm): ?>
<div class="card mb-4 <?= $editChore ? '' : 'border-secondary' ?>">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<span><?= $editChore ? 'Edit chore' : 'New chore' ?></span>
<?php if ($editChore): ?>
<a href="?tab=chores" class="btn btn-sm btn-outline-secondary">Cancel edit</a>
<?php else: ?>
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#choreFormCollapse" aria-expanded="false" aria-controls="choreFormCollapse">
Show form
</button>
<?php endif; ?>
</div>
<?php if ($editChore): ?>
<div class="card-body">
<?php else: ?>
<div class="collapse" id="choreFormCollapse">
<div class="card-body">
<?php endif; ?>
<form id="choreForm" class="row g-3">
<input type="hidden" name="id" id="chore_id" value="<?= $editChore ? htmlspecialchars($editChore['id'] ?? '', ENT_QUOTES, 'UTF-8') : '' ?>">
<div class="col-md-8">
<label class="form-label" for="chore_title">Title</label>
<input class="form-control" id="chore_title" required
value="<?= $editChore ? sanitizeInput($editChore['title'] ?? '') : '' ?>">
</div>
<div class="col-md-4">
<label class="form-label" for="chore_value">Reward (<?= sanitizeInput($currencySymbol) ?>)</label>
<input type="number" class="form-control" id="chore_value" min="0" step="0.01"
value="<?= $editChore ? htmlspecialchars((string) ($editChore['value'] ?? '0'), ENT_QUOTES, 'UTF-8') : '0' ?>">
</div>
<div class="col-12">
<label class="form-label" for="chore_description">Description</label>
<textarea class="form-control" id="chore_description" rows="2"><?= $editChore ? sanitizeInput($editChore['description'] ?? '') : '' ?></textarea>
</div>
<div class="col-md-8">
<label class="form-label" for="chore_image">Image URL (optional)</label>
<input class="form-control" id="chore_image" type="url" placeholder="https://…"
value="<?= $editChore ? sanitizeInput($editChore['image'] ?? '') : '' ?>">
</div>
<div class="col-md-4">
<label class="form-label" for="chore_due_date">Due date</label>
<input class="form-control" id="chore_due_date" type="date"
value="<?= $editChore ? sanitizeInput($editChore['due_date'] ?? '') : '' ?>">
</div>
<div class="col-md-6">
<label class="form-label" for="chore_schedule">Schedule</label>
<select class="form-select" id="chore_schedule">
<option value="once" <?= !$editChore || ($editChore['schedule'] ?? '') === CHORE_SCHEDULE_ONCE ? 'selected' : '' ?>>One-time</option>
<option value="recurring" <?= $editChore && ($editChore['schedule'] ?? '') === CHORE_SCHEDULE_RECURRING ? 'selected' : '' ?>>Recurring</option>
</select>
</div>
<div class="col-md-6" id="chore_recurrence_wrap">
<label class="form-label" for="chore_recurrence_days">Days until next due (recurring)</label>
<input type="number" class="form-control" id="chore_recurrence_days" min="1" value="<?= $editChore ? (int) ($editChore['recurrence_days'] ?? 7) : 7 ?>">
</div>
<div class="col-12">
<label class="form-label">Assign to</label>
<div class="d-flex flex-wrap gap-3">
<?php foreach ($people as $p): ?>
<?php
$pid = (string) ($p['id'] ?? '');
if ($pid === '') {
continue;
}
$checked = $editChore && is_array($editChore['assignee_ids'] ?? null) && in_array($pid, $editChore['assignee_ids'], true);
?>
<div class="form-check">
<input class="form-check-input chore-assignee" type="checkbox" value="<?= htmlspecialchars($pid, ENT_QUOTES, 'UTF-8') ?>" id="as_<?= htmlspecialchars($pid, ENT_QUOTES, 'UTF-8') ?>" <?= $checked ? 'checked' : '' ?>>
<label class="form-check-label" for="as_<?= htmlspecialchars($pid, ENT_QUOTES, 'UTF-8') ?>"><?= sanitizeInput($p['name'] ?? '') ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="chore_anyone_can_complete" <?= $editChore && !empty($editChore['anyone_can_complete']) ? 'checked' : '' ?>>
<label class="form-check-label" for="chore_anyone_can_complete">
Allow anyone to complete this chore using their own NFC/person token.
</label>
</div>
</div>
<div class="col-md-4">
<label class="form-label" for="chore_list_type">Checklist / list type</label>
<select class="form-select" id="chore_list_type">
<option value="checkbox" <?= $prefillList['type'] === 'checkbox' ? 'selected' : '' ?>>Checkbox list</option>
<option value="unordered" <?= $prefillList['type'] === 'unordered' ? 'selected' : '' ?>>Bullet list</option>
<option value="ordered" <?= $prefillList['type'] === 'ordered' ? 'selected' : '' ?>>Numbered list</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label" for="chore_list_items">List items (one per line)</label>
<p class="form-text small mb-1">Shown on the card as a numbered, bullet, or checkbox-style list. Assignees tick these off in real life; finishing the chore is the <strong>Submit for approval</strong> button on the card.</p>
<textarea class="form-control" id="chore_list_items" rows="4" placeholder="Take out recycling&#10;Wipe counters"><?= sanitizeInput(implode("\n", $prefillList['items'])) ?></textarea>
</div>
<div class="col-12 d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-primary"><?= $editChore ? 'Save changes' : 'Add chore' ?></button>
<?php if ($editChore): ?>
<button type="button" class="btn btn-outline-danger" id="choreDeleteBtn" data-id="<?= htmlspecialchars($editChore['id'] ?? '', ENT_QUOTES, 'UTF-8') ?>">Delete chore</button>
<?php endif; ?>
</div>
<div class="col-12">
<div class="alert d-none" id="choreFormFeedback" role="status"></div>
</div>
</form>
<?php if ($editChore): ?>
</div>
<?php else: ?>
</div>
</div>
<?php endif; ?>
</div>
<?php elseif ($activePerson !== null && $editChore !== null && !$editChoreAllowed): ?>
<div class="alert alert-warning mb-4">You cant edit this chore. Only the person who created it or a verified Head of household can.</div>
<?php elseif ($activePerson !== null && $editChore === null && !$canReview): ?>
<p class="text-muted small mb-4">Switch to a verified <strong>Head of household</strong> (top of the page) to add new chores.</p>
<?php endif; ?>
<h3 class="h5 mt-4">Active chores</h3>
<?php if (count($activeChores) === 0): ?>
<p class="text-muted">No active chores.</p>
<?php else: ?>
<div class="row g-3">
<?php foreach ($activeChores as $c): ?>
<?php
$cid = htmlspecialchars($c['id'] ?? '', ENT_QUOTES, 'UTF-8');
$assignees = $c['assignee_ids'] ?? [];
$isAssignee = $actorId !== '' && is_array($assignees) && in_array((string) $actorId, $assignees, true);
$canAnyoneSubmit = !empty($c['anyone_can_complete']);
$canSubmit = $isAssignee || ($activePerson !== null && $canAnyoneSubmit);
$canEdit = $activePerson && ((string) ($c['author_id'] ?? '') === (string) $actorId || $canReview);
$pending = is_array($c['pending_submission'] ?? null);
?>
<div class="col-12 col-md-6 col-xl-4">
<div class="card chore-card h-100">
<?php if (!empty($c['image']) && preg_match('#^https?://#i', (string) $c['image'])): ?>
<img src="<?= sanitizeInput($c['image']) ?>" class="card-img-top chore-thumb" alt="">
<?php endif; ?>
<div class="card-body">
<h4 class="h6 card-title"><?= sanitizeInput($c['title'] ?? '') ?></h4>
<p class="card-text small"><?= nl2br(sanitizeInput($c['description'] ?? '')) ?></p>
<p class="small text-muted mb-1">
Reward: <strong><?= sanitizeInput((string) ($c['value'] ?? 0)) ?> <?= sanitizeInput($currencySymbol) ?></strong>
<?php if (!empty($c['due_date'])): ?>
· Due <?= sanitizeInput($c['due_date']) ?>
<?php endif; ?>
</p>
<p class="small text-muted mb-2">
<?= ($c['schedule'] ?? '') === CHORE_SCHEDULE_RECURRING ? 'Recurring' : 'One-time' ?>
<?php if ($pending): ?>
<span class="badge text-bg-warning">Waiting for approval</span>
<?php endif; ?>
<?php if (!empty($c['anyone_can_complete'])): ?>
<span class="badge text-bg-info">Anyone can complete</span>
<?php endif; ?>
</p>
<?php if ($canReview): ?>
<?php $nfcMeta = is_array($c['nfc'] ?? null) ? $c['nfc'] : []; ?>
<p class="small text-muted mb-2">
NFC:
<?php if (!empty($nfcMeta['enabled'])): ?>
<span class="badge text-bg-success">Enabled</span>
<?php elseif (!empty($nfcMeta['token_hash'])): ?>
<span class="badge text-bg-secondary">Disabled</span>
<?php else: ?>
<span class="badge text-bg-light text-dark border">Not generated</span>
<?php endif; ?>
<?php if (!empty($nfcMeta['last_used_at'])): ?>
<span class="ms-1">Last used <?= sanitizeInput((string) $nfcMeta['last_used_at']) ?></span>
<?php endif; ?>
</p>
<p class="small text-muted mb-2">HoH setup: generate links, write each person link to their card/tag, label it, then test one scan.</p>
<?php endif; ?>
<?php if (!empty($c['lists']) && is_array($c['lists'])): ?>
<p class="small text-muted mb-1"><strong>Steps</strong> (reference — use the button below when youre done)</p>
<?php endif; ?>
<?php foreach (($c['lists'] ?? []) as $block): ?>
<?php if (!is_array($block) || empty($block['items'])) { continue; } ?>
<?php $t = $block['type'] ?? 'checkbox'; ?>
<?php if ($t === 'ordered'): ?>
<ol class="small mb-2"><?php foreach ($block['items'] as $it): ?><li><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ol>
<?php elseif ($t === 'unordered'): ?>
<ul class="small mb-2"><?php foreach ($block['items'] as $it): ?><li><?= sanitizeInput((string) $it) ?></li><?php endforeach; ?></ul>
<?php else: ?>
<ul class="list-unstyled small mb-2 chore-checklist-display"><?php foreach ($block['items'] as $it): ?><li><i class="fa-regular fa-square me-1" aria-hidden="true"></i><span><?= sanitizeInput((string) $it) ?></span></li><?php endforeach; ?></ul>
<?php endif; ?>
<?php endforeach; ?>
<div class="d-flex flex-wrap gap-2 mt-auto align-items-center">
<?php if ($canSubmit && !$pending): ?>
<button type="button" class="btn btn-sm btn-success btn-chore-submit" data-id="<?= $cid ?>">Submit for approval</button>
<?php elseif ($activePerson !== null && !$pending && !$isAssignee): ?>
<?php
$nmAssign = [];
foreach ($assignees as $aid) {
$nmAssign[] = $nameById[(string) $aid] ?? (string) $aid;
}
$who = sanitizeInput(implode(', ', $nmAssign));
?>
<span class="small text-muted">To submit when done: switch to <?= $who !== '' ? $who : 'an assignee' ?> at the top.</span>
<?php endif; ?>
<?php if ($canEdit): ?>
<a class="btn btn-sm btn-outline-primary" href="?tab=chores&amp;edit=<?= $cid ?>">Edit</a>
<?php endif; ?>
<?php if ($canReview): ?>
<button type="button" class="btn btn-sm btn-outline-dark btn-chore-nfc-generate" data-id="<?= $cid ?>">Generate NFC links</button>
<button type="button" class="btn btn-sm btn-outline-secondary btn-chore-nfc-toggle" data-id="<?= $cid ?>" data-enable="<?= !empty(($c['nfc']['enabled'] ?? false)) ? '0' : '1' ?>">
<?= !empty(($c['nfc']['enabled'] ?? false)) ? 'Disable NFC' : 'Enable NFC' ?>
</button>
<?php endif; ?>
</div>
<?php if ($canReview): ?>
<div class="alert d-none mt-2 p-2 small chore-nfc-feedback" id="chore_nfc_feedback_<?= $cid ?>"></div>
<div class="d-none mt-2 chore-nfc-links" id="chore_nfc_links_<?= $cid ?>"></div>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (count($completedChores) > 0): ?>
<details class="mt-4">
<summary class="h6">Completed chores (<?= count($completedChores) ?>)</summary>
<ul class="list-group mt-2">
<?php foreach ($completedChores as $c): ?>
<li class="list-group-item text-muted"><?= sanitizeInput($c['title'] ?? '') ?></li>
<?php endforeach; ?>
</ul>
</details>
<?php endif; ?>
</div>