9102ffd8b4
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>
108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
import type { PageServerLoad } from './$types';
|
||
import { db } from '$lib/server/db/index.js';
|
||
import { devices, deviceImages, locations } from '$lib/server/db/schema.js';
|
||
import { eq, ilike, or, count, desc, and, sql } from 'drizzle-orm';
|
||
|
||
export const load: PageServerLoad = async ({ url }) => {
|
||
const category = url.searchParams.get('category');
|
||
const condition = url.searchParams.get('condition');
|
||
const search = url.searchParams.get('q');
|
||
const page = Math.max(1, Number(url.searchParams.get('page') ?? 1));
|
||
const pageSize = 24;
|
||
|
||
const conditions = [eq(devices.disabled, false)];
|
||
|
||
if (category) {
|
||
conditions.push(eq(devices.category, category));
|
||
}
|
||
if (condition === 'needs-repair') {
|
||
conditions.push(
|
||
or(eq(devices.condition, 'In Repair'), eq(devices.condition, 'Waiting for Repair'))!
|
||
);
|
||
} else if (condition) {
|
||
conditions.push(eq(devices.condition, condition));
|
||
}
|
||
if (search) {
|
||
conditions.push(
|
||
or(
|
||
ilike(devices.title, `%${search}%`),
|
||
ilike(devices.brand, `%${search}%`),
|
||
ilike(devices.model, `%${search}%`),
|
||
ilike(devices.serialNumber, `%${search}%`)
|
||
)!
|
||
);
|
||
}
|
||
|
||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||
|
||
const [totalResult] = await db.select({ value: count() }).from(devices).where(where);
|
||
const total = totalResult?.value ?? 0;
|
||
|
||
const deviceList = await db
|
||
.select({
|
||
id: devices.id,
|
||
title: devices.title,
|
||
category: devices.category,
|
||
brand: devices.brand,
|
||
model: devices.model,
|
||
condition: devices.condition,
|
||
year: devices.year,
|
||
locationName: locations.name,
|
||
locationParentId: locations.parentId
|
||
})
|
||
.from(devices)
|
||
.leftJoin(locations, eq(devices.locationId, locations.id))
|
||
.where(where)
|
||
.orderBy(desc(devices.updatedAt))
|
||
.limit(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
|
||
const deviceIds = deviceList.map((d) => d.id);
|
||
let imageMap: Record<string, string> = {};
|
||
if (deviceIds.length > 0) {
|
||
const images = await db
|
||
.select({
|
||
deviceId: deviceImages.deviceId,
|
||
thumbnailPath: deviceImages.thumbnailPath,
|
||
filePath: deviceImages.filePath
|
||
})
|
||
.from(deviceImages)
|
||
.where(sql`${deviceImages.deviceId} IN ${deviceIds}`)
|
||
.orderBy(deviceImages.sortOrder);
|
||
|
||
for (const img of images) {
|
||
if (!imageMap[img.deviceId]) {
|
||
imageMap[img.deviceId] = img.thumbnailPath ?? img.filePath;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
devices: deviceList.map((d) => ({
|
||
...d,
|
||
thumbnail: imageMap[d.id] ?? null,
|
||
fullLocation: d.locationName
|
||
? (d.locationParentId && parentNameMap[d.locationParentId]
|
||
? `${parentNameMap[d.locationParentId]} › ${d.locationName}`
|
||
: d.locationName)
|
||
: null
|
||
})),
|
||
total,
|
||
page,
|
||
pageSize,
|
||
filters: { category, condition, search }
|
||
};
|
||
};
|