Show parent > child location on detail pages, add location move
Deploy to LXC / deploy (push) Successful in 18s
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:
@@ -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} › {/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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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} › {/if}{data.device.locationName}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -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 };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user