Add SI value sorting to parts: parse Ohms/Farads/Henrys/Hz numerically
Deploy to LXC / deploy (push) Failing after 14m27s
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:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "parts" ADD COLUMN "value_numeric" double precision;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 }
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user