Phases 1-5 + rooms/floors, accounts, custom types, users, notifications
Data model - Properties, rooms (+optional floors), assets (typed custom fields + Zod runtime validator + move history), documents (polymorphic scope) - Projects -> work packages -> tasks -> subtasks - Decision events (scoped to project/property/asset/work_package) - Checklist templates + instances, maintenance schedules (time + usage) with auto-materialized checklists on event recording - Wiki (global + per-project) with revisions + tsvector FTS - Property accounts (utility/meter numbers by kind) - Notifications table + per-user channel prefs Infra - RBAC guards (requireCompany / requireAdmin) - Storage abstraction: LocalDiskStorage (HMAC signed URLs) + S3Storage behind the same interface, switchable via STORAGE_BACKEND - CSV export for assets / maintenance / decisions - QR labels: /api/qr SVG endpoint + printable /assets/[id]/label - Notifications: in-app + SMTP (own server via nodemailer) + Matrix (Client-Server API, per-company room) with opt-in per user - Company switcher + auto-select first company on login UI - Topbar: bell with unread count, theme toggle, name, Sign Out (flat) - Sidebar: main nav + dedicated Admin section (Asset types, Users, Company) - Nested-route tabs on property / project / asset detail pages - Admin UIs for users (invite, role, reset pw, deactivate) and company settings (default currency, Matrix room id) - Custom asset type creation + field-def editor with immutable key/type guard and auto-deprecate when removing a field still referenced Graph - graphify-out/ committed: GRAPH_REPORT.md, graph.html, graph.json
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Assets</h1>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Everything you track — switches, ACs, filters, sensors, generators…
|
||||
</p>
|
||||
</div>
|
||||
<a href="/assets/new"
|
||||
class="inline-flex items-center gap-1 rounded-md bg-primary-600 px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700">
|
||||
+ New asset
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="get" class="flex flex-col gap-3 sm:flex-row">
|
||||
<input
|
||||
type="search"
|
||||
name="q"
|
||||
value={data.filterQ}
|
||||
placeholder="Search by name, tag, or serial…"
|
||||
class="block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 sm:max-w-md dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<select name="type" class="block rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100">
|
||||
<option value="">All types</option>
|
||||
{#each data.types as t}
|
||||
<option value={t.slug} selected={data.filterType === t.slug}>{t.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button type="submit" class="rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700">Filter</button>
|
||||
<a href="/assets/export.csv?{new URLSearchParams({ q: data.filterQ, type: data.filterType }).toString()}"
|
||||
class="self-center text-sm text-gray-600 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400">
|
||||
Export CSV →
|
||||
</a>
|
||||
</form>
|
||||
|
||||
{#if data.assets.length === 0}
|
||||
<div class="rounded-lg border border-dashed border-gray-300 bg-white p-10 text-center text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||
<p class="font-medium text-gray-700 dark:text-gray-200">No assets match.</p>
|
||||
<p class="mt-1">Adjust the filter above, or <a href="/assets/new" class="text-primary-600 hover:underline dark:text-primary-400">add a new asset</a>.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/40">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Name</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Type</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Tag</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Serial</th>
|
||||
<th class="px-4 py-2 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{#each data.assets as a}
|
||||
<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">
|
||||
<a href="/assets/{a.id}" class="hover:text-primary-600 dark:hover:text-primary-400">{a.name}</a>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{a.assetTypeName}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{a.tag ?? '—'}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500 dark:text-gray-400">{a.serialNumber ?? '—'}</td>
|
||||
<td class="px-4 py-2 text-right text-xs text-gray-400 dark:text-gray-500">{new Date(a.updatedAt).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user