3.06 Template Library Redenumire, Versionare, Multi-format
- Renamed from 'Sabloane Word' to 'Biblioteca Sabloane' (Template Library) - Multi-format support: Word, Excel, PDF, DWG, Archicad with auto-detection - Auto versioning: 'Revizie Noua' button archives current version, bumps semver - Version history dialog: browse and download any previous version - Simplified UX: file upload vs external link toggle, auto-detect placeholders silently for .docx, hide placeholders section for non-Word formats - File type icons: distinct icons for docx, xlsx, archicad, dwg, pdf - Updated stats cards: Word/Excel count, DWG/Archicad count, versioned count - Backward compatible: old entries without fileType/versionHistory get defaults
This commit is contained in:
@@ -91,8 +91,9 @@ export const ro: Labels = {
|
|||||||
description: "Clienți, furnizori, instituții",
|
description: "Clienți, furnizori, instituții",
|
||||||
},
|
},
|
||||||
"word-templates": {
|
"word-templates": {
|
||||||
title: "Șabloane Word",
|
title: "Bibliotecă Șabloane",
|
||||||
description: "Bibliotecă contracte, oferte, rapoarte",
|
description:
|
||||||
|
"Șabloane contracte, oferte, rapoarte (Word, Excel, Archicad, DWG, PDF)",
|
||||||
},
|
},
|
||||||
"tag-manager": {
|
"tag-manager": {
|
||||||
title: "Manager Etichete",
|
title: "Manager Etichete",
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useSignatureConfig } from '../hooks/use-signature-config';
|
import { useSignatureConfig } from "../hooks/use-signature-config";
|
||||||
import { useSavedSignatures } from '../hooks/use-saved-signatures';
|
import { useSavedSignatures } from "../hooks/use-saved-signatures";
|
||||||
import { SignatureConfigurator } from './signature-configurator';
|
import { SignatureConfigurator } from "./signature-configurator";
|
||||||
import { SignaturePreview } from './signature-preview';
|
import { SignaturePreview } from "./signature-preview";
|
||||||
import { SavedSignaturesPanel } from './saved-signatures-panel';
|
import { SavedSignaturesPanel } from "./saved-signatures-panel";
|
||||||
import { Separator } from '@/shared/components/ui/separator';
|
import { Separator } from "@/shared/components/ui/separator";
|
||||||
import { Button } from '@/shared/components/ui/button';
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { RotateCcw } from 'lucide-react';
|
import { RotateCcw } from "lucide-react";
|
||||||
import type { SignatureBanner } from '../types';
|
import type { SignatureBanner } from "../types";
|
||||||
|
|
||||||
export function EmailSignatureModule() {
|
export function EmailSignatureModule() {
|
||||||
const {
|
const {
|
||||||
config, updateField, updateColor, updateLayout,
|
config,
|
||||||
setVariant, setCompany, setAddress, resetToDefaults, loadConfig,
|
updateField,
|
||||||
|
updateColor,
|
||||||
|
updateLayout,
|
||||||
|
setVariant,
|
||||||
|
setCompany,
|
||||||
|
setAddress,
|
||||||
|
resetToDefaults,
|
||||||
|
loadConfig,
|
||||||
} = useSignatureConfig();
|
} = useSignatureConfig();
|
||||||
|
|
||||||
const { saved, loading, save, remove } = useSavedSignatures();
|
const { saved, loading, save, remove } = useSavedSignatures();
|
||||||
|
|
||||||
const setBanner = (banner: SignatureBanner | undefined) => {
|
const setBanner = (banner: SignatureBanner | undefined) => {
|
||||||
updateField('banner', banner);
|
updateField("banner", banner);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,7 +49,9 @@ export function EmailSignatureModule() {
|
|||||||
<SavedSignaturesPanel
|
<SavedSignaturesPanel
|
||||||
saved={saved}
|
saved={saved}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onSave={async (label, cfg) => { await save(label, cfg); }}
|
onSave={async (label, cfg) => {
|
||||||
|
await save(label, cfg);
|
||||||
|
}}
|
||||||
onLoad={loadConfig}
|
onLoad={loadConfig}
|
||||||
onRemove={remove}
|
onRemove={remove}
|
||||||
currentConfig={config}
|
currentConfig={config}
|
||||||
@@ -50,7 +59,12 @@ export function EmailSignatureModule() {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={resetToDefaults} className="w-full">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={resetToDefaults}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
||||||
Resetare la valorile implicite
|
Resetare la valorile implicite
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
Search,
|
Search,
|
||||||
FileText,
|
FileText,
|
||||||
|
FileSpreadsheet,
|
||||||
|
FileArchive,
|
||||||
|
File,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Copy,
|
Copy,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Wand2,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
|
History,
|
||||||
|
Link,
|
||||||
|
Upload,
|
||||||
|
RotateCcw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import { Input } from "@/shared/components/ui/input";
|
import { Input } from "@/shared/components/ui/input";
|
||||||
@@ -39,12 +45,13 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from "@/shared/components/ui/dialog";
|
} from "@/shared/components/ui/dialog";
|
||||||
import type { CompanyId } from "@/core/auth/types";
|
import type { CompanyId } from "@/core/auth/types";
|
||||||
import type { WordTemplate, TemplateCategory } from "../types";
|
import type {
|
||||||
|
WordTemplate,
|
||||||
|
TemplateCategory,
|
||||||
|
TemplateFileType,
|
||||||
|
} from "../types";
|
||||||
import { useTemplates } from "../hooks/use-templates";
|
import { useTemplates } from "../hooks/use-templates";
|
||||||
import {
|
import { parsePlaceholdersFromBuffer } from "../services/placeholder-parser";
|
||||||
parsePlaceholdersFromBuffer,
|
|
||||||
parsePlaceholdersFromUrl,
|
|
||||||
} from "../services/placeholder-parser";
|
|
||||||
|
|
||||||
const CATEGORY_LABELS: Record<TemplateCategory, string> = {
|
const CATEGORY_LABELS: Record<TemplateCategory, string> = {
|
||||||
contract: "Contract",
|
contract: "Contract",
|
||||||
@@ -57,6 +64,44 @@ const CATEGORY_LABELS: Record<TemplateCategory, string> = {
|
|||||||
altele: "Altele",
|
altele: "Altele",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FILE_TYPE_LABELS: Record<TemplateFileType, string> = {
|
||||||
|
docx: "Word (.docx)",
|
||||||
|
xlsx: "Excel (.xlsx)",
|
||||||
|
pdf: "PDF",
|
||||||
|
dwg: "DWG",
|
||||||
|
archicad: "Archicad (.tpl/.pln)",
|
||||||
|
other: "Altul",
|
||||||
|
};
|
||||||
|
|
||||||
|
function detectFileType(url: string): TemplateFileType {
|
||||||
|
const ext = url.split(".").pop()?.toLowerCase() ?? "";
|
||||||
|
if (ext === "docx" || ext === "doc") return "docx";
|
||||||
|
if (ext === "xlsx" || ext === "xls") return "xlsx";
|
||||||
|
if (ext === "pdf") return "pdf";
|
||||||
|
if (ext === "dwg" || ext === "dxf") return "dwg";
|
||||||
|
if (["tpl", "pln", "pla", "bpn"].includes(ext)) return "archicad";
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileTypeIcon({
|
||||||
|
fileType,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
fileType: TemplateFileType;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
switch (fileType) {
|
||||||
|
case "docx":
|
||||||
|
return <FileText className={className} />;
|
||||||
|
case "xlsx":
|
||||||
|
return <FileSpreadsheet className={className} />;
|
||||||
|
case "archicad":
|
||||||
|
return <FileArchive className={className} />;
|
||||||
|
default:
|
||||||
|
return <File className={className} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ViewMode = "list" | "add" | "edit";
|
type ViewMode = "list" | "add" | "edit";
|
||||||
|
|
||||||
export function WordTemplatesModule() {
|
export function WordTemplatesModule() {
|
||||||
@@ -68,6 +113,7 @@ export function WordTemplatesModule() {
|
|||||||
updateFilter,
|
updateFilter,
|
||||||
addTemplate,
|
addTemplate,
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
|
createRevision,
|
||||||
cloneTemplate,
|
cloneTemplate,
|
||||||
removeTemplate,
|
removeTemplate,
|
||||||
} = useTemplates();
|
} = useTemplates();
|
||||||
@@ -76,6 +122,13 @@ export function WordTemplatesModule() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
|
const [revisingTemplate, setRevisingTemplate] =
|
||||||
|
useState<WordTemplate | null>(null);
|
||||||
|
const [revisionUrl, setRevisionUrl] = useState("");
|
||||||
|
const [revisionNotes, setRevisionNotes] = useState("");
|
||||||
|
const [historyTemplate, setHistoryTemplate] = useState<WordTemplate | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = async (
|
const handleSubmit = async (
|
||||||
data: Omit<WordTemplate, "id" | "createdAt" | "updatedAt">,
|
data: Omit<WordTemplate, "id" | "createdAt" | "updatedAt">,
|
||||||
@@ -96,6 +149,15 @@ export function WordTemplatesModule() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRevisionConfirm = async () => {
|
||||||
|
if (revisingTemplate) {
|
||||||
|
await createRevision(revisingTemplate.id, revisionUrl, revisionNotes);
|
||||||
|
setRevisingTemplate(null);
|
||||||
|
setRevisionUrl("");
|
||||||
|
setRevisionNotes("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
@@ -108,28 +170,25 @@ export function WordTemplatesModule() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<p className="text-xs text-muted-foreground">Beletage</p>
|
<p className="text-xs text-muted-foreground">Word/Excel</p>
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">
|
||||||
{allTemplates.filter((t) => t.company === "beletage").length}
|
{allTemplates.filter((t) => (t.fileType ?? "docx") === "docx" || t.fileType === "xlsx").length}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<p className="text-xs text-muted-foreground">Urban Switch</p>
|
<p className="text-xs text-muted-foreground">DWG/Archicad</p>
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">
|
||||||
{allTemplates.filter((t) => t.company === "urban-switch").length}
|
{allTemplates.filter((t) => t.fileType === "dwg" || t.fileType === "archicad").length}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<p className="text-xs text-muted-foreground">Studii de Teren</p>
|
<p className="text-xs text-muted-foreground">Cu versiuni</p>
|
||||||
<p className="text-2xl font-bold">
|
<p className="text-2xl font-bold">
|
||||||
{
|
{allTemplates.filter((t) => (t.versionHistory ?? []).length > 0).length}
|
||||||
allTemplates.filter((t) => t.company === "studii-de-teren")
|
|
||||||
.length
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -193,7 +252,7 @@ export function WordTemplatesModule() {
|
|||||||
</p>
|
</p>
|
||||||
) : templates.length === 0 ? (
|
) : templates.length === 0 ? (
|
||||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||||
Niciun șablon găsit. Adaugă primul șablon Word.
|
Niciun șablon găsit. Adaugă primul șablon.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
@@ -201,6 +260,15 @@ export function WordTemplatesModule() {
|
|||||||
<Card key={tpl.id} className="group relative">
|
<Card key={tpl.id} className="group relative">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
title="Revizie nouă"
|
||||||
|
onClick={() => setRevisingTemplate(tpl)}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -232,7 +300,10 @@ export function WordTemplatesModule() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border bg-muted/30">
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border bg-muted/30">
|
||||||
<FileText className="h-5 w-5 text-muted-foreground" />
|
<FileTypeIcon
|
||||||
|
fileType={tpl.fileType ?? "docx"}
|
||||||
|
className="h-5 w-5 text-muted-foreground"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-medium">{tpl.name}</p>
|
<p className="font-medium">{tpl.name}</p>
|
||||||
@@ -248,33 +319,49 @@ export function WordTemplatesModule() {
|
|||||||
<Badge variant="secondary" className="text-[10px]">
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
v{tpl.version}
|
v{tpl.version}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
|
{FILE_TYPE_LABELS[tpl.fileType ?? "docx"]}
|
||||||
|
</Badge>
|
||||||
{tpl.clonedFrom && (
|
{tpl.clonedFrom && (
|
||||||
<Badge variant="secondary" className="text-[10px]">
|
<Badge variant="secondary" className="text-[10px]">
|
||||||
Clonă
|
Clonă
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Placeholders display */}
|
{/* Placeholders display (only for .docx) */}
|
||||||
{(tpl.placeholders ?? []).length > 0 && (
|
{(tpl.fileType ?? "docx") === "docx" &&
|
||||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
(tpl.placeholders ?? []).length > 0 && (
|
||||||
{(tpl.placeholders ?? []).map((p) => (
|
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||||
<span
|
{(tpl.placeholders ?? []).map((p) => (
|
||||||
key={p}
|
<span
|
||||||
className="rounded bg-muted px-1 py-0.5 font-mono text-[10px] text-muted-foreground"
|
key={p}
|
||||||
>{`{{${p}}}`}</span>
|
className="rounded bg-muted px-1 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||||
))}
|
>{`{{${p}}}`}</span>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
{tpl.fileUrl && (
|
)}
|
||||||
<a
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
href={tpl.fileUrl}
|
{tpl.fileUrl && (
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
href={tpl.fileUrl}
|
||||||
className="mt-1 inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
target="_blank"
|
||||||
>
|
rel="noopener noreferrer"
|
||||||
<ExternalLink className="h-3 w-3" /> Deschide fișier
|
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
|
||||||
</a>
|
>
|
||||||
)}
|
<ExternalLink className="h-3 w-3" /> Deschide
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{(tpl.versionHistory ?? []).length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setHistoryTemplate(tpl)}
|
||||||
|
>
|
||||||
|
<History className="h-3 w-3" />{" "}
|
||||||
|
{(tpl.versionHistory ?? []).length} versiuni
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -330,6 +417,144 @@ export function WordTemplatesModule() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Revision dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={revisingTemplate !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setRevisingTemplate(null);
|
||||||
|
setRevisionUrl("");
|
||||||
|
setRevisionNotes("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Revizie nouă — {revisingTemplate?.name}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Versiunea curentă <strong>v{revisingTemplate?.version}</strong> va
|
||||||
|
fi arhivată. Se va crea automat versiunea următoare.
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<Label>URL fișier nou (opțional)</Label>
|
||||||
|
<Input
|
||||||
|
value={revisionUrl}
|
||||||
|
onChange={(e) => setRevisionUrl(e.target.value)}
|
||||||
|
placeholder="Lasă gol pentru a păstra URL-ul actual"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Note revizie</Label>
|
||||||
|
<Textarea
|
||||||
|
value={revisionNotes}
|
||||||
|
onChange={(e) => setRevisionNotes(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Ce s-a modificat..."
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setRevisingTemplate(null);
|
||||||
|
setRevisionUrl("");
|
||||||
|
setRevisionNotes("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Anulează
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRevisionConfirm}>
|
||||||
|
<RotateCcw className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
Crează revizia
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Version history dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={historyTemplate !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setHistoryTemplate(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Istoric versiuni — {historyTemplate?.name}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Current version */}
|
||||||
|
<div className="rounded-md border p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Badge>v{historyTemplate?.version} (curentă)</Badge>
|
||||||
|
{historyTemplate?.fileUrl && (
|
||||||
|
<a
|
||||||
|
href={historyTemplate.fileUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Deschide
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Previous versions */}
|
||||||
|
{[...(historyTemplate?.versionHistory ?? [])]
|
||||||
|
.reverse()
|
||||||
|
.map((entry, idx) => (
|
||||||
|
<div key={idx} className="rounded-md border p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Badge variant="secondary">v{entry.version}</Badge>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{new Date(entry.createdAt).toLocaleDateString("ro-RO")}
|
||||||
|
</span>
|
||||||
|
{entry.fileUrl && (
|
||||||
|
<a
|
||||||
|
href={entry.fileUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Deschide
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{entry.notes && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{entry.notes}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(historyTemplate?.versionHistory ?? []).length === 0 && (
|
||||||
|
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||||
|
Nu există versiuni anterioare.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setHistoryTemplate(null)}
|
||||||
|
>
|
||||||
|
Închide
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -351,79 +576,83 @@ function TemplateForm({
|
|||||||
initial?.category ?? "contract",
|
initial?.category ?? "contract",
|
||||||
);
|
);
|
||||||
const [fileUrl, setFileUrl] = useState(initial?.fileUrl ?? "");
|
const [fileUrl, setFileUrl] = useState(initial?.fileUrl ?? "");
|
||||||
|
const [fileType, setFileType] = useState<TemplateFileType>(
|
||||||
|
initial?.fileType ?? "docx",
|
||||||
|
);
|
||||||
const [company, setCompany] = useState<CompanyId>(
|
const [company, setCompany] = useState<CompanyId>(
|
||||||
initial?.company ?? "beletage",
|
initial?.company ?? "beletage",
|
||||||
);
|
);
|
||||||
const [version, setVersion] = useState(initial?.version ?? "1.0.0");
|
const [placeholders, setPlaceholders] = useState<string[]>(
|
||||||
const [placeholdersText, setPlaceholdersText] = useState(
|
initial?.placeholders ?? [],
|
||||||
(initial?.placeholders ?? []).join(", "),
|
|
||||||
);
|
);
|
||||||
const [parsing, setParsing] = useState(false);
|
const [parsing, setParsing] = useState(false);
|
||||||
const [parseError, setParseError] = useState<string | null>(null);
|
const [parseMessage, setParseMessage] = useState<string | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [urlMode, setUrlMode] = useState<"link" | "upload">(
|
||||||
|
initial?.fileUrl ? "link" : "link",
|
||||||
|
);
|
||||||
|
|
||||||
const applyPlaceholders = (found: string[]) => {
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (found.length === 0) {
|
|
||||||
setParseError(
|
|
||||||
"Nu s-au găsit placeholder-e de forma {{VARIABILA}} în fișier.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPlaceholdersText(found.join(", "));
|
|
||||||
setParseError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileDetect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setParsing(true);
|
|
||||||
setParseError(null);
|
// Set name from filename if empty
|
||||||
try {
|
if (!name) {
|
||||||
const buffer = await file.arrayBuffer();
|
setName(file.name.replace(/\.[^.]+$/, ""));
|
||||||
const found = await parsePlaceholdersFromBuffer(buffer);
|
|
||||||
applyPlaceholders(found);
|
|
||||||
} catch (err) {
|
|
||||||
setParseError(
|
|
||||||
`Eroare la parsare: ${err instanceof Error ? err.message : String(err)}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setParsing(false);
|
|
||||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect file type from extension
|
||||||
|
const detected = detectFileType(file.name);
|
||||||
|
setFileType(detected);
|
||||||
|
|
||||||
|
// Auto-detect placeholders for .docx (silently in background)
|
||||||
|
if (detected === "docx") {
|
||||||
|
setParsing(true);
|
||||||
|
setParseMessage(null);
|
||||||
|
try {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const found = await parsePlaceholdersFromBuffer(buffer);
|
||||||
|
setPlaceholders(found);
|
||||||
|
if (found.length > 0) {
|
||||||
|
setParseMessage(`${found.length} placeholder-e detectate automat`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silent fail — user can still add manually
|
||||||
|
} finally {
|
||||||
|
setParsing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now store nothing (URL mode is "link extern" or future MinIO)
|
||||||
|
// Show message about file storage
|
||||||
|
setParseMessage(
|
||||||
|
(prev) =>
|
||||||
|
(prev ? prev + ". " : "") +
|
||||||
|
"Fișierul va fi disponibil după integrarea MinIO. Adaugă un link extern.",
|
||||||
|
);
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUrlDetect = async () => {
|
const handleUrlChange = (url: string) => {
|
||||||
if (!fileUrl) return;
|
setFileUrl(url);
|
||||||
setParsing(true);
|
const detected = detectFileType(url);
|
||||||
setParseError(null);
|
setFileType(detected);
|
||||||
try {
|
|
||||||
const found = await parsePlaceholdersFromUrl(fileUrl);
|
|
||||||
applyPlaceholders(found);
|
|
||||||
} catch (err) {
|
|
||||||
setParseError(
|
|
||||||
`Nu s-a putut accesa URL-ul (CORS sau rețea): ${err instanceof Error ? err.message : String(err)}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setParsing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const placeholders = placeholdersText
|
|
||||||
.split(",")
|
|
||||||
.map((p) => p.trim())
|
|
||||||
.filter((p) => p.length > 0);
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
category,
|
category,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
|
fileType,
|
||||||
company,
|
company,
|
||||||
version,
|
version: initial?.version ?? "1.0",
|
||||||
placeholders,
|
placeholders,
|
||||||
|
versionHistory: initial?.versionHistory ?? [],
|
||||||
clonedFrom: initial?.clonedFrom,
|
clonedFrom: initial?.clonedFrom,
|
||||||
tags: initial?.tags ?? [],
|
tags: initial?.tags ?? [],
|
||||||
visibility: initial?.visibility ?? "all",
|
visibility: initial?.visibility ?? "all",
|
||||||
@@ -469,7 +698,7 @@ function TemplateForm({
|
|||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<Label>Companie</Label>
|
<Label>Companie</Label>
|
||||||
<Select
|
<Select
|
||||||
@@ -488,88 +717,118 @@ function TemplateForm({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Versiune</Label>
|
<Label>Tip fișier</Label>
|
||||||
<Input
|
<Select
|
||||||
value={version}
|
value={fileType}
|
||||||
onChange={(e) => setVersion(e.target.value)}
|
onValueChange={(v) => setFileType(v as TemplateFileType)}
|
||||||
className="mt-1"
|
>
|
||||||
/>
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(
|
||||||
|
Object.keys(FILE_TYPE_LABELS) as TemplateFileType[]
|
||||||
|
).map((ft) => (
|
||||||
|
<SelectItem key={ft} value={ft}>
|
||||||
|
{FILE_TYPE_LABELS[ft]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<Label>URL fișier</Label>
|
|
||||||
<div className="mt-1 flex gap-1.5">
|
{/* File source — upload or link */}
|
||||||
<Input
|
<div>
|
||||||
value={fileUrl}
|
<div className="mb-2 flex items-center gap-2">
|
||||||
onChange={(e) => setFileUrl(e.target.value)}
|
<Label>Sursă fișier</Label>
|
||||||
className="flex-1"
|
<div className="flex rounded-md border">
|
||||||
placeholder="https://..."
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant={urlMode === "link" ? "secondary" : "ghost"}
|
||||||
size="icon"
|
size="sm"
|
||||||
title="Detectează placeholder-e din URL"
|
className="h-7 rounded-r-none text-xs"
|
||||||
disabled={!fileUrl || parsing}
|
onClick={() => setUrlMode("link")}
|
||||||
onClick={handleUrlDetect}
|
|
||||||
>
|
>
|
||||||
{parsing ? (
|
<Link className="mr-1 h-3 w-3" /> Link extern
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
</Button>
|
||||||
) : (
|
<Button
|
||||||
<Wand2 className="h-4 w-4" />
|
type="button"
|
||||||
)}
|
variant={urlMode === "upload" ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 rounded-l-none text-xs"
|
||||||
|
onClick={() => setUrlMode("upload")}
|
||||||
|
>
|
||||||
|
<Upload className="mr-1 h-3 w-3" /> Încarcă fișier
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{urlMode === "link" ? (
|
||||||
<div>
|
<Input
|
||||||
<div className="flex items-center justify-between">
|
value={fileUrl}
|
||||||
<Label>Placeholder-e detectate</Label>
|
onChange={(e) => handleUrlChange(e.target.value)}
|
||||||
<div className="flex items-center gap-2">
|
placeholder="https://sharepoint.com/... sau link NextCloud"
|
||||||
<span className="text-xs text-muted-foreground">
|
/>
|
||||||
sau detectează automat:
|
) : (
|
||||||
</span>
|
<div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
type="file"
|
type="file"
|
||||||
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
accept=".docx,.xlsx,.xls,.pdf,.dwg,.dxf,.tpl,.pln,.pla,.bpn"
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={handleFileDetect}
|
onChange={handleFileUpload}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
className="w-full"
|
||||||
disabled={parsing}
|
disabled={parsing}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{parsing ? (
|
{parsing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />{" "}
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />{" "}
|
||||||
Parsare...
|
Analiză fișier...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FolderOpen className="mr-1.5 h-3.5 w-3.5" /> Alege fișier
|
<FolderOpen className="mr-1.5 h-3.5 w-3.5" /> Alege fișier
|
||||||
.docx
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Selectează un fișier pentru a detecta automat tipul și
|
||||||
|
placeholder-ele (pentru .docx). Stocare pe MinIO — în curând.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<Input
|
{parseMessage && (
|
||||||
value={placeholdersText}
|
<p className="mt-1 text-xs text-blue-600">{parseMessage}</p>
|
||||||
onChange={(e) => setPlaceholdersText(e.target.value)}
|
|
||||||
className="mt-1"
|
|
||||||
placeholder="NUME_BENEFICIAR, DATA_CONTRACT, NR_PROIECT..."
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
Variabilele din șablon de forma {"{{VARIABILA}}"} — detectate automat
|
|
||||||
sau introduse manual, separate prin virgulă.
|
|
||||||
</p>
|
|
||||||
{parseError && (
|
|
||||||
<p className="mt-1 text-xs text-destructive">{parseError}</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Placeholders — only for docx */}
|
||||||
|
{fileType === "docx" && (
|
||||||
|
<div>
|
||||||
|
<Label>Placeholder-e detectate</Label>
|
||||||
|
{placeholders.length > 0 ? (
|
||||||
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
|
{placeholders.map((p) => (
|
||||||
|
<span
|
||||||
|
key={p}
|
||||||
|
className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground"
|
||||||
|
>{`{{${p}}}`}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Încarcă un fișier .docx pentru detecție automată, sau vor fi
|
||||||
|
detectate la încărcarea fișierului.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
Anulează
|
Anulează
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import type { ModuleConfig } from '@/core/module-registry/types';
|
import type { ModuleConfig } from "@/core/module-registry/types";
|
||||||
|
|
||||||
export const wordTemplatesConfig: ModuleConfig = {
|
export const wordTemplatesConfig: ModuleConfig = {
|
||||||
id: 'word-templates',
|
id: "word-templates",
|
||||||
name: 'Șabloane Word',
|
name: "Bibliotecă Șabloane",
|
||||||
description: 'Bibliotecă de șabloane Word organizate pe categorii cu suport versionare',
|
description:
|
||||||
icon: 'file-text',
|
"Bibliotecă de șabloane documente cu versionare automată (Word, Excel, Archicad, DWG, PDF)",
|
||||||
route: '/word-templates',
|
icon: "file-text",
|
||||||
category: 'generators',
|
route: "/word-templates",
|
||||||
featureFlag: 'module.word-templates',
|
category: "generators",
|
||||||
visibility: 'all',
|
featureFlag: "module.word-templates",
|
||||||
version: '0.1.0',
|
visibility: "all",
|
||||||
|
version: "0.2.0",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
storageNamespace: 'word-templates',
|
storageNamespace: "word-templates",
|
||||||
navOrder: 22,
|
navOrder: 22,
|
||||||
tags: ['word', 'șabloane', 'documente'],
|
tags: ["word", "șabloane", "documente", "excel", "archicad", "dwg"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ export interface TemplateFilters {
|
|||||||
company: string;
|
company: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Increment a semver-like version string (e.g. "1.2" → "1.3") */
|
||||||
|
function bumpVersion(ver: string): string {
|
||||||
|
const parts = ver.split(".");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const minor = parseInt(parts[parts.length - 1] ?? "0", 10);
|
||||||
|
parts[parts.length - 1] = String(isNaN(minor) ? 1 : minor + 1);
|
||||||
|
return parts.join(".");
|
||||||
|
}
|
||||||
|
return `${ver}.1`;
|
||||||
|
}
|
||||||
|
|
||||||
export function useTemplates() {
|
export function useTemplates() {
|
||||||
const storage = useStorage("word-templates");
|
const storage = useStorage("word-templates");
|
||||||
const [templates, setTemplates] = useState<WordTemplate[]>([]);
|
const [templates, setTemplates] = useState<WordTemplate[]>([]);
|
||||||
@@ -29,7 +40,11 @@ export function useTemplates() {
|
|||||||
const results: WordTemplate[] = [];
|
const results: WordTemplate[] = [];
|
||||||
for (const [key, value] of Object.entries(all)) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX) && value) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
results.push(value as WordTemplate);
|
const tpl = value as WordTemplate;
|
||||||
|
// Backfill new fields for old entries
|
||||||
|
if (!tpl.fileType) tpl.fileType = "docx";
|
||||||
|
if (!tpl.versionHistory) tpl.versionHistory = [];
|
||||||
|
results.push(tpl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
results.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
@@ -75,6 +90,36 @@ export function useTemplates() {
|
|||||||
[storage, refresh, templates],
|
[storage, refresh, templates],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Create a new revision: archives current version in history, bumps version */
|
||||||
|
const createRevision = useCallback(
|
||||||
|
async (id: string, newFileUrl: string, notes: string) => {
|
||||||
|
const existing = templates.find((t) => t.id === id);
|
||||||
|
if (!existing) return;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const history = [...(existing.versionHistory ?? [])];
|
||||||
|
// Archive current version
|
||||||
|
history.push({
|
||||||
|
version: existing.version,
|
||||||
|
fileUrl: existing.fileUrl,
|
||||||
|
notes: "",
|
||||||
|
createdAt: existing.updatedAt,
|
||||||
|
});
|
||||||
|
const updated: WordTemplate = {
|
||||||
|
...existing,
|
||||||
|
version: bumpVersion(existing.version),
|
||||||
|
fileUrl: newFileUrl || existing.fileUrl,
|
||||||
|
versionHistory: history,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
if (notes) {
|
||||||
|
updated.versionHistory[updated.versionHistory.length - 1]!.notes = notes;
|
||||||
|
}
|
||||||
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh, templates],
|
||||||
|
);
|
||||||
|
|
||||||
const cloneTemplate = useCallback(
|
const cloneTemplate = useCallback(
|
||||||
async (id: string) => {
|
async (id: string) => {
|
||||||
const existing = templates.find((t) => t.id === id);
|
const existing = templates.find((t) => t.id === id);
|
||||||
@@ -85,6 +130,7 @@ export function useTemplates() {
|
|||||||
id: uuid(),
|
id: uuid(),
|
||||||
name: `${existing.name} (copie)`,
|
name: `${existing.name} (copie)`,
|
||||||
clonedFrom: existing.id,
|
clonedFrom: existing.id,
|
||||||
|
versionHistory: [],
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
@@ -133,6 +179,7 @@ export function useTemplates() {
|
|||||||
updateFilter,
|
updateFilter,
|
||||||
addTemplate,
|
addTemplate,
|
||||||
updateTemplate,
|
updateTemplate,
|
||||||
|
createRevision,
|
||||||
cloneTemplate,
|
cloneTemplate,
|
||||||
removeTemplate,
|
removeTemplate,
|
||||||
refresh,
|
refresh,
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
export { wordTemplatesConfig } from './config';
|
export { wordTemplatesConfig } from "./config";
|
||||||
export { WordTemplatesModule } from './components/word-templates-module';
|
export { WordTemplatesModule } from "./components/word-templates-module";
|
||||||
export type { WordTemplate, TemplateCategory } from './types';
|
export type {
|
||||||
|
WordTemplate,
|
||||||
|
TemplateCategory,
|
||||||
|
TemplateFileType,
|
||||||
|
TemplateVersionEntry,
|
||||||
|
} from "./types";
|
||||||
|
|||||||
@@ -1,29 +1,49 @@
|
|||||||
import type { Visibility } from '@/core/module-registry/types';
|
import type { Visibility } from "@/core/module-registry/types";
|
||||||
import type { CompanyId } from '@/core/auth/types';
|
import type { CompanyId } from "@/core/auth/types";
|
||||||
|
|
||||||
export type TemplateCategory =
|
export type TemplateCategory =
|
||||||
| 'contract'
|
| "contract"
|
||||||
| 'memoriu'
|
| "memoriu"
|
||||||
| 'oferta'
|
| "oferta"
|
||||||
| 'raport'
|
| "raport"
|
||||||
| 'cerere'
|
| "cerere"
|
||||||
| 'aviz'
|
| "aviz"
|
||||||
| 'scrisoare'
|
| "scrisoare"
|
||||||
| 'altele';
|
| "altele";
|
||||||
|
|
||||||
|
export type TemplateFileType =
|
||||||
|
| "docx"
|
||||||
|
| "xlsx"
|
||||||
|
| "pdf"
|
||||||
|
| "dwg"
|
||||||
|
| "archicad"
|
||||||
|
| "other";
|
||||||
|
|
||||||
|
export interface TemplateVersionEntry {
|
||||||
|
version: string;
|
||||||
|
fileUrl: string;
|
||||||
|
notes: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WordTemplate {
|
export interface WordTemplate {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
category: TemplateCategory;
|
category: TemplateCategory;
|
||||||
|
/** Primary URL or link to current version */
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
|
/** Detected file type from extension */
|
||||||
|
fileType: TemplateFileType;
|
||||||
company: CompanyId;
|
company: CompanyId;
|
||||||
/** Detected placeholders in template */
|
/** Detected placeholders in template (only for .docx) */
|
||||||
placeholders: string[];
|
placeholders: string[];
|
||||||
/** Cloned from template ID */
|
/** Cloned from template ID */
|
||||||
clonedFrom?: string;
|
clonedFrom?: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
version: string;
|
version: string;
|
||||||
|
/** History of previous versions */
|
||||||
|
versionHistory: TemplateVersionEntry[];
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user