Add SI value sorting to parts: parse Ohms/Farads/Henrys/Hz numerically
Deploy to LXC / deploy (push) Failing after 14m27s

- New value_numeric column (double precision) stores parsed base-SI value
- parseValueNumeric() handles SI prefixes (p/n/µ/m/k/M/G) for F, H, Ω, Hz
- Computed on save in new/edit actions
- List page sortable by value_numeric (sorts within category, nulls last)
- Sort toggle added to filter bar

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 16:51:58 +07:00
co-authored by Claude Sonnet 4.6
parent dc9217f3b6
commit 4eed83e4dd
9 changed files with 2476 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "parts" ADD COLUMN "value_numeric" double precision;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -8,6 +8,13 @@
"when": 1782725202288,
"tag": "0000_clear_sister_grimm",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1782726648360,
"tag": "0001_rainy_rictor",
"breakpoints": true
}
]
}
+2
View File
@@ -5,6 +5,7 @@ import {
integer,
boolean,
timestamp,
doublePrecision,
index,
check
} from 'drizzle-orm/pg-core';
@@ -383,6 +384,7 @@ export const parts = pgTable(
category: text('category').notNull(),
mpn: text('mpn'),
value: text('value'),
valueNumeric: doublePrecision('value_numeric'),
voltage: text('voltage'),
tolerance: text('tolerance'),
power: text('power'),
+32
View File
@@ -0,0 +1,32 @@
const PREFIX: Record<string, number> = {
p: 1e-12,
n: 1e-9,
u: 1e-6,
µ: 1e-6,
m: 1e-3,
k: 1e3,
K: 1e3,
M: 1e6,
G: 1e9
};
// Parses a human value string into a base-SI number (Farads, Ohms, Henrys, Hz).
// Returns null if unparseable (e.g. part codes like "BC547", "1N4148").
export function parseValueNumeric(value: string | null | undefined): number | null {
if (!value) return null;
const s = value.trim();
// Match: digits, optional decimal, optional SI prefix, optional unit letters
// Handles: "100µF", "10kΩ", "4.7uH", "32.768kHz", "47R", "1M", "220"
const match = s.match(/^([0-9]+(?:\.[0-9]+)?)\s*([pnuµmkKMG]?)\s*([FHhzΩRω]|Hz|hz|Ohm|ohm)?/i);
if (!match) return null;
const num = parseFloat(match[1]);
if (isNaN(num)) return null;
const prefix = match[2] ?? '';
const mult = PREFIX[prefix] ?? 1;
return num * mult;
}
+10 -3
View File
@@ -1,12 +1,13 @@
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
import { parts, locations } from '$lib/server/db/schema.js';
import { eq, ilike, or, and, count } from 'drizzle-orm';
import { eq, ilike, or, and, count, asc, sql } from 'drizzle-orm';
export const load: PageServerLoad = async ({ url }) => {
const category = url.searchParams.get('category');
const mounting = url.searchParams.get('mounting');
const search = url.searchParams.get('q');
const sort = url.searchParams.get('sort') ?? 'category'; // 'category' | 'value'
const page = Math.max(1, Number(url.searchParams.get('page') ?? 1));
const pageSize = 48;
@@ -28,6 +29,11 @@ export const load: PageServerLoad = async ({ url }) => {
const [totalResult] = await db.select({ value: count() }).from(parts).where(where);
const orderBy =
sort === 'value'
? [asc(parts.category), sql`${parts.valueNumeric} ASC NULLS LAST`, asc(parts.value)]
: [asc(parts.category), asc(parts.value)];
const partList = await db
.select({
id: parts.id,
@@ -35,6 +41,7 @@ export const load: PageServerLoad = async ({ url }) => {
category: parts.category,
mpn: parts.mpn,
value: parts.value,
valueNumeric: parts.valueNumeric,
voltage: parts.voltage,
tolerance: parts.tolerance,
power: parts.power,
@@ -47,7 +54,7 @@ export const load: PageServerLoad = async ({ url }) => {
.from(parts)
.leftJoin(locations, eq(parts.locationId, locations.id))
.where(where)
.orderBy(parts.category, parts.value)
.orderBy(...orderBy)
.limit(pageSize)
.offset((page - 1) * pageSize);
@@ -56,6 +63,6 @@ export const load: PageServerLoad = async ({ url }) => {
total: totalResult?.value ?? 0,
page,
pageSize,
filters: { category, mounting, search }
filters: { category, mounting, search, sort }
};
};
+11
View File
@@ -21,6 +21,7 @@
}
const totalPages = $derived(Math.ceil(data.total / data.pageSize));
const sortedByValue = $derived(data.filters.sort === 'value');
function mountingColor(mounting: string | null) {
if (mounting === 'SMT') return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300';
@@ -66,6 +67,16 @@
<option value="THT" selected={data.filters.mounting === 'THT'}>THT</option>
<option value="SMT" selected={data.filters.mounting === 'SMT'}>SMT</option>
</select>
<div class="ml-auto flex items-center gap-1 rounded-md border border-gray-300 p-0.5 dark:border-gray-600">
<button onclick={() => applyFilter('sort', null)}
class="rounded px-3 py-1 text-sm {!sortedByValue ? 'bg-gray-100 font-medium text-gray-800 dark:bg-gray-600 dark:text-white' : 'text-gray-500 dark:text-gray-400'}">
Category
</button>
<button onclick={() => applyFilter('sort', 'value')}
class="rounded px-3 py-1 text-sm {sortedByValue ? 'bg-gray-100 font-medium text-gray-800 dark:bg-gray-600 dark:text-white' : 'text-gray-500 dark:text-gray-400'}">
Value ↑
</button>
</div>
</div>
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">{data.total} part{data.total !== 1 ? 's' : ''}</p>
@@ -4,6 +4,7 @@ import { parts, locations } from '$lib/server/db/schema.js';
import { eq, sql } from 'drizzle-orm';
import { error, fail, redirect } from '@sveltejs/kit';
import { z } from 'zod';
import { parseValueNumeric } from '$lib/utils/valueParser.js';
const partSchema = z.object({
name: z.string().optional(),
@@ -56,6 +57,7 @@ export const actions: Actions = {
category: data.category,
mpn: data.mpn || null,
value: data.value || null,
valueNumeric: parseValueNumeric(data.value),
voltage: data.voltage || null,
tolerance: data.tolerance || null,
power: data.power || null,
@@ -3,6 +3,7 @@ import { db } from '$lib/server/db/index.js';
import { parts, locations } from '$lib/server/db/schema.js';
import { fail, redirect } from '@sveltejs/kit';
import { z } from 'zod';
import { parseValueNumeric } from '$lib/utils/valueParser.js';
const partSchema = z.object({
name: z.string().optional(),
@@ -47,6 +48,7 @@ export const actions: Actions = {
category: data.category,
mpn: data.mpn || null,
value: data.value || null,
valueNumeric: parseValueNumeric(data.value),
voltage: data.voltage || null,
tolerance: data.tolerance || null,
power: data.power || null,