feat(registratura): NAS network path attachments (\\newamun / P:\)

- New nas-paths.ts config: drive mappings, UNC normalization, file:/// URL builder
- RegistryAttachment type extended with optional networkPath field
- 'Link NAS' button in attachment section opens inline path input
- Network path entries shown with blue HardDrive icon + NAS badge
- Click opens in Explorer via file:/// URL, copy path button on hover
- P:\ auto-converted to \\newamun\Proiecte UNC path
- Short display path shows share + last 2 segments
- Validation: warns if path doesn't match known NAS mappings
This commit is contained in:
AI Assistant
2026-02-28 17:13:26 +02:00
parent eaaec49eb1
commit 4f00cb2de8
3 changed files with 268 additions and 29 deletions
@@ -15,6 +15,9 @@ import {
Globe,
ArrowDownToLine,
ArrowUpFromLine,
HardDrive,
FolderOpen,
Link2,
} from "lucide-react";
import type { CompanyId } from "@/core/auth/types";
import type {
@@ -27,6 +30,13 @@ import type {
ACValidityTracking,
} from "../types";
import { DEFAULT_DOC_TYPE_LABELS } from "../types";
import {
isNetworkPath,
toUncPath,
toFileUrl,
pathFileName,
shortDisplayPath,
} from "@/config/nas-paths";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
import { Textarea } from "@/shared/components/ui/textarea";
@@ -367,6 +377,31 @@ export function RegistryEntryForm({
setAttachments((prev) => prev.filter((a) => a.id !== id));
};
// Network path support
const [networkPathInput, setNetworkPathInput] = useState("");
const [showNetworkInput, setShowNetworkInput] = useState(false);
const handleAddNetworkPath = () => {
const raw = networkPathInput.trim();
if (!raw) return;
const unc = toUncPath(raw);
const fileName = pathFileName(raw);
setAttachments((prev) => [
...prev,
{
id: uuid(),
name: fileName,
data: "__network__",
type: "network/path",
size: 0,
addedAt: new Date().toISOString(),
networkPath: unc,
},
]);
setNetworkPathInput("");
setShowNetworkInput(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSubmitting || isUploading) return;
@@ -1152,15 +1187,27 @@ export function RegistryEntryForm({
</span>
)}
</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isSubmitting}
>
<Paperclip className="mr-1 h-3.5 w-3.5" /> Adaugă fișier
</Button>
<div className="flex gap-1.5">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowNetworkInput((v) => !v)}
disabled={isSubmitting}
title="Link către fișier pe NAS"
>
<HardDrive className="mr-1 h-3.5 w-3.5" /> Link NAS
</Button>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => fileInputRef.current?.click()}
disabled={isSubmitting}
>
<Paperclip className="mr-1 h-3.5 w-3.5" /> Adaugă fișier
</Button>
</div>
<input
ref={fileInputRef}
type="file"
@@ -1170,27 +1217,119 @@ export function RegistryEntryForm({
className="hidden"
/>
</div>
{/* Network path input */}
{showNetworkInput && (
<div className="mt-2 rounded-md border border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/30 p-3 space-y-2">
<Label className="text-xs flex items-center gap-1">
<HardDrive className="h-3 w-3" />
Cale fișier pe NAS (\\newamun sau P:\\...)
</Label>
<div className="flex gap-2">
<Input
value={networkPathInput}
onChange={(e) => setNetworkPathInput(e.target.value)}
placeholder="P:\\095 - 2020 - Duplex\\99_DOC\\CU 1348-2024.pdf"
className="flex-1 font-mono text-xs"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddNetworkPath();
}
}}
/>
<Button
type="button"
size="sm"
onClick={handleAddNetworkPath}
disabled={!networkPathInput.trim()}
>
<Link2 className="mr-1 h-3.5 w-3.5" /> Adaugă
</Button>
</div>
{networkPathInput.trim() && isNetworkPath(networkPathInput) && (
<p className="text-[10px] text-muted-foreground">
{shortDisplayPath(networkPathInput)}
</p>
)}
{networkPathInput.trim() && !isNetworkPath(networkPathInput) && (
<p className="text-[10px] text-amber-600 dark:text-amber-400">
Calea nu pare a fi pe NAS. Introdu o cale de tip P:\\... sau \\newamun\\...
</p>
)}
</div>
)}
{attachments.length > 0 && (
<div className="mt-2 space-y-1">
{attachments.map((att) => (
<div
key={att.id}
className="flex items-center gap-2 rounded border px-2 py-1 text-sm"
>
<Paperclip className="h-3 w-3 text-muted-foreground" />
<span className="flex-1 truncate">{att.name}</span>
<Badge variant="outline" className="text-[10px]">
{(att.size / 1024).toFixed(0)} KB
</Badge>
<button
type="button"
onClick={() => removeAttachment(att.id)}
className="text-destructive"
{attachments.map((att) =>
att.networkPath ? (
// Network path attachment — distinct visual
<div
key={att.id}
className="flex items-center gap-2 rounded border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20 px-2 py-1.5 text-sm group"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
))}
<HardDrive className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 shrink-0" />
<a
href={toFileUrl(att.networkPath)}
className="flex-1 min-w-0 flex items-center gap-1 text-blue-700 dark:text-blue-300 hover:underline cursor-pointer"
title={`Deschide în Explorer: ${att.networkPath}`}
onClick={(e) => {
// file:/// links might be blocked by browser; copy path as fallback
e.preventDefault();
window.open(toFileUrl(att.networkPath!), "_blank");
}}
>
<FolderOpen className="h-3 w-3 shrink-0" />
<span className="truncate font-mono text-xs">
{shortDisplayPath(att.networkPath)}
</span>
</a>
<Badge
variant="outline"
className="text-[10px] border-blue-300 dark:border-blue-700 text-blue-600 dark:text-blue-400 shrink-0"
>
NAS
</Badge>
<button
type="button"
className="text-blue-400 hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => {
navigator.clipboard.writeText(att.networkPath!);
}}
title="Copiază calea"
>
<Link2 className="h-3 w-3" />
</button>
<button
type="button"
onClick={() => removeAttachment(att.id)}
className="text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
) : (
// Normal uploaded file attachment
<div
key={att.id}
className="flex items-center gap-2 rounded border px-2 py-1 text-sm"
>
<Paperclip className="h-3 w-3 text-muted-foreground" />
<span className="flex-1 truncate">{att.name}</span>
<Badge variant="outline" className="text-[10px]">
{(att.size / 1024).toFixed(0)} KB
</Badge>
<button
type="button"
onClick={() => removeAttachment(att.id)}
className="text-destructive"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
),
)}
</div>
)}
</div>
+4 -2
View File
@@ -68,15 +68,17 @@ export interface ClosureInfo {
attachment?: RegistryAttachment;
}
/** File attachment */
/** File attachment — either uploaded (base64) or network path reference */
export interface RegistryAttachment {
id: string;
name: string;
/** base64-encoded content or URL */
/** base64-encoded content, empty string (stripped), or "__network__" for network paths */
data: string;
type: string;
size: number;
addedAt: string;
/** UNC or drive-letter path to a file on network storage (NAS) */
networkPath?: string;
}
// ── Deadline tracking types ──