. The redirect only carries the id — the // offer is re-checked against the spool's real remaining amount before it is // shown, so a stale or hand-typed link cannot put a full spool on the block. function empty_spool_param(int $filamentId): array { $st = db()->prepare('SELECT remaining_g FROM filaments WHERE id = ?'); $st->execute([$filamentId]); $row = $st->fetch(); return $row && spool_is_empty($row) ? ['empty' => $filamentId] : []; } // A spool's last_used_at is just the date of the newest entry in its usage // log, so once an entry is deleted or moved to another spool it has to be // recomputed — removing the newest usage must not leave the spool still // claiming it was used then. No entries left means the spool has no known // usage date, which is what NULL means everywhere else. function sync_last_used(int $filamentId): void { db()->prepare( "UPDATE filaments SET last_used_at = (SELECT MAX(created_at) FROM usage_log WHERE filament_id = ?), updated_at = datetime('now') WHERE id = ?" )->execute([$filamentId, $filamentId]); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { check_csrf(); $action = (string)($_POST['action'] ?? ''); // Taking a used-up spool out of the inventory, from the offer that // appears after the deduction that emptied it. if ($action === 'delete_spool') { $fid = (int)($_POST['filament_id'] ?? 0); $st = db()->prepare('SELECT * FROM filaments WHERE id = ?'); $st->execute([$fid]); $spool = $st->fetch(); if (!$spool) { flash('That spool is no longer in the inventory.'); } else { backup_db(); // usage_log rows for the spool go with it (ON DELETE CASCADE) — // the offer says so before this runs. db()->prepare('DELETE FROM filaments WHERE id = ?')->execute([$fid]); flash('Deleted ' . spool_label($spool) . ' from the inventory.'); } header('Location: ' . usage_return()); exit; } // Correcting a logged print: both paths first put the entry's grams back // on the spool they came off, which is the whole point of the feature — // usage entered by mistake should leave the inventory as it was. if ($action === 'delete' || $action === 'edit') { $uid = (int)($_POST['usage_id'] ?? 0); $st = db()->prepare('SELECT * FROM usage_log WHERE id = ?'); $st->execute([$uid]); $entry = $st->fetch(); if (!$entry) { flash('That usage entry no longer exists.'); header('Location: ' . usage_return()); exit; } $oldFid = (int)$entry['filament_id']; $oldGrams = (float)$entry['grams']; if ($action === 'delete') { backup_db(); $pdo = db(); $pdo->beginTransaction(); $pdo->prepare('UPDATE filaments SET remaining_g = remaining_g + ? WHERE id = ?') ->execute([$oldGrams, $oldFid]); $pdo->prepare('DELETE FROM usage_log WHERE id = ?')->execute([$uid]); $pdo->commit(); sync_last_used($oldFid); flash('Deleted the ' . round($oldGrams, 1) . ' g entry — that filament is back on the spool.'); header('Location: ' . usage_return()); exit; } $fid = (int)($_POST['filament_id'] ?? 0); $grams = (float)($_POST['grams'] ?? 0); $file = trim((string)($_POST['source_file'] ?? '')); $file = $file === '' ? null : mb_substr($file, 0, 200); $st = db()->prepare('SELECT * FROM filaments WHERE id = ?'); $st->execute([$fid]); $spool = $st->fetch(); if (!$spool || $grams <= 0) { flash('Pick a spool and enter a positive amount of grams.'); header('Location: use.php?edit=' . $uid . (isset($_POST['all']) ? '&all=1' : '') . '#u' . $uid); exit; } backup_db(); $pdo = db(); $pdo->beginTransaction(); // Refund the old spool, then charge the new one — the same two steps // whether or not the entry was moved to a different spool. $pdo->prepare('UPDATE filaments SET remaining_g = remaining_g + ? WHERE id = ?') ->execute([$oldGrams, $oldFid]); $pdo->prepare( "UPDATE filaments SET remaining_g = MAX(remaining_g - ?, 0), updated_at = datetime('now') WHERE id = ?" )->execute([$grams, $fid]); $pdo->prepare('UPDATE usage_log SET filament_id = ?, grams = ?, source_file = ? WHERE id = ?') ->execute([$fid, $grams, $file, $uid]); $pdo->commit(); sync_last_used($oldFid); sync_last_used($fid); flash('Updated that entry to ' . round($grams, 1) . ' g on ' . spool_label($spool) . '.'); header('Location: ' . usage_return(empty_spool_param($fid))); exit; } $fid = (int)($_POST['filament_id'] ?? 0); $grams = (float)($_POST['grams'] ?? 0); $file = trim((string)($_POST['source_file'] ?? '')); $file = $file === '' ? null : mb_substr($file, 0, 200); $st = db()->prepare('SELECT * FROM filaments WHERE id = ?'); $st->execute([$fid]); $spool = $st->fetch(); $dest = []; if (!$spool || $grams <= 0) { flash('Pick a spool and enter a positive amount of grams.'); } else { backup_db(); $pdo = db(); $pdo->beginTransaction(); $pdo->prepare( "UPDATE filaments SET remaining_g = MAX(remaining_g - ?, 0), last_used_at = datetime('now'), updated_at = datetime('now') WHERE id = ?" )->execute([$grams, $fid]); $pdo->prepare('INSERT INTO usage_log (filament_id, grams, source_file) VALUES (?,?,?)') ->execute([$fid, $grams, $file]); $pdo->commit(); flash('Deducted ' . round($grams, 1) . ' g from ' . spool_label($spool) . '.'); $dest = empty_spool_param($fid); } header('Location: ' . usage_return($dest)); exit; } $spools = db()->query( 'SELECT id, brand, type, color_name, color_hex, color_hex2, notes, abrasive, highflow, silk, matte, rainbow, transparent, twotone, sparkle, original_g, remaining_g FROM filaments ORDER BY (last_used_at IS NULL), last_used_at DESC, brand COLLATE NOCASE' )->fetchAll(); // "#DA6019 · 812 g left (81%)" — the line under the selected spool's name. function spool_meta(array $s): string { $parts = []; if ($s['color_hex']) { $parts[] = strtoupper($s['color_hex']); } $parts[] = round((float)$s['remaining_g']) . ' g left (' . spool_pct($s) . '%)'; return implode(' · ', $parts); } // The last handful of prints is enough to spot a mistake you just made, but // correcting an older one means being able to reach it, so ?all=1 opens the // full log (capped so a long history cannot blow the page up). $showAll = isset($_GET['all']); $usageCount = (int)db()->query('SELECT COUNT(*) FROM usage_log')->fetchColumn(); $recent = db()->query( 'SELECT u.id, u.filament_id, u.grams, u.source_file, u.created_at, f.brand, f.type, f.color_name FROM usage_log u JOIN filaments f ON f.id = u.filament_id ORDER BY u.id DESC LIMIT ' . ($showAll ? 500 : 8) )->fetchAll(); // The entry expanded into an edit form, from the Edit link on its card. $editId = (int)($_GET['edit'] ?? 0); // The spool the last deduction emptied, offered up for deletion. Re-checked // against the live remaining amount so the offer cannot outlive the emptiness // that prompted it — undoing the usage entry that emptied the spool, then // going back, must not still offer to bin a spool with filament on it. $emptyId = (int)($_GET['empty'] ?? 0); $emptySpool = null; foreach ($spools as $s) { if ((int)$s['id'] === $emptyId && spool_is_empty($s)) { $emptySpool = $s; break; } } $emptyLogs = 0; if ($emptySpool) { $st = db()->prepare('SELECT COUNT(*) FROM usage_log WHERE filament_id = ?'); $st->execute([$emptyId]); $emptyLogs = (int)$st->fetchColumn(); } $sel = (int)($_GET['id'] ?? 0); // The spool the browser will have selected on load — the ?id= one if it still // exists, otherwise the first (most recently used). The preview card is // rendered from it server-side so it is correct before any JS runs. $current = null; foreach ($spools as $s) { if ((int)$s['id'] === $sel) { $current = $s; break; } } $current ??= ($spools[0] ?? null); page_header('Use filament'); ?>
No spools yet — add one first.
Showing the newest = count($recent) ?> of = $usageCount ?> entries.
count($recent) || $showAll): ?>