Files
ArchiTools/docs/DATA-MODEL.md
T
AI Assistant 974d06fff8 feat: add email notification system (Brevo SMTP + N8N daily digest)
- 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>
2026-03-11 01:12:36 +02:00

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.