- 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.
124 lines
5.6 KiB
PHP
124 lines
5.6 KiB
PHP
<?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';
|
|
require_once __DIR__ . '/../includes/family_settings.php';
|
|
|
|
function renderNfcResultPage(string $title, string $message, bool $success, string $redirectUrl = '', bool $autoRedirect = false): void {
|
|
$safeTitle = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
|
|
$safeMessage = htmlspecialchars($message, ENT_QUOTES, 'UTF-8');
|
|
$safeRedirect = htmlspecialchars($redirectUrl, ENT_QUOTES, 'UTF-8');
|
|
$statusClass = $success ? 'success' : 'danger';
|
|
$meta = '';
|
|
if ($autoRedirect && $redirectUrl !== '') {
|
|
$meta = '<meta http-equiv="refresh" content="0;url=' . $safeRedirect . '">';
|
|
}
|
|
echo '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">';
|
|
echo '<title>' . $safeTitle . '</title>' . $meta;
|
|
echo '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"></head><body class="bg-light">';
|
|
echo '<main class="container py-4"><div class="row justify-content-center"><div class="col-12 col-md-8 col-lg-6">';
|
|
echo '<div class="card border-' . $statusClass . '">';
|
|
echo '<div class="card-header bg-' . $statusClass . '-subtle"><strong>' . $safeTitle . '</strong></div>';
|
|
echo '<div class="card-body"><p class="mb-3">' . $safeMessage . '</p>';
|
|
if ($redirectUrl !== '' && !$autoRedirect) {
|
|
echo '<a class="btn btn-primary" href="' . $safeRedirect . '">Open Family Hub</a>';
|
|
}
|
|
echo '</div></div></div></div></main></body></html>';
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
|
http_response_code(405);
|
|
renderNfcResultPage('Method not allowed', 'This NFC URL only supports GET requests.', false);
|
|
}
|
|
|
|
$familySettings = loadFamilySettings();
|
|
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
|
|
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
|
$chores = migrateAllChores($rawChores, $people);
|
|
|
|
$id = isset($_GET['id']) ? trim((string) $_GET['id']) : '';
|
|
$token = isset($_GET['token']) ? trim((string) $_GET['token']) : '';
|
|
$personToken = isset($_GET['person_token']) ? trim((string) $_GET['person_token']) : '';
|
|
$hubUrl = familyHubAppUrl($familySettings);
|
|
$redirectUrl = $hubUrl . '/?tab=chores';
|
|
$showConfirmation = !empty($familySettings['nfc_show_confirmation']);
|
|
|
|
if ($id === '' || $token === '' || $personToken === '') {
|
|
http_response_code(400);
|
|
renderNfcResultPage('Invalid NFC link', 'Required token information is missing from this NFC URL.', false, $redirectUrl, false);
|
|
}
|
|
|
|
$personMatch = findPersonBySubmitToken($people, $personToken);
|
|
if ($personMatch === null) {
|
|
http_response_code(403);
|
|
renderNfcResultPage('Invalid submitter token', 'This NFC submitter token is not valid.', false, $redirectUrl, false);
|
|
}
|
|
|
|
$submitter = $personMatch['person'];
|
|
$submitterId = (string) ($submitter['id'] ?? '');
|
|
$submitterName = (string) ($submitter['name'] ?? 'Unknown');
|
|
|
|
$idx = findChoreIndexById($chores, $id);
|
|
if ($idx === null) {
|
|
http_response_code(404);
|
|
renderNfcResultPage('Chore not found', 'This chore no longer exists.', false, $redirectUrl, false);
|
|
}
|
|
|
|
$row = $chores[$idx];
|
|
if (($row['status'] ?? '') !== 'active') {
|
|
http_response_code(400);
|
|
renderNfcResultPage('Chore inactive', 'This chore is not currently active.', false, $redirectUrl, false);
|
|
}
|
|
|
|
$nfcMeta = normalizeChoreNfcMeta($row['nfc'] ?? null);
|
|
if (empty($nfcMeta['enabled']) || !validateTokenHash($token, (string) ($nfcMeta['token_hash'] ?? ''))) {
|
|
http_response_code(403);
|
|
renderNfcResultPage('Invalid chore token', 'This NFC chore token is invalid or disabled.', false, $redirectUrl, false);
|
|
}
|
|
|
|
if (!empty($row['pending_submission']) && is_array($row['pending_submission'])) {
|
|
http_response_code(400);
|
|
renderNfcResultPage('Already pending review', 'This chore is already waiting for Head of household approval.', false, $redirectUrl, false);
|
|
}
|
|
|
|
$anyoneCanComplete = !empty($row['anyone_can_complete']);
|
|
$assignees = $row['assignee_ids'] ?? [];
|
|
if (!$anyoneCanComplete) {
|
|
if (!is_array($assignees) || !in_array($submitterId, $assignees, true)) {
|
|
http_response_code(403);
|
|
renderNfcResultPage('Not assigned', 'Only assigned family members can submit this chore.', false, $redirectUrl, false);
|
|
}
|
|
}
|
|
|
|
$cooldownSeconds = (int) ($familySettings['nfc_scan_cooldown_seconds'] ?? 0);
|
|
if ($cooldownSeconds > 0) {
|
|
$lastUsedAt = trim((string) ($nfcMeta['last_used_at'] ?? ''));
|
|
if ($lastUsedAt !== '') {
|
|
$lastTs = strtotime($lastUsedAt);
|
|
if ($lastTs !== false && (time() - $lastTs) < $cooldownSeconds) {
|
|
http_response_code(429);
|
|
renderNfcResultPage('Please wait', 'This tag was just scanned. Try again in a few moments.', false, $redirectUrl, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
$row['pending_submission'] = [
|
|
'submitted_at' => gmdate('c'),
|
|
'submitted_by' => $submitterId,
|
|
'note' => 'Submitted via NFC by ' . $submitterName,
|
|
];
|
|
$nfcMeta['last_used_at'] = gmdate('c');
|
|
$nfcMeta['last_used_ip'] = requestClientAddress();
|
|
$row['nfc'] = $nfcMeta;
|
|
$chores[$idx] = migrateLegacyChoreRow($row, $people);
|
|
|
|
if (!writeJsonFile('chores.json', $chores)) {
|
|
http_response_code(500);
|
|
renderNfcResultPage('Save failed', 'Could not save this submission. Please try again.', false, $redirectUrl, false);
|
|
}
|
|
|
|
renderNfcResultPage('Submitted for approval', 'Submitted as ' . $submitterName . '. A Head of household will approve before credit is applied.', true, $redirectUrl, !$showConfirmation);
|