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:
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* NAS / network storage path mappings.
|
||||||
|
*
|
||||||
|
* The office NAS (\\newamun / 10.10.10.10) exposes several shares.
|
||||||
|
* Windows maps these to drive letters. This config lets us normalise
|
||||||
|
* user-pasted paths to UNC and back, and build clickable `file:///` links.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface NasDriveMapping {
|
||||||
|
/** Drive letter (upper-case, no colon) */
|
||||||
|
drive: string;
|
||||||
|
/** UNC prefix WITHOUT trailing backslash, e.g. \\\\newamun\\Proiecte */
|
||||||
|
unc: string;
|
||||||
|
/** Human-readable label */
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** All known drive-letter → UNC mappings */
|
||||||
|
export const NAS_DRIVE_MAPPINGS: NasDriveMapping[] = [
|
||||||
|
{ drive: "P", unc: "\\\\newamun\\Proiecte", label: "Proiecte" },
|
||||||
|
// Add more as needed:
|
||||||
|
// { drive: "S", unc: "\\\\newamun\\Shared", label: "Shared" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** NAS hostname / IP — used for display only */
|
||||||
|
export const NAS_HOST = "newamun";
|
||||||
|
export const NAS_IP = "10.10.10.10";
|
||||||
|
|
||||||
|
// ── helpers ──
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect whether a string looks like a Windows network / mapped-drive path.
|
||||||
|
* Accepts: `P:\folder\file.pdf`, `\\newamun\Proiecte\...`, `\\10.10.10.10\...`
|
||||||
|
*/
|
||||||
|
export function isNetworkPath(input: string): boolean {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
// UNC path
|
||||||
|
if (trimmed.startsWith("\\\\")) return true;
|
||||||
|
// Mapped drive letter that we recognise
|
||||||
|
const match = trimmed.match(/^([A-Z]):\\/i);
|
||||||
|
if (match) {
|
||||||
|
const letter = match[1]!.toUpperCase();
|
||||||
|
return NAS_DRIVE_MAPPINGS.some((m) => m.drive === letter);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise to UNC path (replace drive letter with \\newamun\Share).
|
||||||
|
* If already UNC or unrecognised → returns the original trimmed.
|
||||||
|
*/
|
||||||
|
export function toUncPath(input: string): string {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (trimmed.startsWith("\\\\")) return trimmed;
|
||||||
|
const match = trimmed.match(/^([A-Z]):(\\.*)/i);
|
||||||
|
if (match) {
|
||||||
|
const letter = match[1]!.toUpperCase();
|
||||||
|
const rest = match[2]!;
|
||||||
|
const mapping = NAS_DRIVE_MAPPINGS.find((m) => m.drive === letter);
|
||||||
|
if (mapping) return `${mapping.unc}${rest}`;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a clickable `file:///` URL from a UNC or drive-letter path.
|
||||||
|
* Windows Explorer opens this natively.
|
||||||
|
*/
|
||||||
|
export function toFileUrl(input: string): string {
|
||||||
|
const unc = toUncPath(input);
|
||||||
|
// file:///\\server\share\path → file://///server/share/path
|
||||||
|
const slashed = unc.replace(/\\/g, "/");
|
||||||
|
return `file:///${slashed}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a short display name from a full path.
|
||||||
|
* e.g. `\\newamun\Proiecte\095\doc.pdf` → `doc.pdf`
|
||||||
|
*/
|
||||||
|
export function pathFileName(input: string): string {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
const parts = trimmed.split(/[\\/]/);
|
||||||
|
return parts[parts.length - 1] || trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a short display path (share + last 2 segments).
|
||||||
|
* e.g. `\\newamun\Proiecte\095 - 2020\99_DOC\file.pdf` → `Proiecte\…\99_DOC\file.pdf`
|
||||||
|
*/
|
||||||
|
export function shortDisplayPath(input: string): string {
|
||||||
|
const unc = toUncPath(input);
|
||||||
|
const parts = unc.replace(/^\\\\/, "").split("\\").filter(Boolean);
|
||||||
|
// parts: [server, share, folder1, folder2, ..., file]
|
||||||
|
if (parts.length <= 3) return parts.slice(1).join("\\");
|
||||||
|
const share = parts[1] ?? "";
|
||||||
|
const last2 = parts.slice(-2).join("\\");
|
||||||
|
return `${share}\\…\\${last2}`;
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@ import {
|
|||||||
Globe,
|
Globe,
|
||||||
ArrowDownToLine,
|
ArrowDownToLine,
|
||||||
ArrowUpFromLine,
|
ArrowUpFromLine,
|
||||||
|
HardDrive,
|
||||||
|
FolderOpen,
|
||||||
|
Link2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { CompanyId } from "@/core/auth/types";
|
import type { CompanyId } from "@/core/auth/types";
|
||||||
import type {
|
import type {
|
||||||
@@ -27,6 +30,13 @@ import type {
|
|||||||
ACValidityTracking,
|
ACValidityTracking,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { DEFAULT_DOC_TYPE_LABELS } 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 { Input } from "@/shared/components/ui/input";
|
||||||
import { Label } from "@/shared/components/ui/label";
|
import { Label } from "@/shared/components/ui/label";
|
||||||
import { Textarea } from "@/shared/components/ui/textarea";
|
import { Textarea } from "@/shared/components/ui/textarea";
|
||||||
@@ -367,6 +377,31 @@ export function RegistryEntryForm({
|
|||||||
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isSubmitting || isUploading) return;
|
if (isSubmitting || isUploading) return;
|
||||||
@@ -1152,15 +1187,27 @@ export function RegistryEntryForm({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Label>
|
</Label>
|
||||||
<Button
|
<div className="flex gap-1.5">
|
||||||
type="button"
|
<Button
|
||||||
variant="outline"
|
type="button"
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => fileInputRef.current?.click()}
|
size="sm"
|
||||||
disabled={isSubmitting}
|
onClick={() => setShowNetworkInput((v) => !v)}
|
||||||
>
|
disabled={isSubmitting}
|
||||||
<Paperclip className="mr-1 h-3.5 w-3.5" /> Adaugă fișier
|
title="Link către fișier pe NAS"
|
||||||
</Button>
|
>
|
||||||
|
<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
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -1170,27 +1217,119 @@ export function RegistryEntryForm({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{attachments.length > 0 && (
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2 space-y-1">
|
||||||
{attachments.map((att) => (
|
{attachments.map((att) =>
|
||||||
<div
|
att.networkPath ? (
|
||||||
key={att.id}
|
// Network path attachment — distinct visual
|
||||||
className="flex items-center gap-2 rounded border px-2 py-1 text-sm"
|
<div
|
||||||
>
|
key={att.id}
|
||||||
<Paperclip className="h-3 w-3 text-muted-foreground" />
|
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"
|
||||||
<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" />
|
<HardDrive className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||||
</button>
|
<a
|
||||||
</div>
|
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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,15 +68,17 @@ export interface ClosureInfo {
|
|||||||
attachment?: RegistryAttachment;
|
attachment?: RegistryAttachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** File attachment */
|
/** File attachment — either uploaded (base64) or network path reference */
|
||||||
export interface RegistryAttachment {
|
export interface RegistryAttachment {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
/** base64-encoded content or URL */
|
/** base64-encoded content, empty string (stripped), or "__network__" for network paths */
|
||||||
data: string;
|
data: string;
|
||||||
type: string;
|
type: string;
|
||||||
size: number;
|
size: number;
|
||||||
addedAt: string;
|
addedAt: string;
|
||||||
|
/** UNC or drive-letter path to a file on network storage (NAS) */
|
||||||
|
networkPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Deadline tracking types ──
|
// ── Deadline tracking types ──
|
||||||
|
|||||||
Reference in New Issue
Block a user