120 lines · 4.9 KB
Raw Download
1
<?php
2
require_once __DIR__ . '/inc/layout.php';
3
4
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
5
header('Pragma: no-cache');
6
header('Expires: 0');
7
8
cache_buster_redirect();
9
10
if (!is_logged_in() && !config()['public_view']) {
11
    header('Location: login.php');
12
    exit;
13
}
14
15
$rows = db()->query('SELECT * FROM filaments')->fetchAll();
16
$cur  = config()['currency'];
17
18
// Filament with no recorded price is valued at a flat DEFAULT_VALUE per full
19
// spool; the remaining value scales that (or the real price) by how much is
20
// left. spool_pct() caps at 100, so an over-full spool never exceeds its value.
21
const DEFAULT_VALUE = 20.0;
22
23
$totalSpools    = count($rows);
24
$remainingG     = 0.0;   // grams still on the spools
25
$originalG      = 0.0;   // grams originally purchased (nominal)
26
$remainingValue = 0.0;   // $ worth of filament still on the spools
27
$purchaseValue  = 0.0;   // $ actually recorded as paid
28
$pricedCount    = 0;     // spools with a recorded price
29
$fullCount      = 0;     // spools still at (or above) 100%
30
$brands         = [];    // lower-cased brand => true
31
$colors         = [];    // color-name (or hex) key => true
32
$materials      = [];    // type => remaining grams
33
34
foreach ($rows as $r) {
35
    $orig = (float)($r['original_g'] ?? 0);
36
    if ($orig <= 0) {
37
        $orig = 1000.0;
38
    }
39
    $rem = max(0.0, (float)$r['remaining_g']);
40
    $originalG  += $orig;
41
    $remainingG += $rem;
42
43
    $hasPrice = $r['cost'] !== null && $r['cost'] !== '';
44
    if ($hasPrice) {
45
        $purchaseValue += (float)$r['cost'];
46
        $pricedCount++;
47
    }
48
    $spoolValue      = $hasPrice ? (float)$r['cost'] : DEFAULT_VALUE;
49
    $remainingValue += $spoolValue * min(1.0, $rem / $orig);
50
51
    $brand = trim((string)$r['brand']);
52
    if ($brand !== '') {
53
        $brands[mb_strtolower($brand)] = true;
54
    }
55
56
    // Group colors by name when the spool has one, otherwise by hex, so unnamed
57
    // spools of the same shade still collapse to a single "color".
58
    $name = trim((string)($r['color_name'] ?? ''));
59
    $hex  = mb_strtolower(trim((string)($r['color_hex'] ?? '')));
60
    if ($name !== '') {
61
        $colors['n:' . mb_strtolower($name)] = true;
62
    } elseif ($hex !== '') {
63
        $colors['h:' . $hex] = true;
64
    }
65
66
    $type = trim((string)$r['type']);
67
    if ($type !== '') {
68
        $materials[$type] = ($materials[$type] ?? 0) + $rem;
69
    }
70
71
    if (spool_pct($r) >= 100) {
72
        $fullCount++;
73
    }
74
}
75
$inUseCount = $totalSpools - $fullCount;
76
arsort($materials);
77
78
$money = fn(float $v): string => $cur . number_format($v, 2);
79
$kg    = fn(float $g): string => number_format($g / 1000, 2) . ' kg';
80
$grams = fn(float $g): string => number_format(round($g)) . ' g';
81
// kg for a kilo or more, grams below — reads better across the range.
82
$amt   = fn(float $g): string => $g >= 1000 ? number_format($g / 1000, 2) . ' kg' : number_format(round($g)) . ' g';
83
84
page_header('Stats');
85
?>
86
<h1>Collection stats</h1>
87
<?php if (!$totalSpools): ?>
88
<p class="muted">No filaments yet — <a href="index.php">add your first spool</a>.</p>
89
<?php else: ?>
90
<div class="statgrid">
91
  <div class="stat"><b><?= number_format($totalSpools) ?></b><span>Spools owned</span></div>
92
  <div class="stat"><b><?= e($kg($remainingG)) ?></b><span>Filament remaining · <?= e($grams($remainingG)) ?></span></div>
93
  <div class="stat"><b><?= e($kg($originalG)) ?></b><span>Originally purchased · <?= e($grams($originalG)) ?></span></div>
94
  <div class="stat"><b><?= e($money($remainingValue)) ?></b><span>Est. value remaining · <?= $money(DEFAULT_VALUE) ?>/spool if unpriced</span></div>
95
  <div class="stat"><b><?= e($money($purchaseValue)) ?></b><span>Purchase prices<?= $pricedCount < $totalSpools ? ' · ' . $pricedCount . ' of ' . $totalSpools . ' priced' : '' ?></span></div>
96
  <div class="stat"><b><?= number_format(count($brands)) ?></b><span>Unique brand<?= count($brands) === 1 ? '' : 's' ?></span></div>
97
  <div class="stat"><b><?= number_format(count($colors)) ?></b><span>Unique color<?= count($colors) === 1 ? '' : 's' ?></span></div>
98
  <div class="stat"><b><?= number_format(count($materials)) ?></b><span>Material type<?= count($materials) === 1 ? '' : 's' ?></span></div>
99
  <div class="stat"><b><?= number_format($inUseCount) ?> <span style="font-size:16px;color:var(--muted)">in use</span></b><span><?= number_format($fullCount) ?> still full</span></div>
100
</div>
101
102
<div class="card">
103
  <strong>Collection by material</strong>
104
  <p class="muted" style="margin-top:4px">Share of your remaining filament by weight.</p>
105
  <?php foreach ($materials as $type => $g):
106
      $pct = $remainingG > 0 ? round(100 * $g / $remainingG) : 0;
107
  ?>
108
  <div class="matrow">
109
    <div class="row"><span class="grow"><strong><?= e($type) ?></strong></span><span class="muted"><?= $pct ?>% · <?= e($amt($g)) ?></span></div>
110
    <div class="bar"><i style="width:<?= $pct ?>%"></i></div>
111
  </div>
112
  <?php endforeach; ?>
113
</div>
114
<div class="actions">
115
  <a class="btn secondary" href="index.php">Back to inventory</a>
116
</div>
117
<?php endif; ?>
118
<?php
119
page_footer();
120