First pass of features

This commit is contained in:
2026-03-30 14:53:13 -05:00
parent 1a65e69d25
commit 6353f9c9b4
54 changed files with 5409 additions and 128 deletions
+40
View File
@@ -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]);
+87
View File
@@ -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]);
+97
View File
@@ -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]);
+59
View File
@@ -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]);
+88
View File
@@ -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']]);
+48
View File
@@ -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 06'], 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]);
+78
View File
@@ -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']]);
+39
View File
@@ -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]);
+70
View File
@@ -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]);
+50
View File
@@ -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]);
+39
View File
@@ -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]);
+59
View File
@@ -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']]);
+68
View File
@@ -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 06 (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]);
+31
View File
@@ -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]);
+34
View File
@@ -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]);
+69
View File
@@ -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]);
+71
View File
@@ -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]);
+59
View File
@@ -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]);
+50
View File
@@ -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]);
+48
View File
@@ -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]);
+96
View File
@@ -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]);
+39
View File
@@ -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]]);
+61
View File
@@ -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]);
+43
View File
@@ -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]);
+35
View File
@@ -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]);