|
|
|
@@ -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>
|
|
|
|
|
);
|
|
|
|
|
}
|