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>
This commit is contained in:
210
src/modules/tag-manager/components/tag-manager-module.tsx
Normal file
210
src/modules/tag-manager/components/tag-manager-module.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, Trash2, Tag as TagIcon } 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 { useTags } from '@/core/tagging';
|
||||
import type { TagCategory, TagScope } from '@/core/tagging/types';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
|
||||
const CATEGORY_LABELS: Record<TagCategory, string> = {
|
||||
project: 'Proiect',
|
||||
phase: 'Fază',
|
||||
activity: 'Activitate',
|
||||
'document-type': 'Tip document',
|
||||
company: 'Companie',
|
||||
priority: 'Prioritate',
|
||||
status: 'Status',
|
||||
custom: 'Personalizat',
|
||||
};
|
||||
|
||||
const SCOPE_LABELS: Record<TagScope, string> = {
|
||||
global: 'Global',
|
||||
module: 'Modul',
|
||||
company: 'Companie',
|
||||
};
|
||||
|
||||
const TAG_COLORS = [
|
||||
'#ef4444', '#f97316', '#f59e0b', '#84cc16',
|
||||
'#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6',
|
||||
'#ec4899', '#64748b',
|
||||
];
|
||||
|
||||
export function TagManagerModule() {
|
||||
const { tags, loading, createTag, deleteTag } = useTags();
|
||||
const [newLabel, setNewLabel] = useState('');
|
||||
const [newCategory, setNewCategory] = useState<TagCategory>('custom');
|
||||
const [newScope, setNewScope] = useState<TagScope>('global');
|
||||
const [newColor, setNewColor] = useState(TAG_COLORS[5]);
|
||||
const [filterCategory, setFilterCategory] = useState<TagCategory | 'all'>('all');
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newLabel.trim()) return;
|
||||
await createTag({
|
||||
label: newLabel.trim(),
|
||||
category: newCategory,
|
||||
scope: newScope,
|
||||
color: newColor,
|
||||
});
|
||||
setNewLabel('');
|
||||
};
|
||||
|
||||
const filteredTags = filterCategory === 'all'
|
||||
? tags
|
||||
: tags.filter((t) => t.category === filterCategory);
|
||||
|
||||
const groupedByCategory = filteredTags.reduce<Record<string, typeof tags>>((acc, tag) => {
|
||||
const key = tag.category;
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push(tag);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<Card><CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">Total etichete</p>
|
||||
<p className="text-2xl font-bold">{tags.length}</p>
|
||||
</CardContent></Card>
|
||||
<Card><CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">Categorii folosite</p>
|
||||
<p className="text-2xl font-bold">{new Set(tags.map((t) => t.category)).size}</p>
|
||||
</CardContent></Card>
|
||||
<Card><CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">Globale</p>
|
||||
<p className="text-2xl font-bold">{tags.filter((t) => t.scope === 'global').length}</p>
|
||||
</CardContent></Card>
|
||||
<Card><CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">Personalizate</p>
|
||||
<p className="text-2xl font-bold">{tags.filter((t) => t.category === 'custom').length}</p>
|
||||
</CardContent></Card>
|
||||
</div>
|
||||
|
||||
{/* Create new tag */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Etichetă nouă</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<div className="min-w-[200px] flex-1">
|
||||
<Label>Nume</Label>
|
||||
<Input
|
||||
value={newLabel}
|
||||
onChange={(e) => setNewLabel(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
placeholder="Numele etichetei..."
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-[160px]">
|
||||
<Label>Categorie</Label>
|
||||
<Select value={newCategory} onValueChange={(v) => setNewCategory(v as TagCategory)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(CATEGORY_LABELS) as TagCategory[]).map((cat) => (
|
||||
<SelectItem key={cat} value={cat}>{CATEGORY_LABELS[cat]}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-[130px]">
|
||||
<Label>Vizibilitate</Label>
|
||||
<Select value={newScope} onValueChange={(v) => setNewScope(v as TagScope)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(SCOPE_LABELS) as TagScope[]).map((s) => (
|
||||
<SelectItem key={s} value={s}>{SCOPE_LABELS[s]}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-1.5 block">Culoare</Label>
|
||||
<div className="flex gap-1">
|
||||
{TAG_COLORS.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
onClick={() => setNewColor(color)}
|
||||
className={cn(
|
||||
'h-7 w-7 rounded-full border-2 transition-all',
|
||||
newColor === color ? 'border-primary scale-110' : 'border-transparent hover:scale-105'
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleCreate} disabled={!newLabel.trim()}>
|
||||
<Plus className="mr-1 h-4 w-4" /> Adaugă
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Label>Filtrează:</Label>
|
||||
<Select value={filterCategory} onValueChange={(v) => setFilterCategory(v as TagCategory | 'all')}>
|
||||
<SelectTrigger className="w-[180px]"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toate categoriile</SelectItem>
|
||||
{(Object.keys(CATEGORY_LABELS) as TagCategory[]).map((cat) => (
|
||||
<SelectItem key={cat} value={cat}>{CATEGORY_LABELS[cat]}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Tag list by category */}
|
||||
{loading ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
||||
) : Object.keys(groupedByCategory).length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">Nicio etichetă găsită. Creează prima etichetă.</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(groupedByCategory).map(([category, catTags]) => (
|
||||
<Card key={category}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<TagIcon className="h-4 w-4" />
|
||||
{CATEGORY_LABELS[category as TagCategory] ?? category}
|
||||
<Badge variant="secondary" className="ml-1">{catTags.length}</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{catTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="group flex items-center gap-1.5 rounded-full border py-1 pl-3 pr-1.5 text-sm"
|
||||
>
|
||||
{tag.color && (
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: tag.color }} />
|
||||
)}
|
||||
<span>{tag.label}</span>
|
||||
<Badge variant="outline" className="text-[10px] px-1">{SCOPE_LABELS[tag.scope]}</Badge>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteTag(tag.id)}
|
||||
className="ml-0.5 rounded-full p-0.5 opacity-0 transition-opacity hover:bg-destructive/10 group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/modules/tag-manager/config.ts
Normal file
17
src/modules/tag-manager/config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ModuleConfig } from '@/core/module-registry/types';
|
||||
|
||||
export const tagManagerConfig: ModuleConfig = {
|
||||
id: 'tag-manager',
|
||||
name: 'Manager Etichete',
|
||||
description: 'Administrare centralizată a etichetelor și categoriilor din platformă',
|
||||
icon: 'tags',
|
||||
route: '/tag-manager',
|
||||
category: 'tools',
|
||||
featureFlag: 'module.tag-manager',
|
||||
visibility: 'all',
|
||||
version: '0.1.0',
|
||||
dependencies: [],
|
||||
storageNamespace: 'tag-manager',
|
||||
navOrder: 40,
|
||||
tags: ['etichete', 'categorii', 'organizare'],
|
||||
};
|
||||
3
src/modules/tag-manager/index.ts
Normal file
3
src/modules/tag-manager/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { tagManagerConfig } from './config';
|
||||
export { TagManagerModule } from './components/tag-manager-module';
|
||||
export type { Tag, TagCategory, TagScope } from './types';
|
||||
1
src/modules/tag-manager/types.ts
Normal file
1
src/modules/tag-manager/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type { Tag, TagCategory, TagScope } from '@/core/tagging/types';
|
||||
Reference in New Issue
Block a user