First pass of features
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
|
||||
$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 delete this chore'], 403);
|
||||
}
|
||||
|
||||
array_splice($chores, $idx, 1);
|
||||
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chores'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can review chores'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
$decision = isset($body['decision']) ? trim((string) $body['decision']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
if (!in_array($decision, ['approve', 'reject'], true)) {
|
||||
sendJson(['success' => false, 'error' => 'decision must be approve or reject'], 400);
|
||||
}
|
||||
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
|
||||
$idx = findChoreIndexById($chores, $id);
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Chore not found'], 404);
|
||||
}
|
||||
|
||||
$row = $chores[$idx];
|
||||
$pending = $row['pending_submission'] ?? null;
|
||||
if (!is_array($pending)) {
|
||||
sendJson(['success' => false, 'error' => 'Nothing is waiting for approval on this chore'], 400);
|
||||
}
|
||||
|
||||
if ($decision === 'reject') {
|
||||
$row['pending_submission'] = null;
|
||||
$chores[$idx] = migrateLegacyChoreRow($row, $people);
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chores'], 500);
|
||||
}
|
||||
sendJson(['success' => true]);
|
||||
}
|
||||
|
||||
$assignees = $row['assignee_ids'] ?? [];
|
||||
if (!is_array($assignees)) {
|
||||
$assignees = [];
|
||||
}
|
||||
$value = (float) ($row['value'] ?? 0);
|
||||
$n = count($assignees);
|
||||
$share = $n > 0 ? round($value / $n, 2) : 0.0;
|
||||
|
||||
$row['pending_submission'] = null;
|
||||
|
||||
if (($row['schedule'] ?? CHORE_SCHEDULE_ONCE) === CHORE_SCHEDULE_RECURRING) {
|
||||
$days = (int) ($row['recurrence_days'] ?? 7);
|
||||
$days = max(1, $days);
|
||||
$row['due_date'] = gmdate('Y-m-d', time() + ($days * 86400));
|
||||
$row['status'] = 'active';
|
||||
} else {
|
||||
$row['status'] = 'completed';
|
||||
}
|
||||
|
||||
$chores[$idx] = migrateLegacyChoreRow($row, $people);
|
||||
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chores'], 500);
|
||||
}
|
||||
|
||||
foreach ($people as $pi => $p) {
|
||||
$pid = (string) ($p['id'] ?? '');
|
||||
if ($pid === '' || !in_array($pid, $assignees, true)) {
|
||||
continue;
|
||||
}
|
||||
$bal = $p['currency_balance'] ?? 0;
|
||||
$people[$pi]['currency_balance'] = (is_numeric($bal) ? (float) $bal : 0.0) + $share;
|
||||
}
|
||||
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people balances'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'credited_each' => $share]);
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = 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',
|
||||
];
|
||||
$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;
|
||||
|
||||
$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]);
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/chore_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
$actorId = (string) ($actor['id'] ?? '');
|
||||
if ($actorId === '') {
|
||||
sendJson(['success' => false, 'error' => 'Invalid session'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$rawChores = normalizeChoresList(readJsonFile('chores.json'));
|
||||
$chores = migrateAllChores($rawChores, $people);
|
||||
|
||||
$idx = findChoreIndexById($chores, $id);
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Chore not found'], 404);
|
||||
}
|
||||
|
||||
$row = $chores[$idx];
|
||||
if (($row['status'] ?? '') !== 'active') {
|
||||
sendJson(['success' => false, 'error' => 'This chore is not active'], 400);
|
||||
}
|
||||
|
||||
$assignees = $row['assignee_ids'] ?? [];
|
||||
if (!is_array($assignees) || !in_array($actorId, $assignees, true)) {
|
||||
sendJson(['success' => false, 'error' => 'Only assignees can mark this chore complete'], 403);
|
||||
}
|
||||
|
||||
if (!empty($row['pending_submission']) && is_array($row['pending_submission'])) {
|
||||
sendJson(['success' => false, 'error' => 'This chore is already waiting for approval'], 400);
|
||||
}
|
||||
|
||||
$note = isset($body['note']) ? trim((string) $body['note']) : '';
|
||||
|
||||
$row['pending_submission'] = [
|
||||
'submitted_at' => gmdate('c'),
|
||||
'submitted_by' => $actorId,
|
||||
'note' => $note,
|
||||
];
|
||||
|
||||
$chores[$idx] = migrateLegacyChoreRow($row, $people);
|
||||
|
||||
if (!writeJsonFile('chores.json', $chores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save chores'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/expense_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can record expenses'], 403);
|
||||
}
|
||||
|
||||
$creatorId = (string) ($actor['id'] ?? '');
|
||||
|
||||
$body = readJsonBody();
|
||||
$title = isset($body['title']) ? trim((string) $body['title']) : '';
|
||||
$description = isset($body['description']) ? trim((string) $body['description']) : '';
|
||||
$date = isset($body['date']) ? trim((string) $body['date']) : '';
|
||||
$assigneeId = isset($body['assignee_id']) ? trim((string) $body['assignee_id']) : '';
|
||||
|
||||
if ($title === '') {
|
||||
sendJson(['success' => false, 'error' => 'Title is required'], 400);
|
||||
}
|
||||
if ($date === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
sendJson(['success' => false, 'error' => 'date must be YYYY-MM-DD'], 400);
|
||||
}
|
||||
if ($assigneeId === '') {
|
||||
sendJson(['success' => false, 'error' => 'assignee_id is required'], 400);
|
||||
}
|
||||
|
||||
$valRaw = $body['value'] ?? null;
|
||||
if (!is_numeric($valRaw)) {
|
||||
sendJson(['success' => false, 'error' => 'value must be a number'], 400);
|
||||
}
|
||||
$value = round((float) $valRaw, 2);
|
||||
if ($value <= 0) {
|
||||
sendJson(['success' => false, 'error' => 'value must be greater than zero'], 400);
|
||||
}
|
||||
|
||||
$targetIdx = null;
|
||||
foreach ($people as $i => $p) {
|
||||
if (($p['id'] ?? '') === $assigneeId) {
|
||||
$targetIdx = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($targetIdx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Assignee not found'], 400);
|
||||
}
|
||||
|
||||
$bal = $people[$targetIdx]['currency_balance'] ?? 0;
|
||||
$bal = is_numeric($bal) ? (float) $bal : 0.0;
|
||||
if ($bal < $value) {
|
||||
sendJson([
|
||||
'success' => false,
|
||||
'error' => 'Insufficient balance: has ' . number_format($bal, 2, '.', '') . ', expense is ' . number_format($value, 2, '.', ''),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$people[$targetIdx]['currency_balance'] = round($bal - $value, 2);
|
||||
|
||||
$expense = [
|
||||
'id' => bin2hex(random_bytes(8)),
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'date' => $date,
|
||||
'value' => $value,
|
||||
'assignee_id' => $assigneeId,
|
||||
'created_at' => gmdate('c'),
|
||||
'created_by' => $creatorId,
|
||||
];
|
||||
|
||||
$expenses = normalizeExpensesList(readJsonFile('expenses.json'));
|
||||
$expenses[] = $expense;
|
||||
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to update balance'], 500);
|
||||
}
|
||||
if (!writeJsonFile('expenses.json', $expenses)) {
|
||||
$people[$targetIdx]['currency_balance'] = $bal;
|
||||
writeJsonFile('people.json', $people);
|
||||
sendJson(['success' => false, 'error' => 'Failed to save expense; balance was not changed'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'expense' => $expense, 'new_balance' => $people[$targetIdx]['currency_balance']]);
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/family_settings.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
if (count($people) > 0) {
|
||||
assertHoHCanManagePeople($people);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$merged = loadFamilySettings();
|
||||
|
||||
$allowedPermanence = ['permanent', 'weekly', 'biweekly', 'monthly', 'quarterly', 'yearly'];
|
||||
|
||||
if (isset($body['currency_symbol'])) {
|
||||
$merged['currency_symbol'] = trim((string) $body['currency_symbol']);
|
||||
}
|
||||
if (isset($body['currency_name'])) {
|
||||
$merged['currency_name'] = trim((string) $body['currency_name']);
|
||||
}
|
||||
if (isset($body['currency_permanence'])) {
|
||||
$p = (string) $body['currency_permanence'];
|
||||
if (!in_array($p, $allowedPermanence, true)) {
|
||||
sendJson(['success' => false, 'error' => 'Invalid currency_permanence'], 400);
|
||||
}
|
||||
$merged['currency_permanence'] = $p;
|
||||
}
|
||||
if (isset($body['timezone'])) {
|
||||
$merged['timezone'] = trim((string) $body['timezone']);
|
||||
}
|
||||
if (isset($body['week_starts_on'])) {
|
||||
$w = (int) $body['week_starts_on'];
|
||||
if ($w < 0 || $w > 6) {
|
||||
sendJson(['success' => false, 'error' => 'week_starts_on must be 0–6'], 400);
|
||||
}
|
||||
$merged['week_starts_on'] = $w;
|
||||
}
|
||||
|
||||
if (!writeJsonFile('family_settings.json', $merged)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save settings'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'settings' => $merged]);
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
migrateLegacyGroceriesIfNeeded();
|
||||
ensureDefaultGroceryStore();
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
if (count($stores) === 0) {
|
||||
sendJson(['success' => false, 'error' => 'No stores available'], 400);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$storeId = isset($body['storeId']) ? trim((string) $body['storeId']) : '';
|
||||
if ($storeId === '' || findStoreById($stores, $storeId) === null) {
|
||||
sendJson(['success' => false, 'error' => 'Invalid store'], 400);
|
||||
}
|
||||
|
||||
$name = isset($body['name']) ? trim((string) $body['name']) : '';
|
||||
$description = isset($body['description']) ? trim((string) $body['description']) : '';
|
||||
$size = isset($body['size']) ? trim((string) $body['size']) : '';
|
||||
$quantity = isset($body['quantity']) ? trim((string) $body['quantity']) : '1';
|
||||
$price = isset($body['price']) ? trim((string) $body['price']) : '';
|
||||
$image = isset($body['image']) ? trim((string) $body['image']) : '';
|
||||
$source = isset($body['source']) ? (string) $body['source'] : 'manual';
|
||||
if (!in_array($source, ['manual', 'meal_plan', 'pending_review'], true)) {
|
||||
$source = 'manual';
|
||||
}
|
||||
$recurring = isset($body['recurringIntervalDays']) ? max(0, (int) $body['recurringIntervalDays']) : 0;
|
||||
|
||||
$catalogPickId = isset($body['catalogPickId']) ? trim((string) $body['catalogPickId']) : '';
|
||||
$catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
|
||||
|
||||
if ($catalogPickId !== '') {
|
||||
$pick = findCatalogById($catalog, $catalogPickId);
|
||||
if ($pick !== null && (string) ($pick['storeId'] ?? '') === $storeId) {
|
||||
if ($name === '') {
|
||||
$name = trim((string) ($pick['name'] ?? ''));
|
||||
}
|
||||
if ($description === '') {
|
||||
$description = trim((string) ($pick['description'] ?? ''));
|
||||
}
|
||||
if ($size === '') {
|
||||
$size = trim((string) ($pick['defaultSize'] ?? ''));
|
||||
}
|
||||
if ($image === '') {
|
||||
$image = trim((string) ($pick['defaultImage'] ?? ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = groceryAppendShoppingLine(
|
||||
$stores,
|
||||
$storeId,
|
||||
$name,
|
||||
$description,
|
||||
$size,
|
||||
$quantity,
|
||||
$price,
|
||||
$image,
|
||||
$source,
|
||||
$recurring,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
if (!$result['ok']) {
|
||||
sendJson(['success' => false, 'error' => $result['error'] ?? 'Failed'], 400);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'item' => $result['item']]);
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
$storeId = isset($body['storeId']) ? trim((string) $body['storeId']) : '';
|
||||
$itemId = isset($body['itemId']) ? trim((string) $body['itemId']) : '';
|
||||
if ($storeId === '' || $itemId === '') {
|
||||
sendJson(['success' => false, 'error' => 'storeId and itemId are required'], 400);
|
||||
}
|
||||
|
||||
$lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
|
||||
$items = $lists['byStore'][$storeId] ?? [];
|
||||
$next = [];
|
||||
foreach ($items as $it) {
|
||||
if (($it['id'] ?? '') === $itemId) {
|
||||
continue;
|
||||
}
|
||||
$next[] = $it;
|
||||
}
|
||||
if (count($next) === count($items)) {
|
||||
sendJson(['success' => false, 'error' => 'Item not found'], 404);
|
||||
}
|
||||
|
||||
$lists['byStore'][$storeId] = $next;
|
||||
|
||||
if (!writeJsonFile('grocery_lists.json', $lists)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save grocery list'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
$storeId = isset($body['storeId']) ? trim((string) $body['storeId']) : '';
|
||||
$itemId = isset($body['itemId']) ? trim((string) $body['itemId']) : '';
|
||||
$purchased = !empty($body['purchased']);
|
||||
if ($storeId === '' || $itemId === '') {
|
||||
sendJson(['success' => false, 'error' => 'storeId and itemId are required'], 400);
|
||||
}
|
||||
|
||||
$lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
|
||||
$items = $lists['byStore'][$storeId] ?? [];
|
||||
$idx = null;
|
||||
foreach ($items as $i => $it) {
|
||||
if (($it['id'] ?? '') === $itemId) {
|
||||
$idx = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Item not found'], 404);
|
||||
}
|
||||
|
||||
$row = $items[$idx];
|
||||
$catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
|
||||
|
||||
if ($purchased) {
|
||||
$row['status'] = 'purchased';
|
||||
$row['purchasedAt'] = gmdate('c');
|
||||
$cid = $row['catalogId'] ?? null;
|
||||
if (is_string($cid) && $cid !== '') {
|
||||
foreach ($catalog as $ci => $c) {
|
||||
if (($c['id'] ?? '') === $cid) {
|
||||
$catalog[$ci]['lastPurchaseAt'] = gmdate('c');
|
||||
$days = max(0, (int) ($row['recurringIntervalDays'] ?? 0));
|
||||
if ($days > 0) {
|
||||
$catalog[$ci]['recurringIntervalDays'] = $days;
|
||||
$catalog[$ci]['nextDueDate'] = gmdate('Y-m-d', strtotime('+' . $days . ' days'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$row['status'] = 'active';
|
||||
$row['purchasedAt'] = null;
|
||||
}
|
||||
|
||||
$row = normalizeGroceryLineItem($row);
|
||||
$items[$idx] = $row;
|
||||
$lists['byStore'][$storeId] = $items;
|
||||
|
||||
if (!writeJsonFile('grocery_catalog.json', $catalog)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save catalog'], 500);
|
||||
}
|
||||
if (!writeJsonFile('grocery_lists.json', $lists)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save grocery list'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can approve pending items'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$storeId = isset($body['storeId']) ? trim((string) $body['storeId']) : '';
|
||||
$itemId = isset($body['itemId']) ? trim((string) $body['itemId']) : '';
|
||||
if ($storeId === '' || $itemId === '') {
|
||||
sendJson(['success' => false, 'error' => 'storeId and itemId are required'], 400);
|
||||
}
|
||||
|
||||
$lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
|
||||
$items = $lists['byStore'][$storeId] ?? [];
|
||||
$idx = null;
|
||||
foreach ($items as $i => $it) {
|
||||
if (($it['id'] ?? '') === $itemId) {
|
||||
$idx = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Item not found'], 404);
|
||||
}
|
||||
|
||||
$row = $items[$idx];
|
||||
if (($row['status'] ?? '') !== 'pending_review') {
|
||||
sendJson(['success' => false, 'error' => 'Item is not pending review'], 400);
|
||||
}
|
||||
|
||||
$row['status'] = 'active';
|
||||
$row = normalizeGroceryLineItem($row);
|
||||
$items[$idx] = $row;
|
||||
$lists['byStore'][$storeId] = $items;
|
||||
|
||||
if (!writeJsonFile('grocery_lists.json', $lists)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save grocery list'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/meal_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
$actorId = (string) ($actor['id'] ?? '');
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$meals = migrateLegacyMealsList(normalizeMealsList(readJsonFile('meals.json')));
|
||||
$idx = findMealIndexById($meals, $id);
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Meal not found'], 404);
|
||||
}
|
||||
|
||||
$existing = $meals[$idx];
|
||||
$isAuthor = ($existing['author_id'] ?? '') === $actorId;
|
||||
$isHoH = ($actor['role'] ?? '') === ROLE_HEAD && isHohVerified();
|
||||
if (!$isAuthor && !$isHoH) {
|
||||
sendJson(['success' => false, 'error' => 'You cannot delete this meal'], 403);
|
||||
}
|
||||
|
||||
array_splice($meals, $idx, 1);
|
||||
|
||||
if (!writeJsonFile('meals.json', $meals)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save meals'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/meal_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
migrateLegacyGroceriesIfNeeded();
|
||||
ensureDefaultGroceryStore();
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
|
||||
$body = readJsonBody();
|
||||
$mealId = isset($body['mealId']) ? trim((string) $body['mealId']) : '';
|
||||
$ingredient = isset($body['ingredient']) ? trim((string) $body['ingredient']) : '';
|
||||
$storeId = isset($body['storeId']) ? trim((string) $body['storeId']) : '';
|
||||
|
||||
if ($ingredient === '') {
|
||||
sendJson(['success' => false, 'error' => 'ingredient is required'], 400);
|
||||
}
|
||||
if (count($stores) === 0) {
|
||||
sendJson(['success' => false, 'error' => 'Add a grocery store first'], 400);
|
||||
}
|
||||
|
||||
$meals = migrateLegacyMealsList(normalizeMealsList(readJsonFile('meals.json')));
|
||||
$meal = $mealId !== '' ? findMealById($meals, $mealId) : null;
|
||||
$mealTitle = $meal !== null ? (string) ($meal['title'] ?? '') : '';
|
||||
|
||||
if ($storeId === '' || findStoreById($stores, $storeId) === null) {
|
||||
$storeId = groceryFirstStoreId($stores);
|
||||
}
|
||||
if ($storeId === '') {
|
||||
sendJson(['success' => false, 'error' => 'No valid store'], 400);
|
||||
}
|
||||
|
||||
$res = groceryAppendShoppingLine(
|
||||
$stores,
|
||||
$storeId,
|
||||
$ingredient,
|
||||
$mealTitle !== '' ? 'From meal: ' . $mealTitle : 'From meal ingredient',
|
||||
'',
|
||||
'1',
|
||||
'',
|
||||
'',
|
||||
'meal_plan',
|
||||
0,
|
||||
$mealId !== '' ? $mealId : null,
|
||||
$mealTitle !== '' ? $mealTitle : null
|
||||
);
|
||||
|
||||
if (!$res['ok']) {
|
||||
sendJson(['success' => false, 'error' => $res['error'] ?? 'Failed'], 400);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'item' => $res['item']]);
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/meal_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
migrateLegacyGroceriesIfNeeded();
|
||||
ensureDefaultGroceryStore();
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
|
||||
$body = readJsonBody();
|
||||
$weekStart = isset($body['weekStart']) ? trim((string) $body['weekStart']) : '';
|
||||
$day = isset($body['day']) ? (int) $body['day'] : -1;
|
||||
$mealType = isset($body['mealType']) ? trim((string) $body['mealType']) : '';
|
||||
$mealId = isset($body['mealId']) ? trim((string) $body['mealId']) : '';
|
||||
$mealId = $mealId === '' ? null : $mealId;
|
||||
$pushGrocery = array_key_exists('pushGrocery', $body) ? !empty($body['pushGrocery']) : true;
|
||||
|
||||
if ($weekStart === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $weekStart)) {
|
||||
sendJson(['success' => false, 'error' => 'weekStart must be YYYY-MM-DD'], 400);
|
||||
}
|
||||
if ($day < 0 || $day > 6) {
|
||||
sendJson(['success' => false, 'error' => 'day must be 0–6 (0 = week start day)'], 400);
|
||||
}
|
||||
if (!in_array($mealType, mealSlotTypes(), true)) {
|
||||
sendJson(['success' => false, 'error' => 'Invalid mealType'], 400);
|
||||
}
|
||||
|
||||
$plan = normalizeMealPlan(readJsonFile('meal_plans.json'));
|
||||
if ($plan['weekStart'] !== $weekStart) {
|
||||
sendJson(['success' => false, 'error' => 'That week is not the active plan. Set the week first (Head of household).'], 400);
|
||||
}
|
||||
|
||||
$key = (string) $day;
|
||||
if (!isset($plan['slots'][$key])) {
|
||||
$plan['slots'][$key] = [
|
||||
MEAL_SLOT_BREAKFAST => null,
|
||||
MEAL_SLOT_LUNCH => null,
|
||||
MEAL_SLOT_DINNER => null,
|
||||
];
|
||||
}
|
||||
|
||||
$meals = migrateLegacyMealsList(normalizeMealsList(readJsonFile('meals.json')));
|
||||
$pushed = 0;
|
||||
if ($mealId !== null) {
|
||||
$meal = findMealById($meals, $mealId);
|
||||
if ($meal === null) {
|
||||
sendJson(['success' => false, 'error' => 'Meal not found'], 404);
|
||||
}
|
||||
$plan['slots'][$key][$mealType] = $mealId;
|
||||
if ($pushGrocery && count($stores) > 0) {
|
||||
$pushed = pushMealItemsToGrocery(normalizeMealRow($meal), $stores);
|
||||
}
|
||||
} else {
|
||||
$plan['slots'][$key][$mealType] = null;
|
||||
}
|
||||
|
||||
if (!writeJsonFile('meal_plans.json', $plan)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save meal plan'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'plan' => $plan, 'groceryLinesAdded' => $pushed]);
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/meal_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can change the planning week'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$weekStart = isset($body['weekStart']) ? trim((string) $body['weekStart']) : '';
|
||||
if ($weekStart === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $weekStart)) {
|
||||
sendJson(['success' => false, 'error' => 'weekStart must be YYYY-MM-DD (use the Monday of the week)'], 400);
|
||||
}
|
||||
|
||||
$plan = [
|
||||
'weekStart' => $weekStart,
|
||||
'slots' => mealDefaultEmptySlots(),
|
||||
];
|
||||
|
||||
if (!writeJsonFile('meal_plans.json', $plan)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save meal plan'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'plan' => $plan]);
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/meal_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
requireActivePerson($people);
|
||||
|
||||
migrateLegacyGroceriesIfNeeded();
|
||||
ensureDefaultGroceryStore();
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$meals = migrateLegacyMealsList(normalizeMealsList(readJsonFile('meals.json')));
|
||||
$meal = findMealById($meals, $id);
|
||||
if ($meal === null) {
|
||||
sendJson(['success' => false, 'error' => 'Meal not found'], 404);
|
||||
}
|
||||
|
||||
if (count($stores) === 0) {
|
||||
sendJson(['success' => false, 'error' => 'Add a grocery store first'], 400);
|
||||
}
|
||||
|
||||
$n = pushMealItemsToGrocery(normalizeMealRow($meal), $stores);
|
||||
sendJson(['success' => true, 'groceryLinesAdded' => $n]);
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/meal_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
$actorId = (string) ($actor['id'] ?? '');
|
||||
|
||||
$body = readJsonBody();
|
||||
$meals = normalizeMealsList(readJsonFile('meals.json'));
|
||||
$meals = migrateLegacyMealsList($meals);
|
||||
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
|
||||
if ($id !== '') {
|
||||
$idx = findMealIndexById($meals, $id);
|
||||
if ($idx === null) {
|
||||
sendJson(['success' => false, 'error' => 'Meal not found'], 404);
|
||||
}
|
||||
$existing = $meals[$idx];
|
||||
$isAuthor = ($existing['author_id'] ?? '') === $actorId;
|
||||
$isHoH = ($actor['role'] ?? '') === ROLE_HEAD && isHohVerified();
|
||||
if (!$isAuthor && !$isHoH) {
|
||||
sendJson(['success' => false, 'error' => 'You cannot edit this meal'], 403);
|
||||
}
|
||||
$row = $existing;
|
||||
} else {
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can create meals'], 403);
|
||||
}
|
||||
$row = [
|
||||
'id' => bin2hex(random_bytes(8)),
|
||||
'author_id' => $actorId,
|
||||
];
|
||||
$idx = null;
|
||||
}
|
||||
|
||||
$title = isset($body['title']) ? trim((string) $body['title']) : '';
|
||||
if ($title === '') {
|
||||
sendJson(['success' => false, 'error' => 'Title is required'], 400);
|
||||
}
|
||||
|
||||
$row['title'] = $title;
|
||||
$row['image'] = isset($body['image']) ? trim((string) $body['image']) : '';
|
||||
$row['description'] = isset($body['description']) ? trim((string) $body['description']) : '';
|
||||
$row['directions'] = isset($body['directions']) ? trim((string) $body['directions']) : '';
|
||||
$row['lists'] = normalizeChoreLists($body['lists'] ?? []);
|
||||
$row['tags'] = $body['tags'] ?? [];
|
||||
$row['ingredients'] = $body['ingredients'] ?? [];
|
||||
$row['items'] = $body['items'] ?? [];
|
||||
|
||||
$row = normalizeMealRow($row);
|
||||
|
||||
if ($idx === null) {
|
||||
$meals[] = $row;
|
||||
} else {
|
||||
$meals[$idx] = $row;
|
||||
}
|
||||
|
||||
if (!writeJsonFile('meals.json', $meals)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save meals'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'meal' => $row]);
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
if (count($people) === 0) {
|
||||
sendJson(['success' => false, 'error' => 'Use first-time setup to create the initial Head of Household'], 400);
|
||||
}
|
||||
assertHoHCanManagePeople($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
$name = isset($body['name']) ? trim((string) $body['name']) : '';
|
||||
$role = isset($body['role']) ? (string) $body['role'] : '';
|
||||
$pin = isset($body['pin']) ? (string) $body['pin'] : '';
|
||||
|
||||
if ($name === '') {
|
||||
sendJson(['success' => false, 'error' => 'Name is required'], 400);
|
||||
}
|
||||
|
||||
$allowedRoles = [ROLE_HEAD, ROLE_ADULT, ROLE_CHILD];
|
||||
if (!in_array($role, $allowedRoles, true)) {
|
||||
sendJson(['success' => false, 'error' => 'Invalid role'], 400);
|
||||
}
|
||||
|
||||
if ($role === ROLE_HEAD) {
|
||||
if (strlen($pin) < 4) {
|
||||
sendJson(['success' => false, 'error' => 'PIN must be at least 4 characters'], 400);
|
||||
}
|
||||
$pinHash = password_hash($pin, PASSWORD_DEFAULT);
|
||||
} else {
|
||||
$pinHash = null;
|
||||
}
|
||||
|
||||
$icon = isset($body['icon']) ? trim((string) $body['icon']) : '';
|
||||
$description = isset($body['description']) ? trim((string) $body['description']) : '';
|
||||
$birthday = isset($body['birthday']) ? trim((string) $body['birthday']) : '';
|
||||
$favoriteColor = isset($body['favoriteColor']) ? trim((string) $body['favoriteColor']) : '#4a90e2';
|
||||
|
||||
if ($favoriteColor !== '' && !preg_match('/^#[0-9A-Fa-f]{6}$/', $favoriteColor)) {
|
||||
sendJson(['success' => false, 'error' => 'favoriteColor must be a #RRGGBB value'], 400);
|
||||
}
|
||||
|
||||
if ($birthday !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) {
|
||||
sendJson(['success' => false, 'error' => 'birthday must be YYYY-MM-DD'], 400);
|
||||
}
|
||||
|
||||
$newPerson = [
|
||||
'id' => bin2hex(random_bytes(8)),
|
||||
'name' => $name,
|
||||
'role' => $role,
|
||||
'pin_hash' => $pinHash,
|
||||
'icon' => $icon,
|
||||
'description' => $description,
|
||||
'birthday' => $birthday,
|
||||
'favoriteColor' => $favoriteColor,
|
||||
'currency_balance' => 0,
|
||||
'created_at' => gmdate('c'),
|
||||
];
|
||||
|
||||
$people[] = $newPerson;
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
|
||||
}
|
||||
|
||||
$safe = $newPerson;
|
||||
unset($safe['pin_hash']);
|
||||
sendJson(['success' => true, 'person' => $safe]);
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
if (count($people) > 0) {
|
||||
sendJson(['success' => false, 'error' => 'People already exist; use signed-in Head of Household'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$name = isset($body['name']) ? trim((string) $body['name']) : '';
|
||||
$pin = isset($body['pin']) ? (string) $body['pin'] : '';
|
||||
|
||||
if ($name === '') {
|
||||
sendJson(['success' => false, 'error' => 'Name is required'], 400);
|
||||
}
|
||||
|
||||
if (strlen($pin) < 4) {
|
||||
sendJson(['success' => false, 'error' => 'PIN must be at least 4 characters'], 400);
|
||||
}
|
||||
|
||||
$icon = isset($body['icon']) ? trim((string) $body['icon']) : '';
|
||||
$description = isset($body['description']) ? trim((string) $body['description']) : '';
|
||||
$birthday = isset($body['birthday']) ? trim((string) $body['birthday']) : '';
|
||||
$favoriteColor = isset($body['favoriteColor']) ? trim((string) $body['favoriteColor']) : '#4a90e2';
|
||||
|
||||
if ($favoriteColor !== '' && !preg_match('/^#[0-9A-Fa-f]{6}$/', $favoriteColor)) {
|
||||
sendJson(['success' => false, 'error' => 'favoriteColor must be a #RRGGBB value'], 400);
|
||||
}
|
||||
|
||||
if ($birthday !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) {
|
||||
sendJson(['success' => false, 'error' => 'birthday must be YYYY-MM-DD'], 400);
|
||||
}
|
||||
|
||||
$newPerson = [
|
||||
'id' => bin2hex(random_bytes(8)),
|
||||
'name' => $name,
|
||||
'role' => ROLE_HEAD,
|
||||
'pin_hash' => password_hash($pin, PASSWORD_DEFAULT),
|
||||
'icon' => $icon,
|
||||
'description' => $description,
|
||||
'birthday' => $birthday,
|
||||
'favoriteColor' => $favoriteColor,
|
||||
'currency_balance' => 0,
|
||||
'created_at' => gmdate('c'),
|
||||
];
|
||||
|
||||
if (!writeJsonFile('people.json', [$newPerson])) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
|
||||
}
|
||||
|
||||
setSessionPerson($newPerson['id'], true);
|
||||
$safe = $newPerson;
|
||||
unset($safe['pin_hash']);
|
||||
sendJson(['success' => true, 'person' => $safe]);
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = 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);
|
||||
}
|
||||
|
||||
$next = [];
|
||||
$removed = null;
|
||||
foreach ($people as $p) {
|
||||
if (($p['id'] ?? '') === $id) {
|
||||
$removed = $p;
|
||||
continue;
|
||||
}
|
||||
$next[] = $p;
|
||||
}
|
||||
|
||||
if ($removed === null) {
|
||||
sendJson(['success' => false, 'error' => 'Person not found'], 404);
|
||||
}
|
||||
|
||||
$headCount = 0;
|
||||
foreach ($next as $p) {
|
||||
if (($p['role'] ?? '') === ROLE_HEAD) {
|
||||
$headCount++;
|
||||
}
|
||||
}
|
||||
if ($headCount < 1) {
|
||||
sendJson(['success' => false, 'error' => 'Cannot remove the last Head of Household'], 400);
|
||||
}
|
||||
|
||||
if (!writeJsonFile('people.json', $next)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
|
||||
}
|
||||
|
||||
if (getActivePersonId() === $id) {
|
||||
clearPersonaSession();
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
assertHoHCanManagePeople($people);
|
||||
|
||||
$body = readJsonBody();
|
||||
$targetId = isset($body['targetPersonId']) ? trim((string) $body['targetPersonId']) : '';
|
||||
$newPin = isset($body['newPin']) ? (string) $body['newPin'] : '';
|
||||
|
||||
if ($targetId === '') {
|
||||
sendJson(['success' => false, 'error' => 'targetPersonId is required'], 400);
|
||||
}
|
||||
if (strlen($newPin) < 4) {
|
||||
sendJson(['success' => false, 'error' => 'newPin must be at least 4 characters'], 400);
|
||||
}
|
||||
|
||||
$target = findPersonById($people, $targetId);
|
||||
if ($target === null) {
|
||||
sendJson(['success' => false, 'error' => 'Person not found'], 404);
|
||||
}
|
||||
if (($target['role'] ?? '') !== ROLE_HEAD) {
|
||||
sendJson(['success' => false, 'error' => 'PIN applies only to Head of Household profiles'], 400);
|
||||
}
|
||||
|
||||
$updated = false;
|
||||
foreach ($people as $i => $p) {
|
||||
if (($p['id'] ?? '') === $targetId) {
|
||||
$people[$i]['pin_hash'] = password_hash($newPin, PASSWORD_DEFAULT);
|
||||
$updated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$updated) {
|
||||
sendJson(['success' => false, 'error' => 'Update failed'], 500);
|
||||
}
|
||||
|
||||
if (!writeJsonFile('people.json', $people)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = 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]);
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can manage stores'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$name = isset($body['name']) ? trim((string) $body['name']) : '';
|
||||
if ($name === '') {
|
||||
sendJson(['success' => false, 'error' => 'Name is required'], 400);
|
||||
}
|
||||
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
$newId = bin2hex(random_bytes(8));
|
||||
$sort = count($stores);
|
||||
$stores[] = ['id' => $newId, 'name' => $name, 'sort' => $sort];
|
||||
|
||||
if (!writeJsonFile('stores.json', $stores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save stores'], 500);
|
||||
}
|
||||
|
||||
$lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
|
||||
if (!isset($lists['byStore'][$newId])) {
|
||||
$lists['byStore'][$newId] = [];
|
||||
}
|
||||
if (!writeJsonFile('grocery_lists.json', $lists)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to init store list'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true, 'store' => ['id' => $newId, 'name' => $name, 'sort' => $sort]]);
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can manage stores'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
if ($id === '') {
|
||||
sendJson(['success' => false, 'error' => 'id is required'], 400);
|
||||
}
|
||||
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
if (count($stores) <= 1) {
|
||||
sendJson(['success' => false, 'error' => 'You must keep at least one store'], 400);
|
||||
}
|
||||
|
||||
$lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
|
||||
if (groceryStoreHasItems($lists, $id)) {
|
||||
sendJson(['success' => false, 'error' => 'Remove or purchase all items in this store before deleting it'], 400);
|
||||
}
|
||||
|
||||
$next = [];
|
||||
foreach ($stores as $s) {
|
||||
if (($s['id'] ?? '') === $id) {
|
||||
continue;
|
||||
}
|
||||
$next[] = $s;
|
||||
}
|
||||
|
||||
unset($lists['byStore'][$id]);
|
||||
|
||||
$catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
|
||||
$catalogNext = [];
|
||||
foreach ($catalog as $row) {
|
||||
if ((string) ($row['storeId'] ?? '') === $id) {
|
||||
continue;
|
||||
}
|
||||
$catalogNext[] = $row;
|
||||
}
|
||||
|
||||
if (!writeJsonFile('stores.json', $next)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save stores'], 500);
|
||||
}
|
||||
if (!writeJsonFile('grocery_lists.json', $lists)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save grocery lists'], 500);
|
||||
}
|
||||
if (!writeJsonFile('grocery_catalog.json', $catalogNext)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save catalog'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$actor = requireActivePerson($people);
|
||||
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
||||
sendJson(['success' => false, 'error' => 'Only a verified Head of household can manage stores'], 403);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$id = isset($body['id']) ? trim((string) $body['id']) : '';
|
||||
$name = isset($body['name']) ? trim((string) $body['name']) : '';
|
||||
if ($id === '' || $name === '') {
|
||||
sendJson(['success' => false, 'error' => 'id and name are required'], 400);
|
||||
}
|
||||
|
||||
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
||||
$found = false;
|
||||
foreach ($stores as $i => $s) {
|
||||
if (($s['id'] ?? '') === $id) {
|
||||
$stores[$i]['name'] = $name;
|
||||
if (isset($body['sort']) && is_numeric($body['sort'])) {
|
||||
$stores[$i]['sort'] = (int) $body['sort'];
|
||||
}
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$found) {
|
||||
sendJson(['success' => false, 'error' => 'Store not found'], 404);
|
||||
}
|
||||
|
||||
if (!writeJsonFile('stores.json', $stores)) {
|
||||
sendJson(['success' => false, 'error' => 'Failed to save stores'], 500);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../includes/api_bootstrap.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$body = readJsonBody();
|
||||
$personId = isset($body['personId']) ? trim((string) $body['personId']) : '';
|
||||
$pin = isset($body['pin']) ? (string) $body['pin'] : '';
|
||||
|
||||
if ($personId === '') {
|
||||
sendJson(['success' => false, 'error' => 'personId is required'], 400);
|
||||
}
|
||||
|
||||
$people = normalizePeopleList(readJsonFile('people.json'));
|
||||
$person = findPersonById($people, $personId);
|
||||
if ($person === null) {
|
||||
sendJson(['success' => false, 'error' => 'Person not found'], 404);
|
||||
}
|
||||
|
||||
$role = $person['role'] ?? '';
|
||||
$pinHash = $person['pin_hash'] ?? null;
|
||||
|
||||
if ($role === ROLE_HEAD && is_string($pinHash) && $pinHash !== '') {
|
||||
if ($pin === '' || !password_verify($pin, $pinHash)) {
|
||||
sendJson(['success' => false, 'error' => 'PIN required or incorrect'], 403);
|
||||
}
|
||||
setSessionPerson($personId, true);
|
||||
} else {
|
||||
setSessionPerson($personId, false);
|
||||
}
|
||||
|
||||
sendJson(['success' => true]);
|
||||
Reference in New Issue
Block a user