Initial commit: ArchiTools modular dashboard platform
Complete Next.js 16 application with 13 fully implemented modules: Email Signature, Word XML Generator, Registratura, Dashboard, Tag Manager, IT Inventory, Address Book, Password Vault, Mini Utilities, Prompt Generator, Digital Signatures, Word Templates, and AI Chat. Includes core platform systems (module registry, feature flags, storage abstraction, i18n, theming, auth stub, tagging), 16 technical documentation files, Docker deployment config, and legacy HTML tool reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Plus, RotateCcw, Trash2, X } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Textarea } from '@/shared/components/ui/textarea';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
import { isPresetCategory } from '../services/category-presets';
|
||||
|
||||
interface CategoryManagerProps {
|
||||
categories: Record<string, { name: string; fieldsText: string }>;
|
||||
currentCategory: string;
|
||||
onSelectCategory: (name: string) => void;
|
||||
onUpdateFields: (name: string, fieldsText: string) => void;
|
||||
onAddCategory: (name: string) => void;
|
||||
onRemoveCategory: (name: string) => void;
|
||||
onResetToPreset: (name: string) => void;
|
||||
onClearFields: (name: string) => void;
|
||||
baseNamespace: string;
|
||||
}
|
||||
|
||||
function getCategoryNamespace(baseNs: string, category: string): string {
|
||||
const safe = category.replace(/\s+/g, '_').replace(/[^A-Za-z0-9_.-]/g, '');
|
||||
return baseNs.replace(/\/+$/, '') + '/' + safe;
|
||||
}
|
||||
|
||||
function getCategoryRoot(category: string): string {
|
||||
const safe = category.replace(/\s+/g, '_').replace(/[^A-Za-z0-9_.-]/g, '');
|
||||
return safe + 'Data';
|
||||
}
|
||||
|
||||
export function CategoryManager({
|
||||
categories, currentCategory, onSelectCategory, onUpdateFields,
|
||||
onAddCategory, onRemoveCategory, onResetToPreset, onClearFields,
|
||||
baseNamespace,
|
||||
}: CategoryManagerProps) {
|
||||
const [newCatName, setNewCatName] = useState('');
|
||||
const [showNewCat, setShowNewCat] = useState(false);
|
||||
|
||||
const catNames = Object.keys(categories);
|
||||
const currentFields = categories[currentCategory]?.fieldsText ?? '';
|
||||
const ns = getCategoryNamespace(baseNamespace, currentCategory);
|
||||
const root = getCategoryRoot(currentCategory);
|
||||
|
||||
const handleAddCategory = () => {
|
||||
const name = newCatName.trim();
|
||||
if (!name || categories[name]) return;
|
||||
onAddCategory(name);
|
||||
setNewCatName('');
|
||||
setShowNewCat(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Category pills */}
|
||||
<div>
|
||||
<Label className="mb-1.5 block">Categorii de date</Label>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{catNames.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
onClick={() => onSelectCategory(cat)}
|
||||
className={cn(
|
||||
'group inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-colors',
|
||||
cat === currentCategory
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
{cat}
|
||||
{!isPresetCategory(cat) && (
|
||||
<X
|
||||
className="h-3 w-3 opacity-50 group-hover:opacity-100"
|
||||
onClick={(e) => { e.stopPropagation(); onRemoveCategory(cat); }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{showNewCat ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
value={newCatName}
|
||||
onChange={(e) => setNewCatName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddCategory()}
|
||||
placeholder="Nume categorie..."
|
||||
className="h-7 w-36 text-xs"
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={handleAddCategory}>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => { setShowNewCat(false); setNewCatName(''); }}>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewCat(true)}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-dashed px-3 py-1 text-xs text-muted-foreground hover:bg-accent"
|
||||
>
|
||||
<Plus className="h-3 w-3" /> Adaugă
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fields editor */}
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<Label>Câmpuri — {currentCategory}</Label>
|
||||
<div className="flex gap-1">
|
||||
{isPresetCategory(currentCategory) && (
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => onResetToPreset(currentCategory)}>
|
||||
<RotateCcw className="mr-1 h-3 w-3" /> Reset preset
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => onClearFields(currentCategory)}>
|
||||
<Trash2 className="mr-1 h-3 w-3" /> Curăță
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
value={currentFields}
|
||||
onChange={(e) => onUpdateFields(currentCategory, e.target.value)}
|
||||
rows={8}
|
||||
className="font-mono text-xs"
|
||||
placeholder="Un câmp pe linie (ex: NumeClient)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Namespace info */}
|
||||
<div className="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
<p><strong>Namespace:</strong> <code>{ns}</code></p>
|
||||
<p><strong>Root element:</strong> <code><{root}></code></p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import { useXmlConfig } from '../hooks/use-xml-config';
|
||||
import { XmlSettings } from './xml-settings';
|
||||
import { CategoryManager } from './category-manager';
|
||||
import { XmlPreview } from './xml-preview';
|
||||
import { Separator } from '@/shared/components/ui/separator';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
|
||||
export function WordXmlModule() {
|
||||
const {
|
||||
config, setMode, setBaseNamespace, setComputeMetrics,
|
||||
setCurrentCategory, updateCategoryFields, addCategory,
|
||||
removeCategory, resetCategoryToPreset, clearCategoryFields, resetAll,
|
||||
} = useXmlConfig();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Settings */}
|
||||
<XmlSettings
|
||||
baseNamespace={config.baseNamespace}
|
||||
mode={config.mode}
|
||||
computeMetrics={config.computeMetrics}
|
||||
onSetBaseNamespace={setBaseNamespace}
|
||||
onSetMode={setMode}
|
||||
onSetComputeMetrics={setComputeMetrics}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Category manager + field editor */}
|
||||
<CategoryManager
|
||||
categories={config.categories}
|
||||
currentCategory={config.currentCategory}
|
||||
onSelectCategory={setCurrentCategory}
|
||||
onUpdateFields={updateCategoryFields}
|
||||
onAddCategory={addCategory}
|
||||
onRemoveCategory={removeCategory}
|
||||
onResetToPreset={resetCategoryToPreset}
|
||||
onClearFields={clearCategoryFields}
|
||||
baseNamespace={config.baseNamespace}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Preview & export */}
|
||||
<XmlPreview config={config} />
|
||||
|
||||
<Separator />
|
||||
|
||||
<Button variant="outline" size="sm" onClick={resetAll}>
|
||||
<RotateCcw className="mr-1.5 h-3.5 w-3.5" /> Reset complet
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Copy, Download, FileArchive } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import type { XmlGeneratorConfig } from '../types';
|
||||
import { generateAllCategories, downloadXmlFile, downloadZipAll } from '../services/xml-generator';
|
||||
|
||||
interface XmlPreviewProps {
|
||||
config: XmlGeneratorConfig;
|
||||
}
|
||||
|
||||
export function XmlPreview({ config }: XmlPreviewProps) {
|
||||
const [copied, setCopied] = useState<'xml' | 'xpath' | null>(null);
|
||||
|
||||
const allOutputs = useMemo(
|
||||
() => generateAllCategories(config.categories, config.baseNamespace, config.mode, config.computeMetrics),
|
||||
[config.categories, config.baseNamespace, config.mode, config.computeMetrics],
|
||||
);
|
||||
|
||||
const current = allOutputs[config.currentCategory];
|
||||
const xml = current?.xml || '';
|
||||
const xpaths = current?.xpaths || '';
|
||||
|
||||
const handleCopy = async (text: string, type: 'xml' | 'xpath') => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(type);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
};
|
||||
|
||||
const safeCatName = (config.currentCategory || 'unknown')
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^A-Za-z0-9_.-]/g, '');
|
||||
|
||||
const handleDownloadCurrent = () => {
|
||||
if (!xml) return;
|
||||
downloadXmlFile(xml, `${safeCatName}Data.xml`);
|
||||
};
|
||||
|
||||
const handleDownloadZip = async () => {
|
||||
await downloadZipAll(config.categories, config.baseNamespace, config.mode, config.computeMetrics);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-lg font-semibold">Preview & Export</h2>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleDownloadCurrent} disabled={!xml}>
|
||||
<Download className="mr-1 h-4 w-4" /> XML curent
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleDownloadZip}>
|
||||
<FileArchive className="mr-1 h-4 w-4" /> ZIP toate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* XML preview */}
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">XML — {config.currentCategory}</span>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => handleCopy(xml, 'xml')} disabled={!xml}>
|
||||
<Copy className="mr-1 h-3 w-3" />
|
||||
{copied === 'xml' ? 'Copiat!' : 'Copiază'}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="max-h-80 overflow-auto rounded-lg border bg-muted/30 p-3 text-xs">
|
||||
{xml || '<!-- Niciun XML generat. Adaugă câmpuri în categoria curentă. -->'}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* XPath preview */}
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">XPaths — {config.currentCategory}</span>
|
||||
<Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => handleCopy(xpaths, 'xpath')} disabled={!xpaths}>
|
||||
<Copy className="mr-1 h-3 w-3" />
|
||||
{copied === 'xpath' ? 'Copiat!' : 'Copiază'}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className="max-h-80 overflow-auto rounded-lg border bg-muted/30 p-3 text-xs">
|
||||
{xpaths || ''}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import type { XmlGeneratorMode } from '../types';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Switch } from '@/shared/components/ui/switch';
|
||||
import { cn } from '@/shared/lib/utils';
|
||||
|
||||
interface XmlSettingsProps {
|
||||
baseNamespace: string;
|
||||
mode: XmlGeneratorMode;
|
||||
computeMetrics: boolean;
|
||||
onSetBaseNamespace: (ns: string) => void;
|
||||
onSetMode: (mode: XmlGeneratorMode) => void;
|
||||
onSetComputeMetrics: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export function XmlSettings({
|
||||
baseNamespace, mode, computeMetrics,
|
||||
onSetBaseNamespace, onSetMode, onSetComputeMetrics,
|
||||
}: XmlSettingsProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="xml-ns">Bază Namespace</Label>
|
||||
<Input
|
||||
id="xml-ns"
|
||||
value={baseNamespace}
|
||||
onChange={(e) => onSetBaseNamespace(e.target.value)}
|
||||
className="mt-1 font-mono text-xs"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Se completează automat cu <code>/Categorie</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<Label className="mb-1.5 block">Mod generare</Label>
|
||||
<div className="flex gap-1.5">
|
||||
{(['simple', 'advanced'] as XmlGeneratorMode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
type="button"
|
||||
onClick={() => onSetMode(m)}
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1 text-xs font-medium transition-colors',
|
||||
mode === m
|
||||
? 'border-primary bg-primary text-primary-foreground'
|
||||
: 'border-border hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
{m === 'simple' ? 'Simple' : 'Advanced'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{mode === 'simple' ? 'Doar câmpurile definite.' : '+ Short / Upper / Lower / Initials / First.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Switch checked={computeMetrics} onCheckedChange={onSetComputeMetrics} id="xml-metrics" />
|
||||
<Label htmlFor="xml-metrics" className="cursor-pointer text-sm">POT / CUT automat</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ModuleConfig } from '@/core/module-registry/types';
|
||||
|
||||
export const wordXmlConfig: ModuleConfig = {
|
||||
id: 'word-xml',
|
||||
name: 'Generator XML Word',
|
||||
description: 'Generator de structuri XML compatibile Word pentru documente și formulare',
|
||||
icon: 'file-code-2',
|
||||
route: '/word-xml',
|
||||
category: 'generators',
|
||||
featureFlag: 'module.word-xml',
|
||||
visibility: 'all',
|
||||
version: '0.1.0',
|
||||
dependencies: [],
|
||||
storageNamespace: 'word-xml',
|
||||
navOrder: 21,
|
||||
tags: ['xml', 'word', 'generator'],
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { XmlGeneratorConfig, XmlGeneratorMode } from '../types';
|
||||
import { DEFAULT_PRESETS } from '../services/category-presets';
|
||||
|
||||
function createDefaultConfig(): XmlGeneratorConfig {
|
||||
const categories: Record<string, { name: string; fieldsText: string }> = {};
|
||||
for (const [name, fields] of Object.entries(DEFAULT_PRESETS)) {
|
||||
categories[name] = { name, fieldsText: fields.join('\n') };
|
||||
}
|
||||
return {
|
||||
baseNamespace: 'http://schemas.beletage.ro/contract',
|
||||
mode: 'advanced',
|
||||
computeMetrics: true,
|
||||
categories,
|
||||
currentCategory: 'Beneficiar',
|
||||
};
|
||||
}
|
||||
|
||||
export function useXmlConfig() {
|
||||
const [config, setConfig] = useState<XmlGeneratorConfig>(createDefaultConfig);
|
||||
|
||||
const setMode = useCallback((mode: XmlGeneratorMode) => {
|
||||
setConfig((prev) => ({ ...prev, mode }));
|
||||
}, []);
|
||||
|
||||
const setBaseNamespace = useCallback((baseNamespace: string) => {
|
||||
setConfig((prev) => ({ ...prev, baseNamespace }));
|
||||
}, []);
|
||||
|
||||
const setComputeMetrics = useCallback((computeMetrics: boolean) => {
|
||||
setConfig((prev) => ({ ...prev, computeMetrics }));
|
||||
}, []);
|
||||
|
||||
const setCurrentCategory = useCallback((name: string) => {
|
||||
setConfig((prev) => ({ ...prev, currentCategory: name }));
|
||||
}, []);
|
||||
|
||||
const updateCategoryFields = useCallback((categoryName: string, fieldsText: string) => {
|
||||
setConfig((prev) => {
|
||||
const existing = prev.categories[categoryName];
|
||||
if (!existing) return prev;
|
||||
return {
|
||||
...prev,
|
||||
categories: {
|
||||
...prev.categories,
|
||||
[categoryName]: { name: existing.name, fieldsText },
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const addCategory = useCallback((name: string) => {
|
||||
setConfig((prev) => {
|
||||
if (prev.categories[name]) return prev;
|
||||
return {
|
||||
...prev,
|
||||
categories: { ...prev.categories, [name]: { name, fieldsText: '' } },
|
||||
currentCategory: name,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeCategory = useCallback((name: string) => {
|
||||
setConfig((prev) => {
|
||||
const next = { ...prev.categories };
|
||||
delete next[name];
|
||||
const keys = Object.keys(next);
|
||||
return {
|
||||
...prev,
|
||||
categories: next,
|
||||
currentCategory: keys.includes(prev.currentCategory) ? prev.currentCategory : keys[0] || '',
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetCategoryToPreset = useCallback((name: string) => {
|
||||
const preset = DEFAULT_PRESETS[name];
|
||||
if (!preset) return;
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
categories: {
|
||||
...prev.categories,
|
||||
[name]: { name, fieldsText: preset.join('\n') },
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const clearCategoryFields = useCallback((name: string) => {
|
||||
setConfig((prev) => {
|
||||
const existing = prev.categories[name];
|
||||
if (!existing) return prev;
|
||||
return {
|
||||
...prev,
|
||||
categories: {
|
||||
...prev.categories,
|
||||
[name]: { name: existing.name, fieldsText: '' },
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const resetAll = useCallback(() => {
|
||||
setConfig(createDefaultConfig());
|
||||
}, []);
|
||||
|
||||
const loadConfig = useCallback((loaded: XmlGeneratorConfig) => {
|
||||
setConfig(loaded);
|
||||
}, []);
|
||||
|
||||
return useMemo(() => ({
|
||||
config,
|
||||
setMode,
|
||||
setBaseNamespace,
|
||||
setComputeMetrics,
|
||||
setCurrentCategory,
|
||||
updateCategoryFields,
|
||||
addCategory,
|
||||
removeCategory,
|
||||
resetCategoryToPreset,
|
||||
clearCategoryFields,
|
||||
resetAll,
|
||||
loadConfig,
|
||||
}), [config, setMode, setBaseNamespace, setComputeMetrics, setCurrentCategory,
|
||||
updateCategoryFields, addCategory, removeCategory, resetCategoryToPreset,
|
||||
clearCategoryFields, resetAll, loadConfig]);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { wordXmlConfig } from './config';
|
||||
export { WordXmlModule } from './components/word-xml-module';
|
||||
export type { XmlGeneratorConfig, XmlGeneratorMode, XmlCategory, GeneratedOutput } from './types';
|
||||
@@ -0,0 +1,36 @@
|
||||
export const DEFAULT_PRESETS: Record<string, string[]> = {
|
||||
Beneficiar: [
|
||||
'NumeClient',
|
||||
'Adresa',
|
||||
'CUI',
|
||||
'CNP',
|
||||
'Reprezentant',
|
||||
'Email',
|
||||
'Telefon',
|
||||
],
|
||||
Proiect: [
|
||||
'TitluProiect',
|
||||
'AdresaImobil',
|
||||
'NrCadastral',
|
||||
'NrCF',
|
||||
'Localitate',
|
||||
'Judet',
|
||||
],
|
||||
Suprafete: [
|
||||
'SuprafataTeren',
|
||||
'SuprafataConstruitaLaSol',
|
||||
'SuprafataDesfasurata',
|
||||
'SuprafataUtila',
|
||||
],
|
||||
Meta: [
|
||||
'NrContract',
|
||||
'DataContract',
|
||||
'Responsabil',
|
||||
'VersiuneDocument',
|
||||
'DataGenerarii',
|
||||
],
|
||||
};
|
||||
|
||||
export function isPresetCategory(name: string): boolean {
|
||||
return name in DEFAULT_PRESETS;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import type { XmlGeneratorMode, XmlCategory, GeneratedOutput } from '../types';
|
||||
|
||||
function sanitizeName(name: string): string | null {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return null;
|
||||
let n = trimmed.replace(/\s+/g, '_').replace(/[^A-Za-z0-9_.-]/g, '');
|
||||
if (!/^[A-Za-z_]/.test(n)) n = '_' + n;
|
||||
return n || null;
|
||||
}
|
||||
|
||||
function getCategoryNamespace(baseNs: string, category: string): string {
|
||||
const safeCat = sanitizeName(category) || category;
|
||||
return baseNs.replace(/\/+$/, '') + '/' + safeCat;
|
||||
}
|
||||
|
||||
function getCategoryRoot(category: string): string {
|
||||
const safeCat = sanitizeName(category) || category;
|
||||
return safeCat + 'Data';
|
||||
}
|
||||
|
||||
interface FieldEntry {
|
||||
label: string;
|
||||
baseName: string;
|
||||
variants: string[];
|
||||
}
|
||||
|
||||
export function generateCategoryXml(
|
||||
category: string,
|
||||
catData: XmlCategory,
|
||||
baseNamespace: string,
|
||||
mode: XmlGeneratorMode,
|
||||
computeMetrics: boolean,
|
||||
): GeneratedOutput {
|
||||
const raw = catData.fieldsText
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0);
|
||||
|
||||
if (raw.length === 0) return { xml: '', xpaths: '' };
|
||||
|
||||
const ns = getCategoryNamespace(baseNamespace, category);
|
||||
const root = getCategoryRoot(category);
|
||||
|
||||
const usedNames = new Set<string>();
|
||||
const fields: FieldEntry[] = [];
|
||||
|
||||
for (const label of raw) {
|
||||
const base = sanitizeName(label);
|
||||
if (!base) continue;
|
||||
|
||||
let baseName = base;
|
||||
let idx = 2;
|
||||
while (usedNames.has(baseName)) {
|
||||
baseName = base + '_' + idx;
|
||||
idx++;
|
||||
}
|
||||
usedNames.add(baseName);
|
||||
|
||||
const variants = [baseName];
|
||||
if (mode === 'advanced') {
|
||||
const suffixes = ['Short', 'Upper', 'Lower', 'Initials', 'First'];
|
||||
for (const suffix of suffixes) {
|
||||
let vn = baseName + suffix;
|
||||
let k = 2;
|
||||
while (usedNames.has(vn)) {
|
||||
vn = baseName + suffix + '_' + k;
|
||||
k++;
|
||||
}
|
||||
usedNames.add(vn);
|
||||
variants.push(vn);
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({ label, baseName, variants });
|
||||
}
|
||||
|
||||
// Auto-add POT/CUT for Suprafete category
|
||||
const extraMetricFields: FieldEntry[] = [];
|
||||
if (computeMetrics && category.toLowerCase().includes('suprafete')) {
|
||||
const hasTeren = fields.some((f) => f.baseName.toLowerCase().includes('suprafatateren'));
|
||||
const hasLaSol = fields.some((f) => f.baseName.toLowerCase().includes('suprafataconstruitalasol'));
|
||||
const hasDesf = fields.some((f) => f.baseName.toLowerCase().includes('suprafatadesfasurata'));
|
||||
|
||||
if (hasTeren && hasLaSol && !usedNames.has('POT')) {
|
||||
usedNames.add('POT');
|
||||
extraMetricFields.push({ label: 'Procent Ocupare Teren', baseName: 'POT', variants: ['POT'] });
|
||||
}
|
||||
if (hasTeren && hasDesf && !usedNames.has('CUT')) {
|
||||
usedNames.add('CUT');
|
||||
extraMetricFields.push({ label: 'Coeficient Utilizare Teren', baseName: 'CUT', variants: ['CUT'] });
|
||||
}
|
||||
}
|
||||
|
||||
const allFields = fields.concat(extraMetricFields);
|
||||
|
||||
// Build XML
|
||||
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
xml += `<${root} xmlns="${ns}">\n`;
|
||||
for (const f of allFields) {
|
||||
for (const v of f.variants) {
|
||||
xml += ` <${v}></${v}>\n`;
|
||||
}
|
||||
}
|
||||
xml += `</${root}>\n`;
|
||||
|
||||
// Build XPaths
|
||||
let xp = `Categorie: ${category}\nNamespace: ${ns}\nRoot: /${root}\n\n`;
|
||||
for (const f of fields) {
|
||||
xp += `# ${f.label}\n`;
|
||||
for (const v of f.variants) {
|
||||
xp += `/${root}/${v}\n`;
|
||||
}
|
||||
xp += '\n';
|
||||
}
|
||||
if (extraMetricFields.length > 0) {
|
||||
xp += '# Metrici auto (POT / CUT)\n';
|
||||
for (const f of extraMetricFields) {
|
||||
for (const v of f.variants) {
|
||||
xp += `/${root}/${v}\n`;
|
||||
}
|
||||
}
|
||||
xp += '\n';
|
||||
}
|
||||
|
||||
return { xml, xpaths: xp };
|
||||
}
|
||||
|
||||
export function generateAllCategories(
|
||||
categories: Record<string, XmlCategory>,
|
||||
baseNamespace: string,
|
||||
mode: XmlGeneratorMode,
|
||||
computeMetrics: boolean,
|
||||
): Record<string, GeneratedOutput> {
|
||||
const results: Record<string, GeneratedOutput> = {};
|
||||
for (const cat of Object.keys(categories)) {
|
||||
const catData = categories[cat];
|
||||
if (!catData) continue;
|
||||
results[cat] = generateCategoryXml(cat, catData, baseNamespace, mode, computeMetrics);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function downloadXmlFile(xml: string, filename: string): void {
|
||||
const blob = new Blob([xml], { type: 'application/xml' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
export async function downloadZipAll(
|
||||
categories: Record<string, XmlCategory>,
|
||||
baseNamespace: string,
|
||||
mode: XmlGeneratorMode,
|
||||
computeMetrics: boolean,
|
||||
): Promise<void> {
|
||||
const JSZip = (await import('jszip')).default;
|
||||
const results = generateAllCategories(categories, baseNamespace, mode, computeMetrics);
|
||||
|
||||
const zip = new JSZip();
|
||||
const folder = zip.folder('customXmlParts')!;
|
||||
|
||||
let hasAny = false;
|
||||
for (const cat of Object.keys(results)) {
|
||||
const output = results[cat];
|
||||
if (!output?.xml) continue;
|
||||
const { xml } = output;
|
||||
hasAny = true;
|
||||
const safeCat = sanitizeName(cat) || cat;
|
||||
folder.file(`${safeCat}Data.xml`, xml);
|
||||
}
|
||||
|
||||
if (!hasAny) return;
|
||||
|
||||
const content = await zip.generateAsync({ type: 'blob' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(content);
|
||||
a.download = 'beletage_custom_xml_parts.zip';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
export type XmlGeneratorMode = 'simple' | 'advanced';
|
||||
|
||||
export interface XmlCategory {
|
||||
name: string;
|
||||
fieldsText: string;
|
||||
}
|
||||
|
||||
export interface XmlGeneratorConfig {
|
||||
baseNamespace: string;
|
||||
mode: XmlGeneratorMode;
|
||||
computeMetrics: boolean;
|
||||
categories: Record<string, XmlCategory>;
|
||||
currentCategory: string;
|
||||
}
|
||||
|
||||
export interface GeneratedOutput {
|
||||
xml: string;
|
||||
xpaths: string;
|
||||
}
|
||||
|
||||
export interface SavedXmlConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
config: XmlGeneratorConfig;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user