prepare( 'UPDATE filaments SET ' . implode(', ', $set) . ", updated_at=datetime('now') WHERE id IN ($ph)" )->execute(array_merge($vals, $ids)); flash('Updated ' . count($ids) . ' spool' . (count($ids) === 1 ? '' : 's') . '.'); header('Location: index.php'); exit; } if (!is_logged_in() && !config()['public_view']) { header('Location: login.php'); exit; } // hex_hsl(), hue_key(), pie_color() and spool_pct() live in inc/layout.php — // use.php renders the same swatch. $sorts = [ 'used' => '(last_used_at IS NULL), last_used_at DESC, brand COLLATE NOCASE, type COLLATE NOCASE', 'type' => 'type COLLATE NOCASE, color_name COLLATE NOCASE, brand COLLATE NOCASE', 'color' => 'id', // real ordering happens in PHP below (hue needs math SQLite can't do) 'name' => "(color_name IS NULL OR color_name = ''), color_name COLLATE NOCASE, brand COLLATE NOCASE", 'brand' => "(brand IS NULL OR brand = ''), brand COLLATE NOCASE, type COLLATE NOCASE", 'left' => 'remaining_g DESC, type COLLATE NOCASE', 'added' => 'created_at DESC, id DESC', ]; $sortLabels = [ 'used' => 'Recently used', 'type' => 'Type', 'color' => 'Color', 'name' => 'Color name', 'brand' => 'Manufacturer', 'left' => 'Amount left', 'added' => 'Recently added', ]; $sort = $_GET['sort'] ?? $_SESSION['sort'] ?? 'used'; if (!is_string($sort) || !isset($sorts[$sort])) { $sort = 'used'; } $_SESSION['sort'] = $sort; // List or color-grid view, remembered in the session like the sort. $view = $_GET['view'] ?? $_SESSION['view'] ?? 'list'; if (!is_string($view) || !in_array($view, ['list', 'grid'], true)) { $view = 'list'; } $_SESSION['view'] = $view; $rows = db()->query('SELECT * FROM filaments ORDER BY ' . $sorts[$sort])->fetchAll(); if ($sort === 'color') { // Rainbow spools have no single hue — group them ahead of the hue wheel. $colorKey = fn(array $r) => !empty($r['rainbow']) ? [-1, 0.0, 0.0] : hue_key($r['color_hex']); usort($rows, fn($a, $b) => $colorKey($a) <=> $colorKey($b)); } $cur = config()['currency']; // ---- filters: AND across the groups, OR within a group ---- $flagDefs = ['abrasive' => 'Abrasive', 'highflow' => 'High-flow', 'silk' => 'Silk', 'matte' => 'Matte', 'rainbow' => 'Rainbow', 'twotone' => 'Two-tone', 'transparent' => 'Clear', 'sparkle' => 'Sparkle']; $remDefs = ['full' => 'Nearly full (85%+)', 'partial' => 'Partly used', 'low' => 'Low (under 15%)']; $clean = fn($v) => array_values(array_unique(array_filter((array)$v, fn($s) => is_string($s) && $s !== ''))); $lc = fn(?string $s) => mb_strtolower(trim((string)$s)); $fTypes = $clean($_GET['ft'] ?? []); $fBrands = $clean($_GET['fb'] ?? []); $fBrandKeys = array_map($lc, $fBrands); $fRem = array_values(array_intersect(array_keys($remDefs), $clean($_GET['fr'] ?? []))); $fProps = array_values(array_intersect(array_keys($flagDefs), $clean($_GET['fp'] ?? []))); $q = trim((string)($_GET['q'] ?? '')); $nFilters = count($fTypes) + count($fBrands) + count($fRem) + count($fProps) + ($q !== '' ? 1 : 0); $filterOpen = isset($_GET['filter']) || $nFilters > 0; $allCount = count($rows); if ($nFilters) { $rows = array_values(array_filter($rows, function (array $r) use ($fTypes, $fBrandKeys, $fRem, $fProps, $q, $lc) { if ($fTypes && !in_array($r['type'], $fTypes, true)) { return false; } if ($fBrandKeys && !in_array($lc($r['brand']), $fBrandKeys, true)) { return false; } if ($fRem) { $pct = spool_pct($r); $bucket = $pct >= 85 ? 'full' : ($pct < 15 ? 'low' : 'partial'); if (!in_array($bucket, $fRem, true)) { return false; } } if ($fProps) { $any = false; foreach ($fProps as $f) { if (!empty($r[$f])) { $any = true; break; } } if (!$any) { return false; } } if ($q !== '' && !str_contains($lc($r['type'] . ' ' . $r['brand'] . ' ' . $r['color_name'] . ' ' . $r['notes']), $lc($q))) { return false; } return true; })); } // index.php URL keeping the current bulk/filter query; null drops a key. // sort is left out — the session remembers it. function index_url(array $overrides = []): string { $params = $_GET; unset($params['cb'], $params['sort']); foreach ($overrides as $k => $v) { if ($v === null) { unset($params[$k]); } else { $params[$k] = $v; } } $qs = http_build_query($params); return 'index.php' . ($qs === '' ? '' : '?' . $qs); } // One spool's full card: list view renders it directly (optionally with the // bulk-edit checkbox), the color grid renders it hidden with a DOM id so // tapping the spool's mini swatch can reveal it. function spool_card(array $r, bool $bulk = false, ?string $domId = null): void { $cur = config()['currency']; $pct = spool_pct($r); $details = [round((float)$r['remaining_g']) . ' g left (' . $pct . '%)']; if ($r['cost'] !== null && $r['cost'] !== '') { $details[] = $cur . number_format((float)$r['cost'], 2); } // A part-used spool with no logged usage has an unknown history — // "never used" would be wrong, so the label is only shown when there is // a usage date or the spool is still full. if ($r['last_used_at'] || $pct >= 100) { $details[] = rel_time($r['last_used_at']); } ?>
>
· Abrasive High-flow Silk Matte Clear Sparkle
query('SELECT type FROM filaments GROUP BY type COLLATE NOCASE ORDER BY type COLLATE NOCASE')->fetchAll(PDO::FETCH_COLUMN); $brands = db()->query( "SELECT brand FROM filaments WHERE brand IS NOT NULL AND brand <> '' GROUP BY brand COLLATE NOCASE ORDER BY brand COLLATE NOCASE" )->fetchAll(PDO::FETCH_COLUMN); page_header('Inventory'); ?>

Inventory

+ Add spool Use filament Bulk edit

No filaments yet

Filters
Type
Manufacturer
Amount left
$label): ?>
Properties
$label): ?>
Search
Showing of spools

No spools match the filters.

Bulk edit

Tick the spools below, set only the fields you want to change (everything on "keep" stays as is), then apply.

'Abrasive', 'highflow' => 'High-flow', 'silk' => 'Silk', 'matte' => 'Matte', 'rainbow' => 'Rainbow', 'transparent' => 'Transparent', 'twotone' => 'Two-tone', 'sparkle' => 'Sparkle'] as $flag => $label): ?>
Done
$group): ?>

spool · View stats