diff --git a/src/core/tagging/index.ts b/src/core/tagging/index.ts index e9f3d43..5e2588f 100644 --- a/src/core/tagging/index.ts +++ b/src/core/tagging/index.ts @@ -1,3 +1,4 @@ export type { Tag, TagCategory, TagScope } from './types'; +export { TAG_CATEGORY_ORDER, TAG_CATEGORY_LABELS } from './types'; export { TagService } from './tag-service'; export { useTags } from './use-tags'; diff --git a/src/core/tagging/tag-service.ts b/src/core/tagging/tag-service.ts index d1328b7..d955599 100644 --- a/src/core/tagging/tag-service.ts +++ b/src/core/tagging/tag-service.ts @@ -30,6 +30,10 @@ export class TagService { }); } + async getChildren(parentId: string): Promise { + return this.storage.query(NAMESPACE, (tag) => tag.parentId === parentId); + } + async createTag(data: Omit): Promise { const tag: Tag = { ...data, @@ -43,19 +47,41 @@ export class TagService { async updateTag(id: string, updates: Partial>): Promise { const existing = await this.storage.get(NAMESPACE, id); if (!existing) return null; - const updated = { ...existing, ...updates }; + const updated: Tag = { ...existing, ...updates, updatedAt: new Date().toISOString() }; await this.storage.set(NAMESPACE, id, updated); return updated; } async deleteTag(id: string): Promise { + // Also delete children + const children = await this.getChildren(id); + for (const child of children) { + await this.storage.delete(NAMESPACE, child.id); + } await this.storage.delete(NAMESPACE, id); } async searchTags(query: string): Promise { const lower = query.toLowerCase(); return this.storage.query(NAMESPACE, (tag) => - tag.label.toLowerCase().includes(lower) + tag.label.toLowerCase().includes(lower) || + (tag.projectCode?.toLowerCase().includes(lower) ?? false) ); } + + /** Bulk import tags (for seed data). Skips tags whose label already exists in same category. */ + async importTags(tags: Omit[]): Promise { + const existing = await this.getAllTags(); + const existingKeys = new Set(existing.map((t) => `${t.category}::${t.label}`)); + let imported = 0; + for (const data of tags) { + const key = `${data.category}::${data.label}`; + if (!existingKeys.has(key)) { + await this.createTag(data); + existingKeys.add(key); + imported++; + } + } + return imported; + } } diff --git a/src/core/tagging/types.ts b/src/core/tagging/types.ts index d167d74..40d6720 100644 --- a/src/core/tagging/types.ts +++ b/src/core/tagging/types.ts @@ -5,11 +5,25 @@ export type TagCategory = | 'phase' | 'activity' | 'document-type' - | 'company' - | 'priority' - | 'status' | 'custom'; +/** Display order for categories — project & phase are mandatory */ +export const TAG_CATEGORY_ORDER: TagCategory[] = [ + 'project', + 'phase', + 'activity', + 'document-type', + 'custom', +]; + +export const TAG_CATEGORY_LABELS: Record = { + project: 'Proiect', + phase: 'Fază', + activity: 'Activitate', + 'document-type': 'Tip document', + custom: 'Personalizat', +}; + export type TagScope = 'global' | 'module' | 'company'; export interface Tag { @@ -21,7 +35,11 @@ export interface Tag { scope: TagScope; moduleId?: string; companyId?: CompanyId; + /** For hierarchy: parent tag id */ parentId?: string; + /** For project tags: numbered code e.g. "B-001", "US-024" */ + projectCode?: string; metadata?: Record; createdAt: string; + updatedAt?: string; } diff --git a/src/core/tagging/use-tags.ts b/src/core/tagging/use-tags.ts index 8db237a..f128983 100644 --- a/src/core/tagging/use-tags.ts +++ b/src/core/tagging/use-tags.ts @@ -33,6 +33,15 @@ export function useTags(category?: TagCategory) { [service, refresh] ); + const updateTag = useCallback( + async (id: string, updates: Partial>) => { + const tag = await service.updateTag(id, updates); + await refresh(); + return tag; + }, + [service, refresh] + ); + const deleteTag = useCallback( async (id: string) => { await service.deleteTag(id); @@ -41,5 +50,21 @@ export function useTags(category?: TagCategory) { [service, refresh] ); - return { tags, loading, createTag, deleteTag, refresh }; + const importTags = useCallback( + async (data: Omit[]) => { + const count = await service.importTags(data); + await refresh(); + return count; + }, + [service, refresh] + ); + + const searchTags = useCallback( + async (query: string) => { + return service.searchTags(query); + }, + [service] + ); + + return { tags, loading, createTag, updateTag, deleteTag, importTags, searchTags, refresh, service }; } diff --git a/src/modules/tag-manager/components/tag-manager-module.tsx b/src/modules/tag-manager/components/tag-manager-module.tsx index aa8da2f..3834103 100644 --- a/src/modules/tag-manager/components/tag-manager-module.tsx +++ b/src/modules/tag-manager/components/tag-manager-module.tsx @@ -1,27 +1,27 @@ 'use client'; -import { useState } from 'react'; -import { Plus, Trash2, Tag as TagIcon } from 'lucide-react'; +import { useState, useMemo } from 'react'; +import { + Plus, Trash2, Pencil, Check, X, Download, ChevronDown, ChevronRight, + Tag as TagIcon, Search, FolderTree, +} from 'lucide-react'; import { Button } from '@/shared/components/ui/button'; import { Input } from '@/shared/components/ui/input'; import { Label } from '@/shared/components/ui/label'; import { Badge } from '@/shared/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/shared/components/ui/select'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, +} from '@/shared/components/ui/dialog'; import { useTags } from '@/core/tagging'; -import type { TagCategory, TagScope } from '@/core/tagging/types'; +import type { Tag, TagCategory, TagScope } from '@/core/tagging/types'; +import { TAG_CATEGORY_ORDER, TAG_CATEGORY_LABELS } from '@/core/tagging/types'; +import type { CompanyId } from '@/core/auth/types'; import { cn } from '@/shared/lib/utils'; - -const CATEGORY_LABELS: Record = { - project: 'Proiect', - phase: 'Fază', - activity: 'Activitate', - 'document-type': 'Tip document', - company: 'Companie', - priority: 'Prioritate', - status: 'Status', - custom: 'Personalizat', -}; +import { getManicTimeSeedTags } from '../services/seed-data'; const SCOPE_LABELS: Record = { global: 'Global', @@ -29,20 +29,102 @@ const SCOPE_LABELS: Record = { company: 'Companie', }; +const COMPANY_LABELS: Record = { + beletage: 'Beletage', + 'urban-switch': 'Urban Switch', + 'studii-de-teren': 'Studii de Teren', + group: 'Grup', +}; + const TAG_COLORS = [ '#ef4444', '#f97316', '#f59e0b', '#84cc16', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', - '#ec4899', '#64748b', + '#ec4899', '#64748b', '#22B5AB', '#6366f1', ]; export function TagManagerModule() { - const { tags, loading, createTag, deleteTag } = useTags(); + const { tags, loading, createTag, updateTag, deleteTag, importTags } = useTags(); + + // ── Create form state ── const [newLabel, setNewLabel] = useState(''); const [newCategory, setNewCategory] = useState('custom'); const [newScope, setNewScope] = useState('global'); - const [newColor, setNewColor] = useState(TAG_COLORS[5]); - const [filterCategory, setFilterCategory] = useState('all'); + const [newColor, setNewColor] = useState('#3b82f6'); + const [newCompanyId, setNewCompanyId] = useState('beletage'); + const [newProjectCode, setNewProjectCode] = useState(''); + const [newParentId, setNewParentId] = useState(''); + // ── Filter / search state ── + const [filterCategory, setFilterCategory] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [expandedCategories, setExpandedCategories] = useState>( + () => new Set(TAG_CATEGORY_ORDER) + ); + + // ── Edit state ── + const [editingTag, setEditingTag] = useState(null); + const [editLabel, setEditLabel] = useState(''); + const [editColor, setEditColor] = useState(''); + const [editProjectCode, setEditProjectCode] = useState(''); + const [editScope, setEditScope] = useState('global'); + const [editCompanyId, setEditCompanyId] = useState('beletage'); + + // ── Seed import state ── + const [showSeedDialog, setShowSeedDialog] = useState(false); + const [seedImporting, setSeedImporting] = useState(false); + const [seedResult, setSeedResult] = useState(null); + + // ── Computed ── + const filteredTags = useMemo(() => { + let result = tags; + if (filterCategory !== 'all') { + result = result.filter((t) => t.category === filterCategory); + } + if (searchQuery) { + const q = searchQuery.toLowerCase(); + result = result.filter( + (t) => + t.label.toLowerCase().includes(q) || + (t.projectCode?.toLowerCase().includes(q) ?? false) + ); + } + return result; + }, [tags, filterCategory, searchQuery]); + + const groupedByCategory = useMemo(() => { + const groups: Record = {}; + for (const cat of TAG_CATEGORY_ORDER) { + const catTags = filteredTags.filter((t) => t.category === cat); + if (catTags.length > 0) { + groups[cat] = catTags; + } + } + return groups; + }, [filteredTags]); + + /** Build a parent→children map for hierarchy display */ + const childrenMap = useMemo(() => { + const map: Record = {}; + for (const tag of tags) { + if (tag.parentId) { + const existing = map[tag.parentId]; + if (existing) { + existing.push(tag); + } else { + map[tag.parentId] = [tag]; + } + } + } + return map; + }, [tags]); + + const parentCandidates = useMemo(() => { + return tags.filter( + (t) => t.category === newCategory && !t.parentId + ); + }, [tags, newCategory]); + + // ── Handlers ── const handleCreate = async () => { if (!newLabel.trim()) return; await createTag({ @@ -50,158 +132,475 @@ export function TagManagerModule() { category: newCategory, scope: newScope, color: newColor, + companyId: newScope === 'company' ? newCompanyId : undefined, + projectCode: newCategory === 'project' && newProjectCode ? newProjectCode : undefined, + parentId: newParentId || undefined, }); setNewLabel(''); + setNewProjectCode(''); + setNewParentId(''); }; - const filteredTags = filterCategory === 'all' - ? tags - : tags.filter((t) => t.category === filterCategory); + const startEdit = (tag: Tag) => { + setEditingTag(tag); + setEditLabel(tag.label); + setEditColor(tag.color ?? '#3b82f6'); + setEditProjectCode(tag.projectCode ?? ''); + setEditScope(tag.scope); + setEditCompanyId(tag.companyId ?? 'beletage'); + }; - const groupedByCategory = filteredTags.reduce>((acc, tag) => { - const key = tag.category; - if (!acc[key]) acc[key] = []; - acc[key].push(tag); - return acc; - }, {}); + const saveEdit = async () => { + if (!editingTag || !editLabel.trim()) return; + await updateTag(editingTag.id, { + label: editLabel.trim(), + color: editColor, + projectCode: editingTag.category === 'project' && editProjectCode ? editProjectCode : undefined, + scope: editScope, + companyId: editScope === 'company' ? editCompanyId : undefined, + }); + setEditingTag(null); + }; + + const cancelEdit = () => setEditingTag(null); + + const handleSeedImport = async () => { + setSeedImporting(true); + setSeedResult(null); + const seedTags = getManicTimeSeedTags(); + const count = await importTags(seedTags); + setSeedResult(`${count} etichete importate din ${seedTags.length} disponibile.`); + setSeedImporting(false); + }; + + const toggleCategory = (cat: string) => { + setExpandedCategories((prev) => { + const next = new Set(prev); + if (next.has(cat)) next.delete(cat); + else next.add(cat); + return next; + }); + }; + + // ── Stats ── + const projectCount = tags.filter((t) => t.category === 'project').length; + const phaseCount = tags.filter((t) => t.category === 'phase').length; return (
{/* Stats */} -
+

Total etichete

{tags.length}

- -

Categorii folosite

-

{new Set(tags.map((t) => t.category)).size}

-
- -

Globale

-

{tags.filter((t) => t.scope === 'global').length}

-
- -

Personalizate

-

{tags.filter((t) => t.category === 'custom').length}

-
+ {TAG_CATEGORY_ORDER.map((cat) => ( + +

{TAG_CATEGORY_LABELS[cat]}

+

+ {tags.filter((t) => t.category === cat).length} +

+
+ ))}
+ {/* Seed import banner */} + {tags.length === 0 && !loading && ( + + +
+

Nicio etichetă găsită

+

+ Importă datele din ManicTime pentru a popula proiectele, fazele și activitățile. +

+
+ +
+
+ )} + {/* Create new tag */} Etichetă nouă -
-
- - setNewLabel(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleCreate()} - placeholder="Numele etichetei..." - className="mt-1" - /> -
-
- - -
-
- - -
-
- -
- {TAG_COLORS.map((color) => ( -
+
+
-
- {/* Filter */} -
- + {/* Search + Filter bar */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> +
+ {tags.length > 0 && ( + + )}
- {/* Tag list by category */} + {/* Tag list by category with hierarchy */} {loading ? (

Se încarcă...

) : Object.keys(groupedByCategory).length === 0 ? ( -

Nicio etichetă găsită. Creează prima etichetă.

+

+ Nicio etichetă găsită. Creează prima etichetă sau importă datele inițiale. +

) : ( -
- {Object.entries(groupedByCategory).map(([category, catTags]) => ( - - - - - {CATEGORY_LABELS[category as TagCategory] ?? category} - {catTags.length} - - - -
- {catTags.map((tag) => ( -
- {tag.color && ( - - )} - {tag.label} - {SCOPE_LABELS[tag.scope]} - +
+ {Object.entries(groupedByCategory).map(([category, catTags]) => { + const isExpanded = expandedCategories.has(category); + const rootTags = catTags.filter((t) => !t.parentId); + return ( + + toggleCategory(category)} + > + + {isExpanded + ? + : } + + {TAG_CATEGORY_LABELS[category as TagCategory] ?? category} + {catTags.length} + {(category === 'project' || category === 'phase') && ( + obligatoriu + )} + + + {isExpanded && ( + +
+ {rootTags.map((tag) => ( + + ))}
- ))} -
- - + + )} + + ); + })} +
+ )} + + {/* Seed Import Dialog */} + + + + Importă date inițiale ManicTime + +
+

+ Aceasta va importa proiectele Beletage, fazele, activitățile și tipurile de documente + din lista ManicTime. Etichetele existente nu vor fi duplicate. +

+ {seedResult && ( +

{seedResult}

+ )} +
+ + + + +
+
+
+ ); +} + +// ── Tag Row with inline editing ── + +interface TagRowProps { + tag: Tag; + children?: Tag[]; + editingTag: Tag | null; + editLabel: string; + editColor: string; + editProjectCode: string; + editScope: TagScope; + editCompanyId: CompanyId; + onStartEdit: (tag: Tag) => void; + onSaveEdit: () => void; + onCancelEdit: () => void; + onDelete: (id: string) => void; + setEditLabel: (v: string) => void; + setEditColor: (v: string) => void; + setEditProjectCode: (v: string) => void; + setEditScope: (v: TagScope) => void; + setEditCompanyId: (v: CompanyId) => void; +} + +function TagRow({ + tag, children, editingTag, editLabel, editColor, editProjectCode, + editScope, editCompanyId, + onStartEdit, onSaveEdit, onCancelEdit, onDelete, + setEditLabel, setEditColor, setEditProjectCode, setEditScope, setEditCompanyId, +}: TagRowProps) { + const isEditing = editingTag?.id === tag.id; + const [showChildren, setShowChildren] = useState(false); + const hasChildren = children && children.length > 0; + + if (isEditing) { + return ( +
+ {tag.category === 'project' && ( + setEditProjectCode(e.target.value)} + className="w-[100px] font-mono text-xs" + placeholder="B-001" + /> + )} + setEditLabel(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') onSaveEdit(); if (e.key === 'Escape') onCancelEdit(); }} + className="min-w-[200px] flex-1" + autoFocus + /> + + {editScope === 'company' && ( + + )} +
+ {TAG_COLORS.slice(0, 6).map((c) => ( +
+ + +
+ ); + } + + return ( +
+
+ {hasChildren && ( + + )} + {!hasChildren && } + {tag.color && ( + + )} + {tag.projectCode && ( + {tag.projectCode} + )} + {tag.label} + {tag.companyId && ( + + {COMPANY_LABELS[tag.companyId]} + + )} + + {SCOPE_LABELS[tag.scope]} + +
+ + +
+
+ {hasChildren && showChildren && ( +
+ {children.map((child) => ( +
+ + {child.color && ( + + )} + {child.projectCode && ( + {child.projectCode} + )} + {child.label} +
+ + +
+
))}
)} diff --git a/src/modules/tag-manager/index.ts b/src/modules/tag-manager/index.ts index 6208976..e14e2e1 100644 --- a/src/modules/tag-manager/index.ts +++ b/src/modules/tag-manager/index.ts @@ -1,3 +1,4 @@ export { tagManagerConfig } from './config'; export { TagManagerModule } from './components/tag-manager-module'; export type { Tag, TagCategory, TagScope } from './types'; +export { TAG_CATEGORY_ORDER, TAG_CATEGORY_LABELS } from './types'; diff --git a/src/modules/tag-manager/services/seed-data.ts b/src/modules/tag-manager/services/seed-data.ts new file mode 100644 index 0000000..656bcdc --- /dev/null +++ b/src/modules/tag-manager/services/seed-data.ts @@ -0,0 +1,188 @@ +import type { Tag, TagCategory } from '@/core/tagging/types'; +import type { CompanyId } from '@/core/auth/types'; + +type SeedTag = Omit; + +/** Parse project line like "000 Farmacie" → { code: "B-000", label: "Farmacie" } */ +function parseProjectLine(line: string, prefix: string): { code: string; label: string } | null { + const match = line.match(/^(\w?\d+)\s+(.+)$/); + if (!match?.[1] || !match[2]) return null; + const num = match[1]; + const label = match[2].trim(); + const padded = num.replace(/^[A-Z]/, '').padStart(3, '0'); + const codePrefix = num.startsWith('L') ? `${prefix}L` : prefix; + return { code: `${codePrefix}-${padded}`, label }; +} + +export function getManicTimeSeedTags(): SeedTag[] { + const tags: SeedTag[] = []; + + // ── Beletage projects ── + const beletageProjects = [ + '000 Farmacie', + '002 Cladire birouri Stratec', + '003 PUZ Bellavista', + '007 Design Apartament Teodora', + '010 Casa Doinei', + '016 Duplex Eremia', + '024 Bloc Petofi', + '028 PUZ Borhanci-Sopor', + '033 Mansardare Branului', + '039 Cabinete Stoma Scala', + '041 Imobil mixt Progresului', + '045 Casa Andrei Muresanu', + '052 PUZ Carpenului', + '059 PUZ Nordului', + '064 Casa Salicea', + '066 Terasa Gherase', + '070 Bloc Fanatelor', + '073 Case Frumoasa', + '074 PUG Cosbuc', + '076 Casa Copernicus', + '077 PUZ Schimbare destinatie Brancusi', + '078 Service auto Linistei', + '079 Amenajare drum Servitute Eremia', + '080 Bloc Tribunul', + '081 Extindere casa Gherase', + '083 Modificari casa Zsigmund 18', + '084 Mansardare Petofi 21', + '085 Container CT Spital Tabacarilor', + '086 Imprejmuire casa sat Gheorgheni', + '087 Duplex Oasului fn', + '089 PUZ A-Liu Sopor', + '090 VR MedEvents', + '091 Reclama Caparol', + '092 Imobil birouri 13 Septembrie', + '093 Casa Salistea Noua', + '094 PUD Casa Rediu', + '095 Duplex Vanatorului', + '096 Design apartament Sopor', + '097 Cabana Gilau', + '101 PUZ Gilau', + '102 PUZ Ghimbav', + '103 Piscine Lunca Noua', + '104 PUZ REGHIN', + '105 CUT&Crust', + '106 PUZ Mihai Romanu Nord', + '108 Reabilitare Bloc Beiusului', + '109 Case Samboleni', + '110 Penny Crasna', + '111 Anexa Piscina Borhanci', + '112 PUZ Blocuri Bistrita', + '113 PUZ VARATEC-FIRIZA', + '114 PUG Husi', + '115 PUG Josenii Bargaului', + '116 PUG Monor', + '117 Schimbare Destinatie Mihai Viteazu 2', + '120 Anexa Brasov', + '121 Imprejurare imobil Mesterul Manole 9', + '122 Fastfood Bashar', + '123 PUD Rediu 2', + '127 Casa Socaciu Ciurila', + '128 Schimbare de destinatie Danubius', + '129 (re) Casa Sarca-Sorescu', + '130 Casa Suta-Wonderland', + '131 PUD Oasului Hufi', + '132 Reabilitare Camin Cultural Baciu', + '133 PUG Feldru', + '134 DALI Blocuri Murfatlar', + '135 Case de vacanta Dianei', + '136 PUG BROSTENI', + '139 Casa Turda', + '140 Releveu Bistrita (Morariu)', + '141 PUZ Janovic Jeno', + '142 Penny Borhanci', + '143 Pavilion Politie Radauti', + '149 Duplex Sorescu 31-33', + '150 DALI SF Scoala Baciu', + '151 Casa Alexandru Bohatiel 17', + '152 PUZ Penny Tautii Magheraus', + '153 PUG Banita', + '155 PT Scoala Floresti', + '156 Case Sorescu', + '157 Gradi-Cresa Baciu', + '158 Duplex Sorescu 21-23', + '159 Amenajare Spatiu Grenke PBC', + '160 Etajare Primaria Baciu', + '161 Extindere Ap Baciu', + '164 SD salon Aurel Vlaicu', + '165 Reclama Marasti', + '166 Catei Apahida', + '167 Apartament Mircea Zaciu 13-15', + '169 Casa PETRILA 37', + '170 Cabana Campeni AB', + '171 Camin Apahida', + 'L089 PUZ TUSA-BOJAN', + '172 Design casa Iugoslaviei 18', + '173 Reabilitare spitale Sighetu', + '174 StudX UMFST', + '176 - 2025 - ReAC Ansamblu rezi Bibescu', + ]; + + for (const line of beletageProjects) { + const parsed = parseProjectLine(line, 'B'); + if (parsed) { + tags.push({ + label: parsed.label, + category: 'project', + scope: 'company', + companyId: 'beletage' as CompanyId, + projectCode: parsed.code, + color: '#22B5AB', + }); + } + } + + // ── Phase tags ── + const phases = [ + 'CU', 'Schita', 'Avize', 'PUD', 'AO', 'PUZ', 'PUG', + 'DTAD', 'DTAC', 'PT', 'Detalii de Executie', 'Studii de fundamentare', + 'Regulament', 'Parte desenata', 'Parte scrisa', + 'Consultanta client', 'Macheta', 'Consultanta receptie', + 'Redactare', 'Depunere', 'Ridicare', 'Verificare proiect', + 'Vizita santier', + ]; + + for (const phase of phases) { + tags.push({ + label: phase, + category: 'phase', + scope: 'global', + color: '#3b82f6', + }); + } + + // ── Activity tags ── + const activities = [ + 'Ofertare', 'Configurari', 'Organizare initiala', 'Pregatire Portofoliu', + 'Website', 'Documentare', 'Design grafic', 'Design interior', + 'Design exterior', 'Releveu', 'Reclama', 'Master MATDR', + 'Pauza de masa', 'Timp personal', 'Concediu', 'Compensare overtime', + ]; + + for (const activity of activities) { + tags.push({ + label: activity, + category: 'activity', + scope: 'global', + color: '#8b5cf6', + }); + } + + // ── Document type tags ── + const docTypes = [ + 'Contract', 'Ofertă', 'Factură', 'Scrisoare', + 'Aviz', 'Notă de comandă', 'Raport', 'Cerere', 'Altele', + ]; + + for (const dt of docTypes) { + tags.push({ + label: dt, + category: 'document-type', + scope: 'global', + color: '#f59e0b', + }); + } + + return tags; +} diff --git a/src/modules/tag-manager/types.ts b/src/modules/tag-manager/types.ts index acf3236..cad695b 100644 --- a/src/modules/tag-manager/types.ts +++ b/src/modules/tag-manager/types.ts @@ -1 +1,2 @@ export type { Tag, TagCategory, TagScope } from '@/core/tagging/types'; +export { TAG_CATEGORY_ORDER, TAG_CATEGORY_LABELS } from '@/core/tagging/types';