feat(i18n): add internationalization support with English and Spanish translations
feat(kiosk): implement kiosk report submission functionality with offline support test(outbox): add tests for outbox functionality including report submission and error handling feat(outbox): create outbox management for queued reports with IndexedDB persistence feat(uuid): add UUID generation utility for client IDs feat(qr): implement QR code generation for reporting links by site and area feat(kiosk): create Kiosk page for reporting incidents without an account
This commit is contained in:
1 parent
3f6dff5f24
commit
3e964d1176
64 files changed
+10056
-267
No files matched your search
@@ -13,6 +13,7 @@ export const rootCauseCategoryEnum = pgEnum('root_cause_category', vocab.rootCau
|
||||
export const actionKindEnum = pgEnum('action_kind', vocab.actionKinds)
|
||||
export const actionStatusEnum = pgEnum('action_status', vocab.actionStatuses)
|
||||
export const authTokenPurposeEnum = pgEnum('auth_token_purpose', ['magic_link', 'password_reset'])
|
||||
export const reportChannelEnum = pgEnum('report_channel', ['web', 'kiosk'])
|
||||
|
||||
const id = () => uuid().primaryKey().defaultRandom()
|
||||
const timestamptz = () => timestamp({ withTimezone: true })
|
||||
@@ -115,6 +116,29 @@ export const areas = pgTable(
|
||||
(t) => [index('areas_site_idx').on(t.siteId), index('areas_parent_idx').on(t.parentId)],
|
||||
)
|
||||
|
||||
/**
|
||||
* Reporting links let people without accounts file reports: on a shared kiosk tablet, or by scanning
|
||||
* a QR code. The token only allows creating reports, so it is stored as-is to let admins reprint QR codes.
|
||||
*/
|
||||
export const kiosks = pgTable(
|
||||
'kiosks',
|
||||
{
|
||||
id: id(),
|
||||
organizationId: uuid()
|
||||
.notNull()
|
||||
.references(() => organizations.id),
|
||||
name: text().notNull(),
|
||||
token: text().notNull(),
|
||||
/** Limits the link to one site; null allows any. */
|
||||
siteId: uuid().references(() => sites.id, { onDelete: 'set null' }),
|
||||
createdById: uuid().references(() => users.id, { onDelete: 'set null' }),
|
||||
lastUsedAt: timestamptz(),
|
||||
revokedAt: timestamptz(),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [uniqueIndex('kiosks_token_unique').on(t.token), index('kiosks_organization_idx').on(t.organizationId)],
|
||||
)
|
||||
|
||||
/** Per-organization counter for gap-free, human-friendly incident numbers. */
|
||||
export const incidentCounters = pgTable('incident_counters', {
|
||||
organizationId: uuid()
|
||||
@@ -146,12 +170,19 @@ export const incidents = pgTable(
|
||||
/** Null when reported anonymously. */
|
||||
reporterId: uuid().references(() => users.id, { onDelete: 'set null' }),
|
||||
anonymous: boolean().notNull().default(false),
|
||||
/** Name typed by someone reporting without an account (kiosk/QR). */
|
||||
reporterName: text(),
|
||||
channel: reportChannelEnum().notNull().default('web'),
|
||||
kioskId: uuid().references(() => kiosks.id, { onDelete: 'set null' }),
|
||||
/** Generated on the reporting device so retries after a lost response don't create duplicates. */
|
||||
clientId: uuid(),
|
||||
closedAt: timestamptz(),
|
||||
createdAt: createdAt(),
|
||||
updatedAt: updatedAt(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('incidents_organization_number_unique').on(t.organizationId, t.number),
|
||||
uniqueIndex('incidents_organization_client_id_unique').on(t.organizationId, t.clientId),
|
||||
index('incidents_organization_occurred_idx').on(t.organizationId, t.occurredAt),
|
||||
index('incidents_organization_status_idx').on(t.organizationId, t.status),
|
||||
index('incidents_reporter_idx').on(t.reporterId),
|
||||
|
||||
Reference in new issue
Block a user