feat(tag-manager): overhaul with 5 ordered categories, hierarchy, editing, and ManicTime seed import
- Reduce TagCategory to 5 ordered types: project, phase, activity, document-type, custom - Add tag hierarchy (parent-child), projectCode field, updatedAt timestamp - Add getChildren, updateTag, cascading deleteTag, searchTags, importTags to TagService - ManicTime seed data parser (~95 Beletage projects, phases, activities, document types) - Full UI rewrite: seed import dialog, inline editing, collapsible category sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -30,6 +30,10 @@ export class TagService {
|
||||
});
|
||||
}
|
||||
|
||||
async getChildren(parentId: string): Promise<Tag[]> {
|
||||
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.parentId === parentId);
|
||||
}
|
||||
|
||||
async createTag(data: Omit<Tag, 'id' | 'createdAt'>): Promise<Tag> {
|
||||
const tag: Tag = {
|
||||
...data,
|
||||
@@ -43,19 +47,41 @@ export class TagService {
|
||||
async updateTag(id: string, updates: Partial<Omit<Tag, 'id' | 'createdAt'>>): Promise<Tag | null> {
|
||||
const existing = await this.storage.get<Tag>(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<void> {
|
||||
// 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<Tag[]> {
|
||||
const lower = query.toLowerCase();
|
||||
return this.storage.query<Tag>(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<Tag, 'id' | 'createdAt'>[]): Promise<number> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TagCategory, string> = {
|
||||
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<string, string>;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ export function useTags(category?: TagCategory) {
|
||||
[service, refresh]
|
||||
);
|
||||
|
||||
const updateTag = useCallback(
|
||||
async (id: string, updates: Partial<Omit<Tag, 'id' | 'createdAt'>>) => {
|
||||
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<Tag, 'id' | 'createdAt'>[]) => {
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user