Restructure components into types + instances
Deploy to LXC / deploy (push) Successful in 29s

Components are now split into two concepts:
- Component (type definition): title, componentType, brand, partNumber,
  specs, notes, default condition/firmware/location
- Component Instance (individual physical unit): serialNumber, condition,
  firmwareVersion, notes, currentDeviceId, locationId

Key changes:
- New component_instances table with per-unit tracking
- Component list shows cards with total/installed/available counts
- Component detail page shows all instances with inline edit
- Add instances: bulk (quantity) or one at a time
- Each instance has install/remove/edit/delete actions
- Installation log now references instances, not components
- Device detail shows installed instances with instance numbers
- Dashboard and sidebar counts use instance totals
- Location pages show instances, not component types
- Labels show component type info (serial is per-instance)

IMPORTANT: Run db:push on the server after deploy to create the new
tables and columns. Existing component data will need manual migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:41:06 +07:00
parent bb8a96d281
commit 5c4595ed16
26 changed files with 553 additions and 373 deletions
+31 -9
View File
@@ -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')`)