237 lines · 12.3 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
// Same stale-cache defense as index.php; save/delete POSTs pass through.
9
cache_buster_redirect();
10
11
require_login();
12
13
$types = filament_types();
14
15
// Brands already in the inventory, deduplicated case-insensitively,
16
// offered as autocomplete suggestions on the brand field.
17
$brands = db()->query(
18
    "SELECT brand FROM filaments WHERE brand IS NOT NULL AND brand <> ''
19
     GROUP BY brand COLLATE NOCASE ORDER BY brand COLLATE NOCASE"
20
)->fetchAll(PDO::FETCH_COLUMN);
21
22
$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
23
24
// Manufacturers tend to reuse one spool design, so when adding a spool the
25
// empty-spool weight is pre-filled from the brand's most common value.
26
// Keyed by lowercased brand; ties go to the most recently added spool.
27
$spoolWeights = [];
28
if (!$id) {
29
    $wrows = db()->query(
30
        "SELECT brand, spool_weight_g AS w, COUNT(*) AS n FROM filaments
31
         WHERE brand IS NOT NULL AND brand <> '' AND spool_weight_g > 0
32
         GROUP BY brand COLLATE NOCASE, spool_weight_g
33
         ORDER BY n, MAX(id)"
34
    )->fetchAll();
35
    foreach ($wrows as $r) {
36
        $spoolWeights[mb_strtolower(trim($r['brand']))] = ['w' => (float)$r['w'], 'n' => (int)$r['n'], 'brand' => $r['brand']];
37
    }
38
}
39
$spool = null;
40
if ($id) {
41
    $st = db()->prepare('SELECT * FROM filaments WHERE id = ?');
42
    $st->execute([$id]);
43
    $spool = $st->fetch();
44
    if (!$spool) {
45
        flash('Spool not found.');
46
        header('Location: index.php');
47
        exit;
48
    }
49
}
50
51
$error = '';
52
53
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
54
    check_csrf();
55
56
    if (($_POST['action'] ?? '') === 'delete' && $spool) {
57
        backup_db();
58
        db()->prepare('DELETE FROM filaments WHERE id = ?')->execute([$id]);
59
        flash('Deleted ' . spool_label($spool) . '.');
60
        header('Location: index.php');
61
        exit;
62
    }
63
64
    $type   = trim((string)($_POST['type'] ?? ''));
65
    $brand  = trim((string)($_POST['brand'] ?? ''));
66
    $colorName = trim((string)($_POST['color_name'] ?? ''));
67
    $color  = (string)($_POST['color_hex'] ?? '');
68
    $color2 = (string)($_POST['color_hex2'] ?? '');
69
    $total  = (float)($_POST['total_weight_g'] ?? 0);
70
    $sw     = (float)($_POST['spool_weight_g'] ?? 0);
71
    $origRaw = trim((string)($_POST['original_g'] ?? ''));
72
    $orig   = $origRaw === '' ? 1000.0 : (float)$origRaw;
73
    $cost   = trim((string)($_POST['cost'] ?? ''));
74
    $cost   = $cost === '' ? null : (float)$cost;
75
    $notes  = trim((string)($_POST['notes'] ?? ''));
76
    $abrasive = isset($_POST['abrasive']) ? 1 : 0;
77
    $highflow = isset($_POST['highflow']) ? 1 : 0;
78
    $silk     = isset($_POST['silk']) ? 1 : 0;
79
    $matte    = isset($_POST['matte']) ? 1 : 0;
80
    $rainbow  = isset($_POST['rainbow']) ? 1 : 0;
81
    $transparent = isset($_POST['transparent']) ? 1 : 0;
82
    $twotone  = isset($_POST['twotone']) ? 1 : 0;
83
    $sparkle  = isset($_POST['sparkle']) ? 1 : 0;
84
    $small    = isset($_POST['small_spool']) ? 1 : 0;
85
86
    if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) {
87
        $color = '#888888';
88
    }
89
    if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color2)) {
90
        $color2 = $twotone ? '#888888' : null;
91
    }
92
    if ($type === '') {
93
        $error = 'Filament type is required.';
94
    } elseif ($total <= 0 || $sw < 0 || $total <= $sw) {
95
        $error = 'Total weight must be greater than the spool weight.';
96
    } elseif ($orig <= 0) {
97
        $error = 'Original spool amount must be greater than zero.';
98
    } else {
99
        $net = $total - $sw;
100
        backup_db();
101
        if ($spool) {
102
            // A changed weight means the spool was re-weighed: reset remaining
103
            // to total - spool. Otherwise keep remaining (usage already deducted).
104
            $weightsChanged = abs($total - (float)$spool['total_weight_g']) > 0.001
105
                           || abs($sw - (float)$spool['spool_weight_g']) > 0.001;
106
            $remaining = $weightsChanged ? $net : (float)$spool['remaining_g'];
107
            db()->prepare(
108
                'UPDATE filaments SET type=?, brand=?, color_name=?, color_hex=?, color_hex2=?, total_weight_g=?, spool_weight_g=?,
109
                 original_g=?, remaining_g=?, abrasive=?, highflow=?, silk=?, matte=?, rainbow=?, transparent=?, twotone=?, sparkle=?, small_spool=?, cost=?, notes=?, updated_at=datetime(\'now\') WHERE id=?'
110
            )->execute([$type, $brand, $colorName, $color, $color2, $total, $sw, $orig, $remaining, $abrasive, $highflow, $silk, $matte, $rainbow, $transparent, $twotone, $sparkle, $small, $cost, $notes, $id]);
111
            flash('Saved ' . spool_label(['brand' => $brand, 'type' => $type, 'color_name' => $colorName]) . '.');
112
            $dest = 'index.php';
113
        } else {
114
            db()->prepare(
115
                'INSERT INTO filaments (type, brand, color_name, color_hex, color_hex2, total_weight_g, spool_weight_g,
116
                 original_g, remaining_g, abrasive, highflow, silk, matte, rainbow, transparent, twotone, sparkle, small_spool, cost, notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
117
            )->execute([$type, $brand, $colorName, $color, $color2, $total, $sw, $orig, $net, $abrasive, $highflow, $silk, $matte, $rainbow, $transparent, $twotone, $sparkle, $small, $cost, $notes]);
118
            flash('Added ' . spool_label(['brand' => $brand, 'type' => $type, 'color_name' => $colorName]) . '.');
119
            // Land on the "recently added" sort so the new spool is at the top.
120
            $dest = 'index.php?sort=added';
121
        }
122
        header('Location: index.php');
123
        exit;
124
    }
125
    // Validation failed: re-show submitted values.
126
    $spool = array_merge($spool ?? [], [
127
        'type' => $type, 'brand' => $brand, 'color_name' => $colorName, 'color_hex' => $color,
128
        'color_hex2' => $color2, 'total_weight_g' => $total, 'spool_weight_g' => $sw,
129
        'original_g' => $orig, 'cost' => $cost,
130
        'abrasive' => $abrasive, 'highflow' => $highflow, 'silk' => $silk, 'matte' => $matte,
131
        'rainbow' => $rainbow, 'transparent' => $transparent, 'twotone' => $twotone, 'sparkle' => $sparkle,
132
        'small_spool' => $small, 'notes' => $notes,
133
    ]);
134
}
135
136
page_header($id ? 'Edit spool' : 'Add spool');
137
?>
138
<h1><?= $id ? 'Edit spool' : 'Add spool' ?></h1>
139
<?php if ($error): ?>
140
<p class="error"><?= e($error) ?></p>
141
<?php endif; ?>
142
<form method="post" id="spoolForm">
143
  <?= csrf_field() ?>
144
  <?php if ($id): ?><input type="hidden" name="id" value="<?= $id ?>"><?php endif; ?>
145
146
  <label for="type">Type</label>
147
  <?php
148
  $curType = (string)($spool['type'] ?? '');
149
  // A spool saved with a type outside the list must stay selectable.
150
  $typeOptions = in_array($curType, $types, true) || $curType === '' ? $types : array_merge($types, [$curType]);
151
  ?>
152
  <select id="type" name="type" required>
153
    <option value="" disabled <?= $curType === '' ? 'selected' : '' ?>>Select type…</option>
154
    <?php foreach ($typeOptions as $t): ?>
155
    <option value="<?= e($t) ?>" <?= $t === $curType ? 'selected' : '' ?>><?= e($t) ?></option>
156
    <?php endforeach; ?>
157
  </select>
158
159
  <label for="color_name">Color name (optional)</label>
160
  <input id="color_name" name="color_name" value="<?= e($spool['color_name'] ?? '') ?>" placeholder="Galaxy Black, Signal White…">
161
162
  <label for="brand">Manufacturer / brand</label>
163
  <input id="brand" name="brand" list="brandList" value="<?= e($spool['brand'] ?? '') ?>" placeholder="Prusament, Polymaker…">
164
  <datalist id="brandList">
165
    <?php foreach ($brands as $b): ?><option value="<?= e($b) ?>"></option><?php endforeach; ?>
166
  </datalist>
167
168
  <label for="color">Color</label>
169
  <div class="row">
170
    <input type="color" id="color" name="color_hex" value="<?= e($spool['color_hex'] ?? '#da6019') ?>">
171
    <input type="color" id="color2" name="color_hex2" aria-label="Second color" value="<?= e(!empty($spool['color_hex2']) ? $spool['color_hex2'] : '#3355ff') ?>" <?= empty($spool['twotone']) ? 'hidden' : '' ?>>
172
    <button type="button" class="secondary" id="camBtn" hidden>&#128247; Camera</button>
173
    <label class="btn secondary" style="margin:0">&#128444; Photo<input type="file" id="photoIn" accept="image/*" capture="environment" hidden></label>
174
  </div>
175
  <div id="camPanel" hidden>
176
    <video id="camVideo" autoplay playsinline muted></video>
177
    <div class="actions">
178
      <button type="button" id="sampleBtn">Sample color</button>
179
      <button type="button" class="secondary" id="camClose">Close</button>
180
    </div>
181
  </div>
182
  <p class="muted" id="colorMsg">Point the camera at the filament — the average color of the center of the frame is sampled.</p>
183
  <label class="check"><input type="checkbox" name="rainbow" <?= !empty($spool['rainbow']) ? 'checked' : '' ?>> Rainbow / multicolor (shows an animated all-hue swatch)</label>
184
  <label class="check"><input type="checkbox" name="twotone" id="twotoneChk" <?= !empty($spool['twotone']) ? 'checked' : '' ?>> Color-shifting / two-tone (pick a second color; a moving gradient blends them)</label>
185
  <label class="check"><input type="checkbox" name="transparent" <?= !empty($spool['transparent']) ? 'checked' : '' ?>> Transparent / clear (a checkerboard shows through the swatch)</label>
186
  <label class="check"><input type="checkbox" name="sparkle" <?= !empty($spool['sparkle']) ? 'checked' : '' ?>> Sparkle / glitter (galaxy-style flecks twinkle on the swatch)</label>
187
188
  <label for="total_weight_g">Total weight (g) — spool + filament</label>
189
  <input type="number" id="total_weight_g" name="total_weight_g" step="0.1" min="0" inputmode="decimal" required value="<?= e(isset($spool['total_weight_g']) ? (string)(float)$spool['total_weight_g'] : '') ?>">
190
191
  <label for="spool_weight_g">Empty spool weight (g)</label>
192
  <input type="number" id="spool_weight_g" name="spool_weight_g" step="0.1" min="0" inputmode="decimal" required value="<?= e(isset($spool['spool_weight_g']) ? (string)(float)$spool['spool_weight_g'] : '') ?>">
193
  <p class="muted" id="swMsg"></p>
194
  <p class="muted" id="netMsg"></p>
195
196
  <label for="original_g">Original spool amount (g) — filament when new</label>
197
  <input type="number" id="original_g" name="original_g" step="0.1" min="0" inputmode="decimal" required value="<?= e(isset($spool['original_g']) && $spool['original_g'] !== null ? (string)(float)$spool['original_g'] : '1000') ?>">
198
  <div class="chips" id="origPresets" role="group" aria-label="Common spool amounts">
199
    <button type="button" class="chip" data-g="1000">1 kg</button>
200
    <button type="button" class="chip" data-g="500">500 g</button>
201
    <button type="button" class="chip" data-g="250">250 g</button>
202
    <button type="button" class="chip" data-g="100">100 g</button>
203
  </div>
204
  <label class="check"><input type="checkbox" name="small_spool" <?= !empty($spool['small_spool']) ? 'checked' : '' ?>> Small spool (draws a smaller swatch — 250 g and under are small automatically)</label>
205
206
  <label for="cost">Cost (<?= e(config()['currency']) ?>)</label>
207
  <input type="number" id="cost" name="cost" step="0.01" min="0" inputmode="decimal" value="<?= e(isset($spool['cost']) && $spool['cost'] !== null ? (string)(float)$spool['cost'] : '') ?>">
208
209
  <div class="check-row">
210
    <label class="check"><input type="checkbox" name="abrasive" <?= !empty($spool['abrasive']) ? 'checked' : '' ?>> Abrasive (CF, GF, glow…)</label>
211
    <label class="check"><input type="checkbox" name="highflow" <?= !empty($spool['highflow']) ? 'checked' : '' ?>> High-flow</label>
212
    <label class="check"><input type="checkbox" name="silk" <?= !empty($spool['silk']) ? 'checked' : '' ?>> Silk</label>
213
    <label class="check"><input type="checkbox" name="matte" <?= !empty($spool['matte']) ? 'checked' : '' ?>> Matte</label>
214
  </div>
215
216
  <label for="notes">Notes</label>
217
  <textarea id="notes" name="notes" rows="1"><?= e($spool['notes'] ?? '') ?></textarea>
218
219
  <div class="actions">
220
    <button>Save</button>
221
    <a class="btn secondary" href="index.php">Cancel</a>
222
  </div>
223
</form>
224
<?php if ($spoolWeights): ?>
225
<script type="application/json" id="spoolWeights"><?= json_encode($spoolWeights, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE) ?></script>
226
<?php endif; ?>
227
<?php if ($id): ?>
228
<form method="post" onsubmit="return confirm('Delete this spool and its usage history?')">
229
  <?= csrf_field() ?>
230
  <input type="hidden" name="id" value="<?= $id ?>">
231
  <input type="hidden" name="action" value="delete">
232
  <div class="actions"><button class="danger">Delete spool</button></div>
233
</form>
234
<?php endif; ?>
235
<?php
236
page_footer();
237