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,31 @@
|
||||
import { pgTable, varchar, text, timestamp, index } from 'drizzle-orm/pg-core';
|
||||
import { companies, users } from './tenancy';
|
||||
import { notificationKindEnum, pk, fk, createdAt } from './_shared';
|
||||
|
||||
export const notifications = pgTable(
|
||||
'notifications',
|
||||
{
|
||||
id: pk(),
|
||||
userId: fk('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
companyId: fk('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id, { onDelete: 'cascade' }),
|
||||
kind: notificationKindEnum('kind').notNull(),
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
body: text('body').notNull(),
|
||||
// Absolute or relative URL back into the app (e.g. /assets/<id>).
|
||||
link: varchar('link', { length: 1024 }),
|
||||
readAt: timestamp('read_at', { withTimezone: true }),
|
||||
createdAt: createdAt()
|
||||
},
|
||||
(t) => ({
|
||||
// Unread-list query: user + read_at IS NULL + ordered by created_at desc.
|
||||
byUserUnread: index('notifications_by_user_unread').on(t.userId, t.readAt, t.createdAt),
|
||||
byUserCompany: index('notifications_by_user_company').on(t.userId, t.companyId, t.createdAt)
|
||||
})
|
||||
);
|
||||
|
||||
export type Notification = typeof notifications.$inferSelect;
|
||||
export type NewNotification = typeof notifications.$inferInsert;
|
||||
Reference in New Issue
Block a user