| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- <?php
- require_once __DIR__ . '/auth.php';
- require_once __DIR__ . '/layout.php';
- require_admin();
- $pdo = get_pdo();
- // Handle add/remove mapping
- if ($_SERVER['REQUEST_METHOD'] === 'POST') {
- require_csrf();
- $action = $_POST['action'] ?? '';
- $aid = (int)($_POST['activityID'] ?? 0);
- $iid = (int)($_POST['itemID'] ?? 0);
- if ($action === 'add' && $aid > 0 && $iid > 0) {
- $qty = max(1, (int)($_POST['quantity'] ?? 1));
- try {
- $pdo->prepare("INSERT IGNORE INTO activity_item_map (activityID,itemID,quantity) VALUES (?,?,?)")
- ->execute([$aid, $iid, $qty]);
- flash('success', 'Mapping added.');
- } catch (Exception $e) {
- flash('error', 'Could not add mapping.');
- }
- }
- if ($action === 'update' && $aid > 0 && $iid > 0) {
- $qty = max(1, (int)($_POST['quantity'] ?? 1));
- $pdo->prepare("UPDATE activity_item_map SET quantity=? WHERE activityID=? AND itemID=?")
- ->execute([$qty, $aid, $iid]);
- flash('success', 'Quantity updated.');
- }
- if ($action === 'remove' && $aid > 0 && $iid > 0) {
- $pdo->prepare("DELETE FROM activity_item_map WHERE activityID=? AND itemID=?")
- ->execute([$aid, $iid]);
- flash('success', 'Mapping removed.');
- }
- header('Location: mappings.php' . (isset($_GET['aid']) ? '?aid=' . (int)$_GET['aid'] : ''));
- exit;
- }
- // Load all activities and items for selects
- $activities = $pdo->query("
- SELECT a.activityID, a.activityName, ag.groupName
- FROM activities a
- JOIN activity_groups ag ON ag.groupID = a.groupID
- ORDER BY ag.sortOrder, a.sortOrder, a.activityID
- ")->fetchAll();
- $allItems = $pdo->query("
- SELECT i.itemID, i.itemName, ig.groupName
- FROM items i
- JOIN item_groups ig ON ig.groupID = i.groupID
- ORDER BY ig.sortOrder, i.itemName, i.itemID
- ")->fetchAll();
- // Selected activity filter
- $selectedAid = isset($_GET['aid']) ? (int)$_GET['aid'] : 0;
- if (!$selectedAid && !empty($activities)) {
- $selectedAid = $activities[0]['activityID'];
- }
- // Get current mappings for selected activity
- $mappedItemIds = [];
- $mappedItems = [];
- if ($selectedAid) {
- $stmt = $pdo->prepare("
- SELECT i.itemID, i.itemName, ig.groupName, aim.quantity
- FROM activity_item_map aim
- JOIN items i ON i.itemID = aim.itemID
- JOIN item_groups ig ON ig.groupID = i.groupID
- WHERE aim.activityID = ?
- ORDER BY ig.sortOrder, i.sortOrder
- ");
- $stmt->execute([$selectedAid]);
- $mappedItems = $stmt->fetchAll();
- $mappedItemIds = array_column($mappedItems, 'itemID');
- }
- admin_head('Mappings', 'mappings.php');
- show_alerts();
- ?>
- <h1>Activity ↔ Item Mappings</h1>
- <style>
- /* Searchable item picker (replaces the unwieldy 100+ entry select) */
- .item-picker{position:relative;}
- .item-list{position:absolute;z-index:20;top:calc(100% + 4px);left:0;right:0;max-height:280px;overflow-y:auto;
- background:var(--surface);border:1.5px solid var(--border);border-radius:var(--radius);
- box-shadow:0 8px 24px rgba(0,0,0,.45);padding:.25rem 0;}
- .item-group-label{padding:.4rem .8rem .2rem;font-size:.7rem;font-weight:600;letter-spacing:.08em;
- text-transform:uppercase;color:var(--accent2);}
- .item-option{padding:.35rem .8rem .35rem 1.3rem;font-size:.88rem;cursor:pointer;}
- .item-option:hover,.item-option.active{background:rgba(232,197,71,.12);color:var(--accent);}
- .item-empty{padding:.5rem .8rem;font-size:.85rem;color:var(--muted);}
- /* Activity selector: one heading per group, activities listed below it, so a long
- activity name can wrap without colliding with a per-row category label. */
- .activity-picker{flex:0 0 260px;min-width:260px;}
- .activity-group{margin-bottom:1rem;}
- .activity-group:last-child{margin-bottom:0;}
- .activity-group-label{padding:0 .15rem .3rem;font-size:.7rem;font-weight:600;letter-spacing:.08em;
- text-transform:uppercase;color:var(--accent2);}
- .activity-link{display:block;padding:.4rem .6rem;margin-bottom:.2rem;border-radius:6px;
- border:1px solid transparent;font-size:.85rem;line-height:1.35;text-decoration:none;
- color:var(--muted);overflow-wrap:anywhere;
- transition:color .15s,background .15s,border-color .15s;}
- .activity-link:hover{color:var(--text);background:rgba(255,255,255,.03);}
- .activity-link.active{background:rgba(232,197,71,.1);color:var(--accent);border-color:rgba(232,197,71,.3);}
- </style>
- <div style="display:flex;gap:1.5rem;flex-wrap:wrap;align-items:flex-start;">
- <!-- Activity selector -->
- <div class="card activity-picker">
- <h2>Select Activity</h2>
- <?php
- // The query already orders by group, so grouping keeps the intended sort order.
- $activityGroups = [];
- foreach ($activities as $a) { $activityGroups[$a['groupName']][] = $a; }
- foreach ($activityGroups as $grpName => $grpActivities):
- ?>
- <div class="activity-group">
- <div class="activity-group-label"><?= htmlspecialchars($grpName) ?></div>
- <?php foreach ($grpActivities as $a): ?>
- <a href="?aid=<?= $a['activityID'] ?>"
- class="activity-link<?= $selectedAid==$a['activityID'] ? ' active' : '' ?>"><?= htmlspecialchars($a['activityName']) ?></a>
- <?php endforeach; ?>
- </div>
- <?php endforeach; ?>
- </div>
- <!-- Mapping manager -->
- <div style="flex:1;min-width:300px;">
- <?php if ($selectedAid):
- // Get the activity name
- $actName = '';
- foreach ($activities as $a) { if ($a['activityID'] == $selectedAid) $actName = $a['activityName']; }
- ?>
- <div class="card">
- <h2 style="margin-bottom:1rem">Items for: <span style="color:var(--accent)"><?= htmlspecialchars($actName) ?></span></h2>
- <!-- Add new mapping -->
- <form method="POST" style="display:flex;gap:.5rem;align-items:flex-end;margin-bottom:1.25rem;flex-wrap:wrap;">
- <?= csrf_field() ?>
- <input type="hidden" name="action" value="add">
- <input type="hidden" name="activityID" value="<?= $selectedAid ?>">
- <div style="flex:1;min-width:200px;">
- <label for="item-search">Add Item</label>
- <div class="item-picker" id="item-picker">
- <input type="hidden" name="itemID" id="item-id">
- <input type="text" id="item-search" autocomplete="off" role="combobox"
- aria-autocomplete="list" aria-expanded="false" aria-controls="item-list"
- placeholder="Type to search items…" style="margin-bottom:0;">
- <div class="item-list" id="item-list" role="listbox" hidden>
- <?php
- // Group the still-unmapped items; the list is filtered client-side.
- $optGroups = [];
- foreach ($allItems as $it) {
- if (in_array($it['itemID'], $mappedItemIds)) continue; // already mapped
- $optGroups[$it['groupName']][] = $it;
- }
- foreach ($optGroups as $grpName => $grpItems):
- ?>
- <div class="item-group">
- <div class="item-group-label"><?= htmlspecialchars($grpName) ?></div>
- <?php foreach ($grpItems as $it): ?>
- <div class="item-option" role="option" tabindex="-1"
- data-id="<?= $it['itemID'] ?>"
- data-name="<?= htmlspecialchars($it['itemName']) ?>"><?= htmlspecialchars($it['itemName']) ?></div>
- <?php endforeach; ?>
- </div>
- <?php endforeach; ?>
- <div class="item-empty" hidden>No matching items.</div>
- </div>
- </div>
- </div>
- <div style="width:90px;">
- <label>Qty</label>
- <input type="number" name="quantity" value="1" min="1" style="margin-bottom:0;width:100%;">
- </div>
- <button type="submit" class="btn btn-primary">➕ Add</button>
- </form>
- <!-- Current mapped items -->
- <?php if (empty($mappedItems)): ?>
- <p style="color:var(--muted);font-size:.85rem">No items mapped yet.</p>
- <?php else: ?>
- <table class="tbl">
- <thead><tr><th>#</th><th>Item</th><th>Group</th><th>Qty</th><th></th></tr></thead>
- <tbody>
- <?php foreach ($mappedItems as $it): ?>
- <tr>
- <td style="color:var(--muted)"><?= $it['itemID'] ?></td>
- <td><?= htmlspecialchars($it['itemName']) ?></td>
- <td><span class="badge badge-group"><?= htmlspecialchars($it['groupName']) ?></span></td>
- <td>
- <form method="POST" style="display:flex;gap:.3rem;align-items:center;">
- <?= csrf_field() ?>
- <input type="hidden" name="action" value="update">
- <input type="hidden" name="activityID" value="<?= $selectedAid ?>">
- <input type="hidden" name="itemID" value="<?= $it['itemID'] ?>">
- <input type="number" name="quantity" value="<?= (int)$it['quantity'] ?>" min="1" style="margin:0;width:70px;">
- <button class="btn btn-sm btn-primary">💾</button>
- </form>
- </td>
- <td style="text-align:right">
- <form method="POST" onsubmit="return confirm('Remove this mapping?')">
- <?= csrf_field() ?>
- <input type="hidden" name="action" value="remove">
- <input type="hidden" name="activityID" value="<?= $selectedAid ?>">
- <input type="hidden" name="itemID" value="<?= $it['itemID'] ?>">
- <button class="btn btn-sm btn-danger">Remove</button>
- </form>
- </td>
- </tr>
- <?php endforeach; ?>
- </tbody>
- </table>
- <?php endif; ?>
- </div>
- <?php endif; ?>
- </div>
- </div>
- <script>
- (function () {
- var picker = document.getElementById('item-picker');
- if (!picker) return;
- var search = document.getElementById('item-search');
- var hidden = document.getElementById('item-id');
- var list = document.getElementById('item-list');
- var empty = list.querySelector('.item-empty');
- var groups = Array.prototype.slice.call(list.querySelectorAll('.item-group'));
- var options = Array.prototype.slice.call(list.querySelectorAll('.item-option'));
- var active = null;
- // Lower-cased and stripped of diacritics, so "muhe" also finds "Mühe".
- function norm(s) {
- return s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
- }
- options.forEach(function (o) { o.dataset.search = norm(o.dataset.name); });
- function setActive(el) {
- if (active) active.classList.remove('active');
- active = el;
- if (active) {
- active.classList.add('active');
- active.scrollIntoView({block: 'nearest'});
- }
- }
- function visibleOptions() {
- return options.filter(function (o) { return !o.hidden; });
- }
- function filter(showAll) {
- var q = showAll ? '' : norm(search.value.trim());
- options.forEach(function (o) {
- o.hidden = q !== '' && o.dataset.search.indexOf(q) === -1;
- });
- // Hide a group entirely when none of its items matched.
- groups.forEach(function (g) {
- var items = Array.prototype.slice.call(g.querySelectorAll('.item-option'));
- g.hidden = !items.some(function (o) { return !o.hidden; });
- });
- empty.hidden = visibleOptions().length > 0;
- setActive(visibleOptions()[0] || null);
- }
- function open() {
- list.hidden = false;
- search.setAttribute('aria-expanded', 'true');
- }
- function close() {
- list.hidden = true;
- search.setAttribute('aria-expanded', 'false');
- setActive(null);
- }
- function choose(option) {
- if (!option) return;
- hidden.value = option.dataset.id;
- search.value = option.dataset.name;
- filter();
- close();
- }
- search.addEventListener('input', function () {
- hidden.value = ''; // typing invalidates a previous pick
- filter();
- open();
- });
- search.addEventListener('focus', function () {
- // After a pick the field holds the full item name; offer the whole list again.
- var picked = !!hidden.value;
- if (picked) search.select();
- filter(picked);
- open();
- });
- search.addEventListener('keydown', function (e) {
- if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
- e.preventDefault();
- if (list.hidden) { filter(!!hidden.value); open(); return; }
- var vis = visibleOptions();
- if (!vis.length) return;
- var i = vis.indexOf(active);
- i = e.key === 'ArrowDown' ? (i + 1) % vis.length : (i <= 0 ? vis.length : i) - 1;
- setActive(vis[i]);
- } else if (e.key === 'Enter') {
- if (!list.hidden && active) { e.preventDefault(); choose(active); }
- } else if (e.key === 'Escape') {
- close();
- }
- });
- list.addEventListener('mousedown', function (e) {
- var option = e.target.closest('.item-option');
- if (option) { e.preventDefault(); choose(option); }
- });
- document.addEventListener('click', function (e) {
- if (!picker.contains(e.target)) close();
- });
- search.form.addEventListener('submit', function (e) {
- if (!hidden.value) {
- e.preventDefault();
- search.focus();
- filter();
- open();
- }
- });
- filter();
- })();
- </script>
- <?php admin_foot(); ?>
|