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,
|
"when": 1782725202288,
|
||||||
"tag": "0000_clear_sister_grimm",
|
"tag": "0000_clear_sister_grimm",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782726648360,
|
||||||
|
"tag": "0001_rainy_rictor",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
integer,
|
integer,
|
||||||
boolean,
|
boolean,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
doublePrecision,
|
||||||
index,
|
index,
|
||||||
check
|
check
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
@@ -383,6 +384,7 @@ export const parts = pgTable(
|
|||||||
category: text('category').notNull(),
|
category: text('category').notNull(),
|
||||||
mpn: text('mpn'),
|
mpn: text('mpn'),
|
||||||
value: text('value'),
|
value: text('value'),
|
||||||
|
valueNumeric: doublePrecision('value_numeric'),
|
||||||
voltage: text('voltage'),
|
voltage: text('voltage'),
|
||||||
tolerance: text('tolerance'),
|
tolerance: text('tolerance'),
|
||||||
power: text('power'),
|
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 type { PageServerLoad } from './$types';
|
||||||
import { db } from '$lib/server/db/index.js';
|
import { db } from '$lib/server/db/index.js';
|
||||||
import { parts, locations } from '$lib/server/db/schema.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 }) => {
|
export const load: PageServerLoad = async ({ url }) => {
|
||||||
const category = url.searchParams.get('category');
|
const category = url.searchParams.get('category');
|
||||||
const mounting = url.searchParams.get('mounting');
|
const mounting = url.searchParams.get('mounting');
|
||||||
const search = url.searchParams.get('q');
|
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 page = Math.max(1, Number(url.searchParams.get('page') ?? 1));
|
||||||
const pageSize = 48;
|
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 [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
|
const partList = await db
|
||||||
.select({
|
.select({
|
||||||
id: parts.id,
|
id: parts.id,
|
||||||
@@ -35,6 +41,7 @@ export const load: PageServerLoad = async ({ url }) => {
|
|||||||
category: parts.category,
|
category: parts.category,
|
||||||
mpn: parts.mpn,
|
mpn: parts.mpn,
|
||||||
value: parts.value,
|
value: parts.value,
|
||||||
|
valueNumeric: parts.valueNumeric,
|
||||||
voltage: parts.voltage,
|
voltage: parts.voltage,
|
||||||
tolerance: parts.tolerance,
|
tolerance: parts.tolerance,
|
||||||
power: parts.power,
|
power: parts.power,
|
||||||
@@ -47,7 +54,7 @@ export const load: PageServerLoad = async ({ url }) => {
|
|||||||
.from(parts)
|
.from(parts)
|
||||||
.leftJoin(locations, eq(parts.locationId, locations.id))
|
.leftJoin(locations, eq(parts.locationId, locations.id))
|
||||||
.where(where)
|
.where(where)
|
||||||
.orderBy(parts.category, parts.value)
|
.orderBy(...orderBy)
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset((page - 1) * pageSize);
|
.offset((page - 1) * pageSize);
|
||||||
|
|
||||||
@@ -56,6 +63,6 @@ export const load: PageServerLoad = async ({ url }) => {
|
|||||||
total: totalResult?.value ?? 0,
|
total: totalResult?.value ?? 0,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
filters: { category, mounting, search }
|
filters: { category, mounting, search, sort }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = $derived(Math.ceil(data.total / data.pageSize));
|
const totalPages = $derived(Math.ceil(data.total / data.pageSize));
|
||||||
|
const sortedByValue = $derived(data.filters.sort === 'value');
|
||||||
|
|
||||||
function mountingColor(mounting: string | null) {
|
function mountingColor(mounting: string | null) {
|
||||||
if (mounting === 'SMT') return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300';
|
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="THT" selected={data.filters.mounting === 'THT'}>THT</option>
|
||||||
<option value="SMT" selected={data.filters.mounting === 'SMT'}>SMT</option>
|
<option value="SMT" selected={data.filters.mounting === 'SMT'}>SMT</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
|
|
||||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">{data.total} part{data.total !== 1 ? 's' : ''}</p>
|
<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 { eq, sql } from 'drizzle-orm';
|
||||||
import { error, fail, redirect } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { parseValueNumeric } from '$lib/utils/valueParser.js';
|
||||||
|
|
||||||
const partSchema = z.object({
|
const partSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -56,6 +57,7 @@ export const actions: Actions = {
|
|||||||
category: data.category,
|
category: data.category,
|
||||||
mpn: data.mpn || null,
|
mpn: data.mpn || null,
|
||||||
value: data.value || null,
|
value: data.value || null,
|
||||||
|
valueNumeric: parseValueNumeric(data.value),
|
||||||
voltage: data.voltage || null,
|
voltage: data.voltage || null,
|
||||||
tolerance: data.tolerance || null,
|
tolerance: data.tolerance || null,
|
||||||
power: data.power || 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 { parts, locations } from '$lib/server/db/schema.js';
|
||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { parseValueNumeric } from '$lib/utils/valueParser.js';
|
||||||
|
|
||||||
const partSchema = z.object({
|
const partSchema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -47,6 +48,7 @@ export const actions: Actions = {
|
|||||||
category: data.category,
|
category: data.category,
|
||||||
mpn: data.mpn || null,
|
mpn: data.mpn || null,
|
||||||
value: data.value || null,
|
value: data.value || null,
|
||||||
|
valueNumeric: parseValueNumeric(data.value),
|
||||||
voltage: data.voltage || null,
|
voltage: data.voltage || null,
|
||||||
tolerance: data.tolerance || null,
|
tolerance: data.tolerance || null,
|
||||||
power: data.power || null,
|
power: data.power || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user