feat(properties): list view renders parent/child as a depth-first tree
The flat list ordered by updatedAt scattered apartments away from their building. Server-side flatten now does a depth-first walk — parents render immediately above their children, alphabetical at each level — and tags each row with its depth. The UI indents the name column by 1.5rem per level and prefixes children with "└" for a visible parent/child line. Orphan rows (parent_id pointing outside the live company set) fall back to root depth so nothing is silently dropped, even though the restrict-on-delete FK should prevent that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,53 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { listProperties } from '$lib/server/services/properties';
|
||||
import type { Property } from '$lib/server/db/schema/properties';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export type PropertyRow = Property & { depth: number };
|
||||
|
||||
/**
|
||||
* Reorder the flat company-scoped property list into a depth-first traversal
|
||||
* so parents render immediately above their children. Within each level we
|
||||
* sort by name. Orphan rows (parent_id points outside the visible set —
|
||||
* shouldn't happen with the current restrict-on-delete policy, but defended
|
||||
* here so the UI never silently drops a row) are appended at the end as roots.
|
||||
*/
|
||||
function flattenTree(rows: Property[]): PropertyRow[] {
|
||||
const byParent = new Map<string | null, Property[]>();
|
||||
for (const r of rows) {
|
||||
const key = r.parentId;
|
||||
const list = byParent.get(key);
|
||||
if (list) list.push(r);
|
||||
else byParent.set(key, [r]);
|
||||
}
|
||||
for (const list of byParent.values()) {
|
||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
const out: PropertyRow[] = [];
|
||||
const visible = new Set(rows.map((r) => r.id));
|
||||
function walk(parentId: string | null, depth: number): void {
|
||||
const list = byParent.get(parentId) ?? [];
|
||||
for (const r of list) {
|
||||
out.push({ ...r, depth });
|
||||
walk(r.id, depth + 1);
|
||||
}
|
||||
}
|
||||
walk(null, 0);
|
||||
|
||||
if (out.length < rows.length) {
|
||||
const seen = new Set(out.map((r) => r.id));
|
||||
for (const r of rows) {
|
||||
if (!seen.has(r.id) && (!r.parentId || !visible.has(r.parentId))) {
|
||||
out.push({ ...r, depth: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (!locals.company) throw error(400, 'No active company. Pick one from the sidebar.');
|
||||
const rows = await listProperties(locals.company.id);
|
||||
return { properties: rows };
|
||||
const flat = await listProperties(locals.company.id);
|
||||
return { properties: flattenTree(flat) };
|
||||
};
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{#each data.properties as p}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-4 py-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<td class="py-2 pr-4 text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
style:padding-left="{1 + p.depth * 1.5}rem">
|
||||
{#if p.depth > 0}<span class="mr-1 select-none text-gray-400 dark:text-gray-500">└</span>{/if}
|
||||
<a href="/properties/{p.id}" class="hover:text-primary-600 dark:hover:text-primary-400">{p.name}</a>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{p.kind ?? '—'}</td>
|
||||
|
||||
Reference in New Issue
Block a user