Add location detail page showing devices and components at that location
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:
2026-04-07 17:10:25 +07:00
parent 9102ffd8b4
commit 0113803378
5 changed files with 195 additions and 3 deletions
@@ -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>
</p>
{: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} &rsaquo; {/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} &rsaquo; {/if}{c.locationName}</a></p>
{:else}
<p class="text-sm text-gray-500 dark:text-gray-400">In storage (no location set)</p>
{/if}
@@ -545,7 +545,9 @@
<div>
<dt class="text-gray-500 dark:text-gray-400">Location</dt>
<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} &rsaquo; {/if}{data.device.locationName}
</a>
</dd>
</div>
{/if}
+1 -1
View File
@@ -124,7 +124,7 @@
{#if loc.depth > 0}
<span class="text-gray-400 dark:text-gray-500">&nbsp;</span>
{/if}
{loc.name}
<a href="/locations/{loc.id}" class="hover:text-blue-600 dark:hover:text-blue-400">{loc.name}</a>
</h3>
{#if loc.description}
<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>&rsaquo;</span>
<a href="/locations" class="hover:text-blue-600 dark:hover:text-blue-400">{data.parentName}</a>
{/if}
<span>&rsaquo;</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>