Add location detail page showing devices and components at that location
Deploy to LXC / deploy (push) Successful in 17s
Deploy to LXC / deploy (push) Successful in 17s
- /locations/[id] shows all devices and stored components at a location - Breadcrumb navigation with parent location - Sub-location chips for navigating to children - Location names are clickable links everywhere: location list page, device detail sidebar, component detail current location Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@
|
|||||||
<a href="/devices/{c.currentDeviceId}" class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400">{c.deviceTitle}</a>
|
<a href="/devices/{c.currentDeviceId}" class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400">{c.deviceTitle}</a>
|
||||||
</p>
|
</p>
|
||||||
{:else if c.locationName}
|
{:else if c.locationName}
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">In storage at <span class="font-medium">{#if data.parentLocationName}{data.parentLocationName} › {/if}{c.locationName}</span></p>
|
<p class="text-sm text-gray-700 dark:text-gray-300">In storage at <a href="/locations/{c.locationId}" class="font-medium hover:text-blue-600 dark:hover:text-blue-400">{#if data.parentLocationName}{data.parentLocationName} › {/if}{c.locationName}</a></p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">In storage (no location set)</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">In storage (no location set)</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -545,7 +545,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<dt class="text-gray-500 dark:text-gray-400">Location</dt>
|
<dt class="text-gray-500 dark:text-gray-400">Location</dt>
|
||||||
<dd class="text-gray-900 dark:text-white">
|
<dd class="text-gray-900 dark:text-white">
|
||||||
|
<a href="/locations/{data.device.locationId}" class="hover:text-blue-600 dark:hover:text-blue-400">
|
||||||
{#if data.parentLocationName}{data.parentLocationName} › {/if}{data.device.locationName}
|
{#if data.parentLocationName}{data.parentLocationName} › {/if}{data.device.locationName}
|
||||||
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
{#if loc.depth > 0}
|
{#if loc.depth > 0}
|
||||||
<span class="text-gray-400 dark:text-gray-500">└ </span>
|
<span class="text-gray-400 dark:text-gray-500">└ </span>
|
||||||
{/if}
|
{/if}
|
||||||
{loc.name}
|
<a href="/locations/{loc.id}" class="hover:text-blue-600 dark:hover:text-blue-400">{loc.name}</a>
|
||||||
</h3>
|
</h3>
|
||||||
{#if loc.description}
|
{#if loc.description}
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{loc.description}</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">{loc.description}</p>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { db } from '$lib/server/db/index.js';
|
||||||
|
import { locations, devices, components } from '$lib/server/db/schema.js';
|
||||||
|
import { eq, and, isNull } from 'drizzle-orm';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const [location] = await db
|
||||||
|
.select()
|
||||||
|
.from(locations)
|
||||||
|
.where(eq(locations.id, params.id));
|
||||||
|
|
||||||
|
if (!location) error(404, 'Location not found');
|
||||||
|
|
||||||
|
// Parent name
|
||||||
|
let parentName: string | null = null;
|
||||||
|
if (location.parentId) {
|
||||||
|
const [parent] = await db
|
||||||
|
.select({ name: locations.name })
|
||||||
|
.from(locations)
|
||||||
|
.where(eq(locations.id, location.parentId));
|
||||||
|
parentName = parent?.name ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child locations
|
||||||
|
const children = await db
|
||||||
|
.select({ id: locations.id, name: locations.name })
|
||||||
|
.from(locations)
|
||||||
|
.where(eq(locations.parentId, params.id))
|
||||||
|
.orderBy(locations.name);
|
||||||
|
|
||||||
|
// Devices at this location
|
||||||
|
const deviceList = await db
|
||||||
|
.select({
|
||||||
|
id: devices.id,
|
||||||
|
title: devices.title,
|
||||||
|
category: devices.category,
|
||||||
|
brand: devices.brand,
|
||||||
|
model: devices.model,
|
||||||
|
condition: devices.condition
|
||||||
|
})
|
||||||
|
.from(devices)
|
||||||
|
.where(and(eq(devices.locationId, params.id), eq(devices.disabled, false)))
|
||||||
|
.orderBy(devices.title);
|
||||||
|
|
||||||
|
// Components stored at this location (not installed in a device)
|
||||||
|
const componentList = await db
|
||||||
|
.select({
|
||||||
|
id: components.id,
|
||||||
|
title: components.title,
|
||||||
|
componentType: components.componentType,
|
||||||
|
condition: components.condition
|
||||||
|
})
|
||||||
|
.from(components)
|
||||||
|
.where(and(eq(components.locationId, params.id), isNull(components.currentDeviceId)))
|
||||||
|
.orderBy(components.title);
|
||||||
|
|
||||||
|
return { location, parentName, children, devices: deviceList, components: componentList };
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.location.name} - Locations - B4L Repair</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-4xl">
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="mb-1 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<a href="/locations" class="hover:text-blue-600 dark:hover:text-blue-400">Locations</a>
|
||||||
|
{#if data.parentName}
|
||||||
|
<span>›</span>
|
||||||
|
<a href="/locations" class="hover:text-blue-600 dark:hover:text-blue-400">{data.parentName}</a>
|
||||||
|
{/if}
|
||||||
|
<span>›</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{data.location.name}</h1>
|
||||||
|
{#if data.location.description}
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{data.location.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-locations -->
|
||||||
|
{#if data.children.length > 0}
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Sub-locations</h2>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each data.children as child}
|
||||||
|
<a href="/locations/{child.id}"
|
||||||
|
class="rounded-md border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:border-blue-300 hover:text-blue-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:border-blue-600 dark:hover:text-blue-400">
|
||||||
|
{child.name}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Devices -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
|
Devices <span class="text-gray-300 dark:text-gray-600">({data.devices.length})</span>
|
||||||
|
</h2>
|
||||||
|
{#if data.devices.length === 0}
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">No devices at this location.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-100 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Device</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Category</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Condition</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each data.devices as device}
|
||||||
|
<tr class="border-b border-gray-100 last:border-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700/30">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<a href="/devices/{device.id}" class="font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
|
||||||
|
{device.title}
|
||||||
|
</a>
|
||||||
|
{#if device.brand || device.model}
|
||||||
|
<span class="ml-1 text-gray-400 dark:text-gray-500">{[device.brand, device.model].filter(Boolean).join(' ')}</span>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">{device.category}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="rounded-full px-2 py-0.5 text-xs font-medium
|
||||||
|
{device.condition === 'Working' ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' : ''}
|
||||||
|
{device.condition === 'In Repair' ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300' : ''}
|
||||||
|
{device.condition === 'Waiting for Repair' ? 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300' : ''}
|
||||||
|
{device.condition === 'Waiting to be Tested' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' : ''}
|
||||||
|
{device.condition === 'Unrepairable' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' : ''}
|
||||||
|
">
|
||||||
|
{device.condition}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Components -->
|
||||||
|
<div>
|
||||||
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
|
Components in Storage <span class="text-gray-300 dark:text-gray-600">({data.components.length})</span>
|
||||||
|
</h2>
|
||||||
|
{#if data.components.length === 0}
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">No components stored at this location.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-100 bg-gray-50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Component</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Type</th>
|
||||||
|
<th class="px-4 py-2 text-left font-medium text-gray-500 dark:text-gray-400">Condition</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each data.components as comp}
|
||||||
|
<tr class="border-b border-gray-100 last:border-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-700/30">
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<a href="/components/{comp.id}" class="font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400">
|
||||||
|
{comp.title}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">{comp.componentType}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="rounded-full px-2 py-0.5 text-xs font-medium
|
||||||
|
{comp.condition === 'Working' ? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' : ''}
|
||||||
|
{comp.condition === 'Faulty' ? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' : ''}
|
||||||
|
{comp.condition === 'Unknown' ? 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' : ''}
|
||||||
|
{comp.condition === 'Refurbished' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' : ''}
|
||||||
|
">
|
||||||
|
{comp.condition}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user