false, 'error' => 'Method not allowed'], 405); } $people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json'))); $actor = requireActivePerson($people); $body = readJsonBody(); $rawChores = normalizeChoresList(readJsonFile('chores.json')); $chores = migrateAllChores($rawChores, $people); $id = isset($body['id']) ? trim((string) $body['id']) : ''; if ($id !== '') { $idx = findChoreIndexById($chores, $id); if ($idx === null) { sendJson(['success' => false, 'error' => 'Chore not found'], 404); } $existing = $chores[$idx]; $isAuthor = ($existing['author_id'] ?? '') === ($actor['id'] ?? ''); $isHoH = ($actor['role'] ?? '') === ROLE_HEAD && isHohVerified(); if (!$isAuthor && !$isHoH) { sendJson(['success' => false, 'error' => 'You cannot edit this chore'], 403); } $row = $existing; } else { if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) { sendJson(['success' => false, 'error' => 'Only a verified Head of household can create chores'], 403); } $row = [ 'id' => bin2hex(random_bytes(8)), 'author_id' => (string) ($actor['id'] ?? ''), 'pending_submission' => null, 'status' => 'active', 'anyone_can_complete' => false, 'nfc' => normalizeChoreNfcMeta(null), ]; $idx = null; } $title = isset($body['title']) ? trim((string) $body['title']) : ''; if ($title === '') { sendJson(['success' => false, 'error' => 'Title is required'], 400); } $assigneeIds = $body['assignee_ids'] ?? []; if (!is_array($assigneeIds)) { $assigneeIds = []; } $assigneeIdsClean = []; foreach ($assigneeIds as $aid) { $aid = trim((string) $aid); if ($aid !== '') { $assigneeIdsClean[] = $aid; } } $assigneeIdsClean = array_values(array_unique($assigneeIdsClean)); if (!choreAssigneeIdsValid($assigneeIdsClean, $people)) { sendJson(['success' => false, 'error' => 'One or more assignees are invalid'], 400); } $row['title'] = $title; $row['description'] = isset($body['description']) ? trim((string) $body['description']) : ''; $row['image'] = isset($body['image']) ? trim((string) $body['image']) : ''; $row['lists'] = normalizeChoreLists($body['lists'] ?? []); $row['assignee_ids'] = $assigneeIdsClean; $row['anyone_can_complete'] = !empty($body['anyone_can_complete']); $val = isset($body['value']) ? $body['value'] : 0; $row['value'] = is_numeric($val) ? max(0.0, (float) $val) : 0.0; $due = isset($body['due_date']) ? trim((string) $body['due_date']) : ''; if ($due !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $due)) { sendJson(['success' => false, 'error' => 'due_date must be YYYY-MM-DD'], 400); } $row['due_date'] = $due; $sched = isset($body['schedule']) ? (string) $body['schedule'] : CHORE_SCHEDULE_ONCE; $row['schedule'] = $sched === CHORE_SCHEDULE_RECURRING ? CHORE_SCHEDULE_RECURRING : CHORE_SCHEDULE_ONCE; $rd = isset($body['recurrence_days']) ? (int) $body['recurrence_days'] : ($row['recurrence_days'] ?? 7); $row['recurrence_days'] = max(1, $rd); if ($idx === null) { $chores[] = migrateLegacyChoreRow($row, $people); } else { $chores[$idx] = migrateLegacyChoreRow($row, $people); } if (!writeJsonFile('chores.json', $chores)) { sendJson(['success' => false, 'error' => 'Failed to save chores'], 500); } sendJson(['success' => true, 'chore' => $row]);