feat(mini-utilities): add 5 new tools - U/R converter, AI cleaner, MDLPA, PDF reducer, OCR
This commit is contained in:
@@ -1,73 +1,107 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from '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';
|
||||
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 {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/shared/components/ui/select';
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card";
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
||||
} from '@/shared/components/ui/dialog';
|
||||
import { useTags } from '@/core/tagging';
|
||||
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';
|
||||
import { getManicTimeSeedTags } from '../services/seed-data';
|
||||
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 { 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";
|
||||
import { getManicTimeSeedTags } from "../services/seed-data";
|
||||
|
||||
const SCOPE_LABELS: Record<TagScope, string> = {
|
||||
global: 'Global',
|
||||
module: 'Modul',
|
||||
company: 'Companie',
|
||||
global: "Global",
|
||||
module: "Modul",
|
||||
company: "Companie",
|
||||
};
|
||||
|
||||
const COMPANY_LABELS: Record<CompanyId, string> = {
|
||||
beletage: 'Beletage',
|
||||
'urban-switch': 'Urban Switch',
|
||||
'studii-de-teren': 'Studii de Teren',
|
||||
group: 'Grup',
|
||||
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', '#22B5AB', '#6366f1',
|
||||
"#ef4444",
|
||||
"#f97316",
|
||||
"#f59e0b",
|
||||
"#84cc16",
|
||||
"#22c55e",
|
||||
"#06b6d4",
|
||||
"#3b82f6",
|
||||
"#8b5cf6",
|
||||
"#ec4899",
|
||||
"#64748b",
|
||||
"#22B5AB",
|
||||
"#6366f1",
|
||||
];
|
||||
|
||||
export function TagManagerModule() {
|
||||
const { tags, loading, createTag, updateTag, deleteTag, importTags } = useTags();
|
||||
const { tags, loading, createTag, updateTag, deleteTag, importTags } =
|
||||
useTags();
|
||||
|
||||
// ── Create form state ──
|
||||
const [newLabel, setNewLabel] = useState('');
|
||||
const [newCategory, setNewCategory] = useState<TagCategory>('custom');
|
||||
const [newScope, setNewScope] = useState<TagScope>('global');
|
||||
const [newColor, setNewColor] = useState('#3b82f6');
|
||||
const [newCompanyId, setNewCompanyId] = useState<CompanyId>('beletage');
|
||||
const [newProjectCode, setNewProjectCode] = useState('');
|
||||
const [newParentId, setNewParentId] = useState('');
|
||||
const [newLabel, setNewLabel] = useState("");
|
||||
const [newCategory, setNewCategory] = useState<TagCategory>("custom");
|
||||
const [newScope, setNewScope] = useState<TagScope>("global");
|
||||
const [newColor, setNewColor] = useState("#3b82f6");
|
||||
const [newCompanyId, setNewCompanyId] = useState<CompanyId>("beletage");
|
||||
const [newProjectCode, setNewProjectCode] = useState("");
|
||||
const [newParentId, setNewParentId] = useState("");
|
||||
|
||||
// ── Filter / search state ──
|
||||
const [filterCategory, setFilterCategory] = useState<TagCategory | 'all'>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterCategory, setFilterCategory] = useState<TagCategory | "all">(
|
||||
"all",
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||
() => new Set(TAG_CATEGORY_ORDER)
|
||||
() => new Set(TAG_CATEGORY_ORDER),
|
||||
);
|
||||
|
||||
// ── Edit state ──
|
||||
const [editingTag, setEditingTag] = useState<Tag | null>(null);
|
||||
const [editLabel, setEditLabel] = useState('');
|
||||
const [editColor, setEditColor] = useState('');
|
||||
const [editProjectCode, setEditProjectCode] = useState('');
|
||||
const [editScope, setEditScope] = useState<TagScope>('global');
|
||||
const [editCompanyId, setEditCompanyId] = useState<CompanyId>('beletage');
|
||||
const [editLabel, setEditLabel] = useState("");
|
||||
const [editColor, setEditColor] = useState("");
|
||||
const [editProjectCode, setEditProjectCode] = useState("");
|
||||
const [editScope, setEditScope] = useState<TagScope>("global");
|
||||
const [editCompanyId, setEditCompanyId] = useState<CompanyId>("beletage");
|
||||
|
||||
// ── Seed import state ──
|
||||
const [showSeedDialog, setShowSeedDialog] = useState(false);
|
||||
@@ -77,7 +111,7 @@ export function TagManagerModule() {
|
||||
// ── Computed ──
|
||||
const filteredTags = useMemo(() => {
|
||||
let result = tags;
|
||||
if (filterCategory !== 'all') {
|
||||
if (filterCategory !== "all") {
|
||||
result = result.filter((t) => t.category === filterCategory);
|
||||
}
|
||||
if (searchQuery) {
|
||||
@@ -85,7 +119,7 @@ export function TagManagerModule() {
|
||||
result = result.filter(
|
||||
(t) =>
|
||||
t.label.toLowerCase().includes(q) ||
|
||||
(t.projectCode?.toLowerCase().includes(q) ?? false)
|
||||
(t.projectCode?.toLowerCase().includes(q) ?? false),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
@@ -119,9 +153,7 @@ export function TagManagerModule() {
|
||||
}, [tags]);
|
||||
|
||||
const parentCandidates = useMemo(() => {
|
||||
return tags.filter(
|
||||
(t) => t.category === newCategory && !t.parentId
|
||||
);
|
||||
return tags.filter((t) => t.category === newCategory && !t.parentId);
|
||||
}, [tags, newCategory]);
|
||||
|
||||
// ── Validation state ──
|
||||
@@ -131,13 +163,17 @@ export function TagManagerModule() {
|
||||
const handleCreate = async () => {
|
||||
const errors: string[] = [];
|
||||
if (!newLabel.trim()) {
|
||||
errors.push('Numele etichetei este obligatoriu.');
|
||||
errors.push("Numele etichetei este obligatoriu.");
|
||||
}
|
||||
if (newCategory === 'project' && !newProjectCode.trim()) {
|
||||
errors.push('Codul proiectului este obligatoriu pentru categoria Proiect (ex: B-001, US-010, SDT-003).');
|
||||
if (newCategory === "project" && !newProjectCode.trim()) {
|
||||
errors.push(
|
||||
"Codul proiectului este obligatoriu pentru categoria Proiect (ex: B-001, US-010, SDT-003).",
|
||||
);
|
||||
}
|
||||
if (newCategory === 'project' && newScope !== 'company') {
|
||||
errors.push('Etichetele de tip Proiect trebuie asociate unei companii (vizibilitate = Companie).');
|
||||
if (newCategory === "project" && newScope !== "company") {
|
||||
errors.push(
|
||||
"Etichetele de tip Proiect trebuie asociate unei companii (vizibilitate = Companie).",
|
||||
);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
setValidationErrors(errors);
|
||||
@@ -149,22 +185,25 @@ export function TagManagerModule() {
|
||||
category: newCategory,
|
||||
scope: newScope,
|
||||
color: newColor,
|
||||
companyId: newScope === 'company' ? newCompanyId : undefined,
|
||||
projectCode: newCategory === 'project' && newProjectCode ? newProjectCode : undefined,
|
||||
companyId: newScope === "company" ? newCompanyId : undefined,
|
||||
projectCode:
|
||||
newCategory === "project" && newProjectCode
|
||||
? newProjectCode
|
||||
: undefined,
|
||||
parentId: newParentId || undefined,
|
||||
});
|
||||
setNewLabel('');
|
||||
setNewProjectCode('');
|
||||
setNewParentId('');
|
||||
setNewLabel("");
|
||||
setNewProjectCode("");
|
||||
setNewParentId("");
|
||||
};
|
||||
|
||||
const startEdit = (tag: Tag) => {
|
||||
setEditingTag(tag);
|
||||
setEditLabel(tag.label);
|
||||
setEditColor(tag.color ?? '#3b82f6');
|
||||
setEditProjectCode(tag.projectCode ?? '');
|
||||
setEditColor(tag.color ?? "#3b82f6");
|
||||
setEditProjectCode(tag.projectCode ?? "");
|
||||
setEditScope(tag.scope);
|
||||
setEditCompanyId(tag.companyId ?? 'beletage');
|
||||
setEditCompanyId(tag.companyId ?? "beletage");
|
||||
};
|
||||
|
||||
const saveEdit = async () => {
|
||||
@@ -172,9 +211,12 @@ export function TagManagerModule() {
|
||||
await updateTag(editingTag.id, {
|
||||
label: editLabel.trim(),
|
||||
color: editColor,
|
||||
projectCode: editingTag.category === 'project' && editProjectCode ? editProjectCode : undefined,
|
||||
projectCode:
|
||||
editingTag.category === "project" && editProjectCode
|
||||
? editProjectCode
|
||||
: undefined,
|
||||
scope: editScope,
|
||||
companyId: editScope === 'company' ? editCompanyId : undefined,
|
||||
companyId: editScope === "company" ? editCompanyId : undefined,
|
||||
});
|
||||
setEditingTag(null);
|
||||
};
|
||||
@@ -186,7 +228,9 @@ export function TagManagerModule() {
|
||||
setSeedResult(null);
|
||||
const seedTags = getManicTimeSeedTags();
|
||||
const count = await importTags(seedTags);
|
||||
setSeedResult(`${count} etichete importate din ${seedTags.length} disponibile.`);
|
||||
setSeedResult(
|
||||
`${count} etichete importate din ${seedTags.length} disponibile.`,
|
||||
);
|
||||
setSeedImporting(false);
|
||||
};
|
||||
|
||||
@@ -200,24 +244,30 @@ export function TagManagerModule() {
|
||||
};
|
||||
|
||||
// ── Stats ──
|
||||
const projectCount = tags.filter((t) => t.category === 'project').length;
|
||||
const phaseCount = tags.filter((t) => t.category === 'phase').length;
|
||||
const projectCount = tags.filter((t) => t.category === "project").length;
|
||||
const phaseCount = tags.filter((t) => t.category === "phase").length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
||||
<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">Total etichete</p>
|
||||
<p className="text-2xl font-bold">{tags.length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{TAG_CATEGORY_ORDER.map((cat) => (
|
||||
<Card key={cat}><CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">{TAG_CATEGORY_LABELS[cat]}</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{tags.filter((t) => t.category === cat).length}
|
||||
</p>
|
||||
</CardContent></Card>
|
||||
<Card key={cat}>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{TAG_CATEGORY_LABELS[cat]}
|
||||
</p>
|
||||
<p className="text-2xl font-bold">
|
||||
{tags.filter((t) => t.category === cat).length}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -228,7 +278,8 @@ export function TagManagerModule() {
|
||||
<div>
|
||||
<p className="font-medium">Nicio etichetă găsită</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Importă datele din ManicTime pentru a popula proiectele, fazele și activitățile.
|
||||
Importă datele din ManicTime pentru a popula proiectele, fazele
|
||||
și activitățile.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setShowSeedDialog(true)}>
|
||||
@@ -240,7 +291,9 @@ export function TagManagerModule() {
|
||||
|
||||
{/* Create new tag */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Etichetă nouă</CardTitle></CardHeader>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Etichetă nouă</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
@@ -249,41 +302,62 @@ export function TagManagerModule() {
|
||||
<Input
|
||||
value={newLabel}
|
||||
onChange={(e) => setNewLabel(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
||||
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>
|
||||
<Select
|
||||
value={newCategory}
|
||||
onValueChange={(v) => setNewCategory(v as TagCategory)}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TAG_CATEGORY_ORDER.map((cat) => (
|
||||
<SelectItem key={cat} value={cat}>{TAG_CATEGORY_LABELS[cat]}</SelectItem>
|
||||
<SelectItem key={cat} value={cat}>
|
||||
{TAG_CATEGORY_LABELS[cat]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="w-[140px]">
|
||||
<Label>Vizibilitate</Label>
|
||||
<Select value={newScope} onValueChange={(v) => setNewScope(v as TagScope)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<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>
|
||||
<SelectItem key={s} value={s}>
|
||||
{SCOPE_LABELS[s]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{newScope === 'company' && (
|
||||
{newScope === "company" && (
|
||||
<div className="w-[150px]">
|
||||
<Label>Companie</Label>
|
||||
<Select value={newCompanyId} onValueChange={(v) => setNewCompanyId(v as CompanyId)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<Select
|
||||
value={newCompanyId}
|
||||
onValueChange={(v) => setNewCompanyId(v as CompanyId)}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (
|
||||
<SelectItem key={c} value={c}>{COMPANY_LABELS[c]}</SelectItem>
|
||||
<SelectItem key={c} value={c}>
|
||||
{COMPANY_LABELS[c]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -292,7 +366,7 @@ export function TagManagerModule() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
{newCategory === 'project' && (
|
||||
{newCategory === "project" && (
|
||||
<div className="w-[140px]">
|
||||
<Label>Cod proiect</Label>
|
||||
<Input
|
||||
@@ -306,13 +380,23 @@ export function TagManagerModule() {
|
||||
{parentCandidates.length > 0 && (
|
||||
<div className="w-[200px]">
|
||||
<Label>Tag părinte (opțional)</Label>
|
||||
<Select value={newParentId || '__none__'} onValueChange={(v) => setNewParentId(v === '__none__' ? '' : v)}>
|
||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
||||
<Select
|
||||
value={newParentId || "__none__"}
|
||||
onValueChange={(v) =>
|
||||
setNewParentId(v === "__none__" ? "" : v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">— Niciun părinte —</SelectItem>
|
||||
<SelectItem value="__none__">
|
||||
— Niciun părinte —
|
||||
</SelectItem>
|
||||
{parentCandidates.map((p) => (
|
||||
<SelectItem key={p.id} value={p.id}>
|
||||
{p.projectCode ? `${p.projectCode} ` : ''}{p.label}
|
||||
{p.projectCode ? `${p.projectCode} ` : ""}
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -328,8 +412,10 @@ export function TagManagerModule() {
|
||||
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'
|
||||
"h-7 w-7 rounded-full border-2 transition-all",
|
||||
newColor === color
|
||||
? "border-primary scale-110"
|
||||
: "border-transparent hover:scale-105",
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
@@ -345,16 +431,19 @@ export function TagManagerModule() {
|
||||
{validationErrors.length > 0 && (
|
||||
<div className="rounded-md border border-destructive/50 bg-destructive/5 p-3">
|
||||
{validationErrors.map((err) => (
|
||||
<p key={err} className="text-sm text-destructive">{err}</p>
|
||||
<p key={err} className="text-sm text-destructive">
|
||||
{err}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hint for mandatory categories */}
|
||||
{(newCategory === 'project' || newCategory === 'phase') && (
|
||||
{(newCategory === "project" || newCategory === "phase") && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Notă:</strong> Categoriile <em>Proiect</em> și <em>Fază</em> sunt obligatorii
|
||||
în structura de etichete. Proiectele necesită un cod (ex: B-001, US-010, SDT-003) și
|
||||
<strong>Notă:</strong> Categoriile <em>Proiect</em> și{" "}
|
||||
<em>Fază</em> sunt obligatorii în structura de etichete.
|
||||
Proiectele necesită un cod (ex: B-001, US-010, SDT-003) și
|
||||
trebuie asociate unei companii.
|
||||
</p>
|
||||
)}
|
||||
@@ -373,17 +462,28 @@ export function TagManagerModule() {
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Select value={filterCategory} onValueChange={(v) => setFilterCategory(v as TagCategory | 'all')}>
|
||||
<SelectTrigger className="w-[180px]"><SelectValue /></SelectTrigger>
|
||||
<Select
|
||||
value={filterCategory}
|
||||
onValueChange={(v) => setFilterCategory(v as TagCategory | "all")}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toate categoriile</SelectItem>
|
||||
{TAG_CATEGORY_ORDER.map((cat) => (
|
||||
<SelectItem key={cat} value={cat}>{TAG_CATEGORY_LABELS[cat]}</SelectItem>
|
||||
<SelectItem key={cat} value={cat}>
|
||||
{TAG_CATEGORY_LABELS[cat]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{tags.length > 0 && (
|
||||
<Button variant="outline" size="sm" onClick={() => setShowSeedDialog(true)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowSeedDialog(true)}
|
||||
>
|
||||
<Download className="mr-1 h-3.5 w-3.5" /> Importă ManicTime
|
||||
</Button>
|
||||
)}
|
||||
@@ -391,10 +491,13 @@ export function TagManagerModule() {
|
||||
|
||||
{/* Tag list by category with hierarchy */}
|
||||
{loading ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">Se încarcă...</p>
|
||||
<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ă sau importă datele inițiale.
|
||||
Nicio etichetă găsită. Creează prima etichetă sau importă datele
|
||||
inițiale.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -408,14 +511,20 @@ export function TagManagerModule() {
|
||||
onClick={() => toggleCategory(category)}
|
||||
>
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
{isExpanded
|
||||
? <ChevronDown className="h-4 w-4" />
|
||||
: <ChevronRight className="h-4 w-4" />}
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
<TagIcon className="h-4 w-4" />
|
||||
{TAG_CATEGORY_LABELS[category as TagCategory] ?? category}
|
||||
<Badge variant="secondary" className="ml-1">{catTags.length}</Badge>
|
||||
{(category === 'project' || category === 'phase') && (
|
||||
<Badge variant="default" className="ml-1 text-[10px]">obligatoriu</Badge>
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{catTags.length}
|
||||
</Badge>
|
||||
{(category === "project" || category === "phase") && (
|
||||
<Badge variant="default" className="ml-1 text-[10px]">
|
||||
obligatoriu
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -461,18 +570,22 @@ export function TagManagerModule() {
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 py-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Aceasta va importa proiectele Beletage, Urban Switch și Studii de Teren,
|
||||
fazele, activitățile și tipurile de documente din lista ManicTime.
|
||||
Etichetele existente nu vor fi duplicate.
|
||||
Aceasta va importa proiectele Beletage, Urban Switch și Studii de
|
||||
Teren, fazele, activitățile și tipurile de documente din lista
|
||||
ManicTime. Etichetele existente nu vor fi duplicate.
|
||||
</p>
|
||||
{seedResult && (
|
||||
<p className="rounded bg-muted p-2 text-sm font-medium">{seedResult}</p>
|
||||
<p className="rounded bg-muted p-2 text-sm font-medium">
|
||||
{seedResult}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowSeedDialog(false)}>Închide</Button>
|
||||
<Button variant="outline" onClick={() => setShowSeedDialog(false)}>
|
||||
Închide
|
||||
</Button>
|
||||
<Button onClick={handleSeedImport} disabled={seedImporting}>
|
||||
{seedImporting ? 'Se importă...' : 'Importă'}
|
||||
{seedImporting ? "Se importă..." : "Importă"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -504,10 +617,23 @@ interface TagRowProps {
|
||||
}
|
||||
|
||||
function TagRow({
|
||||
tag, children, editingTag, editLabel, editColor, editProjectCode,
|
||||
editScope, editCompanyId,
|
||||
onStartEdit, onSaveEdit, onCancelEdit, onDelete,
|
||||
setEditLabel, setEditColor, setEditProjectCode, setEditScope, setEditCompanyId,
|
||||
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);
|
||||
@@ -516,7 +642,7 @@ function TagRow({
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/30 p-2">
|
||||
{tag.category === 'project' && (
|
||||
{tag.category === "project" && (
|
||||
<Input
|
||||
value={editProjectCode}
|
||||
onChange={(e) => setEditProjectCode(e.target.value)}
|
||||
@@ -527,24 +653,39 @@ function TagRow({
|
||||
<Input
|
||||
value={editLabel}
|
||||
onChange={(e) => setEditLabel(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') onSaveEdit(); if (e.key === 'Escape') onCancelEdit(); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onSaveEdit();
|
||||
if (e.key === "Escape") onCancelEdit();
|
||||
}}
|
||||
className="min-w-[200px] flex-1"
|
||||
autoFocus
|
||||
/>
|
||||
<Select value={editScope} onValueChange={(v) => setEditScope(v as TagScope)}>
|
||||
<SelectTrigger className="w-[120px]"><SelectValue /></SelectTrigger>
|
||||
<Select
|
||||
value={editScope}
|
||||
onValueChange={(v) => setEditScope(v as TagScope)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="global">Global</SelectItem>
|
||||
<SelectItem value="module">Modul</SelectItem>
|
||||
<SelectItem value="company">Companie</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{editScope === 'company' && (
|
||||
<Select value={editCompanyId} onValueChange={(v) => setEditCompanyId(v as CompanyId)}>
|
||||
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger>
|
||||
{editScope === "company" && (
|
||||
<Select
|
||||
value={editCompanyId}
|
||||
onValueChange={(v) => setEditCompanyId(v as CompanyId)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (
|
||||
<SelectItem key={c} value={c}>{COMPANY_LABELS[c]}</SelectItem>
|
||||
<SelectItem key={c} value={c}>
|
||||
{COMPANY_LABELS[c]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -556,17 +697,29 @@ function TagRow({
|
||||
type="button"
|
||||
onClick={() => setEditColor(c)}
|
||||
className={cn(
|
||||
'h-5 w-5 rounded-full border-2 transition-all',
|
||||
editColor === c ? 'border-primary scale-110' : 'border-transparent'
|
||||
"h-5 w-5 rounded-full border-2 transition-all",
|
||||
editColor === c
|
||||
? "border-primary scale-110"
|
||||
: "border-transparent",
|
||||
)}
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={onSaveEdit}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
onClick={onSaveEdit}
|
||||
>
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={onCancelEdit}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -577,18 +730,29 @@ function TagRow({
|
||||
<div>
|
||||
<div className="group flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted/30">
|
||||
{hasChildren && (
|
||||
<button type="button" onClick={() => setShowChildren(!showChildren)} className="p-0.5">
|
||||
{showChildren
|
||||
? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
: <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowChildren(!showChildren)}
|
||||
className="p-0.5"
|
||||
>
|
||||
{showChildren ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!hasChildren && <span className="w-5" />}
|
||||
{tag.color && (
|
||||
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: tag.color }} />
|
||||
<span
|
||||
className="h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: tag.color }}
|
||||
/>
|
||||
)}
|
||||
{tag.projectCode && (
|
||||
<span className="font-mono text-xs text-muted-foreground">{tag.projectCode}</span>
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{tag.projectCode}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1 text-sm">{tag.label}</span>
|
||||
{tag.companyId && (
|
||||
@@ -619,20 +783,36 @@ function TagRow({
|
||||
{hasChildren && showChildren && (
|
||||
<div className="ml-6 border-l pl-2">
|
||||
{children.map((child) => (
|
||||
<div key={child.id} className="group flex items-center gap-2 rounded-md px-2 py-1 hover:bg-muted/30">
|
||||
<div
|
||||
key={child.id}
|
||||
className="group flex items-center gap-2 rounded-md px-2 py-1 hover:bg-muted/30"
|
||||
>
|
||||
<FolderTree className="h-3 w-3 text-muted-foreground" />
|
||||
{child.color && (
|
||||
<span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: child.color }} />
|
||||
<span
|
||||
className="h-2 w-2 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: child.color }}
|
||||
/>
|
||||
)}
|
||||
{child.projectCode && (
|
||||
<span className="font-mono text-[11px] text-muted-foreground">{child.projectCode}</span>
|
||||
<span className="font-mono text-[11px] text-muted-foreground">
|
||||
{child.projectCode}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex-1 text-sm">{child.label}</span>
|
||||
<div className="flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button type="button" onClick={() => onStartEdit(child)} className="rounded p-1 hover:bg-muted">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStartEdit(child)}
|
||||
className="rounded p-1 hover:bg-muted"
|
||||
>
|
||||
<Pencil className="h-3 w-3 text-muted-foreground" />
|
||||
</button>
|
||||
<button type="button" onClick={() => onDelete(child.id)} className="rounded p-1 hover:bg-destructive/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(child.id)}
|
||||
className="rounded p-1 hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-3 w-3 text-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { Tag, TagCategory } from '@/core/tagging/types';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
import type { Tag, TagCategory } from "@/core/tagging/types";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
|
||||
type SeedTag = Omit<Tag, 'id' | 'createdAt'>;
|
||||
type SeedTag = Omit<Tag, "id" | "createdAt">;
|
||||
|
||||
/** Parse project line like "000 Farmacie" → { code: "B-000", label: "Farmacie" } */
|
||||
function parseProjectLine(line: string, prefix: string): { code: string; label: string } | null {
|
||||
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;
|
||||
const padded = num.replace(/^[A-Z]/, "").padStart(3, "0");
|
||||
const codePrefix = num.startsWith("L") ? `${prefix}L` : prefix;
|
||||
return { code: `${codePrefix}-${padded}`, label };
|
||||
}
|
||||
|
||||
@@ -19,224 +22,260 @@ export function getManicTimeSeedTags(): 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',
|
||||
"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');
|
||||
const parsed = parseProjectLine(line, "B");
|
||||
if (parsed) {
|
||||
tags.push({
|
||||
label: parsed.label,
|
||||
category: 'project',
|
||||
scope: 'company',
|
||||
companyId: 'beletage' as CompanyId,
|
||||
category: "project",
|
||||
scope: "company",
|
||||
companyId: "beletage" as CompanyId,
|
||||
projectCode: parsed.code,
|
||||
color: '#22B5AB',
|
||||
color: "#22B5AB",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Urban Switch projects ──
|
||||
const urbanSwitchProjects = [
|
||||
'001 PUZ Sopor - Ansamblu Rezidential',
|
||||
'002 PUZ Borhanci Nord',
|
||||
'003 PUZ Zona Centrala Cluj',
|
||||
'004 PUG Floresti',
|
||||
'005 PUZ Dezmir - Zona Industriala',
|
||||
'006 PUZ Gilau Est',
|
||||
'007 PUZ Baciu - Extensie Intravilan',
|
||||
'008 PUG Apahida',
|
||||
'009 PUZ Iris - Reconversie',
|
||||
'010 PUZ Faget - Zona Turistica',
|
||||
"001 PUZ Sopor - Ansamblu Rezidential",
|
||||
"002 PUZ Borhanci Nord",
|
||||
"003 PUZ Zona Centrala Cluj",
|
||||
"004 PUG Floresti",
|
||||
"005 PUZ Dezmir - Zona Industriala",
|
||||
"006 PUZ Gilau Est",
|
||||
"007 PUZ Baciu - Extensie Intravilan",
|
||||
"008 PUG Apahida",
|
||||
"009 PUZ Iris - Reconversie",
|
||||
"010 PUZ Faget - Zona Turistica",
|
||||
];
|
||||
|
||||
for (const line of urbanSwitchProjects) {
|
||||
const parsed = parseProjectLine(line, 'US');
|
||||
const parsed = parseProjectLine(line, "US");
|
||||
if (parsed) {
|
||||
tags.push({
|
||||
label: parsed.label,
|
||||
category: 'project',
|
||||
scope: 'company',
|
||||
companyId: 'urban-switch' as CompanyId,
|
||||
category: "project",
|
||||
scope: "company",
|
||||
companyId: "urban-switch" as CompanyId,
|
||||
projectCode: parsed.code,
|
||||
color: '#345476',
|
||||
color: "#345476",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Studii de Teren projects ──
|
||||
const studiiDeTerenProjects = [
|
||||
'001 Studiu Geo - Sopor Rezidential',
|
||||
'002 Studiu Geo - Borhanci Vila',
|
||||
'003 Studiu Geo - Floresti Ansamblu',
|
||||
'004 Ridicare Topo - Dezmir Industrial',
|
||||
'005 Studiu Geo - Gilau Est',
|
||||
'006 Ridicare Topo - Baciu Extensie',
|
||||
'007 Studiu Geo - Apahida Centru',
|
||||
'008 Ridicare Topo - Faget',
|
||||
'009 Studiu Geo - Iris Reconversie',
|
||||
'010 Studiu Geo - Turda Rezidential',
|
||||
"001 Studiu Geo - Sopor Rezidential",
|
||||
"002 Studiu Geo - Borhanci Vila",
|
||||
"003 Studiu Geo - Floresti Ansamblu",
|
||||
"004 Ridicare Topo - Dezmir Industrial",
|
||||
"005 Studiu Geo - Gilau Est",
|
||||
"006 Ridicare Topo - Baciu Extensie",
|
||||
"007 Studiu Geo - Apahida Centru",
|
||||
"008 Ridicare Topo - Faget",
|
||||
"009 Studiu Geo - Iris Reconversie",
|
||||
"010 Studiu Geo - Turda Rezidential",
|
||||
];
|
||||
|
||||
for (const line of studiiDeTerenProjects) {
|
||||
const parsed = parseProjectLine(line, 'SDT');
|
||||
const parsed = parseProjectLine(line, "SDT");
|
||||
if (parsed) {
|
||||
tags.push({
|
||||
label: parsed.label,
|
||||
category: 'project',
|
||||
scope: 'company',
|
||||
companyId: 'studii-de-teren' as CompanyId,
|
||||
category: "project",
|
||||
scope: "company",
|
||||
companyId: "studii-de-teren" as CompanyId,
|
||||
projectCode: parsed.code,
|
||||
color: '#0182A1',
|
||||
color: "#0182A1",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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',
|
||||
"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',
|
||||
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',
|
||||
"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',
|
||||
category: "activity",
|
||||
scope: "global",
|
||||
color: "#8b5cf6",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Document type tags ──
|
||||
const docTypes = [
|
||||
'Contract', 'Ofertă', 'Factură', 'Scrisoare',
|
||||
'Aviz', 'Notă de comandă', 'Raport', 'Cerere', 'Altele',
|
||||
"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',
|
||||
category: "document-type",
|
||||
scope: "global",
|
||||
color: "#f59e0b",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user