'; } echo ''; echo '' . $safeTitle . '' . $meta; echo ''; echo '
'; echo '
'; echo '
' . $safeTitle . '
'; echo '

' . $safeMessage . '

'; if ($redirectUrl !== '' && !$autoRedirect) { echo 'Open Family Hub'; } echo '
'; 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);