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