Initial commit: ArchiTools modular dashboard platform

Complete Next.js 16 application with 13 fully implemented modules:
Email Signature, Word XML Generator, Registratura, Dashboard,
Tag Manager, IT Inventory, Address Book, Password Vault,
Mini Utilities, Prompt Generator, Digital Signatures,
Word Templates, and AI Chat.

Includes core platform systems (module registry, feature flags,
storage abstraction, i18n, theming, auth stub, tagging),
16 technical documentation files, Docker deployment config,
and legacy HTML tool reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions

View File

@@ -0,0 +1,571 @@
# Tagging System
> ArchiTools internal architecture reference -- cross-module tagging, tag model, and Tag Manager module.
---
## Overview
ArchiTools uses a unified tagging system across all modules. Tags provide categorization, filtering, cross-referencing, and visual identification for entities throughout the platform. The tagging system is inspired by the existing ManicTime tag structure used by the office group and extends it into a structured, typed model.
The Tag Manager module provides a dedicated CRUD interface for managing tags. All other modules consume tags through a shared `TagService` and a set of reusable UI components.
---
## Tag Model
```typescript
// src/types/tags.ts
interface Tag {
id: string; // unique identifier (UUID v4)
label: string; // display text (Romanian), e.g. "Verificare proiect"
category: TagCategory; // semantic grouping
color?: string; // hex color for visual distinction, e.g. "#2563eb"
icon?: string; // optional Lucide icon name, e.g. "building"
scope: TagScope; // visibility scope
moduleId?: string; // if scope is 'module', which module owns this tag
companyId?: CompanyId; // if scope is 'company', which company owns this tag
parentId?: string; // for hierarchical tags (references another Tag's id)
metadata?: Record<string, string>; // extensible key-value pairs
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}
```
### Tag Categories
```typescript
type TagCategory =
| 'project' // project identifiers (e.g., "076 Casa Copernicus")
| 'phase' // project phases (CU, DTAC, PT, etc.)
| 'activity' // work activities (Releveu, Design interior, etc.)
| 'document-type' // document classification (Regulament, Parte desenata, etc.)
| 'company' // company association (Beletage, Urban Switch, Studii de Teren)
| 'priority' // priority levels (Urgent, Normal, Scazut)
| 'status' // status indicators (In lucru, Finalizat, In asteptare)
| 'custom'; // user-defined tags that don't fit other categories
```
Each category serves a distinct semantic purpose. Tags within the same category are mutually comparable -- you can filter all `phase` tags to get a list of project phases, or all `activity` tags to see work activities. Categories are fixed in code; adding a new category requires a code change. The `custom` category is the escape hatch for tags that do not fit the predefined categories.
### Tag Scope
```typescript
type TagScope = 'global' | 'module' | 'company';
```
| Scope | Meaning | Example |
|---|---|---|
| `global` | Available to all modules across all companies | Phase tags like "DTAC", "PT" |
| `module` | Scoped to a single module, not visible elsewhere | "Template favorit" in Prompt Generator |
| `company` | Scoped to a single company | "Ofertare" scoped to Beletage |
When `scope` is `module`, the `moduleId` field must be set. When `scope` is `company`, the `companyId` field must be set. When `scope` is `global`, both are `undefined`.
---
## Hierarchical Tags
Tags support parent-child relationships through the `parentId` field. This enables structured navigation and drill-down filtering.
### Hierarchy Convention
```
Project (category: 'project')
└── Phase (category: 'phase', parentId: project tag id)
└── Task (category: 'activity', parentId: phase tag id)
```
**Example:**
```
076 Casa Copernicus (project)
├── CU (phase)
│ ├── Redactare (activity)
│ └── Depunere (activity)
├── DTAC (phase)
│ ├── Redactare (activity)
│ ├── Verificare proiect (activity)
│ └── Vizita santier (activity)
└── PT (phase)
└── Detalii de Executie (activity)
```
### Hierarchy Rules
- A tag's `parentId` must reference an existing tag.
- Deleting a parent tag does not cascade-delete children. Children become root-level tags (their `parentId` is cleared).
- Maximum nesting depth: 3 levels. This is enforced in the `TagService` on create/update.
- A tag's category is independent of its parent's category. A `phase` tag can be a child of a `project` tag.
### Querying the Hierarchy
```typescript
// Get all children of a tag
function getChildren(tags: Tag[], parentId: string): Tag[] {
return tags.filter((tag) => tag.parentId === parentId);
}
// Get the full ancestry chain (bottom-up)
function getAncestors(tags: Tag[], tagId: string): Tag[] {
const tagMap = new Map(tags.map((t) => [t.id, t]));
const ancestors: Tag[] = [];
let current = tagMap.get(tagId);
while (current?.parentId) {
const parent = tagMap.get(current.parentId);
if (parent) ancestors.push(parent);
current = parent;
}
return ancestors.reverse(); // root-first order
}
// Build a tree structure from flat tag list
function buildTagTree(tags: Tag[]): TagTreeNode[] {
const map = new Map<string, TagTreeNode>();
const roots: TagTreeNode[] = [];
for (const tag of tags) {
map.set(tag.id, { tag, children: [] });
}
for (const tag of tags) {
const node = map.get(tag.id)!;
if (tag.parentId && map.has(tag.parentId)) {
map.get(tag.parentId)!.children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
interface TagTreeNode {
tag: Tag;
children: TagTreeNode[];
}
```
---
## TagService
The `TagService` is the central data access layer for tags. All modules interact with tags exclusively through this service.
```typescript
// src/lib/tags/tag-service.ts
interface TagService {
/** Get all tags, optionally filtered by scope */
getAllTags(): Promise<Tag[]>;
/** Get tags belonging to a specific category */
getTagsByCategory(category: TagCategory): Promise<Tag[]>;
/** Get tags by scope, with optional scope identifier */
getTagsByScope(scope: TagScope, scopeId?: string): Promise<Tag[]>;
/** Get a single tag by ID */
getTag(id: string): Promise<Tag | null>;
/** Get all children of a parent tag */
getChildTags(parentId: string): Promise<Tag[]>;
/** Create a new tag. Validates uniqueness of label within category+scope. */
createTag(tag: Omit<Tag, 'id' | 'createdAt' | 'updatedAt'>): Promise<Tag>;
/** Update an existing tag. Partial updates supported. */
updateTag(id: string, updates: Partial<Omit<Tag, 'id' | 'createdAt'>>): Promise<Tag>;
/** Delete a tag. Clears parentId on children. */
deleteTag(id: string): Promise<void>;
/** Full-text search on tag labels */
searchTags(query: string): Promise<Tag[]>;
/** Bulk import tags (used by Tag Manager for import/export) */
importTags(tags: Omit<Tag, 'id' | 'createdAt' | 'updatedAt'>[]): Promise<Tag[]>;
/** Export all tags as a serializable array */
exportTags(): Promise<Tag[]>;
}
```
### Storage
Tags are stored under the `architools.tags` namespace in the storage abstraction layer. The storage key layout:
```
architools.tags.all -> Tag[] (master list)
architools.tags.index -> Record<TagCategory, string[]> (category -> tag ID index)
```
The index is a denormalized lookup table rebuilt on every write operation. It allows `getTagsByCategory` to resolve without scanning the full tag list.
### Validation Rules
| Rule | Enforcement |
|---|---|
| Label must be non-empty and <= 100 characters | `createTag`, `updateTag` |
| Label must be unique within the same `category` + `scope` + `scopeId` | `createTag`, `updateTag` |
| `moduleId` required when `scope` is `module` | `createTag`, `updateTag` |
| `companyId` required when `scope` is `company` | `createTag`, `updateTag` |
| `parentId` must reference an existing tag | `createTag`, `updateTag` |
| Max nesting depth: 3 | `createTag`, `updateTag` |
| `color` must be a valid 6-digit hex (`#rrggbb`) | `createTag`, `updateTag` |
| `icon` must be a valid Lucide icon name | `createTag`, `updateTag` |
---
## Tag UI Components
Four reusable components provide the tag interface across all modules.
### `TagBadge`
Displays a single tag as a colored chip.
```typescript
// src/components/shared/tags/TagBadge.tsx
interface TagBadgeProps {
tag: Tag;
size?: 'sm' | 'md'; // default: 'sm'
removable?: boolean; // shows X button
onRemove?: (tagId: string) => void;
}
```
Renders a rounded badge with the tag's `color` as background (with opacity for readability), the `icon` if present, and the `label` as text. The `sm` size is used inline in tables and lists; `md` is used in detail views and forms.
### `TagSelector`
Multi-select tag picker with category filtering, search, and tag creation.
```typescript
// src/components/shared/tags/TagSelector.tsx
interface TagSelectorProps {
selectedTagIds: string[];
onChange: (tagIds: string[]) => void;
categories?: TagCategory[]; // restrict to specific categories
scope?: TagScope; // restrict to specific scope
scopeId?: string; // module or company ID for scope filtering
allowCreate?: boolean; // allow inline tag creation (default: false)
placeholder?: string;
maxTags?: number; // max selectable tags (default: unlimited)
}
```
**Behavior:**
- Opens a popover with a search input and categorized tag list.
- Tags are grouped by category with category headers.
- Search filters tags by label (case-insensitive, diacritics-insensitive).
- Selected tags appear as `TagBadge` components above the input.
- When `allowCreate` is true, typing a label that does not match any existing tag shows a "Creeaza tag: [label]" option.
### `TagFilter`
Filter bar for lists and tables. Allows users to select tags and see only entities matching those tags.
```typescript
// src/components/shared/tags/TagFilter.tsx
interface TagFilterProps {
activeTags: string[]; // currently active filter tag IDs
onChange: (tagIds: string[]) => void;
categories?: TagCategory[]; // which categories to show filter chips for
tagCounts?: Record<string, number>; // tag ID -> count of matching entities
mode?: 'and' | 'or'; // filter logic (default: 'or')
}
```
**Behavior:**
- Displays as a horizontal chip bar above the list/table.
- Each active tag is shown as a `TagBadge` with a remove button.
- A "+" button opens the `TagSelector` to add more filter tags.
- When `tagCounts` is provided, each tag shows its count in parentheses.
- `mode: 'and'` requires entities to match all selected tags; `mode: 'or'` matches any.
### `TagManager`
This is the root component of the **Tag Manager module** -- a dedicated interface for full CRUD operations on the global tag catalog.
**Capabilities:**
- Browse all tags in a searchable, filterable table.
- Create, edit, and delete tags.
- Bulk operations: delete multiple tags, change category, change scope.
- Import tags from JSON or CSV.
- Export all tags to JSON.
- Visual hierarchy browser: tree view of parent-child relationships.
- Color picker and icon selector for tag customization.
The Tag Manager module config:
```typescript
{
id: 'tag-manager',
name: 'Manager Etichete',
description: 'Gestionare centralizata a etichetelor folosite in toate modulele.',
icon: 'tags',
route: '/tag-manager',
category: 'management',
featureFlag: 'module.tag-manager',
visibility: 'internal',
version: '1.0.0',
storageNamespace: 'architools.tag-manager',
navOrder: 40,
tags: ['tags', 'management', 'configuration'],
}
```
---
## Pre-Seeded Tags
The following tags are seeded on first initialization, derived from the existing ManicTime tag structure used by the office group. These provide immediate utility without requiring manual setup.
### Project Phases (`category: 'phase'`, `scope: 'global'`)
| Label | Color | Description |
|---|---|---|
| CU | `#3b82f6` | Certificat de Urbanism |
| Schita | `#8b5cf6` | Schita de proiect |
| Avize | `#06b6d4` | Obtinere avize |
| PUD | `#10b981` | Plan Urbanistic de Detaliu |
| AO | `#f59e0b` | Autorizatie de Construire (obtinere) |
| PUZ | `#ef4444` | Plan Urbanistic Zonal |
| PUG | `#ec4899` | Plan Urbanistic General |
| DTAD | `#6366f1` | Documentatie Tehnica pentru Autorizatia de Desfiintare |
| DTAC | `#14b8a6` | Documentatie Tehnica pentru Autorizatia de Construire |
| PT | `#f97316` | Proiect Tehnic |
| Detalii de Executie | `#84cc16` | Detalii de executie |
### Activities (`category: 'activity'`, `scope: 'global'`)
| Label | Color |
|---|---|
| Redactare | `#6366f1` |
| Depunere | `#10b981` |
| Ridicare | `#f59e0b` |
| Verificare proiect | `#ef4444` |
| Vizita santier | `#8b5cf6` |
| Releveu | `#3b82f6` |
| Reclama | `#ec4899` |
| Design grafic | `#06b6d4` |
| Design interior | `#14b8a6` |
| Design exterior | `#84cc16` |
### Document Types (`category: 'document-type'`, `scope: 'global'`)
| Label | Color |
|---|---|
| Regulament | `#6366f1` |
| Parte desenata | `#10b981` |
| Parte scrisa | `#3b82f6` |
### Company Tags (`category: 'company'`, `scope: 'global'`)
| Label | Color | CompanyId |
|---|---|---|
| Beletage | `#2563eb` | `beletage` |
| Urban Switch | `#16a34a` | `urban-switch` |
| Studii de Teren | `#dc2626` | `studii-de-teren` |
### Priority Tags (`category: 'priority'`, `scope: 'global'`)
| Label | Color |
|---|---|
| Urgent | `#ef4444` |
| Normal | `#3b82f6` |
| Scazut | `#6b7280` |
### Status Tags (`category: 'status'`, `scope: 'global'`)
| Label | Color |
|---|---|
| In lucru | `#f59e0b` |
| Finalizat | `#10b981` |
| In asteptare | `#6b7280` |
| Anulat | `#ef4444` |
### Seeding Implementation
```typescript
// src/lib/tags/seed-tags.ts
import type { Tag, TagCategory } from '@/types/tags';
const SEED_TAGS: Omit<Tag, 'id' | 'createdAt' | 'updatedAt'>[] = [
{ label: 'CU', category: 'phase', scope: 'global', color: '#3b82f6' },
{ label: 'DTAC', category: 'phase', scope: 'global', color: '#14b8a6' },
// ... all tags from tables above
];
export async function seedTagsIfEmpty(tagService: TagService): Promise<void> {
const existing = await tagService.getAllTags();
if (existing.length > 0) return; // only seed into empty storage
await tagService.importTags(SEED_TAGS);
}
```
The seeding runs once on application startup. If any tags already exist, seeding is skipped entirely. Users can reset to defaults from the Tag Manager module.
---
## Cross-Module Usage
Every module that supports tagging stores tag IDs as a `string[]` on its entities. The actual tag data lives in the shared tag storage, not duplicated per module.
### Registratura
```typescript
interface RegistryEntry {
id: string;
// ... other fields
tagIds: string[]; // typically: project + document-type + phase tags
}
```
**Typical tagging pattern:** A registry entry for a building permit application might carry tags `["076 Casa Copernicus", "DTAC", "Depunere", "Parte scrisa"]`.
### Prompt Generator
```typescript
interface PromptTemplate {
id: string;
// ... other fields
tagIds: string[]; // typically: activity + custom domain tags
}
```
**Typical tagging pattern:** An architectural prompt template might carry `["Design exterior", "Rendering"]`.
### IT Inventory
```typescript
interface InventoryDevice {
id: string;
// ... other fields
tagIds: string[]; // typically: company + location tags
}
```
**Typical tagging pattern:** A laptop entry might carry `["Beletage", "Birou Centru"]`.
### Address Book
```typescript
interface Contact {
id: string;
// ... other fields
tagIds: string[]; // typically: custom contact-type tags
}
```
**Typical tagging pattern:** A contact might carry `["Client", "Beletage"]` or `["Furnizor", "Materiale constructii"]`.
### Digital Signatures
```typescript
interface SignatureRecord {
id: string;
// ... other fields
tagIds: string[]; // typically: company + signer identity tags
}
```
### Integration Pattern
All modules follow the same pattern for tag integration:
1. Entity has a `tagIds: string[]` field.
2. Forms include a `<TagSelector>` for editing tags.
3. List/table views include a `<TagFilter>` for filtering by tags.
4. Detail views render tags as `<TagBadge>` components.
5. The module's service does not resolve tag data -- the UI layer calls `TagService.getTag()` or uses the `useTagsById(ids)` hook.
```typescript
// src/hooks/useTags.ts
/** Resolve an array of tag IDs into Tag objects */
function useTagsById(tagIds: string[]): Tag[] {
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
const tagService = getTagService();
tagService.getAllTags().then((allTags) => {
const tagMap = new Map(allTags.map((t) => [t.id, t]));
setTags(tagIds.map((id) => tagMap.get(id)).filter(Boolean) as Tag[]);
});
}, [tagIds]);
return tags;
}
```
---
## Tag Auto-Suggest
The `TagSelector` component supports contextual auto-suggestion. When a module provides context about the current entity, the selector can prioritize relevant tags.
```typescript
interface TagSuggestContext {
moduleId: string; // which module is requesting suggestions
existingTagIds: string[]; // tags already applied to the entity
entityType?: string; // e.g., 'registry-entry', 'contact'
}
```
### Suggestion Algorithm
1. **Category affinity:** Suggest tags from categories most commonly used in this module. Registratura prioritizes `project`, `phase`, `document-type`. Address Book prioritizes `company`, `custom`.
2. **Co-occurrence:** Tags frequently applied alongside the already-selected tags are ranked higher. If the user selects "076 Casa Copernicus", phase tags like "DTAC" and "PT" that have been co-applied with that project before are suggested first.
3. **Recency:** Recently used tags (across all entities in the module) are ranked higher than stale ones.
4. **Hierarchy:** If a parent tag is selected, its children are suggested. If "076 Casa Copernicus" is selected, its child phase tags are surfaced.
The suggestion ranking is computed client-side from the module's entity data and the global tag list. No server-side analytics or ML is involved.
---
## Import / Export
### Export Format (JSON)
```json
{
"version": "1.0",
"exportedAt": "2025-03-15T10:30:00Z",
"tags": [
{
"label": "CU",
"category": "phase",
"scope": "global",
"color": "#3b82f6",
"parentId": null,
"metadata": {}
}
]
}
```
Export strips `id`, `createdAt`, and `updatedAt` since these are regenerated on import. The `parentId` field uses the parent's `label` + `category` as a composite key for portability (since IDs are not stable across environments).
### Import Rules
- Duplicate detection: if a tag with the same `label` + `category` + `scope` already exists, the import skips it (no overwrite).
- Parent resolution: `parentId` in the import file is a `label:category` reference, resolved to the actual ID after all tags are created.
- Validation: all import entries are validated against the same rules as `createTag`. Invalid entries are skipped and reported.
### CSV Format
For simpler workflows, tags can be imported from CSV:
```csv
label,category,scope,color,parentLabel,parentCategory
CU,phase,global,#3b82f6,,
Redactare,activity,global,#6366f1,CU,phase
```