117646d2cb
- component_documents table matching device_documents structure - Upload/delete actions on component detail page - Documents section in sidebar with file icon, filename, delete on hover - Devices already had document support; components now match Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
319 lines
13 KiB
TypeScript
319 lines
13 KiB
TypeScript
import {
|
|
pgTable,
|
|
uuid,
|
|
text,
|
|
integer,
|
|
boolean,
|
|
timestamp,
|
|
index,
|
|
check
|
|
} from 'drizzle-orm/pg-core';
|
|
import { sql } from 'drizzle-orm';
|
|
|
|
// ─── Users & Sessions ──────────────────────────────────────────────
|
|
|
|
export const users = pgTable('users', {
|
|
id: text('id').primaryKey(),
|
|
email: text('email').unique().notNull(),
|
|
displayName: text('display_name'),
|
|
passwordHash: text('password_hash').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
export const sessions = pgTable('sessions', {
|
|
id: text('id').primaryKey(), // SHA-256 hash of the session token
|
|
userId: text('user_id')
|
|
.notNull()
|
|
.references(() => users.id, { onDelete: 'cascade' }),
|
|
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull()
|
|
});
|
|
|
|
// ─── Locations ──────────────────────────────────────────────────────
|
|
|
|
export const locations = pgTable('locations', {
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
parentId: uuid('parent_id').references((): any => locations.id),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
// ─── Devices ────────────────────────────────────────────────────────
|
|
|
|
export const devices = pgTable(
|
|
'devices',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
title: text('title').notNull(),
|
|
category: text('category').notNull(),
|
|
brand: text('brand'),
|
|
model: text('model'),
|
|
serialNumber: text('serial_number'),
|
|
year: integer('year'),
|
|
condition: text('condition').notNull().default('Waiting to be Tested'),
|
|
voltage: text('voltage'),
|
|
frequency: text('frequency'),
|
|
origin: text('origin'),
|
|
faultDescription: text('fault_description'),
|
|
repairNotes: text('repair_notes'),
|
|
locationId: uuid('location_id').references(() => locations.id),
|
|
initialCondition: text('initial_condition'),
|
|
generalNotes: text('general_notes'),
|
|
disabled: boolean('disabled').default(false).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [
|
|
index('devices_category_idx').on(table.category),
|
|
index('devices_condition_idx').on(table.condition),
|
|
index('devices_location_idx').on(table.locationId),
|
|
check('devices_category_check', sql`${table.category} IN ('Computer', 'Audio Equipment', 'Peripheral', 'Other')`),
|
|
check('devices_condition_check', sql`${table.condition} IN ('Working', 'In Repair', 'Waiting for Repair', 'Waiting to be Tested', 'Unrepairable')`)
|
|
]
|
|
);
|
|
|
|
// ─── Computer Details (1:1 extension) ───────────────────────────────
|
|
|
|
export const computerDetails = pgTable('computer_details', {
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
deviceId: uuid('device_id')
|
|
.notNull()
|
|
.unique()
|
|
.references(() => devices.id, { onDelete: 'cascade' }),
|
|
osVersion: text('os_version'),
|
|
firmwareVersion: text('firmware_version'),
|
|
installedSoftware: text('installed_software')
|
|
});
|
|
|
|
// ─── Device Images ──────────────────────────────────────────────────
|
|
|
|
export const deviceImages = pgTable(
|
|
'device_images',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
deviceId: uuid('device_id')
|
|
.notNull()
|
|
.references(() => devices.id, { onDelete: 'cascade' }),
|
|
filePath: text('file_path').notNull(),
|
|
thumbnailPath: text('thumbnail_path'),
|
|
caption: text('caption'),
|
|
sortOrder: integer('sort_order').default(0),
|
|
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [index('device_images_device_idx').on(table.deviceId)]
|
|
);
|
|
|
|
// ─── Device Documents ───────────────────────────────────────────────
|
|
|
|
export const deviceDocuments = pgTable(
|
|
'device_documents',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
deviceId: uuid('device_id')
|
|
.notNull()
|
|
.references(() => devices.id, { onDelete: 'cascade' }),
|
|
filePath: text('file_path').notNull(),
|
|
originalFilename: text('original_filename').notNull(),
|
|
fileType: text('file_type'),
|
|
description: text('description'),
|
|
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [index('device_documents_device_idx').on(table.deviceId)]
|
|
);
|
|
|
|
// ─── Components ─────────────────────────────────────────────────────
|
|
|
|
export const components = pgTable(
|
|
'components',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
title: text('title').notNull(),
|
|
componentType: text('component_type').notNull(),
|
|
brand: text('brand'),
|
|
partNumber: text('part_number'),
|
|
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'
|
|
}),
|
|
locationId: uuid('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),
|
|
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')`)
|
|
]
|
|
);
|
|
|
|
// ─── Component Images ───────────────────────────────────────────────
|
|
|
|
export const componentImages = pgTable(
|
|
'component_images',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
componentId: uuid('component_id')
|
|
.notNull()
|
|
.references(() => components.id, { onDelete: 'cascade' }),
|
|
filePath: text('file_path').notNull(),
|
|
thumbnailPath: text('thumbnail_path'),
|
|
caption: text('caption'),
|
|
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [index('component_images_component_idx').on(table.componentId)]
|
|
);
|
|
|
|
// ─── Component Documents ────────────────────────────────────────────
|
|
|
|
export const componentDocuments = pgTable(
|
|
'component_documents',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
componentId: uuid('component_id')
|
|
.notNull()
|
|
.references(() => components.id, { onDelete: 'cascade' }),
|
|
filePath: text('file_path').notNull(),
|
|
originalFilename: text('original_filename').notNull(),
|
|
fileType: text('file_type'),
|
|
description: text('description'),
|
|
uploadedAt: timestamp('uploaded_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [index('component_documents_component_idx').on(table.componentId)]
|
|
);
|
|
|
|
// ─── Installation Log (append-only) ─────────────────────────────────
|
|
|
|
export const installationLog = pgTable(
|
|
'installation_log',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
componentId: uuid('component_id')
|
|
.notNull()
|
|
.references(() => components.id, { onDelete: 'cascade' }),
|
|
deviceId: uuid('device_id')
|
|
.notNull()
|
|
.references(() => devices.id, { onDelete: 'cascade' }),
|
|
action: text('action').notNull(),
|
|
performedBy: text('performed_by'),
|
|
notes: text('notes'),
|
|
performedAt: timestamp('performed_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [
|
|
index('install_log_component_idx').on(table.componentId),
|
|
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')`)
|
|
]
|
|
);
|
|
|
|
// ─── Checklist Templates ────────────────────────────────────────────
|
|
|
|
export const checklistTemplates = pgTable('checklist_templates', {
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
});
|
|
|
|
export const templateItems = pgTable(
|
|
'template_items',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
templateId: uuid('template_id')
|
|
.notNull()
|
|
.references(() => checklistTemplates.id, { onDelete: 'cascade' }),
|
|
text: text('text').notNull(),
|
|
itemType: text('item_type').notNull().default('checkbox'), // 'checkbox' or 'input'
|
|
unit: text('unit'), // e.g. 'RPM', 'dB', '%'
|
|
sortOrder: integer('sort_order').default(0).notNull()
|
|
},
|
|
(table) => [index('template_items_template_idx').on(table.templateId)]
|
|
);
|
|
|
|
// ─── Device Checklists ──────────────────────────────────────────────
|
|
|
|
export const deviceChecklists = pgTable(
|
|
'device_checklists',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
deviceId: uuid('device_id')
|
|
.notNull()
|
|
.references(() => devices.id, { onDelete: 'cascade' }),
|
|
title: text('title').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [index('device_checklists_device_idx').on(table.deviceId)]
|
|
);
|
|
|
|
export const checklistItems = pgTable(
|
|
'checklist_items',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
checklistId: uuid('checklist_id')
|
|
.notNull()
|
|
.references(() => deviceChecklists.id, { onDelete: 'cascade' }),
|
|
text: text('text').notNull(),
|
|
itemType: text('item_type').notNull().default('checkbox'), // 'checkbox' or 'input'
|
|
unit: text('unit'), // e.g. 'RPM', 'dB', '%'
|
|
checked: boolean('checked').default(false).notNull(),
|
|
value: text('value'), // for input type items
|
|
sortOrder: integer('sort_order').default(0).notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [index('checklist_items_checklist_idx').on(table.checklistId)]
|
|
);
|
|
|
|
// ─── Device Log (append-only repair/operation history) ───────────────
|
|
|
|
export const deviceLog = pgTable(
|
|
'device_log',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
deviceId: uuid('device_id')
|
|
.notNull()
|
|
.references(() => devices.id, { onDelete: 'cascade' }),
|
|
type: text('type').notNull(),
|
|
description: text('description').notNull(),
|
|
conditionAfter: text('condition_after'),
|
|
performedBy: text('performed_by'),
|
|
performedAt: timestamp('performed_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [
|
|
index('device_log_device_idx').on(table.deviceId),
|
|
index('device_log_date_idx').on(table.performedAt),
|
|
check('device_log_type_check', sql`${table.type} IN ('repair', 'inspection', 'cleaning', 'modification', 'diagnostic', 'recap', 'other')`)
|
|
]
|
|
);
|
|
|
|
// ─── Todos ──────────────────────────────────────────────────────────
|
|
|
|
export const todos = pgTable(
|
|
'todos',
|
|
{
|
|
id: uuid('id').defaultRandom().primaryKey(),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
status: text('status').notNull().default('todo'),
|
|
priority: integer('priority').notNull().default(2), // 0=urgent, 1=high, 2=medium, 3=low
|
|
deviceId: uuid('device_id').references(() => devices.id, { onDelete: 'set null' }),
|
|
dueDate: timestamp('due_date', { withTimezone: true }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull()
|
|
},
|
|
(table) => [
|
|
index('todos_status_idx').on(table.status),
|
|
index('todos_priority_idx').on(table.priority),
|
|
index('todos_device_idx').on(table.deviceId),
|
|
check('todos_status_check', sql`${table.status} IN ('todo', 'in_progress', 'done')`),
|
|
check('todos_priority_check', sql`${table.priority} IN (0, 1, 2, 3)`)
|
|
]
|
|
);
|