feat(mini-utilities): add 5 new tools - U/R converter, AI cleaner, MDLPA, PDF reducer, OCR
This commit is contained in:
@@ -1,13 +1,35 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useRef } from "react";
|
||||||
import { Copy, Check, Hash, Type, Percent, Ruler } from 'lucide-react';
|
import {
|
||||||
import { Button } from '@/shared/components/ui/button';
|
Copy,
|
||||||
import { Input } from '@/shared/components/ui/input';
|
Check,
|
||||||
import { Label } from '@/shared/components/ui/label';
|
Hash,
|
||||||
import { Textarea } from '@/shared/components/ui/textarea';
|
Type,
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
Percent,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
Ruler,
|
||||||
|
Zap,
|
||||||
|
Wand2,
|
||||||
|
Building2,
|
||||||
|
FileDown,
|
||||||
|
ScanText,
|
||||||
|
} 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 { Textarea } from "@/shared/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/shared/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from "@/shared/components/ui/tabs";
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
function CopyButton({ text }: { text: string }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -16,17 +38,29 @@ function CopyButton({ text }: { text: string }) {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 1500);
|
setTimeout(() => setCopied(false), 1500);
|
||||||
} catch { /* silent */ }
|
} catch {
|
||||||
|
/* silent */
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCopy} disabled={!text}>
|
<Button
|
||||||
{copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={!text}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TextCaseConverter() {
|
function TextCaseConverter() {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState("");
|
||||||
const upper = input.toUpperCase();
|
const upper = input.toUpperCase();
|
||||||
const lower = input.toLowerCase();
|
const lower = input.toLowerCase();
|
||||||
const title = input.replace(/\b\w/g, (c) => c.toUpperCase());
|
const title = input.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
@@ -34,15 +68,26 @@ function TextCaseConverter() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div><Label>Text sursă</Label><Textarea value={input} onChange={(e) => setInput(e.target.value)} rows={3} className="mt-1" placeholder="Introdu text..." /></div>
|
<div>
|
||||||
|
<Label>Text sursă</Label>
|
||||||
|
<Textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="mt-1"
|
||||||
|
placeholder="Introdu text..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{[
|
{[
|
||||||
{ label: 'UPPERCASE', value: upper },
|
{ label: "UPPERCASE", value: upper },
|
||||||
{ label: 'lowercase', value: lower },
|
{ label: "lowercase", value: lower },
|
||||||
{ label: 'Title Case', value: title },
|
{ label: "Title Case", value: title },
|
||||||
{ label: 'Sentence case', value: sentence },
|
{ label: "Sentence case", value: sentence },
|
||||||
].map(({ label, value }) => (
|
].map(({ label, value }) => (
|
||||||
<div key={label} className="flex items-center gap-2">
|
<div key={label} className="flex items-center gap-2">
|
||||||
<code className="flex-1 truncate rounded border bg-muted/30 px-2 py-1 text-xs">{value || '—'}</code>
|
<code className="flex-1 truncate rounded border bg-muted/30 px-2 py-1 text-xs">
|
||||||
|
{value || "—"}
|
||||||
|
</code>
|
||||||
<span className="w-24 text-xs text-muted-foreground">{label}</span>
|
<span className="w-24 text-xs text-muted-foreground">{label}</span>
|
||||||
<CopyButton text={value} />
|
<CopyButton text={value} />
|
||||||
</div>
|
</div>
|
||||||
@@ -52,73 +97,148 @@ function TextCaseConverter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CharacterCounter() {
|
function CharacterCounter() {
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState("");
|
||||||
const chars = input.length;
|
const chars = input.length;
|
||||||
const charsNoSpaces = input.replace(/\s/g, '').length;
|
const charsNoSpaces = input.replace(/\s/g, "").length;
|
||||||
const words = input.trim() ? input.trim().split(/\s+/).length : 0;
|
const words = input.trim() ? input.trim().split(/\s+/).length : 0;
|
||||||
const lines = input ? input.split('\n').length : 0;
|
const lines = input ? input.split("\n").length : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div><Label>Text</Label><Textarea value={input} onChange={(e) => setInput(e.target.value)} rows={5} className="mt-1" placeholder="Introdu text..." /></div>
|
<div>
|
||||||
|
<Label>Text</Label>
|
||||||
|
<Textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
rows={5}
|
||||||
|
className="mt-1"
|
||||||
|
placeholder="Introdu text..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Caractere</p><p className="text-xl font-bold">{chars}</p></CardContent></Card>
|
<Card>
|
||||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Fără spații</p><p className="text-xl font-bold">{charsNoSpaces}</p></CardContent></Card>
|
<CardContent className="p-3">
|
||||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Cuvinte</p><p className="text-xl font-bold">{words}</p></CardContent></Card>
|
<p className="text-xs text-muted-foreground">Caractere</p>
|
||||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Linii</p><p className="text-xl font-bold">{lines}</p></CardContent></Card>
|
<p className="text-xl font-bold">{chars}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Fără spații</p>
|
||||||
|
<p className="text-xl font-bold">{charsNoSpaces}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Cuvinte</p>
|
||||||
|
<p className="text-xl font-bold">{words}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<p className="text-xs text-muted-foreground">Linii</p>
|
||||||
|
<p className="text-xl font-bold">{lines}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PercentageCalculator() {
|
function PercentageCalculator() {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState("");
|
||||||
const [total, setTotal] = useState('');
|
const [total, setTotal] = useState("");
|
||||||
const [percent, setPercent] = useState('');
|
const [percent, setPercent] = useState("");
|
||||||
|
|
||||||
const v = parseFloat(value);
|
const v = parseFloat(value);
|
||||||
const t = parseFloat(total);
|
const t = parseFloat(total);
|
||||||
const p = parseFloat(percent);
|
const p = parseFloat(percent);
|
||||||
|
|
||||||
const pctOfTotal = !isNaN(v) && !isNaN(t) && t !== 0 ? ((v / t) * 100).toFixed(2) : '—';
|
const pctOfTotal =
|
||||||
const valFromPct = !isNaN(p) && !isNaN(t) ? ((p / 100) * t).toFixed(2) : '—';
|
!isNaN(v) && !isNaN(t) && t !== 0 ? ((v / t) * 100).toFixed(2) : "—";
|
||||||
|
const valFromPct = !isNaN(p) && !isNaN(t) ? ((p / 100) * t).toFixed(2) : "—";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
<div><Label>Valoare</Label><Input type="number" value={value} onChange={(e) => setValue(e.target.value)} className="mt-1" /></div>
|
<div>
|
||||||
<div><Label>Total</Label><Input type="number" value={total} onChange={(e) => setTotal(e.target.value)} className="mt-1" /></div>
|
<Label>Valoare</Label>
|
||||||
<div><Label>Procent</Label><Input type="number" value={percent} onChange={(e) => setPercent(e.target.value)} className="mt-1" /></div>
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Total</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={total}
|
||||||
|
onChange={(e) => setTotal(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Procent</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={percent}
|
||||||
|
onChange={(e) => setPercent(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 rounded-md border bg-muted/30 p-3 text-sm">
|
<div className="space-y-2 rounded-md border bg-muted/30 p-3 text-sm">
|
||||||
<p><strong>{value || '?'}</strong> din <strong>{total || '?'}</strong> = <strong>{pctOfTotal}%</strong></p>
|
<p>
|
||||||
<p><strong>{percent || '?'}%</strong> din <strong>{total || '?'}</strong> = <strong>{valFromPct}</strong></p>
|
<strong>{value || "?"}</strong> din <strong>{total || "?"}</strong> ={" "}
|
||||||
|
<strong>{pctOfTotal}%</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>{percent || "?"}%</strong> din <strong>{total || "?"}</strong>{" "}
|
||||||
|
= <strong>{valFromPct}</strong>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AreaConverter() {
|
function AreaConverter() {
|
||||||
const [mp, setMp] = useState('');
|
const [mp, setMp] = useState("");
|
||||||
const v = parseFloat(mp);
|
const v = parseFloat(mp);
|
||||||
|
|
||||||
const conversions = !isNaN(v) ? [
|
const conversions = !isNaN(v)
|
||||||
{ label: 'mp (m²)', value: v.toFixed(2) },
|
? [
|
||||||
{ label: 'ari (100 m²)', value: (v / 100).toFixed(4) },
|
{ label: "mp (m²)", value: v.toFixed(2) },
|
||||||
{ label: 'hectare (10.000 m²)', value: (v / 10000).toFixed(6) },
|
{ label: "ari (100 m²)", value: (v / 100).toFixed(4) },
|
||||||
{ label: 'km²', value: (v / 1000000).toFixed(8) },
|
{ label: "hectare (10.000 m²)", value: (v / 10000).toFixed(6) },
|
||||||
{ label: 'sq ft', value: (v * 10.7639).toFixed(2) },
|
{ label: "km²", value: (v / 1000000).toFixed(8) },
|
||||||
] : [];
|
{ label: "sq ft", value: (v * 10.7639).toFixed(2) },
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div><Label>Suprafață (m²)</Label><Input type="number" value={mp} onChange={(e) => setMp(e.target.value)} className="mt-1" placeholder="Introdu suprafața..." /></div>
|
<div>
|
||||||
|
<Label>Suprafață (m²)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={mp}
|
||||||
|
onChange={(e) => setMp(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
placeholder="Introdu suprafața..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{conversions.length > 0 && (
|
{conversions.length > 0 && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{conversions.map(({ label, value: val }) => (
|
{conversions.map(({ label, value: val }) => (
|
||||||
<div key={label} className="flex items-center gap-2">
|
<div key={label} className="flex items-center gap-2">
|
||||||
<code className="flex-1 rounded border bg-muted/30 px-2 py-1 text-xs">{val}</code>
|
<code className="flex-1 rounded border bg-muted/30 px-2 py-1 text-xs">
|
||||||
<span className="w-36 text-xs text-muted-foreground">{label}</span>
|
{val}
|
||||||
|
</code>
|
||||||
|
<span className="w-36 text-xs text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
<CopyButton text={val} />
|
<CopyButton text={val} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -128,31 +248,491 @@ function AreaConverter() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── U-value → R-value Converter ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
function UValueConverter() {
|
||||||
|
const [uValue, setUValue] = useState("");
|
||||||
|
const [thickness, setThickness] = useState("");
|
||||||
|
|
||||||
|
const u = parseFloat(uValue);
|
||||||
|
const t = parseFloat(thickness);
|
||||||
|
const rValue = !isNaN(u) && u > 0 ? (1 / u).toFixed(4) : null;
|
||||||
|
const rsi = 0.13;
|
||||||
|
const rse = 0.04;
|
||||||
|
const rTotal =
|
||||||
|
rValue !== null ? (parseFloat(rValue) + rsi + rse).toFixed(4) : null;
|
||||||
|
const lambda =
|
||||||
|
rValue !== null && !isNaN(t) && t > 0
|
||||||
|
? (t / 100 / parseFloat(rValue)).toFixed(4)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Label>Coeficient U (W/m²K)</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
value={uValue}
|
||||||
|
onChange={(e) => setUValue(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
placeholder="ex: 0.35"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Grosime material (cm) — opțional</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
value={thickness}
|
||||||
|
onChange={(e) => setThickness(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
placeholder="ex: 20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{rValue !== null && (
|
||||||
|
<div className="space-y-2 rounded-md border bg-muted/30 p-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium">R = 1/U</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<code className="rounded border bg-muted px-2 py-0.5">
|
||||||
|
{rValue} m²K/W
|
||||||
|
</code>
|
||||||
|
<CopyButton text={rValue} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-muted-foreground">
|
||||||
|
<span>Rsi (suprafață interioară)</span>
|
||||||
|
<code className="rounded border bg-muted px-2 py-0.5">
|
||||||
|
{rsi} m²K/W
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-muted-foreground">
|
||||||
|
<span>Rse (suprafață exterioară)</span>
|
||||||
|
<code className="rounded border bg-muted px-2 py-0.5">
|
||||||
|
{rse} m²K/W
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between font-medium border-t pt-2 mt-1">
|
||||||
|
<span>R total (cu Rsi + Rse)</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<code className="rounded border bg-muted px-2 py-0.5">
|
||||||
|
{rTotal} m²K/W
|
||||||
|
</code>
|
||||||
|
<CopyButton text={rTotal ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{lambda !== null && (
|
||||||
|
<div className="flex items-center justify-between text-muted-foreground border-t pt-2 mt-1">
|
||||||
|
<span>Conductivitate λ = d/R</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<code className="rounded border bg-muted px-2 py-0.5">
|
||||||
|
{lambda} W/mK
|
||||||
|
</code>
|
||||||
|
<CopyButton text={lambda} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── AI Artifact Cleaner ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function AiArtifactCleaner() {
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
|
||||||
|
const clean = (text: string): string => {
|
||||||
|
let r = text;
|
||||||
|
// Strip markdown
|
||||||
|
r = r.replace(/^#{1,6}\s+/gm, "");
|
||||||
|
r = r.replace(/\*\*(.+?)\*\*/g, "$1");
|
||||||
|
r = r.replace(/\*(.+?)\*/g, "$1");
|
||||||
|
r = r.replace(/_{2}(.+?)_{2}/g, "$1");
|
||||||
|
r = r.replace(/_(.+?)_/g, "$1");
|
||||||
|
r = r.replace(/```[\s\S]*?```/g, "");
|
||||||
|
r = r.replace(/`(.+?)`/g, "$1");
|
||||||
|
r = r.replace(/^[*\-+]\s+/gm, "");
|
||||||
|
r = r.replace(/^\d+\.\s+/gm, "");
|
||||||
|
r = r.replace(/^[-_*]{3,}$/gm, "");
|
||||||
|
r = r.replace(/\[(.+?)\]\(.*?\)/g, "$1");
|
||||||
|
r = r.replace(/^>\s+/gm, "");
|
||||||
|
// Fix encoding artifacts (UTF-8 mojibake)
|
||||||
|
r = r.replace(/â/g, "â");
|
||||||
|
r = r.replace(/î/g, "î");
|
||||||
|
r = r.replace(/Ã /g, "à");
|
||||||
|
r = r.replace(/Å£/g, "ț");
|
||||||
|
r = r.replace(/È™/g, "ș");
|
||||||
|
r = r.replace(/È›/g, "ț");
|
||||||
|
r = r.replace(/Èš/g, "Ț");
|
||||||
|
r = r.replace(/\u015f/g, "ș");
|
||||||
|
r = r.replace(/\u0163/g, "ț");
|
||||||
|
// Remove zero-width and invisible chars
|
||||||
|
r = r.replace(/[\u200b\u200c\u200d\ufeff]/g, "");
|
||||||
|
// Normalize typography
|
||||||
|
r = r.replace(/[""]/g, '"');
|
||||||
|
r = r.replace(/['']/g, "'");
|
||||||
|
r = r.replace(/[–—]/g, "-");
|
||||||
|
r = r.replace(/…/g, "...");
|
||||||
|
// Normalize spacing
|
||||||
|
r = r.replace(/ {2,}/g, " ");
|
||||||
|
r = r.replace(/\n{3,}/g, "\n\n");
|
||||||
|
return r.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleaned = input ? clean(input) : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<Label>Text original (output AI)</Label>
|
||||||
|
<Textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
className="mt-1 h-72 font-mono text-xs"
|
||||||
|
placeholder="Lipește textul generat de AI..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Text curățat</Label>
|
||||||
|
{cleaned && <CopyButton text={cleaned} />}
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
value={cleaned}
|
||||||
|
readOnly
|
||||||
|
className="mt-1 h-72 font-mono text-xs bg-muted/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Operații: eliminare markdown (###, **, `, liste, citate), corectare
|
||||||
|
encoding românesc (mojibake), curățare Unicode invizibil, normalizare
|
||||||
|
ghilimele / cratime / spații multiple.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── MDLPA Date Locale ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function MdlpaValidator() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||||
|
<a
|
||||||
|
href="https://datelocale.mdlpa.ro"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline underline-offset-2"
|
||||||
|
>
|
||||||
|
Deschide datelocale.mdlpa.ro ↗
|
||||||
|
</a>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<a
|
||||||
|
href="https://datelocale.mdlpa.ro/ro/about/tutorials"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline underline-offset-2"
|
||||||
|
>
|
||||||
|
Tutoriale video ↗
|
||||||
|
</a>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<a
|
||||||
|
href="https://datelocale.mdlpa.ro/ro/about/info/reguli"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline underline-offset-2"
|
||||||
|
>
|
||||||
|
Reguli de calcul ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="overflow-hidden rounded-md border"
|
||||||
|
style={{ height: "560px" }}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
src="https://datelocale.mdlpa.ro"
|
||||||
|
className="h-full w-full"
|
||||||
|
title="MDLPA — Date Locale"
|
||||||
|
allow="fullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── PDF Reducer (Stirling PDF) ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
function PdfReducer() {
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [optimizeLevel, setOptimizeLevel] = useState("2");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleCompress = async () => {
|
||||||
|
if (!file) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
formData.append("optimizeLevel", optimizeLevel);
|
||||||
|
const res = await fetch(
|
||||||
|
"http://10.10.10.166:8087/api/v1/misc/compress-pdf",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`Eroare server: ${res.status}`);
|
||||||
|
const blob = await res.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = file.name.replace(/\.pdf$/i, "-comprimat.pdf");
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch {
|
||||||
|
setError(
|
||||||
|
"Nu s-a putut conecta la Stirling PDF. Folosește linkul de mai jos pentru a deschide aplicația manual.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Fișier PDF</Label>
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
setFile(e.target.files?.[0] ?? null);
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Button variant="outline" onClick={() => fileRef.current?.click()}>
|
||||||
|
Selectează PDF...
|
||||||
|
</Button>
|
||||||
|
{file && (
|
||||||
|
<span className="text-sm text-muted-foreground">{file.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Nivel compresie</Label>
|
||||||
|
<select
|
||||||
|
value={optimizeLevel}
|
||||||
|
onChange={(e) => setOptimizeLevel(e.target.value)}
|
||||||
|
className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<option value="0">0 — fără modificări (test)</option>
|
||||||
|
<option value="1">1 — compresie minimă</option>
|
||||||
|
<option value="2">2 — echilibrat (recomandat)</option>
|
||||||
|
<option value="3">3 — compresie mare</option>
|
||||||
|
<option value="4">4 — compresie maximă</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button onClick={handleCompress} disabled={!file || loading}>
|
||||||
|
{loading ? "Se comprimă..." : "Comprimă PDF"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" asChild>
|
||||||
|
<a
|
||||||
|
href="http://10.10.10.166:8087/compress-pdf"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Deschide Stirling PDF ↗
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Quick OCR ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function QuickOcr() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||||
|
<a
|
||||||
|
href="https://ocr.z.ai"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary underline underline-offset-2"
|
||||||
|
>
|
||||||
|
Deschide ocr.z.ai ↗
|
||||||
|
</a>
|
||||||
|
<span className="text-muted-foreground">•</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
Extragere text din imagini și PDF-uri scanate
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="overflow-hidden rounded-md border"
|
||||||
|
style={{ height: "560px" }}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
src="https://ocr.z.ai"
|
||||||
|
className="h-full w-full"
|
||||||
|
title="OCR — extragere text din imagini"
|
||||||
|
allow="fullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main Module ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function MiniUtilitiesModule() {
|
export function MiniUtilitiesModule() {
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="text-case" className="space-y-4">
|
<Tabs defaultValue="text-case" className="space-y-4">
|
||||||
<TabsList className="flex-wrap">
|
<TabsList className="flex-wrap">
|
||||||
<TabsTrigger value="text-case"><Type className="mr-1 h-3.5 w-3.5" /> Transformare text</TabsTrigger>
|
<TabsTrigger value="text-case">
|
||||||
<TabsTrigger value="char-count"><Hash className="mr-1 h-3.5 w-3.5" /> Numărare caractere</TabsTrigger>
|
<Type className="mr-1 h-3.5 w-3.5" /> Transformare text
|
||||||
<TabsTrigger value="percentage"><Percent className="mr-1 h-3.5 w-3.5" /> Procente</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="area"><Ruler className="mr-1 h-3.5 w-3.5" /> Convertor suprafețe</TabsTrigger>
|
<TabsTrigger value="char-count">
|
||||||
|
<Hash className="mr-1 h-3.5 w-3.5" /> Numărare caractere
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="percentage">
|
||||||
|
<Percent className="mr-1 h-3.5 w-3.5" /> Procente
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="area">
|
||||||
|
<Ruler className="mr-1 h-3.5 w-3.5" /> Suprafețe
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="u-value">
|
||||||
|
<Zap className="mr-1 h-3.5 w-3.5" /> U → R
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="ai-cleaner">
|
||||||
|
<Wand2 className="mr-1 h-3.5 w-3.5" /> Curățare AI
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="mdlpa">
|
||||||
|
<Building2 className="mr-1 h-3.5 w-3.5" /> MDLPA
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="pdf-reducer">
|
||||||
|
<FileDown className="mr-1 h-3.5 w-3.5" /> Reducere PDF
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="ocr">
|
||||||
|
<ScanText className="mr-1 h-3.5 w-3.5" /> OCR
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="text-case">
|
<TabsContent value="text-case">
|
||||||
<Card><CardHeader><CardTitle className="text-base">Transformare text</CardTitle></CardHeader>
|
<Card>
|
||||||
<CardContent><TextCaseConverter /></CardContent></Card>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Transformare text</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<TextCaseConverter />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="char-count">
|
<TabsContent value="char-count">
|
||||||
<Card><CardHeader><CardTitle className="text-base">Numărare caractere</CardTitle></CardHeader>
|
<Card>
|
||||||
<CardContent><CharacterCounter /></CardContent></Card>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Numărare caractere</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CharacterCounter />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="percentage">
|
<TabsContent value="percentage">
|
||||||
<Card><CardHeader><CardTitle className="text-base">Calculator procente</CardTitle></CardHeader>
|
<Card>
|
||||||
<CardContent><PercentageCalculator /></CardContent></Card>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Calculator procente</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<PercentageCalculator />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="area">
|
<TabsContent value="area">
|
||||||
<Card><CardHeader><CardTitle className="text-base">Convertor suprafețe</CardTitle></CardHeader>
|
<Card>
|
||||||
<CardContent><AreaConverter /></CardContent></Card>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Convertor suprafețe</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<AreaConverter />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="u-value">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
Convertor U → R (termoizolație)
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<UValueConverter />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="ai-cleaner">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Curățare text AI</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<AiArtifactCleaner />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="mdlpa">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
MDLPA — Date locale construcții
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<MdlpaValidator />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="pdf-reducer">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Reducere dimensiune PDF</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<PdfReducer />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="ocr">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
OCR — extragere text din imagini
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<QuickOcr />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,73 +1,107 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Plus, Trash2, Pencil, Check, X, Download, ChevronDown, ChevronRight,
|
Plus,
|
||||||
Tag as TagIcon, Search, FolderTree,
|
Trash2,
|
||||||
} from 'lucide-react';
|
Pencil,
|
||||||
import { Button } from '@/shared/components/ui/button';
|
Check,
|
||||||
import { Input } from '@/shared/components/ui/input';
|
X,
|
||||||
import { Label } from '@/shared/components/ui/label';
|
Download,
|
||||||
import { Badge } from '@/shared/components/ui/badge';
|
ChevronDown,
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
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 {
|
import {
|
||||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
Card,
|
||||||
} from '@/shared/components/ui/select';
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/shared/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
Select,
|
||||||
} from '@/shared/components/ui/dialog';
|
SelectContent,
|
||||||
import { useTags } from '@/core/tagging';
|
SelectItem,
|
||||||
import type { Tag, TagCategory, TagScope } from '@/core/tagging/types';
|
SelectTrigger,
|
||||||
import { TAG_CATEGORY_ORDER, TAG_CATEGORY_LABELS } from '@/core/tagging/types';
|
SelectValue,
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
} from "@/shared/components/ui/select";
|
||||||
import { cn } from '@/shared/lib/utils';
|
import {
|
||||||
import { getManicTimeSeedTags } from '../services/seed-data';
|
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> = {
|
const SCOPE_LABELS: Record<TagScope, string> = {
|
||||||
global: 'Global',
|
global: "Global",
|
||||||
module: 'Modul',
|
module: "Modul",
|
||||||
company: 'Companie',
|
company: "Companie",
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMPANY_LABELS: Record<CompanyId, string> = {
|
const COMPANY_LABELS: Record<CompanyId, string> = {
|
||||||
beletage: 'Beletage',
|
beletage: "Beletage",
|
||||||
'urban-switch': 'Urban Switch',
|
"urban-switch": "Urban Switch",
|
||||||
'studii-de-teren': 'Studii de Teren',
|
"studii-de-teren": "Studii de Teren",
|
||||||
group: 'Grup',
|
group: "Grup",
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAG_COLORS = [
|
const TAG_COLORS = [
|
||||||
'#ef4444', '#f97316', '#f59e0b', '#84cc16',
|
"#ef4444",
|
||||||
'#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6',
|
"#f97316",
|
||||||
'#ec4899', '#64748b', '#22B5AB', '#6366f1',
|
"#f59e0b",
|
||||||
|
"#84cc16",
|
||||||
|
"#22c55e",
|
||||||
|
"#06b6d4",
|
||||||
|
"#3b82f6",
|
||||||
|
"#8b5cf6",
|
||||||
|
"#ec4899",
|
||||||
|
"#64748b",
|
||||||
|
"#22B5AB",
|
||||||
|
"#6366f1",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function TagManagerModule() {
|
export function TagManagerModule() {
|
||||||
const { tags, loading, createTag, updateTag, deleteTag, importTags } = useTags();
|
const { tags, loading, createTag, updateTag, deleteTag, importTags } =
|
||||||
|
useTags();
|
||||||
|
|
||||||
// ── Create form state ──
|
// ── Create form state ──
|
||||||
const [newLabel, setNewLabel] = useState('');
|
const [newLabel, setNewLabel] = useState("");
|
||||||
const [newCategory, setNewCategory] = useState<TagCategory>('custom');
|
const [newCategory, setNewCategory] = useState<TagCategory>("custom");
|
||||||
const [newScope, setNewScope] = useState<TagScope>('global');
|
const [newScope, setNewScope] = useState<TagScope>("global");
|
||||||
const [newColor, setNewColor] = useState('#3b82f6');
|
const [newColor, setNewColor] = useState("#3b82f6");
|
||||||
const [newCompanyId, setNewCompanyId] = useState<CompanyId>('beletage');
|
const [newCompanyId, setNewCompanyId] = useState<CompanyId>("beletage");
|
||||||
const [newProjectCode, setNewProjectCode] = useState('');
|
const [newProjectCode, setNewProjectCode] = useState("");
|
||||||
const [newParentId, setNewParentId] = useState('');
|
const [newParentId, setNewParentId] = useState("");
|
||||||
|
|
||||||
// ── Filter / search state ──
|
// ── Filter / search state ──
|
||||||
const [filterCategory, setFilterCategory] = useState<TagCategory | 'all'>('all');
|
const [filterCategory, setFilterCategory] = useState<TagCategory | "all">(
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
"all",
|
||||||
|
);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||||
() => new Set(TAG_CATEGORY_ORDER)
|
() => new Set(TAG_CATEGORY_ORDER),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Edit state ──
|
// ── Edit state ──
|
||||||
const [editingTag, setEditingTag] = useState<Tag | null>(null);
|
const [editingTag, setEditingTag] = useState<Tag | null>(null);
|
||||||
const [editLabel, setEditLabel] = useState('');
|
const [editLabel, setEditLabel] = useState("");
|
||||||
const [editColor, setEditColor] = useState('');
|
const [editColor, setEditColor] = useState("");
|
||||||
const [editProjectCode, setEditProjectCode] = useState('');
|
const [editProjectCode, setEditProjectCode] = useState("");
|
||||||
const [editScope, setEditScope] = useState<TagScope>('global');
|
const [editScope, setEditScope] = useState<TagScope>("global");
|
||||||
const [editCompanyId, setEditCompanyId] = useState<CompanyId>('beletage');
|
const [editCompanyId, setEditCompanyId] = useState<CompanyId>("beletage");
|
||||||
|
|
||||||
// ── Seed import state ──
|
// ── Seed import state ──
|
||||||
const [showSeedDialog, setShowSeedDialog] = useState(false);
|
const [showSeedDialog, setShowSeedDialog] = useState(false);
|
||||||
@@ -77,7 +111,7 @@ export function TagManagerModule() {
|
|||||||
// ── Computed ──
|
// ── Computed ──
|
||||||
const filteredTags = useMemo(() => {
|
const filteredTags = useMemo(() => {
|
||||||
let result = tags;
|
let result = tags;
|
||||||
if (filterCategory !== 'all') {
|
if (filterCategory !== "all") {
|
||||||
result = result.filter((t) => t.category === filterCategory);
|
result = result.filter((t) => t.category === filterCategory);
|
||||||
}
|
}
|
||||||
if (searchQuery) {
|
if (searchQuery) {
|
||||||
@@ -85,7 +119,7 @@ export function TagManagerModule() {
|
|||||||
result = result.filter(
|
result = result.filter(
|
||||||
(t) =>
|
(t) =>
|
||||||
t.label.toLowerCase().includes(q) ||
|
t.label.toLowerCase().includes(q) ||
|
||||||
(t.projectCode?.toLowerCase().includes(q) ?? false)
|
(t.projectCode?.toLowerCase().includes(q) ?? false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -119,9 +153,7 @@ export function TagManagerModule() {
|
|||||||
}, [tags]);
|
}, [tags]);
|
||||||
|
|
||||||
const parentCandidates = useMemo(() => {
|
const parentCandidates = useMemo(() => {
|
||||||
return tags.filter(
|
return tags.filter((t) => t.category === newCategory && !t.parentId);
|
||||||
(t) => t.category === newCategory && !t.parentId
|
|
||||||
);
|
|
||||||
}, [tags, newCategory]);
|
}, [tags, newCategory]);
|
||||||
|
|
||||||
// ── Validation state ──
|
// ── Validation state ──
|
||||||
@@ -131,13 +163,17 @@ export function TagManagerModule() {
|
|||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
if (!newLabel.trim()) {
|
if (!newLabel.trim()) {
|
||||||
errors.push('Numele etichetei este obligatoriu.');
|
errors.push("Numele etichetei este obligatoriu.");
|
||||||
}
|
}
|
||||||
if (newCategory === 'project' && !newProjectCode.trim()) {
|
if (newCategory === "project" && !newProjectCode.trim()) {
|
||||||
errors.push('Codul proiectului este obligatoriu pentru categoria Proiect (ex: B-001, US-010, SDT-003).');
|
errors.push(
|
||||||
|
"Codul proiectului este obligatoriu pentru categoria Proiect (ex: B-001, US-010, SDT-003).",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (newCategory === 'project' && newScope !== 'company') {
|
if (newCategory === "project" && newScope !== "company") {
|
||||||
errors.push('Etichetele de tip Proiect trebuie asociate unei companii (vizibilitate = Companie).');
|
errors.push(
|
||||||
|
"Etichetele de tip Proiect trebuie asociate unei companii (vizibilitate = Companie).",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
setValidationErrors(errors);
|
setValidationErrors(errors);
|
||||||
@@ -149,22 +185,25 @@ export function TagManagerModule() {
|
|||||||
category: newCategory,
|
category: newCategory,
|
||||||
scope: newScope,
|
scope: newScope,
|
||||||
color: newColor,
|
color: newColor,
|
||||||
companyId: newScope === 'company' ? newCompanyId : undefined,
|
companyId: newScope === "company" ? newCompanyId : undefined,
|
||||||
projectCode: newCategory === 'project' && newProjectCode ? newProjectCode : undefined,
|
projectCode:
|
||||||
|
newCategory === "project" && newProjectCode
|
||||||
|
? newProjectCode
|
||||||
|
: undefined,
|
||||||
parentId: newParentId || undefined,
|
parentId: newParentId || undefined,
|
||||||
});
|
});
|
||||||
setNewLabel('');
|
setNewLabel("");
|
||||||
setNewProjectCode('');
|
setNewProjectCode("");
|
||||||
setNewParentId('');
|
setNewParentId("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const startEdit = (tag: Tag) => {
|
const startEdit = (tag: Tag) => {
|
||||||
setEditingTag(tag);
|
setEditingTag(tag);
|
||||||
setEditLabel(tag.label);
|
setEditLabel(tag.label);
|
||||||
setEditColor(tag.color ?? '#3b82f6');
|
setEditColor(tag.color ?? "#3b82f6");
|
||||||
setEditProjectCode(tag.projectCode ?? '');
|
setEditProjectCode(tag.projectCode ?? "");
|
||||||
setEditScope(tag.scope);
|
setEditScope(tag.scope);
|
||||||
setEditCompanyId(tag.companyId ?? 'beletage');
|
setEditCompanyId(tag.companyId ?? "beletage");
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveEdit = async () => {
|
const saveEdit = async () => {
|
||||||
@@ -172,9 +211,12 @@ export function TagManagerModule() {
|
|||||||
await updateTag(editingTag.id, {
|
await updateTag(editingTag.id, {
|
||||||
label: editLabel.trim(),
|
label: editLabel.trim(),
|
||||||
color: editColor,
|
color: editColor,
|
||||||
projectCode: editingTag.category === 'project' && editProjectCode ? editProjectCode : undefined,
|
projectCode:
|
||||||
|
editingTag.category === "project" && editProjectCode
|
||||||
|
? editProjectCode
|
||||||
|
: undefined,
|
||||||
scope: editScope,
|
scope: editScope,
|
||||||
companyId: editScope === 'company' ? editCompanyId : undefined,
|
companyId: editScope === "company" ? editCompanyId : undefined,
|
||||||
});
|
});
|
||||||
setEditingTag(null);
|
setEditingTag(null);
|
||||||
};
|
};
|
||||||
@@ -186,7 +228,9 @@ export function TagManagerModule() {
|
|||||||
setSeedResult(null);
|
setSeedResult(null);
|
||||||
const seedTags = getManicTimeSeedTags();
|
const seedTags = getManicTimeSeedTags();
|
||||||
const count = await importTags(seedTags);
|
const count = await importTags(seedTags);
|
||||||
setSeedResult(`${count} etichete importate din ${seedTags.length} disponibile.`);
|
setSeedResult(
|
||||||
|
`${count} etichete importate din ${seedTags.length} disponibile.`,
|
||||||
|
);
|
||||||
setSeedImporting(false);
|
setSeedImporting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,24 +244,30 @@ export function TagManagerModule() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ── Stats ──
|
// ── Stats ──
|
||||||
const projectCount = tags.filter((t) => t.category === 'project').length;
|
const projectCount = tags.filter((t) => t.category === "project").length;
|
||||||
const phaseCount = tags.filter((t) => t.category === 'phase').length;
|
const phaseCount = tags.filter((t) => t.category === "phase").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-5">
|
||||||
<Card><CardContent className="p-4">
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
<p className="text-xs text-muted-foreground">Total etichete</p>
|
<p className="text-xs text-muted-foreground">Total etichete</p>
|
||||||
<p className="text-2xl font-bold">{tags.length}</p>
|
<p className="text-2xl font-bold">{tags.length}</p>
|
||||||
</CardContent></Card>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
{TAG_CATEGORY_ORDER.map((cat) => (
|
{TAG_CATEGORY_ORDER.map((cat) => (
|
||||||
<Card key={cat}><CardContent className="p-4">
|
<Card key={cat}>
|
||||||
<p className="text-xs text-muted-foreground">{TAG_CATEGORY_LABELS[cat]}</p>
|
<CardContent className="p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{TAG_CATEGORY_LABELS[cat]}
|
||||||
|
</p>
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">
|
||||||
{tags.filter((t) => t.category === cat).length}
|
{tags.filter((t) => t.category === cat).length}
|
||||||
</p>
|
</p>
|
||||||
</CardContent></Card>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -228,7 +278,8 @@ export function TagManagerModule() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Nicio etichetă găsită</p>
|
<p className="font-medium">Nicio etichetă găsită</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setShowSeedDialog(true)}>
|
<Button onClick={() => setShowSeedDialog(true)}>
|
||||||
@@ -240,7 +291,9 @@ export function TagManagerModule() {
|
|||||||
|
|
||||||
{/* Create new tag */}
|
{/* Create new tag */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle className="text-base">Etichetă nouă</CardTitle></CardHeader>
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Etichetă nouă</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-wrap items-end gap-3">
|
<div className="flex flex-wrap items-end gap-3">
|
||||||
@@ -249,41 +302,62 @@ export function TagManagerModule() {
|
|||||||
<Input
|
<Input
|
||||||
value={newLabel}
|
value={newLabel}
|
||||||
onChange={(e) => setNewLabel(e.target.value)}
|
onChange={(e) => setNewLabel(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
|
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
||||||
placeholder="Numele etichetei..."
|
placeholder="Numele etichetei..."
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[160px]">
|
<div className="w-[160px]">
|
||||||
<Label>Categorie</Label>
|
<Label>Categorie</Label>
|
||||||
<Select value={newCategory} onValueChange={(v) => setNewCategory(v as TagCategory)}>
|
<Select
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
value={newCategory}
|
||||||
|
onValueChange={(v) => setNewCategory(v as TagCategory)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{TAG_CATEGORY_ORDER.map((cat) => (
|
{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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[140px]">
|
<div className="w-[140px]">
|
||||||
<Label>Vizibilitate</Label>
|
<Label>Vizibilitate</Label>
|
||||||
<Select value={newScope} onValueChange={(v) => setNewScope(v as TagScope)}>
|
<Select
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
value={newScope}
|
||||||
|
onValueChange={(v) => setNewScope(v as TagScope)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(Object.keys(SCOPE_LABELS) as TagScope[]).map((s) => (
|
{(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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{newScope === 'company' && (
|
{newScope === "company" && (
|
||||||
<div className="w-[150px]">
|
<div className="w-[150px]">
|
||||||
<Label>Companie</Label>
|
<Label>Companie</Label>
|
||||||
<Select value={newCompanyId} onValueChange={(v) => setNewCompanyId(v as CompanyId)}>
|
<Select
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
value={newCompanyId}
|
||||||
|
onValueChange={(v) => setNewCompanyId(v as CompanyId)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (
|
{(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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -292,7 +366,7 @@ export function TagManagerModule() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-end gap-3">
|
<div className="flex flex-wrap items-end gap-3">
|
||||||
{newCategory === 'project' && (
|
{newCategory === "project" && (
|
||||||
<div className="w-[140px]">
|
<div className="w-[140px]">
|
||||||
<Label>Cod proiect</Label>
|
<Label>Cod proiect</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -306,13 +380,23 @@ export function TagManagerModule() {
|
|||||||
{parentCandidates.length > 0 && (
|
{parentCandidates.length > 0 && (
|
||||||
<div className="w-[200px]">
|
<div className="w-[200px]">
|
||||||
<Label>Tag părinte (opțional)</Label>
|
<Label>Tag părinte (opțional)</Label>
|
||||||
<Select value={newParentId || '__none__'} onValueChange={(v) => setNewParentId(v === '__none__' ? '' : v)}>
|
<Select
|
||||||
<SelectTrigger className="mt-1"><SelectValue /></SelectTrigger>
|
value={newParentId || "__none__"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setNewParentId(v === "__none__" ? "" : v)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__none__">— Niciun părinte —</SelectItem>
|
<SelectItem value="__none__">
|
||||||
|
— Niciun părinte —
|
||||||
|
</SelectItem>
|
||||||
{parentCandidates.map((p) => (
|
{parentCandidates.map((p) => (
|
||||||
<SelectItem key={p.id} value={p.id}>
|
<SelectItem key={p.id} value={p.id}>
|
||||||
{p.projectCode ? `${p.projectCode} ` : ''}{p.label}
|
{p.projectCode ? `${p.projectCode} ` : ""}
|
||||||
|
{p.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -328,8 +412,10 @@ export function TagManagerModule() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setNewColor(color)}
|
onClick={() => setNewColor(color)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-7 w-7 rounded-full border-2 transition-all',
|
"h-7 w-7 rounded-full border-2 transition-all",
|
||||||
newColor === color ? 'border-primary scale-110' : 'border-transparent hover:scale-105'
|
newColor === color
|
||||||
|
? "border-primary scale-110"
|
||||||
|
: "border-transparent hover:scale-105",
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: color }}
|
style={{ backgroundColor: color }}
|
||||||
/>
|
/>
|
||||||
@@ -345,16 +431,19 @@ export function TagManagerModule() {
|
|||||||
{validationErrors.length > 0 && (
|
{validationErrors.length > 0 && (
|
||||||
<div className="rounded-md border border-destructive/50 bg-destructive/5 p-3">
|
<div className="rounded-md border border-destructive/50 bg-destructive/5 p-3">
|
||||||
{validationErrors.map((err) => (
|
{validationErrors.map((err) => (
|
||||||
<p key={err} className="text-sm text-destructive">{err}</p>
|
<p key={err} className="text-sm text-destructive">
|
||||||
|
{err}
|
||||||
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hint for mandatory categories */}
|
{/* Hint for mandatory categories */}
|
||||||
{(newCategory === 'project' || newCategory === 'phase') && (
|
{(newCategory === "project" || newCategory === "phase") && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
<strong>Notă:</strong> Categoriile <em>Proiect</em> și <em>Fază</em> sunt obligatorii
|
<strong>Notă:</strong> Categoriile <em>Proiect</em> și{" "}
|
||||||
în structura de etichete. Proiectele necesită un cod (ex: B-001, US-010, SDT-003) ș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.
|
trebuie asociate unei companii.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -373,17 +462,28 @@ export function TagManagerModule() {
|
|||||||
className="pl-9"
|
className="pl-9"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select value={filterCategory} onValueChange={(v) => setFilterCategory(v as TagCategory | 'all')}>
|
<Select
|
||||||
<SelectTrigger className="w-[180px]"><SelectValue /></SelectTrigger>
|
value={filterCategory}
|
||||||
|
onValueChange={(v) => setFilterCategory(v as TagCategory | "all")}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Toate categoriile</SelectItem>
|
<SelectItem value="all">Toate categoriile</SelectItem>
|
||||||
{TAG_CATEGORY_ORDER.map((cat) => (
|
{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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{tags.length > 0 && (
|
{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
|
<Download className="mr-1 h-3.5 w-3.5" /> Importă ManicTime
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -391,10 +491,13 @@ export function TagManagerModule() {
|
|||||||
|
|
||||||
{/* Tag list by category with hierarchy */}
|
{/* Tag list by category with hierarchy */}
|
||||||
{loading ? (
|
{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 ? (
|
) : Object.keys(groupedByCategory).length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
<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>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -408,14 +511,20 @@ export function TagManagerModule() {
|
|||||||
onClick={() => toggleCategory(category)}
|
onClick={() => toggleCategory(category)}
|
||||||
>
|
>
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">
|
<CardTitle className="flex items-center gap-2 text-sm">
|
||||||
{isExpanded
|
{isExpanded ? (
|
||||||
? <ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
: <ChevronRight className="h-4 w-4" />}
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
<TagIcon className="h-4 w-4" />
|
<TagIcon className="h-4 w-4" />
|
||||||
{TAG_CATEGORY_LABELS[category as TagCategory] ?? category}
|
{TAG_CATEGORY_LABELS[category as TagCategory] ?? category}
|
||||||
<Badge variant="secondary" className="ml-1">{catTags.length}</Badge>
|
<Badge variant="secondary" className="ml-1">
|
||||||
{(category === 'project' || category === 'phase') && (
|
{catTags.length}
|
||||||
<Badge variant="default" className="ml-1 text-[10px]">obligatoriu</Badge>
|
</Badge>
|
||||||
|
{(category === "project" || category === "phase") && (
|
||||||
|
<Badge variant="default" className="ml-1 text-[10px]">
|
||||||
|
obligatoriu
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -461,18 +570,22 @@ export function TagManagerModule() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3 py-2">
|
<div className="space-y-3 py-2">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Aceasta va importa proiectele Beletage, Urban Switch și Studii de Teren,
|
Aceasta va importa proiectele Beletage, Urban Switch și Studii de
|
||||||
fazele, activitățile și tipurile de documente din lista ManicTime.
|
Teren, fazele, activitățile și tipurile de documente din lista
|
||||||
Etichetele existente nu vor fi duplicate.
|
ManicTime. Etichetele existente nu vor fi duplicate.
|
||||||
</p>
|
</p>
|
||||||
{seedResult && (
|
{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>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setShowSeedDialog(false)}>Închide</Button>
|
<Button variant="outline" onClick={() => setShowSeedDialog(false)}>
|
||||||
|
Închide
|
||||||
|
</Button>
|
||||||
<Button onClick={handleSeedImport} disabled={seedImporting}>
|
<Button onClick={handleSeedImport} disabled={seedImporting}>
|
||||||
{seedImporting ? 'Se importă...' : 'Importă'}
|
{seedImporting ? "Se importă..." : "Importă"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -504,10 +617,23 @@ interface TagRowProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TagRow({
|
function TagRow({
|
||||||
tag, children, editingTag, editLabel, editColor, editProjectCode,
|
tag,
|
||||||
editScope, editCompanyId,
|
children,
|
||||||
onStartEdit, onSaveEdit, onCancelEdit, onDelete,
|
editingTag,
|
||||||
setEditLabel, setEditColor, setEditProjectCode, setEditScope, setEditCompanyId,
|
editLabel,
|
||||||
|
editColor,
|
||||||
|
editProjectCode,
|
||||||
|
editScope,
|
||||||
|
editCompanyId,
|
||||||
|
onStartEdit,
|
||||||
|
onSaveEdit,
|
||||||
|
onCancelEdit,
|
||||||
|
onDelete,
|
||||||
|
setEditLabel,
|
||||||
|
setEditColor,
|
||||||
|
setEditProjectCode,
|
||||||
|
setEditScope,
|
||||||
|
setEditCompanyId,
|
||||||
}: TagRowProps) {
|
}: TagRowProps) {
|
||||||
const isEditing = editingTag?.id === tag.id;
|
const isEditing = editingTag?.id === tag.id;
|
||||||
const [showChildren, setShowChildren] = useState(false);
|
const [showChildren, setShowChildren] = useState(false);
|
||||||
@@ -516,7 +642,7 @@ function TagRow({
|
|||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/30 p-2">
|
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/30 p-2">
|
||||||
{tag.category === 'project' && (
|
{tag.category === "project" && (
|
||||||
<Input
|
<Input
|
||||||
value={editProjectCode}
|
value={editProjectCode}
|
||||||
onChange={(e) => setEditProjectCode(e.target.value)}
|
onChange={(e) => setEditProjectCode(e.target.value)}
|
||||||
@@ -527,24 +653,39 @@ function TagRow({
|
|||||||
<Input
|
<Input
|
||||||
value={editLabel}
|
value={editLabel}
|
||||||
onChange={(e) => setEditLabel(e.target.value)}
|
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"
|
className="min-w-[200px] flex-1"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<Select value={editScope} onValueChange={(v) => setEditScope(v as TagScope)}>
|
<Select
|
||||||
<SelectTrigger className="w-[120px]"><SelectValue /></SelectTrigger>
|
value={editScope}
|
||||||
|
onValueChange={(v) => setEditScope(v as TagScope)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[120px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="global">Global</SelectItem>
|
<SelectItem value="global">Global</SelectItem>
|
||||||
<SelectItem value="module">Modul</SelectItem>
|
<SelectItem value="module">Modul</SelectItem>
|
||||||
<SelectItem value="company">Companie</SelectItem>
|
<SelectItem value="company">Companie</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{editScope === 'company' && (
|
{editScope === "company" && (
|
||||||
<Select value={editCompanyId} onValueChange={(v) => setEditCompanyId(v as CompanyId)}>
|
<Select
|
||||||
<SelectTrigger className="w-[140px]"><SelectValue /></SelectTrigger>
|
value={editCompanyId}
|
||||||
|
onValueChange={(v) => setEditCompanyId(v as CompanyId)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[140px]">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(Object.keys(COMPANY_LABELS) as CompanyId[]).map((c) => (
|
{(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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -556,17 +697,29 @@ function TagRow({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditColor(c)}
|
onClick={() => setEditColor(c)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-5 w-5 rounded-full border-2 transition-all',
|
"h-5 w-5 rounded-full border-2 transition-all",
|
||||||
editColor === c ? 'border-primary scale-110' : 'border-transparent'
|
editColor === c
|
||||||
|
? "border-primary scale-110"
|
||||||
|
: "border-transparent",
|
||||||
)}
|
)}
|
||||||
style={{ backgroundColor: c }}
|
style={{ backgroundColor: c }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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" />
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
</Button>
|
</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" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -577,18 +730,29 @@ function TagRow({
|
|||||||
<div>
|
<div>
|
||||||
<div className="group flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted/30">
|
<div className="group flex items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted/30">
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<button type="button" onClick={() => setShowChildren(!showChildren)} className="p-0.5">
|
<button
|
||||||
{showChildren
|
type="button"
|
||||||
? <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
onClick={() => setShowChildren(!showChildren)}
|
||||||
: <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />}
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!hasChildren && <span className="w-5" />}
|
{!hasChildren && <span className="w-5" />}
|
||||||
{tag.color && (
|
{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 && (
|
{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>
|
<span className="flex-1 text-sm">{tag.label}</span>
|
||||||
{tag.companyId && (
|
{tag.companyId && (
|
||||||
@@ -619,20 +783,36 @@ function TagRow({
|
|||||||
{hasChildren && showChildren && (
|
{hasChildren && showChildren && (
|
||||||
<div className="ml-6 border-l pl-2">
|
<div className="ml-6 border-l pl-2">
|
||||||
{children.map((child) => (
|
{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" />
|
<FolderTree className="h-3 w-3 text-muted-foreground" />
|
||||||
{child.color && (
|
{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 && (
|
{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>
|
<span className="flex-1 text-sm">{child.label}</span>
|
||||||
<div className="flex gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
<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" />
|
<Pencil className="h-3 w-3 text-muted-foreground" />
|
||||||
</button>
|
</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" />
|
<Trash2 className="h-3 w-3 text-destructive" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import type { Tag, TagCategory } from '@/core/tagging/types';
|
import type { Tag, TagCategory } from "@/core/tagging/types";
|
||||||
import type { CompanyId } from '@/core/auth/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" } */
|
/** 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+(.+)$/);
|
const match = line.match(/^(\w?\d+)\s+(.+)$/);
|
||||||
if (!match?.[1] || !match[2]) return null;
|
if (!match?.[1] || !match[2]) return null;
|
||||||
const num = match[1];
|
const num = match[1];
|
||||||
const label = match[2].trim();
|
const label = match[2].trim();
|
||||||
const padded = num.replace(/^[A-Z]/, '').padStart(3, '0');
|
const padded = num.replace(/^[A-Z]/, "").padStart(3, "0");
|
||||||
const codePrefix = num.startsWith('L') ? `${prefix}L` : prefix;
|
const codePrefix = num.startsWith("L") ? `${prefix}L` : prefix;
|
||||||
return { code: `${codePrefix}-${padded}`, label };
|
return { code: `${codePrefix}-${padded}`, label };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,224 +22,260 @@ export function getManicTimeSeedTags(): SeedTag[] {
|
|||||||
|
|
||||||
// ── Beletage projects ──
|
// ── Beletage projects ──
|
||||||
const beletageProjects = [
|
const beletageProjects = [
|
||||||
'000 Farmacie',
|
"000 Farmacie",
|
||||||
'002 Cladire birouri Stratec',
|
"002 Cladire birouri Stratec",
|
||||||
'003 PUZ Bellavista',
|
"003 PUZ Bellavista",
|
||||||
'007 Design Apartament Teodora',
|
"007 Design Apartament Teodora",
|
||||||
'010 Casa Doinei',
|
"010 Casa Doinei",
|
||||||
'016 Duplex Eremia',
|
"016 Duplex Eremia",
|
||||||
'024 Bloc Petofi',
|
"024 Bloc Petofi",
|
||||||
'028 PUZ Borhanci-Sopor',
|
"028 PUZ Borhanci-Sopor",
|
||||||
'033 Mansardare Branului',
|
"033 Mansardare Branului",
|
||||||
'039 Cabinete Stoma Scala',
|
"039 Cabinete Stoma Scala",
|
||||||
'041 Imobil mixt Progresului',
|
"041 Imobil mixt Progresului",
|
||||||
'045 Casa Andrei Muresanu',
|
"045 Casa Andrei Muresanu",
|
||||||
'052 PUZ Carpenului',
|
"052 PUZ Carpenului",
|
||||||
'059 PUZ Nordului',
|
"059 PUZ Nordului",
|
||||||
'064 Casa Salicea',
|
"064 Casa Salicea",
|
||||||
'066 Terasa Gherase',
|
"066 Terasa Gherase",
|
||||||
'070 Bloc Fanatelor',
|
"070 Bloc Fanatelor",
|
||||||
'073 Case Frumoasa',
|
"073 Case Frumoasa",
|
||||||
'074 PUG Cosbuc',
|
"074 PUG Cosbuc",
|
||||||
'076 Casa Copernicus',
|
"076 Casa Copernicus",
|
||||||
'077 PUZ Schimbare destinatie Brancusi',
|
"077 PUZ Schimbare destinatie Brancusi",
|
||||||
'078 Service auto Linistei',
|
"078 Service auto Linistei",
|
||||||
'079 Amenajare drum Servitute Eremia',
|
"079 Amenajare drum Servitute Eremia",
|
||||||
'080 Bloc Tribunul',
|
"080 Bloc Tribunul",
|
||||||
'081 Extindere casa Gherase',
|
"081 Extindere casa Gherase",
|
||||||
'083 Modificari casa Zsigmund 18',
|
"083 Modificari casa Zsigmund 18",
|
||||||
'084 Mansardare Petofi 21',
|
"084 Mansardare Petofi 21",
|
||||||
'085 Container CT Spital Tabacarilor',
|
"085 Container CT Spital Tabacarilor",
|
||||||
'086 Imprejmuire casa sat Gheorgheni',
|
"086 Imprejmuire casa sat Gheorgheni",
|
||||||
'087 Duplex Oasului fn',
|
"087 Duplex Oasului fn",
|
||||||
'089 PUZ A-Liu Sopor',
|
"089 PUZ A-Liu Sopor",
|
||||||
'090 VR MedEvents',
|
"090 VR MedEvents",
|
||||||
'091 Reclama Caparol',
|
"091 Reclama Caparol",
|
||||||
'092 Imobil birouri 13 Septembrie',
|
"092 Imobil birouri 13 Septembrie",
|
||||||
'093 Casa Salistea Noua',
|
"093 Casa Salistea Noua",
|
||||||
'094 PUD Casa Rediu',
|
"094 PUD Casa Rediu",
|
||||||
'095 Duplex Vanatorului',
|
"095 Duplex Vanatorului",
|
||||||
'096 Design apartament Sopor',
|
"096 Design apartament Sopor",
|
||||||
'097 Cabana Gilau',
|
"097 Cabana Gilau",
|
||||||
'101 PUZ Gilau',
|
"101 PUZ Gilau",
|
||||||
'102 PUZ Ghimbav',
|
"102 PUZ Ghimbav",
|
||||||
'103 Piscine Lunca Noua',
|
"103 Piscine Lunca Noua",
|
||||||
'104 PUZ REGHIN',
|
"104 PUZ REGHIN",
|
||||||
'105 CUT&Crust',
|
"105 CUT&Crust",
|
||||||
'106 PUZ Mihai Romanu Nord',
|
"106 PUZ Mihai Romanu Nord",
|
||||||
'108 Reabilitare Bloc Beiusului',
|
"108 Reabilitare Bloc Beiusului",
|
||||||
'109 Case Samboleni',
|
"109 Case Samboleni",
|
||||||
'110 Penny Crasna',
|
"110 Penny Crasna",
|
||||||
'111 Anexa Piscina Borhanci',
|
"111 Anexa Piscina Borhanci",
|
||||||
'112 PUZ Blocuri Bistrita',
|
"112 PUZ Blocuri Bistrita",
|
||||||
'113 PUZ VARATEC-FIRIZA',
|
"113 PUZ VARATEC-FIRIZA",
|
||||||
'114 PUG Husi',
|
"114 PUG Husi",
|
||||||
'115 PUG Josenii Bargaului',
|
"115 PUG Josenii Bargaului",
|
||||||
'116 PUG Monor',
|
"116 PUG Monor",
|
||||||
'117 Schimbare Destinatie Mihai Viteazu 2',
|
"117 Schimbare Destinatie Mihai Viteazu 2",
|
||||||
'120 Anexa Brasov',
|
"120 Anexa Brasov",
|
||||||
'121 Imprejurare imobil Mesterul Manole 9',
|
"121 Imprejurare imobil Mesterul Manole 9",
|
||||||
'122 Fastfood Bashar',
|
"122 Fastfood Bashar",
|
||||||
'123 PUD Rediu 2',
|
"123 PUD Rediu 2",
|
||||||
'127 Casa Socaciu Ciurila',
|
"127 Casa Socaciu Ciurila",
|
||||||
'128 Schimbare de destinatie Danubius',
|
"128 Schimbare de destinatie Danubius",
|
||||||
'129 (re) Casa Sarca-Sorescu',
|
"129 (re) Casa Sarca-Sorescu",
|
||||||
'130 Casa Suta-Wonderland',
|
"130 Casa Suta-Wonderland",
|
||||||
'131 PUD Oasului Hufi',
|
"131 PUD Oasului Hufi",
|
||||||
'132 Reabilitare Camin Cultural Baciu',
|
"132 Reabilitare Camin Cultural Baciu",
|
||||||
'133 PUG Feldru',
|
"133 PUG Feldru",
|
||||||
'134 DALI Blocuri Murfatlar',
|
"134 DALI Blocuri Murfatlar",
|
||||||
'135 Case de vacanta Dianei',
|
"135 Case de vacanta Dianei",
|
||||||
'136 PUG BROSTENI',
|
"136 PUG BROSTENI",
|
||||||
'139 Casa Turda',
|
"139 Casa Turda",
|
||||||
'140 Releveu Bistrita (Morariu)',
|
"140 Releveu Bistrita (Morariu)",
|
||||||
'141 PUZ Janovic Jeno',
|
"141 PUZ Janovic Jeno",
|
||||||
'142 Penny Borhanci',
|
"142 Penny Borhanci",
|
||||||
'143 Pavilion Politie Radauti',
|
"143 Pavilion Politie Radauti",
|
||||||
'149 Duplex Sorescu 31-33',
|
"149 Duplex Sorescu 31-33",
|
||||||
'150 DALI SF Scoala Baciu',
|
"150 DALI SF Scoala Baciu",
|
||||||
'151 Casa Alexandru Bohatiel 17',
|
"151 Casa Alexandru Bohatiel 17",
|
||||||
'152 PUZ Penny Tautii Magheraus',
|
"152 PUZ Penny Tautii Magheraus",
|
||||||
'153 PUG Banita',
|
"153 PUG Banita",
|
||||||
'155 PT Scoala Floresti',
|
"155 PT Scoala Floresti",
|
||||||
'156 Case Sorescu',
|
"156 Case Sorescu",
|
||||||
'157 Gradi-Cresa Baciu',
|
"157 Gradi-Cresa Baciu",
|
||||||
'158 Duplex Sorescu 21-23',
|
"158 Duplex Sorescu 21-23",
|
||||||
'159 Amenajare Spatiu Grenke PBC',
|
"159 Amenajare Spatiu Grenke PBC",
|
||||||
'160 Etajare Primaria Baciu',
|
"160 Etajare Primaria Baciu",
|
||||||
'161 Extindere Ap Baciu',
|
"161 Extindere Ap Baciu",
|
||||||
'164 SD salon Aurel Vlaicu',
|
"164 SD salon Aurel Vlaicu",
|
||||||
'165 Reclama Marasti',
|
"165 Reclama Marasti",
|
||||||
'166 Catei Apahida',
|
"166 Catei Apahida",
|
||||||
'167 Apartament Mircea Zaciu 13-15',
|
"167 Apartament Mircea Zaciu 13-15",
|
||||||
'169 Casa PETRILA 37',
|
"169 Casa PETRILA 37",
|
||||||
'170 Cabana Campeni AB',
|
"170 Cabana Campeni AB",
|
||||||
'171 Camin Apahida',
|
"171 Camin Apahida",
|
||||||
'L089 PUZ TUSA-BOJAN',
|
"L089 PUZ TUSA-BOJAN",
|
||||||
'172 Design casa Iugoslaviei 18',
|
"172 Design casa Iugoslaviei 18",
|
||||||
'173 Reabilitare spitale Sighetu',
|
"173 Reabilitare spitale Sighetu",
|
||||||
'174 StudX UMFST',
|
"174 StudX UMFST",
|
||||||
'176 - 2025 - ReAC Ansamblu rezi Bibescu',
|
"176 - 2025 - ReAC Ansamblu rezi Bibescu",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const line of beletageProjects) {
|
for (const line of beletageProjects) {
|
||||||
const parsed = parseProjectLine(line, 'B');
|
const parsed = parseProjectLine(line, "B");
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
tags.push({
|
tags.push({
|
||||||
label: parsed.label,
|
label: parsed.label,
|
||||||
category: 'project',
|
category: "project",
|
||||||
scope: 'company',
|
scope: "company",
|
||||||
companyId: 'beletage' as CompanyId,
|
companyId: "beletage" as CompanyId,
|
||||||
projectCode: parsed.code,
|
projectCode: parsed.code,
|
||||||
color: '#22B5AB',
|
color: "#22B5AB",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Urban Switch projects ──
|
// ── Urban Switch projects ──
|
||||||
const urbanSwitchProjects = [
|
const urbanSwitchProjects = [
|
||||||
'001 PUZ Sopor - Ansamblu Rezidential',
|
"001 PUZ Sopor - Ansamblu Rezidential",
|
||||||
'002 PUZ Borhanci Nord',
|
"002 PUZ Borhanci Nord",
|
||||||
'003 PUZ Zona Centrala Cluj',
|
"003 PUZ Zona Centrala Cluj",
|
||||||
'004 PUG Floresti',
|
"004 PUG Floresti",
|
||||||
'005 PUZ Dezmir - Zona Industriala',
|
"005 PUZ Dezmir - Zona Industriala",
|
||||||
'006 PUZ Gilau Est',
|
"006 PUZ Gilau Est",
|
||||||
'007 PUZ Baciu - Extensie Intravilan',
|
"007 PUZ Baciu - Extensie Intravilan",
|
||||||
'008 PUG Apahida',
|
"008 PUG Apahida",
|
||||||
'009 PUZ Iris - Reconversie',
|
"009 PUZ Iris - Reconversie",
|
||||||
'010 PUZ Faget - Zona Turistica',
|
"010 PUZ Faget - Zona Turistica",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const line of urbanSwitchProjects) {
|
for (const line of urbanSwitchProjects) {
|
||||||
const parsed = parseProjectLine(line, 'US');
|
const parsed = parseProjectLine(line, "US");
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
tags.push({
|
tags.push({
|
||||||
label: parsed.label,
|
label: parsed.label,
|
||||||
category: 'project',
|
category: "project",
|
||||||
scope: 'company',
|
scope: "company",
|
||||||
companyId: 'urban-switch' as CompanyId,
|
companyId: "urban-switch" as CompanyId,
|
||||||
projectCode: parsed.code,
|
projectCode: parsed.code,
|
||||||
color: '#345476',
|
color: "#345476",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Studii de Teren projects ──
|
// ── Studii de Teren projects ──
|
||||||
const studiiDeTerenProjects = [
|
const studiiDeTerenProjects = [
|
||||||
'001 Studiu Geo - Sopor Rezidential',
|
"001 Studiu Geo - Sopor Rezidential",
|
||||||
'002 Studiu Geo - Borhanci Vila',
|
"002 Studiu Geo - Borhanci Vila",
|
||||||
'003 Studiu Geo - Floresti Ansamblu',
|
"003 Studiu Geo - Floresti Ansamblu",
|
||||||
'004 Ridicare Topo - Dezmir Industrial',
|
"004 Ridicare Topo - Dezmir Industrial",
|
||||||
'005 Studiu Geo - Gilau Est',
|
"005 Studiu Geo - Gilau Est",
|
||||||
'006 Ridicare Topo - Baciu Extensie',
|
"006 Ridicare Topo - Baciu Extensie",
|
||||||
'007 Studiu Geo - Apahida Centru',
|
"007 Studiu Geo - Apahida Centru",
|
||||||
'008 Ridicare Topo - Faget',
|
"008 Ridicare Topo - Faget",
|
||||||
'009 Studiu Geo - Iris Reconversie',
|
"009 Studiu Geo - Iris Reconversie",
|
||||||
'010 Studiu Geo - Turda Rezidential',
|
"010 Studiu Geo - Turda Rezidential",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const line of studiiDeTerenProjects) {
|
for (const line of studiiDeTerenProjects) {
|
||||||
const parsed = parseProjectLine(line, 'SDT');
|
const parsed = parseProjectLine(line, "SDT");
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
tags.push({
|
tags.push({
|
||||||
label: parsed.label,
|
label: parsed.label,
|
||||||
category: 'project',
|
category: "project",
|
||||||
scope: 'company',
|
scope: "company",
|
||||||
companyId: 'studii-de-teren' as CompanyId,
|
companyId: "studii-de-teren" as CompanyId,
|
||||||
projectCode: parsed.code,
|
projectCode: parsed.code,
|
||||||
color: '#0182A1',
|
color: "#0182A1",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase tags ──
|
// ── Phase tags ──
|
||||||
const phases = [
|
const phases = [
|
||||||
'CU', 'Schita', 'Avize', 'PUD', 'AO', 'PUZ', 'PUG',
|
"CU",
|
||||||
'DTAD', 'DTAC', 'PT', 'Detalii de Executie', 'Studii de fundamentare',
|
"Schita",
|
||||||
'Regulament', 'Parte desenata', 'Parte scrisa',
|
"Avize",
|
||||||
'Consultanta client', 'Macheta', 'Consultanta receptie',
|
"PUD",
|
||||||
'Redactare', 'Depunere', 'Ridicare', 'Verificare proiect',
|
"AO",
|
||||||
'Vizita santier',
|
"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) {
|
for (const phase of phases) {
|
||||||
tags.push({
|
tags.push({
|
||||||
label: phase,
|
label: phase,
|
||||||
category: 'phase',
|
category: "phase",
|
||||||
scope: 'global',
|
scope: "global",
|
||||||
color: '#3b82f6',
|
color: "#3b82f6",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Activity tags ──
|
// ── Activity tags ──
|
||||||
const activities = [
|
const activities = [
|
||||||
'Ofertare', 'Configurari', 'Organizare initiala', 'Pregatire Portofoliu',
|
"Ofertare",
|
||||||
'Website', 'Documentare', 'Design grafic', 'Design interior',
|
"Configurari",
|
||||||
'Design exterior', 'Releveu', 'Reclama', 'Master MATDR',
|
"Organizare initiala",
|
||||||
'Pauza de masa', 'Timp personal', 'Concediu', 'Compensare overtime',
|
"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) {
|
for (const activity of activities) {
|
||||||
tags.push({
|
tags.push({
|
||||||
label: activity,
|
label: activity,
|
||||||
category: 'activity',
|
category: "activity",
|
||||||
scope: 'global',
|
scope: "global",
|
||||||
color: '#8b5cf6',
|
color: "#8b5cf6",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Document type tags ──
|
// ── Document type tags ──
|
||||||
const docTypes = [
|
const docTypes = [
|
||||||
'Contract', 'Ofertă', 'Factură', 'Scrisoare',
|
"Contract",
|
||||||
'Aviz', 'Notă de comandă', 'Raport', 'Cerere', 'Altele',
|
"Ofertă",
|
||||||
|
"Factură",
|
||||||
|
"Scrisoare",
|
||||||
|
"Aviz",
|
||||||
|
"Notă de comandă",
|
||||||
|
"Raport",
|
||||||
|
"Cerere",
|
||||||
|
"Altele",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const dt of docTypes) {
|
for (const dt of docTypes) {
|
||||||
tags.push({
|
tags.push({
|
||||||
label: dt,
|
label: dt,
|
||||||
category: 'document-type',
|
category: "document-type",
|
||||||
scope: 'global',
|
scope: "global",
|
||||||
color: '#f59e0b',
|
color: "#f59e0b",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user