974d06fff8
- Add core notification service: types, email-service (nodemailer/Brevo SMTP), notification-service (digest builder, preference CRUD, HTML renderer) - Add API routes: POST /api/notifications/digest (N8N cron, Bearer auth), GET/PUT /api/notifications/preferences (session auth) - Add NotificationPreferences UI component (Bell button + dialog with per-type toggles) in Registratura toolbar - Add 7 Brevo SMTP env vars to docker-compose.yml - Update CLAUDE.md, ROADMAP.md, DATA-MODEL.md, SYSTEM-ARCHITECTURE.md, CONFIGURATION.md, DOCKER-DEPLOYMENT.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
641 lines
18 KiB
Markdown
641 lines
18 KiB
Markdown
# Data Model Reference
|
|
|
|
> Canonical type definitions and data modeling rules for ArchiTools.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document defines the shared data types, entity conventions, and relationship patterns used across all ArchiTools modules. Module-specific types extend these base types. All types live in TypeScript; the runtime representation is JSON stored through the storage abstraction layer.
|
|
|
|
---
|
|
|
|
## Base Entity
|
|
|
|
Every storable entity in the system extends `BaseEntity`:
|
|
|
|
```typescript
|
|
// src/types/base.ts
|
|
|
|
interface BaseEntity {
|
|
/** Unique identifier. Generated via crypto.randomUUID() or nanoid. */
|
|
id: string;
|
|
|
|
/** ISO 8601 timestamp. Set once at creation, never modified. */
|
|
createdAt: string;
|
|
|
|
/** ISO 8601 timestamp. Updated on every mutation. */
|
|
updatedAt: string;
|
|
|
|
/** User ID of the creator. Omitted until auth is implemented. */
|
|
createdBy?: string;
|
|
|
|
/** Controls who can see this entity. */
|
|
visibility: Visibility;
|
|
|
|
/** Cross-module tags attached to this entity. Array of tag IDs. */
|
|
tags: string[];
|
|
|
|
/** Which company owns this entity. 'group' for shared/cross-company items. */
|
|
company: CompanyId;
|
|
}
|
|
```
|
|
|
|
### Rules
|
|
|
|
1. `id` is always a string. Use `crypto.randomUUID()` for generation. Never use sequential integers (they create conflicts across clients and are not merge-safe).
|
|
2. `createdAt` is set once during creation and must never change.
|
|
3. `updatedAt` is set on every save operation, including the initial creation.
|
|
4. `createdBy` is `undefined` until authentication is implemented. When auth ships, it becomes a required field on new entities. Existing entities with `undefined` are treated as created by the system.
|
|
5. `tags` stores tag IDs (not labels). Tags are resolved through the tag service.
|
|
6. `company` defaults to `'group'` for entities that are not company-specific.
|
|
|
|
---
|
|
|
|
## Company Model
|
|
|
|
```typescript
|
|
// src/types/company.ts
|
|
|
|
type CompanyId = 'beletage' | 'urban-switch' | 'studii-de-teren' | 'group';
|
|
|
|
interface Company {
|
|
id: CompanyId;
|
|
name: string; // Full legal name: "Beletage SRL"
|
|
shortName: string; // Display name: "Beletage"
|
|
cui: string; // Romanian CUI (tax ID)
|
|
address?: string; // Registered address
|
|
email?: string; // Primary contact email
|
|
phone?: string; // Primary phone number
|
|
}
|
|
```
|
|
|
|
### Company Registry
|
|
|
|
Defined as static configuration in `src/config/companies.ts`:
|
|
|
|
```typescript
|
|
export const COMPANIES: Record<CompanyId, Company> = {
|
|
beletage: {
|
|
id: 'beletage',
|
|
name: 'Beletage SRL',
|
|
shortName: 'Beletage',
|
|
cui: '...', // populated in config
|
|
},
|
|
'urban-switch': {
|
|
id: 'urban-switch',
|
|
name: 'Urban Switch SRL',
|
|
shortName: 'Urban Switch',
|
|
cui: '...',
|
|
},
|
|
'studii-de-teren': {
|
|
id: 'studii-de-teren',
|
|
name: 'Studii de Teren SRL',
|
|
shortName: 'Studii de Teren',
|
|
cui: '...',
|
|
},
|
|
group: {
|
|
id: 'group',
|
|
name: 'Grup Beletage',
|
|
shortName: 'Grup',
|
|
cui: '',
|
|
},
|
|
};
|
|
```
|
|
|
|
`'group'` is a virtual company representing the entire organization. Used for shared resources (templates, tags, prompts) that are not company-specific.
|
|
|
|
---
|
|
|
|
## Visibility and Roles
|
|
|
|
### Visibility
|
|
|
|
```typescript
|
|
// src/types/visibility.ts
|
|
|
|
type Visibility = 'all' | 'internal' | 'admin' | 'guest';
|
|
```
|
|
|
|
| Value | Who can see | Use case |
|
|
|---|---|---|
|
|
| `all` | Everyone including future external/guest users | Public templates, shared contacts |
|
|
| `internal` | All authenticated internal users | Most operational data |
|
|
| `admin` | Admin users only | Passwords, sensitive config |
|
|
| `guest` | Guest users and above | Curated external-facing views |
|
|
|
|
Until authentication is implemented, all data is treated as `internal`. The visibility field is stored but not enforced at the storage layer. Enforcement will be added at the API/middleware layer when auth ships.
|
|
|
|
### Roles
|
|
|
|
```typescript
|
|
// src/types/roles.ts
|
|
|
|
type Role = 'admin' | 'user' | 'viewer' | 'guest';
|
|
```
|
|
|
|
| Role | Permissions |
|
|
|---|---|
|
|
| `admin` | Full CRUD on all entities. Access to all modules. System configuration. |
|
|
| `user` | CRUD on own entities. Access to enabled modules. |
|
|
| `viewer` | Read-only access to entities matching their visibility level. |
|
|
| `guest` | Read-only access to `guest` and `all` visibility entities. Limited module access. |
|
|
|
|
### Future Auth Integration
|
|
|
|
When Authentik SSO is integrated:
|
|
|
|
```typescript
|
|
interface UserProfile {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
role: Role;
|
|
companies: CompanyId[]; // Companies this user belongs to
|
|
modules: string[]; // Explicitly allowed modules (empty = all for role)
|
|
}
|
|
```
|
|
|
|
Users may belong to multiple companies. Entity filtering will combine role, visibility, and company membership.
|
|
|
|
---
|
|
|
|
## Tag Model
|
|
|
|
Tags are a cross-module resource managed by the Tag Manager module but consumed everywhere.
|
|
|
|
```typescript
|
|
// src/types/tag.ts
|
|
|
|
interface Tag {
|
|
/** Unique tag ID. */
|
|
id: string;
|
|
|
|
/** Human-readable label. Romanian. Example: "076 Casa Copernicus" */
|
|
label: string;
|
|
|
|
/**
|
|
* Tag category for grouping.
|
|
* Examples: 'project', 'client', 'phase', 'type', 'priority', 'domain'
|
|
*/
|
|
category: TagCategory;
|
|
|
|
/** Display color. Hex string: "#3B82F6" */
|
|
color: string;
|
|
|
|
/**
|
|
* If set, this tag is only visible/usable within the specified modules.
|
|
* If undefined or empty, the tag is available globally.
|
|
*/
|
|
moduleScope?: string[];
|
|
|
|
/** Company this tag belongs to. 'group' for cross-company tags. */
|
|
company: CompanyId;
|
|
|
|
/** Whether this tag is active. Archived tags are hidden from selectors but preserved on entities. */
|
|
archived: boolean;
|
|
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
type TagCategory =
|
|
| 'project'
|
|
| 'client'
|
|
| 'phase'
|
|
| 'type'
|
|
| 'priority'
|
|
| 'domain'
|
|
| 'custom';
|
|
```
|
|
|
|
### Project Reference Pattern
|
|
|
|
Tags with category `'project'` follow the ManicTime naming convention used in the office:
|
|
|
|
```
|
|
"{number} {project name}"
|
|
```
|
|
|
|
Examples:
|
|
- `"076 Casa Copernicus"`
|
|
- `"081 PUZ Sector 3"`
|
|
- `"092 Reabilitare Scoala Nr 5"`
|
|
|
|
The number is a sequential project identifier scoped per company. The tag label is the canonical reference; there is no separate `projectNumber` field.
|
|
|
|
**Parsing utility:**
|
|
|
|
```typescript
|
|
// src/lib/tags/utils.ts
|
|
|
|
interface ParsedProjectTag {
|
|
number: string; // "076"
|
|
name: string; // "Casa Copernicus"
|
|
raw: string; // "076 Casa Copernicus"
|
|
}
|
|
|
|
function parseProjectTag(label: string): ParsedProjectTag | null {
|
|
const match = label.match(/^(\d{3})\s+(.+)$/);
|
|
if (!match) return null;
|
|
return { number: match[1], name: match[2], raw: label };
|
|
}
|
|
```
|
|
|
|
### Tag Relationships
|
|
|
|
Tags create implicit relationships between entities across modules. For example:
|
|
|
|
- A registratura entry tagged `"076 Casa Copernicus"` is linked to the same project as a word template with the same tag.
|
|
- Searching by tag ID across all namespaces yields every entity related to that project.
|
|
|
|
This is the primary cross-module linking mechanism until a dedicated relational backend is introduced.
|
|
|
|
---
|
|
|
|
## Module-Specific Entity Types
|
|
|
|
Each module defines its own entity types that extend `BaseEntity`. Below is an overview of the primary entity per module.
|
|
|
|
### Registratura
|
|
|
|
```typescript
|
|
// src/modules/registratura/types.ts
|
|
|
|
interface RegistryEntry extends BaseEntity {
|
|
/** Sequential registry number per company per year. "B-2025-0042" */
|
|
registryNumber: string;
|
|
|
|
/** Entry or exit. */
|
|
direction: 'incoming' | 'outgoing';
|
|
|
|
/** Date the document was registered. ISO 8601 date (not datetime). */
|
|
registryDate: string;
|
|
|
|
/** Document title / description. */
|
|
title: string;
|
|
|
|
/** Sender (for incoming) or recipient (for outgoing). */
|
|
correspondent: string;
|
|
|
|
/** Number of physical pages, if applicable. */
|
|
pageCount?: number;
|
|
|
|
/** Reference to another registry entry (e.g., reply to incoming). */
|
|
referenceTo?: string;
|
|
|
|
/** Free-text notes. */
|
|
notes?: string;
|
|
|
|
/** Attached file references (future: MinIO object keys). */
|
|
attachments: string[];
|
|
}
|
|
```
|
|
|
|
### IT Inventory
|
|
|
|
```typescript
|
|
// src/modules/it-inventory/types.ts
|
|
|
|
type DeviceType = 'laptop' | 'desktop' | 'monitor' | 'printer' | 'router' |
|
|
'switch' | 'nas' | 'phone' | 'tablet' | 'peripheral' | 'other';
|
|
|
|
type DeviceStatus = 'active' | 'storage' | 'repair' | 'retired' | 'disposed';
|
|
|
|
interface InventoryItem extends BaseEntity {
|
|
name: string;
|
|
deviceType: DeviceType;
|
|
manufacturer?: string;
|
|
model?: string;
|
|
serialNumber?: string;
|
|
purchaseDate?: string;
|
|
warrantyExpiry?: string;
|
|
assignedTo?: string;
|
|
location?: string;
|
|
status: DeviceStatus;
|
|
specs?: Record<string, string>; // CPU, RAM, storage, etc.
|
|
notes?: string;
|
|
}
|
|
```
|
|
|
|
### Address Book
|
|
|
|
```typescript
|
|
// src/modules/address-book/types.ts
|
|
|
|
type ContactType = 'person' | 'company' | 'institution' | 'supplier';
|
|
|
|
interface AddressContact extends BaseEntity {
|
|
contactType: ContactType;
|
|
name: string;
|
|
organization?: string;
|
|
role?: string;
|
|
email?: string;
|
|
phone?: string;
|
|
address?: string;
|
|
city?: string;
|
|
county?: string;
|
|
country?: string;
|
|
cui?: string; // For companies/institutions
|
|
iban?: string;
|
|
bank?: string;
|
|
website?: string;
|
|
notes?: string;
|
|
favorite: boolean;
|
|
}
|
|
```
|
|
|
|
### Prompt Generator
|
|
|
|
```typescript
|
|
// src/modules/prompt-generator/types.ts
|
|
|
|
type PromptDomain = 'architecture' | 'legal' | 'technical' | 'gis' |
|
|
'rendering' | 'urbanism' | 'bim' | 'procurement' | 'administrative';
|
|
|
|
type PromptTargetAI = 'text' | 'image' | 'code' | 'review' | 'rewrite';
|
|
|
|
interface PromptTemplate extends BaseEntity {
|
|
name: string;
|
|
category: string;
|
|
domain: PromptDomain;
|
|
description: string;
|
|
targetAI: PromptTargetAI;
|
|
blocks: PromptBlock[];
|
|
variables: PromptVariable[];
|
|
outputMode: OutputMode;
|
|
providerProfile?: string;
|
|
version: number;
|
|
parentTemplateId?: string; // For cloned/forked templates
|
|
favorite: boolean;
|
|
}
|
|
```
|
|
|
|
### Email Signature
|
|
|
|
```typescript
|
|
// src/modules/email-signature/types.ts
|
|
|
|
interface SignatureConfig extends BaseEntity {
|
|
employeeName: string;
|
|
jobTitle: string;
|
|
email: string;
|
|
phone?: string;
|
|
mobile?: string;
|
|
/** Generated HTML output. */
|
|
generatedHtml: string;
|
|
}
|
|
```
|
|
|
|
### Digital Signatures
|
|
|
|
```typescript
|
|
// src/modules/digital-signatures/types.ts
|
|
|
|
type SignatureAssetType = 'signature' | 'stamp' | 'initials';
|
|
|
|
interface SignatureAsset extends BaseEntity {
|
|
name: string;
|
|
assetType: SignatureAssetType;
|
|
/** Base64-encoded PNG for localStorage. MinIO object key for API backend. */
|
|
imageData: string;
|
|
owner?: string;
|
|
notes?: string;
|
|
}
|
|
```
|
|
|
|
### Password Vault
|
|
|
|
```typescript
|
|
// src/modules/password-vault/types.ts
|
|
|
|
interface VaultEntry extends BaseEntity {
|
|
service: string;
|
|
url?: string;
|
|
username: string;
|
|
/** Stored as plaintext in localStorage (demo-grade security). Encrypted in API backend. */
|
|
password: string;
|
|
notes?: string;
|
|
category?: string;
|
|
lastRotated?: string;
|
|
}
|
|
```
|
|
|
|
> **Security note:** The localStorage adapter stores passwords in plaintext. This is acceptable only for internal demo use. The API backend must encrypt the `password` field at rest.
|
|
|
|
### Word Templates / Word XML Generators
|
|
|
|
```typescript
|
|
// src/modules/word-templates/types.ts
|
|
|
|
type TemplateCategory = 'contract' | 'offer' | 'report' | 'letter' |
|
|
'header' | 'certificate' | 'minutes' | 'other';
|
|
|
|
interface WordTemplate extends BaseEntity {
|
|
name: string;
|
|
category: TemplateCategory;
|
|
description?: string;
|
|
/** File reference: base64 for localStorage, MinIO key for API backend. */
|
|
fileData: string;
|
|
fileName: string;
|
|
fileSize: number;
|
|
version: number;
|
|
}
|
|
```
|
|
|
|
### Email Notifications (platform service)
|
|
|
|
```typescript
|
|
// src/core/notifications/types.ts
|
|
|
|
type NotificationType = "deadline-urgent" | "deadline-overdue" | "document-expiry";
|
|
|
|
interface NotificationPreference {
|
|
userId: string;
|
|
email: string;
|
|
name: string;
|
|
company: CompanyId;
|
|
enabledTypes: NotificationType[];
|
|
globalOptOut: boolean;
|
|
}
|
|
|
|
interface DigestItem {
|
|
entryNumber: string;
|
|
subject: string;
|
|
label: string;
|
|
dueDate: string; // YYYY-MM-DD
|
|
daysRemaining: number; // negative = overdue
|
|
color: "red" | "yellow" | "blue";
|
|
}
|
|
|
|
interface DigestSection {
|
|
type: NotificationType;
|
|
title: string;
|
|
items: DigestItem[];
|
|
}
|
|
```
|
|
|
|
> **Storage:** Preferences stored in `KeyValueStore` (namespace `notifications`, key `pref:<userId>`). No separate Prisma model needed.
|
|
|
|
---
|
|
|
|
## Naming Conventions
|
|
|
|
### Type Names
|
|
|
|
| Convention | Example | Used For |
|
|
|---|---|---|
|
|
| PascalCase | `RegistryEntry` | Interfaces, type aliases, classes |
|
|
| camelCase | `registryNumber` | Properties, variables, functions |
|
|
| UPPER_SNAKE_CASE | `COMPANIES` | Constants, static config objects |
|
|
| kebab-case | `'urban-switch'` | String literal union members (IDs, slugs) |
|
|
|
|
### Entity Type Naming
|
|
|
|
- Main entity per module: descriptive noun (`RegistryEntry`, `InventoryItem`, `AddressContact`).
|
|
- Do not prefix with module name unless ambiguity exists.
|
|
- Supporting types (enums, sub-objects): use descriptive names scoped by context (`DeviceType`, `ContactType`).
|
|
- Union string literal types: use the values themselves, not a wrapper. Prefer `type Direction = 'incoming' | 'outgoing'` over an enum.
|
|
|
|
### File Naming
|
|
|
|
- Type definition files: `types.ts` in the module root.
|
|
- Shared types: `src/types/{name}.ts`.
|
|
- Never put types in a barrel `index.ts` that also exports components (causes circular dependency issues with lazy loading).
|
|
|
|
---
|
|
|
|
## Schema Versioning
|
|
|
|
### Strategy
|
|
|
|
Each module's data schema has a version number tracked in the namespace metadata:
|
|
|
|
```typescript
|
|
// Stored at key "_meta" within each namespace
|
|
interface NamespaceMeta {
|
|
schemaVersion: number;
|
|
lastMigration: string;
|
|
itemCount: number;
|
|
}
|
|
```
|
|
|
|
### Migration Registration
|
|
|
|
```typescript
|
|
// src/modules/registratura/migrations.ts
|
|
|
|
import { defineMigrations } from '@/lib/storage/migrations';
|
|
|
|
export const registraturaMigrations = defineMigrations('registratura', [
|
|
{
|
|
version: 2,
|
|
description: 'Add attachments array to registry entries',
|
|
up: (data: Record<string, unknown>) => {
|
|
for (const [key, value] of Object.entries(data)) {
|
|
if (key === '_meta') continue;
|
|
const entry = value as Record<string, unknown>;
|
|
if (!entry.attachments) {
|
|
entry.attachments = [];
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
},
|
|
{
|
|
version: 3,
|
|
description: 'Rename sender field to correspondent',
|
|
up: (data: Record<string, unknown>) => {
|
|
for (const [key, value] of Object.entries(data)) {
|
|
if (key === '_meta') continue;
|
|
const entry = value as Record<string, unknown>;
|
|
if ('sender' in entry) {
|
|
entry.correspondent = entry.sender;
|
|
delete entry.sender;
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
},
|
|
]);
|
|
```
|
|
|
|
### Migration Execution
|
|
|
|
On module initialization, the storage layer:
|
|
|
|
1. Reads `_meta.schemaVersion` for the module's namespace.
|
|
2. Compares to the latest registered migration version.
|
|
3. If behind, runs each migration sequentially (`version N` to `version N+1`).
|
|
4. Updates `_meta.schemaVersion` after each successful migration.
|
|
5. If any migration fails, the process halts and logs an error. The module should display a degraded state rather than corrupting data.
|
|
|
|
---
|
|
|
|
## Entity Relationships
|
|
|
|
ArchiTools uses a flat document model (not relational). Relationships are expressed through:
|
|
|
|
### 1. Tags (Primary Cross-Module Link)
|
|
|
|
Entities share project tags. Querying all namespaces for a tag ID returns every entity related to that project.
|
|
|
|
```
|
|
RegistryEntry.tags: ["tag-076-casa-copernicus"]
|
|
WordTemplate.tags: ["tag-076-casa-copernicus"]
|
|
PromptTemplate.tags: ["tag-076-casa-copernicus"]
|
|
```
|
|
|
|
### 2. Direct References
|
|
|
|
Some entities reference others by ID within the same namespace:
|
|
|
|
- `RegistryEntry.referenceTo` points to another registry entry ID.
|
|
- `PromptTemplate.parentTemplateId` points to the template it was cloned from.
|
|
|
|
### 3. Company Scoping
|
|
|
|
The `company` field on every entity enables filtering by company. A user at Beletage sees Beletage and group entities; never Urban Switch entities (unless their role grants cross-company access).
|
|
|
|
### 4. Future: Explicit Project Entity
|
|
|
|
When the platform matures, a `Project` entity may be introduced:
|
|
|
|
```typescript
|
|
interface Project extends BaseEntity {
|
|
number: string; // "076"
|
|
name: string; // "Casa Copernicus"
|
|
company: CompanyId;
|
|
status: 'active' | 'completed' | 'archived';
|
|
client?: string; // AddressContact ID
|
|
startDate?: string;
|
|
endDate?: string;
|
|
}
|
|
```
|
|
|
|
At that point, tags with category `'project'` would reference a `Project.id`, and the project number/name would be the single source of truth. Until then, the tag label is the canonical project identifier.
|
|
|
|
---
|
|
|
|
## Shared Type Exports
|
|
|
|
All shared types are re-exported from `src/types/index.ts`:
|
|
|
|
```typescript
|
|
// src/types/index.ts
|
|
|
|
export type { BaseEntity } from './base';
|
|
export type { CompanyId, Company } from './company';
|
|
export type { Visibility } from './visibility';
|
|
export type { Role, UserProfile } from './roles';
|
|
export type { Tag, TagCategory } from './tag';
|
|
```
|
|
|
|
Module-specific types are not exported from this barrel. They are imported directly from the module:
|
|
|
|
```typescript
|
|
import type { RegistryEntry } from '@/modules/registratura/types';
|
|
```
|
|
|
|
This keeps the shared type surface minimal and avoids pulling module code into unrelated bundles.
|