diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 3f14bfa..e5ace3d 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -123,7 +123,7 @@ export const deviceDocuments = pgTable( (table) => [index('device_documents_device_idx').on(table.deviceId)] ); -// ─── Components ───────────────────────────────────────────────────── +// ─── Components (type definitions) ────────────────────────────────── export const components = pgTable( 'components', @@ -133,10 +133,32 @@ export const components = pgTable( componentType: text('component_type').notNull(), brand: text('brand'), partNumber: text('part_number'), + specs: text('specs'), + notes: text('notes'), + defaultCondition: text('default_condition').notNull().default('Working'), + defaultFirmwareVersion: text('default_firmware_version'), + defaultLocationId: uuid('default_location_id').references(() => locations.id), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() + }, + (table) => [ + index('components_type_idx').on(table.componentType) + ] +); + +// ─── Component Instances (individual physical units) ──────────────── + +export const componentInstances = pgTable( + 'component_instances', + { + id: uuid('id').defaultRandom().primaryKey(), + componentId: uuid('component_id') + .notNull() + .references(() => components.id, { onDelete: 'cascade' }), + instanceNumber: integer('instance_number').notNull().default(1), serialNumber: text('serial_number'), condition: text('condition').notNull().default('Working'), firmwareVersion: text('firmware_version'), - specs: text('specs'), notes: text('notes'), currentDeviceId: uuid('current_device_id').references(() => devices.id, { onDelete: 'set null' @@ -146,10 +168,10 @@ export const components = pgTable( updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull() }, (table) => [ - index('components_type_idx').on(table.componentType), - index('components_device_idx').on(table.currentDeviceId), - index('components_location_idx').on(table.locationId), - check('components_condition_check', sql`${table.condition} IN ('Working', 'Faulty', 'Unknown', 'Refurbished')`) + index('instances_component_idx').on(table.componentId), + index('instances_device_idx').on(table.currentDeviceId), + index('instances_location_idx').on(table.locationId), + check('instances_condition_check', sql`${table.condition} IN ('Working', 'Faulty', 'Unknown', 'Refurbished')`) ] ); @@ -194,9 +216,9 @@ export const installationLog = pgTable( 'installation_log', { id: uuid('id').defaultRandom().primaryKey(), - componentId: uuid('component_id') + instanceId: uuid('instance_id') .notNull() - .references(() => components.id, { onDelete: 'cascade' }), + .references(() => componentInstances.id, { onDelete: 'cascade' }), deviceId: uuid('device_id') .notNull() .references(() => devices.id, { onDelete: 'cascade' }), @@ -206,7 +228,7 @@ export const installationLog = pgTable( performedAt: timestamp('performed_at', { withTimezone: true }).defaultNow().notNull() }, (table) => [ - index('install_log_component_idx').on(table.componentId), + index('install_log_instance_idx').on(table.instanceId), index('install_log_device_idx').on(table.deviceId), index('install_log_date_idx').on(table.performedAt), check('install_log_action_check', sql`${table.action} IN ('installed', 'removed', 'swapped')`) diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts index 481febc..e9f2790 100644 --- a/src/routes/(app)/+layout.server.ts +++ b/src/routes/(app)/+layout.server.ts @@ -1,7 +1,7 @@ import type { LayoutServerLoad } from './$types'; import { redirect } from '@sveltejs/kit'; import { db } from '$lib/server/db/index.js'; -import { devices, components } from '$lib/server/db/schema.js'; +import { devices, componentInstances } from '$lib/server/db/schema.js'; import { count, eq, or, and } from 'drizzle-orm'; export const load: LayoutServerLoad = async ({ locals }) => { @@ -10,7 +10,7 @@ export const load: LayoutServerLoad = async ({ locals }) => { } const [deviceCount] = await db.select({ value: count() }).from(devices).where(eq(devices.disabled, false)); - const [componentCount] = await db.select({ value: count() }).from(components); + const [componentCount] = await db.select({ value: count() }).from(componentInstances); const [repairCount] = await db .select({ value: count() }) .from(devices) diff --git a/src/routes/(app)/+page.server.ts b/src/routes/(app)/+page.server.ts index a56feee..24b2455 100644 --- a/src/routes/(app)/+page.server.ts +++ b/src/routes/(app)/+page.server.ts @@ -1,6 +1,6 @@ import type { PageServerLoad } from './$types'; import { db } from '$lib/server/db/index.js'; -import { devices, components, installationLog } from '$lib/server/db/schema.js'; +import { devices, components, componentInstances, installationLog } from '$lib/server/db/schema.js'; import { count, eq, or, and, desc, sql } from 'drizzle-orm'; export const load: PageServerLoad = async () => { @@ -16,7 +16,7 @@ export const load: PageServerLoad = async () => { .select({ value: count() }) .from(devices) .where(and(active, eq(devices.category, 'Audio Equipment'))); - const [totalComponents] = await db.select({ value: count() }).from(components); + const [totalComponents] = await db.select({ value: count() }).from(componentInstances); // Condition breakdown const conditionBreakdown = await db @@ -33,13 +33,15 @@ export const load: PageServerLoad = async () => { .select({ action: installationLog.action, performedAt: installationLog.performedAt, - componentId: installationLog.componentId, + instanceId: installationLog.instanceId, + instanceNumber: componentInstances.instanceNumber, deviceId: installationLog.deviceId, componentTitle: components.title, deviceTitle: devices.title }) .from(installationLog) - .innerJoin(components, eq(installationLog.componentId, components.id)) + .innerJoin(componentInstances, eq(installationLog.instanceId, componentInstances.id)) + .innerJoin(components, eq(componentInstances.componentId, components.id)) .innerJoin(devices, eq(installationLog.deviceId, devices.id)) .orderBy(desc(installationLog.performedAt)) .limit(10); diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 01b0af6..9bab5c8 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -71,7 +71,7 @@
- {entry.componentTitle} + {entry.componentTitle} #{entry.instanceNumber} {entry.action === 'installed' ? 'into' : entry.action === 'removed' ? 'from' : 'in'} {entry.deviceTitle}
diff --git a/src/routes/(app)/components/+page.server.ts b/src/routes/(app)/components/+page.server.ts index b2e75da..ce97d65 100644 --- a/src/routes/(app)/components/+page.server.ts +++ b/src/routes/(app)/components/+page.server.ts @@ -1,29 +1,22 @@ import type { PageServerLoad } from './$types'; import { db } from '$lib/server/db/index.js'; -import { components, devices, locations } from '$lib/server/db/schema.js'; -import { eq, ilike, or, and, isNull, isNotNull, count, desc } from 'drizzle-orm'; +import { components, componentInstances, devices } from '$lib/server/db/schema.js'; +import { eq, ilike, or, and, isNull, isNotNull, count, desc, sql } from 'drizzle-orm'; export const load: PageServerLoad = async ({ url }) => { const type = url.searchParams.get('type'); - const condition = url.searchParams.get('condition'); - const status = url.searchParams.get('status'); // 'installed' | 'storage' const search = url.searchParams.get('q'); const page = Math.max(1, Number(url.searchParams.get('page') ?? 1)); const pageSize = 24; const conditions = []; - if (type) conditions.push(eq(components.componentType, type)); - if (condition) conditions.push(eq(components.condition, condition)); - if (status === 'installed') conditions.push(isNotNull(components.currentDeviceId)); - if (status === 'storage') conditions.push(isNull(components.currentDeviceId)); if (search) { conditions.push( or( ilike(components.title, `%${search}%`), ilike(components.brand, `%${search}%`), - ilike(components.partNumber, `%${search}%`), - ilike(components.serialNumber, `%${search}%`) + ilike(components.partNumber, `%${search}%`) )! ); } @@ -39,24 +32,44 @@ export const load: PageServerLoad = async ({ url }) => { componentType: components.componentType, brand: components.brand, partNumber: components.partNumber, - condition: components.condition, - currentDeviceId: components.currentDeviceId, - deviceTitle: devices.title, - locationName: locations.name + specs: components.specs }) .from(components) - .leftJoin(devices, eq(components.currentDeviceId, devices.id)) - .leftJoin(locations, eq(components.locationId, locations.id)) .where(where) - .orderBy(desc(components.updatedAt)) + .orderBy(components.title) .limit(pageSize) .offset((page - 1) * pageSize); + // Get instance counts per component + const componentIds = componentList.map((c) => c.id); + let instanceCounts: Record- {c.componentType} - {#if c.brand}· {c.brand}{/if} + {#if c.brand}{c.brand}{/if} + {#if c.partNumber} · P/N: {c.partNumber}{/if}
+ +- Installed in - {c.deviceTitle} -
- {:else if c.locationName} -In storage at {#if data.parentLocationName}{data.parentLocationName} › {/if}{c.locationName}
+No instances. Add some above.
{:else} -In storage (no location set)
+{entry.action} + #{entry.instanceNumber} {entry.action === 'installed' ? 'into' : entry.action === 'removed' ? 'from' : 'in'} {entry.deviceTitle}
@@ -98,7 +218,7 @@ {/if}{timeAgo(entry.performedAt)} - {#if entry.performedBy}· {entry.performedBy}{/if} + {#if entry.performedBy}· {entry.performedBy}{/if}
No documents.
{:else} @@ -178,10 +290,8 @@ diff --git a/src/routes/(app)/components/[id]/edit/+page.server.ts b/src/routes/(app)/components/[id]/edit/+page.server.ts index 51a6d8d..3b86f9c 100644 --- a/src/routes/(app)/components/[id]/edit/+page.server.ts +++ b/src/routes/(app)/components/[id]/edit/+page.server.ts @@ -1,6 +1,6 @@ import type { PageServerLoad, Actions } from './$types'; import { db } from '$lib/server/db/index.js'; -import { components, devices, locations } from '$lib/server/db/schema.js'; +import { components, locations } from '$lib/server/db/schema.js'; import { eq } from 'drizzle-orm'; import { error, fail, redirect } from '@sveltejs/kit'; import { z } from 'zod'; @@ -10,12 +10,11 @@ const componentSchema = z.object({ componentType: z.string().min(1), brand: z.string().optional(), partNumber: z.string().optional(), - serialNumber: z.string().optional(), - condition: z.enum(['Working', 'Faulty', 'Unknown', 'Refurbished']), - firmwareVersion: z.string().optional(), specs: z.string().optional(), notes: z.string().optional(), - locationId: z.string().uuid().optional().or(z.literal('')) + defaultCondition: z.enum(['Working', 'Faulty', 'Unknown', 'Refurbished']), + defaultFirmwareVersion: z.string().optional(), + defaultLocationId: z.string().uuid().optional().or(z.literal('')) }); export const load: PageServerLoad = async ({ params }) => { @@ -46,12 +45,11 @@ export const actions: Actions = { componentType: data.componentType, brand: data.brand || null, partNumber: data.partNumber || null, - serialNumber: data.serialNumber || null, - condition: data.condition, - firmwareVersion: data.firmwareVersion || null, specs: data.specs || null, notes: data.notes || null, - locationId: data.locationId || null, + defaultCondition: data.defaultCondition, + defaultFirmwareVersion: data.defaultFirmwareVersion || null, + defaultLocationId: data.defaultLocationId || null, updatedAt: new Date() }) .where(eq(components.id, params.id)); diff --git a/src/routes/(app)/components/[id]/edit/+page.svelte b/src/routes/(app)/components/[id]/edit/+page.svelte index 4cea9d9..07ec490 100644 --- a/src/routes/(app)/components/[id]/edit/+page.svelte +++ b/src/routes/(app)/components/[id]/edit/+page.svelte @@ -20,7 +20,7 @@