false, 'error' => 'Method not allowed'], 405); } $people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json'))); assertHoHCanManagePeople($people); $body = readJsonBody(); $id = isset($body['id']) ? trim((string) $body['id']) : ''; if ($id === '') { sendJson(['success' => false, 'error' => 'id is required'], 400); } $idx = null; foreach ($people as $i => $p) { if (($p['id'] ?? '') === $id) { $idx = $i; break; } } if ($idx === null) { sendJson(['success' => false, 'error' => 'Person not found'], 404); } if (isset($body['name'])) { $name = trim((string) $body['name']); if ($name === '') { sendJson(['success' => false, 'error' => 'Name cannot be empty'], 400); } $people[$idx]['name'] = $name; } if (array_key_exists('icon', $body)) { $people[$idx]['icon'] = trim((string) $body['icon']); } if (array_key_exists('description', $body)) { $people[$idx]['description'] = trim((string) $body['description']); } if (array_key_exists('birthday', $body)) { $birthday = trim((string) $body['birthday']); if ($birthday !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) { sendJson(['success' => false, 'error' => 'birthday must be YYYY-MM-DD'], 400); } $people[$idx]['birthday'] = $birthday; } if (array_key_exists('favoriteColor', $body)) { $c = trim((string) $body['favoriteColor']); if ($c !== '' && !preg_match('/^#[0-9A-Fa-f]{6}$/', $c)) { sendJson(['success' => false, 'error' => 'favoriteColor must be a #RRGGBB value'], 400); } if ($c !== '') { $people[$idx]['favoriteColor'] = $c; } } if (array_key_exists('role', $body)) { $newRole = (string) $body['role']; $allowedRoles = [ROLE_HEAD, ROLE_ADULT, ROLE_CHILD]; if (!in_array($newRole, $allowedRoles, true)) { sendJson(['success' => false, 'error' => 'Invalid role'], 400); } $wasHead = ($people[$idx]['role'] ?? '') === ROLE_HEAD; if ($newRole === ROLE_HEAD && !$wasHead) { $pin = isset($body['pin']) ? (string) $body['pin'] : ''; if (strlen($pin) < 4) { sendJson(['success' => false, 'error' => 'PIN must be at least 4 characters when promoting to Head of Household'], 400); } $people[$idx]['pin_hash'] = password_hash($pin, PASSWORD_DEFAULT); } if ($newRole !== ROLE_HEAD && $wasHead) { $people[$idx]['pin_hash'] = null; } $people[$idx]['role'] = $newRole; } $headCount = 0; foreach ($people as $p) { if (($p['role'] ?? '') === ROLE_HEAD) { $headCount++; } } if ($headCount < 1) { sendJson(['success' => false, 'error' => 'At least one Head of Household is required'], 400); } if (!writeJsonFile('people.json', $people)) { sendJson(['success' => false, 'error' => 'Failed to save people'], 500); } $safe = $people[$idx]; unset($safe['pin_hash']); sendJson(['success' => true, 'person' => $safe]);