feat(word-xml): remove POT/CUT auto-calculation toggle

This commit is contained in:
AI Assistant
2026-02-19 07:12:21 +02:00
parent cd4b0de1e9
commit eaca24aa58
6 changed files with 171 additions and 158 deletions

View File

@@ -1,18 +1,25 @@
'use client'; "use client";
import { useXmlConfig } from '../hooks/use-xml-config'; import { useXmlConfig } from "../hooks/use-xml-config";
import { XmlSettings } from './xml-settings'; import { XmlSettings } from "./xml-settings";
import { CategoryManager } from './category-manager'; import { CategoryManager } from "./category-manager";
import { XmlPreview } from './xml-preview'; import { XmlPreview } from "./xml-preview";
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";
export function WordXmlModule() { export function WordXmlModule() {
const { const {
config, setMode, setBaseNamespace, setComputeMetrics, config,
setCurrentCategory, updateCategoryFields, addCategory, setMode,
removeCategory, resetCategoryToPreset, clearCategoryFields, resetAll, setBaseNamespace,
setCurrentCategory,
updateCategoryFields,
addCategory,
removeCategory,
resetCategoryToPreset,
clearCategoryFields,
resetAll,
} = useXmlConfig(); } = useXmlConfig();
return ( return (
@@ -21,10 +28,8 @@ export function WordXmlModule() {
<XmlSettings <XmlSettings
baseNamespace={config.baseNamespace} baseNamespace={config.baseNamespace}
mode={config.mode} mode={config.mode}
computeMetrics={config.computeMetrics}
onSetBaseNamespace={setBaseNamespace} onSetBaseNamespace={setBaseNamespace}
onSetMode={setMode} onSetMode={setMode}
onSetComputeMetrics={setComputeMetrics}
/> />
<Separator /> <Separator />

View File

@@ -1,28 +1,37 @@
'use client'; "use client";
import { useMemo, useState } from 'react'; import { useMemo, useState } from "react";
import { Copy, Download, FileArchive } from 'lucide-react'; import { Copy, Download, FileArchive } from "lucide-react";
import { Button } from '@/shared/components/ui/button'; import { Button } from "@/shared/components/ui/button";
import type { XmlGeneratorConfig } from '../types'; import type { XmlGeneratorConfig } from "../types";
import { generateAllCategories, downloadXmlFile, downloadZipAll } from '../services/xml-generator'; import {
generateAllCategories,
downloadXmlFile,
downloadZipAll,
} from "../services/xml-generator";
interface XmlPreviewProps { interface XmlPreviewProps {
config: XmlGeneratorConfig; config: XmlGeneratorConfig;
} }
export function XmlPreview({ config }: XmlPreviewProps) { export function XmlPreview({ config }: XmlPreviewProps) {
const [copied, setCopied] = useState<'xml' | 'xpath' | null>(null); const [copied, setCopied] = useState<"xml" | "xpath" | null>(null);
const allOutputs = useMemo( const allOutputs = useMemo(
() => generateAllCategories(config.categories, config.baseNamespace, config.mode, config.computeMetrics), () =>
[config.categories, config.baseNamespace, config.mode, config.computeMetrics], generateAllCategories(
config.categories,
config.baseNamespace,
config.mode,
),
[config.categories, config.baseNamespace, config.mode],
); );
const current = allOutputs[config.currentCategory]; const current = allOutputs[config.currentCategory];
const xml = current?.xml || ''; const xml = current?.xml || "";
const xpaths = current?.xpaths || ''; const xpaths = current?.xpaths || "";
const handleCopy = async (text: string, type: 'xml' | 'xpath') => { const handleCopy = async (text: string, type: "xml" | "xpath") => {
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
setCopied(type); setCopied(type);
@@ -32,9 +41,9 @@ export function XmlPreview({ config }: XmlPreviewProps) {
} }
}; };
const safeCatName = (config.currentCategory || 'unknown') const safeCatName = (config.currentCategory || "unknown")
.replace(/\s+/g, '_') .replace(/\s+/g, "_")
.replace(/[^A-Za-z0-9_.-]/g, ''); .replace(/[^A-Za-z0-9_.-]/g, "");
const handleDownloadCurrent = () => { const handleDownloadCurrent = () => {
if (!xml) return; if (!xml) return;
@@ -42,7 +51,7 @@ export function XmlPreview({ config }: XmlPreviewProps) {
}; };
const handleDownloadZip = async () => { const handleDownloadZip = async () => {
await downloadZipAll(config.categories, config.baseNamespace, config.mode, config.computeMetrics); await downloadZipAll(config.categories, config.baseNamespace, config.mode);
}; };
return ( return (
@@ -50,7 +59,12 @@ export function XmlPreview({ config }: XmlPreviewProps) {
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold">Preview & Export</h2> <h2 className="text-lg font-semibold">Preview & Export</h2>
<div className="ml-auto flex gap-2"> <div className="ml-auto flex gap-2">
<Button variant="outline" size="sm" onClick={handleDownloadCurrent} disabled={!xml}> <Button
variant="outline"
size="sm"
onClick={handleDownloadCurrent}
disabled={!xml}
>
<Download className="mr-1 h-4 w-4" /> XML curent <Download className="mr-1 h-4 w-4" /> XML curent
</Button> </Button>
<Button size="sm" onClick={handleDownloadZip}> <Button size="sm" onClick={handleDownloadZip}>
@@ -63,28 +77,45 @@ export function XmlPreview({ config }: XmlPreviewProps) {
{/* XML preview */} {/* XML preview */}
<div> <div>
<div className="mb-1.5 flex items-center justify-between"> <div className="mb-1.5 flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">XML {config.currentCategory}</span> <span className="text-xs font-medium text-muted-foreground">
<Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => handleCopy(xml, 'xml')} disabled={!xml}> 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" /> <Copy className="mr-1 h-3 w-3" />
{copied === 'xml' ? 'Copiat!' : 'Copiază'} {copied === "xml" ? "Copiat!" : "Copiază"}
</Button> </Button>
</div> </div>
<pre className="max-h-80 overflow-auto rounded-lg border bg-muted/30 p-3 text-xs"> <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ă. -->'} {xml ||
"<!-- Niciun XML generat. Adaugă câmpuri în categoria curentă. -->"}
</pre> </pre>
</div> </div>
{/* XPath preview */} {/* XPath preview */}
<div> <div>
<div className="mb-1.5 flex items-center justify-between"> <div className="mb-1.5 flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground">XPaths {config.currentCategory}</span> <span className="text-xs font-medium text-muted-foreground">
<Button variant="ghost" size="sm" className="h-6 text-xs" onClick={() => handleCopy(xpaths, 'xpath')} disabled={!xpaths}> 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" /> <Copy className="mr-1 h-3 w-3" />
{copied === 'xpath' ? 'Copiat!' : 'Copiază'} {copied === "xpath" ? "Copiat!" : "Copiază"}
</Button> </Button>
</div> </div>
<pre className="max-h-80 overflow-auto rounded-lg border bg-muted/30 p-3 text-xs"> <pre className="max-h-80 overflow-auto rounded-lg border bg-muted/30 p-3 text-xs">
{xpaths || ''} {xpaths || ""}
</pre> </pre>
</div> </div>
</div> </div>

View File

@@ -1,23 +1,22 @@
'use client'; "use client";
import type { XmlGeneratorMode } from '../types'; import type { XmlGeneratorMode } from "../types";
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 { Switch } from '@/shared/components/ui/switch'; import { cn } from "@/shared/lib/utils";
import { cn } from '@/shared/lib/utils';
interface XmlSettingsProps { interface XmlSettingsProps {
baseNamespace: string; baseNamespace: string;
mode: XmlGeneratorMode; mode: XmlGeneratorMode;
computeMetrics: boolean;
onSetBaseNamespace: (ns: string) => void; onSetBaseNamespace: (ns: string) => void;
onSetMode: (mode: XmlGeneratorMode) => void; onSetMode: (mode: XmlGeneratorMode) => void;
onSetComputeMetrics: (v: boolean) => void;
} }
export function XmlSettings({ export function XmlSettings({
baseNamespace, mode, computeMetrics, baseNamespace,
onSetBaseNamespace, onSetMode, onSetComputeMetrics, mode,
onSetBaseNamespace,
onSetMode,
}: XmlSettingsProps) { }: XmlSettingsProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -38,31 +37,28 @@ export function XmlSettings({
<div> <div>
<Label className="mb-1.5 block">Mod generare</Label> <Label className="mb-1.5 block">Mod generare</Label>
<div className="flex gap-1.5"> <div className="flex gap-1.5">
{(['simple', 'advanced'] as XmlGeneratorMode[]).map((m) => ( {(["simple", "advanced"] as XmlGeneratorMode[]).map((m) => (
<button <button
key={m} key={m}
type="button" type="button"
onClick={() => onSetMode(m)} onClick={() => onSetMode(m)}
className={cn( className={cn(
'rounded-full border px-3 py-1 text-xs font-medium transition-colors', "rounded-full border px-3 py-1 text-xs font-medium transition-colors",
mode === m mode === m
? 'border-primary bg-primary text-primary-foreground' ? "border-primary bg-primary text-primary-foreground"
: 'border-border hover:bg-accent' : "border-border hover:bg-accent",
)} )}
> >
{m === 'simple' ? 'Simple' : 'Advanced'} {m === "simple" ? "Simple" : "Advanced"}
</button> </button>
))} ))}
</div> </div>
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
{mode === 'simple' ? 'Doar câmpurile definite.' : '+ Short / Upper / Lower / Initials / First.'} {mode === "simple"
? "Doar câmpurile definite."
: "+ Short / Upper / Lower / Initials / First."}
</p> </p>
</div> </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>
</div> </div>
); );

View File

@@ -1,20 +1,19 @@
'use client'; "use client";
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from "react";
import type { XmlGeneratorConfig, XmlGeneratorMode } from '../types'; import type { XmlGeneratorConfig, XmlGeneratorMode } from "../types";
import { DEFAULT_PRESETS } from '../services/category-presets'; import { DEFAULT_PRESETS } from "../services/category-presets";
function createDefaultConfig(): XmlGeneratorConfig { function createDefaultConfig(): XmlGeneratorConfig {
const categories: Record<string, { name: string; fieldsText: string }> = {}; const categories: Record<string, { name: string; fieldsText: string }> = {};
for (const [name, fields] of Object.entries(DEFAULT_PRESETS)) { for (const [name, fields] of Object.entries(DEFAULT_PRESETS)) {
categories[name] = { name, fieldsText: fields.join('\n') }; categories[name] = { name, fieldsText: fields.join("\n") };
} }
return { return {
baseNamespace: 'http://schemas.beletage.ro/contract', baseNamespace: "http://schemas.beletage.ro/contract",
mode: 'advanced', mode: "advanced",
computeMetrics: true,
categories, categories,
currentCategory: 'Beneficiar', currentCategory: "Beneficiar",
}; };
} }
@@ -29,15 +28,12 @@ export function useXmlConfig() {
setConfig((prev) => ({ ...prev, baseNamespace })); setConfig((prev) => ({ ...prev, baseNamespace }));
}, []); }, []);
const setComputeMetrics = useCallback((computeMetrics: boolean) => {
setConfig((prev) => ({ ...prev, computeMetrics }));
}, []);
const setCurrentCategory = useCallback((name: string) => { const setCurrentCategory = useCallback((name: string) => {
setConfig((prev) => ({ ...prev, currentCategory: name })); setConfig((prev) => ({ ...prev, currentCategory: name }));
}, []); }, []);
const updateCategoryFields = useCallback((categoryName: string, fieldsText: string) => { const updateCategoryFields = useCallback(
(categoryName: string, fieldsText: string) => {
setConfig((prev) => { setConfig((prev) => {
const existing = prev.categories[categoryName]; const existing = prev.categories[categoryName];
if (!existing) return prev; if (!existing) return prev;
@@ -49,14 +45,16 @@ export function useXmlConfig() {
}, },
}; };
}); });
}, []); },
[],
);
const addCategory = useCallback((name: string) => { const addCategory = useCallback((name: string) => {
setConfig((prev) => { setConfig((prev) => {
if (prev.categories[name]) return prev; if (prev.categories[name]) return prev;
return { return {
...prev, ...prev,
categories: { ...prev.categories, [name]: { name, fieldsText: '' } }, categories: { ...prev.categories, [name]: { name, fieldsText: "" } },
currentCategory: name, currentCategory: name,
}; };
}); });
@@ -70,7 +68,9 @@ export function useXmlConfig() {
return { return {
...prev, ...prev,
categories: next, categories: next,
currentCategory: keys.includes(prev.currentCategory) ? prev.currentCategory : keys[0] || '', currentCategory: keys.includes(prev.currentCategory)
? prev.currentCategory
: keys[0] || "",
}; };
}); });
}, []); }, []);
@@ -82,7 +82,7 @@ export function useXmlConfig() {
...prev, ...prev,
categories: { categories: {
...prev.categories, ...prev.categories,
[name]: { name, fieldsText: preset.join('\n') }, [name]: { name, fieldsText: preset.join("\n") },
}, },
})); }));
}, []); }, []);
@@ -95,7 +95,7 @@ export function useXmlConfig() {
...prev, ...prev,
categories: { categories: {
...prev.categories, ...prev.categories,
[name]: { name: existing.name, fieldsText: '' }, [name]: { name: existing.name, fieldsText: "" },
}, },
}; };
}); });
@@ -109,11 +109,11 @@ export function useXmlConfig() {
setConfig(loaded); setConfig(loaded);
}, []); }, []);
return useMemo(() => ({ return useMemo(
() => ({
config, config,
setMode, setMode,
setBaseNamespace, setBaseNamespace,
setComputeMetrics,
setCurrentCategory, setCurrentCategory,
updateCategoryFields, updateCategoryFields,
addCategory, addCategory,
@@ -122,7 +122,19 @@ export function useXmlConfig() {
clearCategoryFields, clearCategoryFields,
resetAll, resetAll,
loadConfig, loadConfig,
}), [config, setMode, setBaseNamespace, setComputeMetrics, setCurrentCategory, }),
updateCategoryFields, addCategory, removeCategory, resetCategoryToPreset, [
clearCategoryFields, resetAll, loadConfig]); config,
setMode,
setBaseNamespace,
setCurrentCategory,
updateCategoryFields,
addCategory,
removeCategory,
resetCategoryToPreset,
clearCategoryFields,
resetAll,
loadConfig,
],
);
} }

View File

@@ -1,21 +1,21 @@
import type { XmlGeneratorMode, XmlCategory, GeneratedOutput } from '../types'; import type { XmlGeneratorMode, XmlCategory, GeneratedOutput } from "../types";
function sanitizeName(name: string): string | null { function sanitizeName(name: string): string | null {
const trimmed = name.trim(); const trimmed = name.trim();
if (!trimmed) return null; if (!trimmed) return null;
let n = trimmed.replace(/\s+/g, '_').replace(/[^A-Za-z0-9_.-]/g, ''); let n = trimmed.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_.-]/g, "");
if (!/^[A-Za-z_]/.test(n)) n = '_' + n; if (!/^[A-Za-z_]/.test(n)) n = "_" + n;
return n || null; return n || null;
} }
function getCategoryNamespace(baseNs: string, category: string): string { function getCategoryNamespace(baseNs: string, category: string): string {
const safeCat = sanitizeName(category) || category; const safeCat = sanitizeName(category) || category;
return baseNs.replace(/\/+$/, '') + '/' + safeCat; return baseNs.replace(/\/+$/, "") + "/" + safeCat;
} }
function getCategoryRoot(category: string): string { function getCategoryRoot(category: string): string {
const safeCat = sanitizeName(category) || category; const safeCat = sanitizeName(category) || category;
return safeCat + 'Data'; return safeCat + "Data";
} }
interface FieldEntry { interface FieldEntry {
@@ -29,14 +29,13 @@ export function generateCategoryXml(
catData: XmlCategory, catData: XmlCategory,
baseNamespace: string, baseNamespace: string,
mode: XmlGeneratorMode, mode: XmlGeneratorMode,
computeMetrics: boolean,
): GeneratedOutput { ): GeneratedOutput {
const raw = catData.fieldsText const raw = catData.fieldsText
.split(/\r?\n/) .split(/\r?\n/)
.map((l) => l.trim()) .map((l) => l.trim())
.filter((l) => l.length > 0); .filter((l) => l.length > 0);
if (raw.length === 0) return { xml: '', xpaths: '' }; if (raw.length === 0) return { xml: "", xpaths: "" };
const ns = getCategoryNamespace(baseNamespace, category); const ns = getCategoryNamespace(baseNamespace, category);
const root = getCategoryRoot(category); const root = getCategoryRoot(category);
@@ -51,19 +50,19 @@ export function generateCategoryXml(
let baseName = base; let baseName = base;
let idx = 2; let idx = 2;
while (usedNames.has(baseName)) { while (usedNames.has(baseName)) {
baseName = base + '_' + idx; baseName = base + "_" + idx;
idx++; idx++;
} }
usedNames.add(baseName); usedNames.add(baseName);
const variants = [baseName]; const variants = [baseName];
if (mode === 'advanced') { if (mode === "advanced") {
const suffixes = ['Short', 'Upper', 'Lower', 'Initials', 'First']; const suffixes = ["Short", "Upper", "Lower", "Initials", "First"];
for (const suffix of suffixes) { for (const suffix of suffixes) {
let vn = baseName + suffix; let vn = baseName + suffix;
let k = 2; let k = 2;
while (usedNames.has(vn)) { while (usedNames.has(vn)) {
vn = baseName + suffix + '_' + k; vn = baseName + suffix + "_" + k;
k++; k++;
} }
usedNames.add(vn); usedNames.add(vn);
@@ -74,24 +73,7 @@ export function generateCategoryXml(
fields.push({ label, baseName, variants }); fields.push({ label, baseName, variants });
} }
// Auto-add POT/CUT for Suprafete category const allFields = fields;
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 // Build XML
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n'; let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
@@ -110,18 +92,8 @@ export function generateCategoryXml(
for (const v of f.variants) { for (const v of f.variants) {
xp += `/${root}/${v}\n`; xp += `/${root}/${v}\n`;
} }
xp += '\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 }; return { xml, xpaths: xp };
} }
@@ -129,20 +101,19 @@ export function generateAllCategories(
categories: Record<string, XmlCategory>, categories: Record<string, XmlCategory>,
baseNamespace: string, baseNamespace: string,
mode: XmlGeneratorMode, mode: XmlGeneratorMode,
computeMetrics: boolean,
): Record<string, GeneratedOutput> { ): Record<string, GeneratedOutput> {
const results: Record<string, GeneratedOutput> = {}; const results: Record<string, GeneratedOutput> = {};
for (const cat of Object.keys(categories)) { for (const cat of Object.keys(categories)) {
const catData = categories[cat]; const catData = categories[cat];
if (!catData) continue; if (!catData) continue;
results[cat] = generateCategoryXml(cat, catData, baseNamespace, mode, computeMetrics); results[cat] = generateCategoryXml(cat, catData, baseNamespace, mode);
} }
return results; return results;
} }
export function downloadXmlFile(xml: string, filename: string): void { export function downloadXmlFile(xml: string, filename: string): void {
const blob = new Blob([xml], { type: 'application/xml' }); const blob = new Blob([xml], { type: "application/xml" });
const a = document.createElement('a'); const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.href = URL.createObjectURL(blob);
a.download = filename; a.download = filename;
document.body.appendChild(a); document.body.appendChild(a);
@@ -155,13 +126,12 @@ export async function downloadZipAll(
categories: Record<string, XmlCategory>, categories: Record<string, XmlCategory>,
baseNamespace: string, baseNamespace: string,
mode: XmlGeneratorMode, mode: XmlGeneratorMode,
computeMetrics: boolean,
): Promise<void> { ): Promise<void> {
const JSZip = (await import('jszip')).default; const JSZip = (await import("jszip")).default;
const results = generateAllCategories(categories, baseNamespace, mode, computeMetrics); const results = generateAllCategories(categories, baseNamespace, mode);
const zip = new JSZip(); const zip = new JSZip();
const folder = zip.folder('customXmlParts')!; const folder = zip.folder("customXmlParts")!;
let hasAny = false; let hasAny = false;
for (const cat of Object.keys(results)) { for (const cat of Object.keys(results)) {
@@ -175,10 +145,10 @@ export async function downloadZipAll(
if (!hasAny) return; if (!hasAny) return;
const content = await zip.generateAsync({ type: 'blob' }); const content = await zip.generateAsync({ type: "blob" });
const a = document.createElement('a'); const a = document.createElement("a");
a.href = URL.createObjectURL(content); a.href = URL.createObjectURL(content);
a.download = 'beletage_custom_xml_parts.zip'; a.download = "beletage_custom_xml_parts.zip";
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);

View File

@@ -1,4 +1,4 @@
export type XmlGeneratorMode = 'simple' | 'advanced'; export type XmlGeneratorMode = "simple" | "advanced";
export interface XmlCategory { export interface XmlCategory {
name: string; name: string;
@@ -8,7 +8,6 @@ export interface XmlCategory {
export interface XmlGeneratorConfig { export interface XmlGeneratorConfig {
baseNamespace: string; baseNamespace: string;
mode: XmlGeneratorMode; mode: XmlGeneratorMode;
computeMetrics: boolean;
categories: Record<string, XmlCategory>; categories: Record<string, XmlCategory>;
currentCategory: string; currentCategory: string;
} }