Show parent > child location on detail pages, add location move
Deploy to LXC / deploy (push) Successful in 18s

- Device/component detail pages show "Parent › Child" for locations
- Device list cards show full location path
- Location edit form now includes a Parent selector to move locations
  between parents or make them top-level
- Prevents setting a location as its own parent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 17:07:08 +07:00
parent 948617a285
commit 9102ffd8b4
8 changed files with 65 additions and 8 deletions
@@ -22,6 +22,7 @@ export const load: PageServerLoad = async ({ params }) => {
deviceTitle: devices.title, deviceTitle: devices.title,
locationId: components.locationId, locationId: components.locationId,
locationName: locations.name, locationName: locations.name,
locationParentId: locations.parentId,
createdAt: components.createdAt, createdAt: components.createdAt,
updatedAt: components.updatedAt updatedAt: components.updatedAt
}) })
@@ -32,6 +33,15 @@ export const load: PageServerLoad = async ({ params }) => {
if (!component) error(404, 'Component not found'); if (!component) error(404, 'Component not found');
let parentLocationName: string | null = null;
if (component.locationParentId) {
const [parent] = await db
.select({ name: locations.name })
.from(locations)
.where(eq(locations.id, component.locationParentId));
parentLocationName = parent?.name ?? null;
}
const images = await db const images = await db
.select() .select()
.from(componentImages) .from(componentImages)
@@ -57,7 +67,7 @@ export const load: PageServerLoad = async ({ params }) => {
.where(eq(installationLog.componentId, params.id)) .where(eq(installationLog.componentId, params.id))
.orderBy(desc(installationLog.performedAt)); .orderBy(desc(installationLog.performedAt));
return { component, images, documents, history }; return { component, parentLocationName, images, documents, history };
}; };
export const actions: Actions = { export const actions: Actions = {
@@ -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">{c.locationName}</span></p> <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>
{: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}
+19 -2
View File
@@ -47,7 +47,8 @@ export const load: PageServerLoad = async ({ url }) => {
model: devices.model, model: devices.model,
condition: devices.condition, condition: devices.condition,
year: devices.year, year: devices.year,
locationName: locations.name locationName: locations.name,
locationParentId: locations.parentId
}) })
.from(devices) .from(devices)
.leftJoin(locations, eq(devices.locationId, locations.id)) .leftJoin(locations, eq(devices.locationId, locations.id))
@@ -56,6 +57,17 @@ export const load: PageServerLoad = async ({ url }) => {
.limit(pageSize) .limit(pageSize)
.offset((page - 1) * pageSize); .offset((page - 1) * pageSize);
// Resolve parent location names
const parentIds = [...new Set(deviceList.filter(d => d.locationParentId).map(d => d.locationParentId!))];
let parentNameMap: Record<string, string> = {};
if (parentIds.length > 0) {
const parents = await db
.select({ id: locations.id, name: locations.name })
.from(locations)
.where(sql`${locations.id} IN ${parentIds}`);
for (const p of parents) parentNameMap[p.id] = p.name;
}
// Fetch first image for each device // Fetch first image for each device
const deviceIds = deviceList.map((d) => d.id); const deviceIds = deviceList.map((d) => d.id);
let imageMap: Record<string, string> = {}; let imageMap: Record<string, string> = {};
@@ -80,7 +92,12 @@ export const load: PageServerLoad = async ({ url }) => {
return { return {
devices: deviceList.map((d) => ({ devices: deviceList.map((d) => ({
...d, ...d,
thumbnail: imageMap[d.id] ?? null thumbnail: imageMap[d.id] ?? null,
fullLocation: d.locationName
? (d.locationParentId && parentNameMap[d.locationParentId]
? `${parentNameMap[d.locationParentId]} ${d.locationName}`
: d.locationName)
: null
})), })),
total, total,
page, page,
+2 -2
View File
@@ -117,8 +117,8 @@
<span class="text-xs text-gray-400 dark:text-gray-500">{device.year}</span> <span class="text-xs text-gray-400 dark:text-gray-500">{device.year}</span>
{/if} {/if}
</div> </div>
{#if device.locationName} {#if device.fullLocation}
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500">{device.locationName}</p> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">{device.fullLocation}</p>
{/if} {/if}
</div> </div>
</a> </a>
@@ -38,6 +38,7 @@ export const load: PageServerLoad = async ({ params }) => {
generalNotes: devices.generalNotes, generalNotes: devices.generalNotes,
locationId: devices.locationId, locationId: devices.locationId,
locationName: locations.name, locationName: locations.name,
locationParentId: locations.parentId,
disabled: devices.disabled, disabled: devices.disabled,
createdAt: devices.createdAt, createdAt: devices.createdAt,
updatedAt: devices.updatedAt updatedAt: devices.updatedAt
@@ -49,6 +50,16 @@ export const load: PageServerLoad = async ({ params }) => {
if (!device) error(404, 'Device not found'); if (!device) error(404, 'Device not found');
if (device.disabled) error(404, 'Device not found'); if (device.disabled) error(404, 'Device not found');
// Resolve parent location name
let parentLocationName: string | null = null;
if (device.locationParentId) {
const [parent] = await db
.select({ name: locations.name })
.from(locations)
.where(eq(locations.id, device.locationParentId));
parentLocationName = parent?.name ?? null;
}
// Computer details // Computer details
let compDetails = null; let compDetails = null;
if (device.category === 'Computer') { if (device.category === 'Computer') {
@@ -138,6 +149,7 @@ export const load: PageServerLoad = async ({ params }) => {
return { return {
device, device,
parentLocationName,
computerDetails: compDetails, computerDetails: compDetails,
images, images,
documents, documents,
+3 -1
View File
@@ -544,7 +544,9 @@
{#if data.device.locationName} {#if data.device.locationName}
<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">{data.device.locationName}</dd> <dd class="text-gray-900 dark:text-white">
{#if data.parentLocationName}{data.parentLocationName} &rsaquo; {/if}{data.device.locationName}
</dd>
</div> </div>
{/if} {/if}
<div> <div>
+5 -1
View File
@@ -73,11 +73,15 @@ export const actions: Actions = {
const id = formData.get('id') as string; const id = formData.get('id') as string;
const name = (formData.get('name') as string)?.trim(); const name = (formData.get('name') as string)?.trim();
const description = (formData.get('description') as string)?.trim(); const description = (formData.get('description') as string)?.trim();
const parentId = (formData.get('parentId') as string) || null;
if (!name) return fail(400, { error: 'Name is required' }); if (!name) return fail(400, { error: 'Name is required' });
// Prevent setting self as parent
if (parentId === id) return fail(400, { error: 'Cannot set location as its own parent' });
await db await db
.update(locations) .update(locations)
.set({ name, description: description || null, updatedAt: new Date() }) .set({ name, description: description || null, parentId, updatedAt: new Date() })
.where(eq(locations.id, id)); .where(eq(locations.id, id));
return { renamed: true }; return { renamed: true };
}, },
+12
View File
@@ -102,6 +102,18 @@
<input type="text" id="desc-{loc.id}" name="description" value={loc.description ?? ''} <input type="text" id="desc-{loc.id}" name="description" value={loc.description ?? ''}
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" /> class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
</div> </div>
<div>
<label for="parent-{loc.id}" class="mb-1 block text-xs text-gray-500 dark:text-gray-400">Parent</label>
<select id="parent-{loc.id}" name="parentId"
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">None (top level)</option>
{#each sortedLocations as other}
{#if other.id !== loc.id}
<option value={other.id} selected={other.id === loc.parentId}>{'—'.repeat(other.depth)} {other.name}</option>
{/if}
{/each}
</select>
</div>
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Save</button> <button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Save</button>
<button type="button" onclick={() => (editingId = null)} class="rounded-md px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">Cancel</button> <button type="button" onclick={() => (editingId = null)} class="rounded-md px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">Cancel</button>
</form> </form>