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>
20 KiB
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
// 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
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
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
parentIdmust reference an existing tag. - Deleting a parent tag does not cascade-delete children. Children become root-level tags (their
parentIdis cleared). - Maximum nesting depth: 3 levels. This is enforced in the
TagServiceon create/update. - A tag's category is independent of its parent's category. A
phasetag can be a child of aprojecttag.
Querying the Hierarchy
// 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.
// 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.
// 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.
// 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
TagBadgecomponents above the input. - When
allowCreateis 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.
// 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
TagBadgewith a remove button. - A "+" button opens the
TagSelectorto add more filter tags. - When
tagCountsis 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:
{
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
// 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
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
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
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
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
interface SignatureRecord {
id: string;
// ... other fields
tagIds: string[]; // typically: company + signer identity tags
}
Integration Pattern
All modules follow the same pattern for tag integration:
- Entity has a
tagIds: string[]field. - Forms include a
<TagSelector>for editing tags. - List/table views include a
<TagFilter>for filtering by tags. - Detail views render tags as
<TagBadge>components. - The module's service does not resolve tag data -- the UI layer calls
TagService.getTag()or uses theuseTagsById(ids)hook.
// 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.
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
- Category affinity: Suggest tags from categories most commonly used in this module. Registratura prioritizes
project,phase,document-type. Address Book prioritizescompany,custom. - 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.
- Recency: Recently used tags (across all entities in the module) are ranked higher than stale ones.
- 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)
{
"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+scopealready exists, the import skips it (no overwrite). - Parent resolution:
parentIdin the import file is alabel:categoryreference, 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:
label,category,scope,color,parentLabel,parentCategory
CU,phase,global,#3b82f6,,
Redactare,activity,global,#6366f1,CU,phase