239 lines
14 KiB
PHP
239 lines
14 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../includes/db.php';
|
|
require_once __DIR__ . '/../includes/utils.php';
|
|
require_once __DIR__ . '/../includes/persona.php';
|
|
require_once __DIR__ . '/../includes/grocery_helpers.php';
|
|
|
|
migrateLegacyGroceriesIfNeeded();
|
|
ensureDefaultGroceryStore();
|
|
|
|
$stores = normalizeStoresList(readJsonFile('stores.json'));
|
|
usort($stores, static function ($a, $b) {
|
|
$sa = (int) ($a['sort'] ?? 0);
|
|
$sb = (int) ($b['sort'] ?? 0);
|
|
if ($sa !== $sb) {
|
|
return $sa <=> $sb;
|
|
}
|
|
return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
|
|
});
|
|
|
|
$lists = normalizeGroceryLists(readJsonFile('grocery_lists.json'));
|
|
$catalog = normalizeCatalogList(readJsonFile('grocery_catalog.json'));
|
|
|
|
$selectedStore = isset($_GET['store']) ? trim((string) $_GET['store']) : '';
|
|
if ($selectedStore === '' && count($stores) > 0) {
|
|
$selectedStore = (string) ($stores[0]['id'] ?? '');
|
|
}
|
|
$currentStore = findStoreById($stores, $selectedStore);
|
|
|
|
$filter = isset($_GET['filter']) ? trim((string) $_GET['filter']) : 'active';
|
|
if (!in_array($filter, ['active', 'purchased', 'pending', 'all'], true)) {
|
|
$filter = 'active';
|
|
}
|
|
|
|
$allItems = $currentStore ? ($lists['byStore'][$selectedStore] ?? []) : [];
|
|
$displayItems = [];
|
|
foreach ($allItems as $it) {
|
|
if (!is_array($it)) {
|
|
continue;
|
|
}
|
|
$st = (string) ($it['status'] ?? 'active');
|
|
if ($filter === 'all') {
|
|
$displayItems[] = $it;
|
|
} elseif ($filter === 'pending' && $st === 'pending_review') {
|
|
$displayItems[] = $it;
|
|
} elseif ($filter === 'purchased' && $st === 'purchased') {
|
|
$displayItems[] = $it;
|
|
} elseif ($filter === 'active' && $st === 'active') {
|
|
$displayItems[] = $it;
|
|
}
|
|
}
|
|
|
|
$pickerOptions = $currentStore ? groceryCatalogPickerOptions($catalog, $selectedStore) : [];
|
|
|
|
$canReviewGroceries = $activePerson !== null
|
|
&& ($activePerson['role'] ?? '') === ROLE_HEAD
|
|
&& isHohVerified();
|
|
?>
|
|
|
|
<div id="groceries" class="tab-content" data-store-id="<?= htmlspecialchars($selectedStore, ENT_QUOTES, 'UTF-8') ?>">
|
|
<h2 class="mb-3">Grocery list</h2>
|
|
|
|
<?php if ($activePerson === null): ?>
|
|
<div class="alert alert-warning">Choose who is using the hub (top) to edit your list.</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (count($stores) === 0): ?>
|
|
<p class="text-muted">No stores yet. Add one under <strong>Family settings</strong>.</p>
|
|
<?php else: ?>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-lg-3">
|
|
<nav class="nav flex-lg-column nav-pills grocery-store-nav gap-1" aria-label="Stores">
|
|
<?php foreach ($stores as $s): ?>
|
|
<?php
|
|
$sidRaw = (string) ($s['id'] ?? '');
|
|
$sid = htmlspecialchars($sidRaw, ENT_QUOTES, 'UTF-8');
|
|
$active = $sidRaw === $selectedStore;
|
|
?>
|
|
<a class="nav-link <?= $active ? 'active' : '' ?> py-2" href="?tab=groceries&store=<?= $sid ?>&filter=<?= htmlspecialchars($filter, ENT_QUOTES, 'UTF-8') ?>">
|
|
<?= sanitizeInput($s['name'] ?? 'Store') ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</nav>
|
|
<p class="small text-muted mt-2 d-none d-lg-block">Manage store names under <a href="?tab=settings">Family settings</a>.</p>
|
|
</div>
|
|
|
|
<div class="col-lg-9">
|
|
<?php if (!$currentStore): ?>
|
|
<p class="alert alert-warning">Pick a store from the list.</p>
|
|
<?php else: ?>
|
|
|
|
<?php if ($activePerson !== null): ?>
|
|
<div class="card border-secondary mb-4">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span>Add item — <?= sanitizeInput($currentStore['name'] ?? '') ?></span>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#groceryAddFormCollapse" aria-expanded="false" aria-controls="groceryAddFormCollapse">
|
|
Show form
|
|
</button>
|
|
</div>
|
|
<div class="collapse" id="groceryAddFormCollapse">
|
|
<div class="card-body">
|
|
<form id="groceryAddForm" class="row g-3">
|
|
<input type="hidden" id="grocery_store_id" value="<?= htmlspecialchars($selectedStore, ENT_QUOTES, 'UTF-8') ?>">
|
|
<div class="col-12">
|
|
<label class="form-label" for="grocery_catalog_pick">Start from saved item (optional, deduped)</label>
|
|
<select class="form-select" id="grocery_catalog_pick">
|
|
<option value="">— New item —</option>
|
|
<?php foreach ($pickerOptions as $opt): ?>
|
|
<option
|
|
value="<?= htmlspecialchars((string) ($opt['id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
|
data-name="<?= htmlspecialchars((string) ($opt['name'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
|
data-description="<?= htmlspecialchars((string) ($opt['description'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
|
data-size="<?= htmlspecialchars((string) ($opt['defaultSize'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
|
data-image="<?= htmlspecialchars((string) ($opt['defaultImage'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"
|
|
><?= sanitizeInput($opt['name'] ?? '') ?><?php if (!empty($opt['defaultSize'])): ?> (<?= sanitizeInput((string) $opt['defaultSize']) ?>)<?php endif; ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="grocery_name">Name</label>
|
|
<input class="form-control" id="grocery_name" required maxlength="200">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="grocery_quantity">Quantity</label>
|
|
<input class="form-control" id="grocery_quantity" value="1">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="grocery_size">Size</label>
|
|
<input class="form-control" id="grocery_size" placeholder="e.g. 1 gal">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="grocery_price">Price</label>
|
|
<input class="form-control" id="grocery_price" placeholder="optional">
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="grocery_description">Description</label>
|
|
<textarea class="form-control" id="grocery_description" rows="2"></textarea>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label class="form-label" for="grocery_image">Image URL</label>
|
|
<input class="form-control" id="grocery_image" type="url" placeholder="https://…">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="grocery_recurring">Recurring (days after purchase)</label>
|
|
<input type="number" class="form-control" id="grocery_recurring" min="0" value="0" title="0 = not recurring; e.g. 7 = suggest again 7 days after you mark it purchased">
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">Add to list</button>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="alert d-none" id="groceryFormFeedback" role="status"></div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
|
|
<span class="text-muted small me-1">Show:</span>
|
|
<?php
|
|
$filtHref = static function ($f) use ($selectedStore) {
|
|
return '?tab=groceries&store=' . rawurlencode($selectedStore) . '&filter=' . rawurlencode($f);
|
|
};
|
|
?>
|
|
<a class="btn btn-sm <?= $filter === 'active' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('active'), ENT_QUOTES, 'UTF-8') ?>">To buy</a>
|
|
<a class="btn btn-sm <?= $filter === 'purchased' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('purchased'), ENT_QUOTES, 'UTF-8') ?>">Purchased</a>
|
|
<a class="btn btn-sm <?= $filter === 'pending' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('pending'), ENT_QUOTES, 'UTF-8') ?>">Pending review</a>
|
|
<a class="btn btn-sm <?= $filter === 'all' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= htmlspecialchars($filtHref('all'), ENT_QUOTES, 'UTF-8') ?>">All</a>
|
|
</div>
|
|
|
|
<h3 class="h6 text-muted"><?= $filter === 'purchased' ? 'Purchased' : ($filter === 'pending' ? 'Pending review' : ($filter === 'all' ? 'All items' : 'To buy')) ?></h3>
|
|
<?php if (count($displayItems) === 0): ?>
|
|
<p class="text-muted">No items in this view.</p>
|
|
<?php else: ?>
|
|
<ul class="list-group grocery-item-list">
|
|
<?php foreach ($displayItems as $it): ?>
|
|
<?php
|
|
$iid = htmlspecialchars((string) ($it['id'] ?? ''), ENT_QUOTES, 'UTF-8');
|
|
$isPurchased = ($it['status'] ?? '') === 'purchased';
|
|
$isPending = ($it['status'] ?? '') === 'pending_review';
|
|
?>
|
|
<li class="list-group-item grocery-line">
|
|
<div class="d-flex flex-column flex-md-row gap-2 justify-content-between align-items-start">
|
|
<div class="flex-grow-1">
|
|
<div class="d-flex align-items-start gap-2">
|
|
<?php if (!empty($it['image']) && preg_match('#^https?://#i', (string) $it['image'])): ?>
|
|
<img src="<?= sanitizeInput((string) $it['image']) ?>" alt="" class="grocery-line-thumb rounded">
|
|
<?php endif; ?>
|
|
<div>
|
|
<strong><?= sanitizeInput((string) ($it['name'] ?? '')) ?></strong>
|
|
<?php if (!empty($it['size'])): ?>
|
|
<span class="text-muted small">· <?= sanitizeInput((string) $it['size']) ?></span>
|
|
<?php endif; ?>
|
|
<?php if ($isPending): ?>
|
|
<span class="badge text-bg-warning ms-1">Pending review</span>
|
|
<?php endif; ?>
|
|
<?php if ((int) ($it['recurringIntervalDays'] ?? 0) > 0): ?>
|
|
<span class="badge text-bg-info ms-1">Recurring</span>
|
|
<?php endif; ?>
|
|
<div class="small text-muted">Qty <?= sanitizeInput((string) ($it['quantity'] ?? '')) ?>
|
|
<?php if (!empty($it['price'])): ?> · <?= sanitizeInput((string) $it['price']) ?><?php endif; ?>
|
|
</div>
|
|
<?php if (!empty($it['description'])): ?>
|
|
<div class="small"><?= nl2br(sanitizeInput((string) $it['description'])) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($isPurchased && !empty($it['purchasedAt'])): ?>
|
|
<div class="small text-muted">Purchased <?= sanitizeInput((string) $it['purchasedAt']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="btn-group flex-shrink-0">
|
|
<?php if ($activePerson !== null && !$isPending): ?>
|
|
<?php if (!$isPurchased): ?>
|
|
<button type="button" class="btn btn-sm btn-success btn-grocery-purchase" data-item-id="<?= $iid ?>" data-purchased="1">Got it</button>
|
|
<?php else: ?>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-grocery-purchase" data-item-id="<?= $iid ?>" data-purchased="0">Mark not bought</button>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
<?php if ($canReviewGroceries && $isPending): ?>
|
|
<button type="button" class="btn btn-sm btn-primary btn-grocery-approve" data-item-id="<?= $iid ?>">Approve</button>
|
|
<?php endif; ?>
|
|
<?php if ($activePerson !== null): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-danger btn-grocery-delete" data-item-id="<?= $iid ?>">Remove</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|