Files
ArchiTools/docs/architecture/TAGGING-SYSTEM.md
Marius Tarau 4c46e8bcdd 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>
2026-02-17 12:50:25 +02:00

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 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

// 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 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.

// 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:

{
  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:

  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.
// 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

  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)

{
  "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:

label,category,scope,color,parentLabel,parentCategory
CU,phase,global,#3b82f6,,
Redactare,activity,global,#6366f1,CU,phase