Add document uploads to components (PDFs, manuals, datasheets)
- 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>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { components, devices, locations, installationLog, componentImages } from '$lib/server/db/schema.js';
|
||||
import { components, devices, locations, installationLog, componentImages, componentDocuments } from '$lib/server/db/schema.js';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { error, fail } from '@sveltejs/kit';
|
||||
import { saveDocument, deleteFile } from '$lib/server/uploads.js';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const [component] = await db
|
||||
@@ -36,6 +37,11 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
.from(componentImages)
|
||||
.where(eq(componentImages.componentId, params.id));
|
||||
|
||||
const documents = await db
|
||||
.select()
|
||||
.from(componentDocuments)
|
||||
.where(eq(componentDocuments.componentId, params.id));
|
||||
|
||||
const history = await db
|
||||
.select({
|
||||
id: installationLog.id,
|
||||
@@ -51,5 +57,39 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
.where(eq(installationLog.componentId, params.id))
|
||||
.orderBy(desc(installationLog.performedAt));
|
||||
|
||||
return { component, images, history };
|
||||
return { component, images, documents, history };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
uploadDocument: async ({ request, params }) => {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('document') as File;
|
||||
if (!file || file.size === 0) return fail(400, { error: 'No file selected' });
|
||||
|
||||
const { filePath, originalFilename } = await saveDocument(file);
|
||||
const description = formData.get('description') as string;
|
||||
|
||||
await db.insert(componentDocuments).values({
|
||||
componentId: params.id,
|
||||
filePath,
|
||||
originalFilename,
|
||||
fileType: file.type,
|
||||
description: description || null
|
||||
});
|
||||
|
||||
return { documentUploaded: true };
|
||||
},
|
||||
|
||||
deleteDocument: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const docId = formData.get('docId') as string;
|
||||
|
||||
const [doc] = await db.select().from(componentDocuments).where(eq(componentDocuments.id, docId));
|
||||
if (doc) {
|
||||
await deleteFile(doc.filePath);
|
||||
await db.delete(componentDocuments).where(eq(componentDocuments.id, docId));
|
||||
}
|
||||
|
||||
return { documentDeleted: true };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { formatDate, timeAgo } from '$lib/utils/date.js';
|
||||
|
||||
let { data } = $props();
|
||||
const c = $derived(data.component);
|
||||
let showDocForm = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -142,6 +144,52 @@
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Documents -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Documents</h2>
|
||||
<button type="button" onclick={() => (showDocForm = !showDocForm)}
|
||||
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
|
||||
{showDocForm ? 'Cancel' : 'Upload'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showDocForm}
|
||||
<form method="POST" action="?/uploadDocument" enctype="multipart/form-data" use:enhance class="mb-3 space-y-2">
|
||||
<input type="file" name="document" required class="text-sm text-gray-600 dark:text-gray-400" />
|
||||
<input type="text" name="description" placeholder="Description"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if data.documents.length === 0}
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No documents.</p>
|
||||
{:else}
|
||||
<div class="space-y-1">
|
||||
{#each data.documents as doc}
|
||||
<div class="group flex items-center gap-2">
|
||||
<a href={doc.filePath} target="_blank"
|
||||
class="flex flex-1 items-center gap-2 rounded-md p-2 text-sm hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<svg class="h-4 w-4 flex-shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="truncate text-gray-700 dark:text-gray-300">{doc.originalFilename}</span>
|
||||
</a>
|
||||
<form method="POST" action="?/deleteDocument" use:enhance>
|
||||
<input type="hidden" name="docId" value={doc.id} />
|
||||
<button type="submit" class="hidden rounded p-1 text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Delete">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if c.notes}
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Notes</h2>
|
||||
|
||||
Reference in New Issue
Block a user