Initial commit: buildfor_life_repair inventory system
SvelteKit + PostgreSQL app for tracking vintage computers, audio equipment, components, and installation history. Features device/component CRUD, operation logs, QR code labels, global search, image uploads, and dark mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pg from 'pg';
|
||||
import * as schema from './schema.js';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const pool = new pg.Pool({
|
||||
connectionString: env.DATABASE_URL
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, { schema });
|
||||
|
||||
export type Database = typeof db;
|
||||
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
integer,
|
||||
boolean,
|
||||
timestamp,
|
||||
index,
|
||||
check
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
// ─── 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)]
|
||||
);
|
||||
|
||||
// ─── 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')`)
|
||||
]
|
||||
);
|
||||
|
||||
// ─── 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')`)
|
||||
]
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import QRCode from 'qrcode';
|
||||
|
||||
export async function generateQrSvg(url: string): Promise<string> {
|
||||
return QRCode.toString(url, { type: 'svg', margin: 1 });
|
||||
}
|
||||
|
||||
export async function generateQrDataUrl(url: string): Promise<string> {
|
||||
return QRCode.toDataURL(url, { margin: 1, width: 256 });
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import { writeFile, unlink, mkdir } from 'fs/promises';
|
||||
import { join, extname } from 'path';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const UPLOAD_BASE = 'static/uploads';
|
||||
const THUMBNAIL_WIDTH = 300;
|
||||
|
||||
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/heic'];
|
||||
const ALLOWED_DOC_TYPES = ['application/pdf', 'text/plain', 'application/zip'];
|
||||
|
||||
export async function saveImage(
|
||||
file: File,
|
||||
subfolder: 'devices' | 'components'
|
||||
): Promise<{ filePath: string; thumbnailPath: string }> {
|
||||
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
|
||||
throw new Error(`Invalid image type: ${file.type}`);
|
||||
}
|
||||
|
||||
const ext = extname(file.name) || '.jpg';
|
||||
const filename = `${randomUUID()}${ext}`;
|
||||
const thumbFilename = `thumb_${filename}`;
|
||||
|
||||
const dir = join(UPLOAD_BASE, subfolder);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// Save original
|
||||
const filePath = join(dir, filename);
|
||||
await writeFile(filePath, buffer);
|
||||
|
||||
// Generate thumbnail
|
||||
const thumbnailPath = join(dir, thumbFilename);
|
||||
await sharp(buffer).resize(THUMBNAIL_WIDTH).jpeg({ quality: 80 }).toFile(thumbnailPath);
|
||||
|
||||
// Return paths relative to static/ for serving
|
||||
return {
|
||||
filePath: `/uploads/${subfolder}/${filename}`,
|
||||
thumbnailPath: `/uploads/${subfolder}/${thumbFilename}`
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveDocument(file: File): Promise<{ filePath: string; originalFilename: string }> {
|
||||
if (![...ALLOWED_IMAGE_TYPES, ...ALLOWED_DOC_TYPES].includes(file.type)) {
|
||||
throw new Error(`Invalid file type: ${file.type}`);
|
||||
}
|
||||
|
||||
const ext = extname(file.name) || '';
|
||||
const filename = `${randomUUID()}${ext}`;
|
||||
|
||||
const dir = join(UPLOAD_BASE, 'documents');
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const filePath = join(dir, filename);
|
||||
await writeFile(filePath, buffer);
|
||||
|
||||
return {
|
||||
filePath: `/uploads/documents/${filename}`,
|
||||
originalFilename: file.name
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteFile(filePath: string): Promise<void> {
|
||||
try {
|
||||
// filePath is like /uploads/devices/xxx.jpg, need to prepend static/
|
||||
const fullPath = join('static', filePath);
|
||||
await unlink(fullPath);
|
||||
} catch {
|
||||
// File may already be deleted
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user