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 { 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';
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,
config,
setMode,
setBaseNamespace,
setCurrentCategory,
updateCategoryFields,
addCategory,
removeCategory,
resetCategoryToPreset,
clearCategoryFields,
resetAll,
} = useXmlConfig();
return (
@@ -21,10 +28,8 @@ export function WordXmlModule() {
<XmlSettings
baseNamespace={config.baseNamespace}
mode={config.mode}
computeMetrics={config.computeMetrics}
onSetBaseNamespace={setBaseNamespace}
onSetMode={setMode}
onSetComputeMetrics={setComputeMetrics}
/>
<Separator />

View File

@@ -1,28 +1,37 @@
'use client';
"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';
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 [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],
() =>
generateAllCategories(
config.categories,
config.baseNamespace,
config.mode,
),
[config.categories, config.baseNamespace, config.mode],
);
const current = allOutputs[config.currentCategory];
const xml = current?.xml || '';
const xpaths = current?.xpaths || '';
const xml = current?.xml || "";
const xpaths = current?.xpaths || "";
const handleCopy = async (text: string, type: 'xml' | 'xpath') => {
const handleCopy = async (text: string, type: "xml" | "xpath") => {
try {
await navigator.clipboard.writeText(text);
setCopied(type);
@@ -32,9 +41,9 @@ export function XmlPreview({ config }: XmlPreviewProps) {
}
};
const safeCatName = (config.currentCategory || 'unknown')
.replace(/\s+/g, '_')
.replace(/[^A-Za-z0-9_.-]/g, '');
const safeCatName = (config.currentCategory || "unknown")
.replace(/\s+/g, "_")
.replace(/[^A-Za-z0-9_.-]/g, "");
const handleDownloadCurrent = () => {
if (!xml) return;
@@ -42,7 +51,7 @@ export function XmlPreview({ config }: XmlPreviewProps) {
};
const handleDownloadZip = async () => {
await downloadZipAll(config.categories, config.baseNamespace, config.mode, config.computeMetrics);
await downloadZipAll(config.categories, config.baseNamespace, config.mode);
};
return (
@@ -50,7 +59,12 @@ export function XmlPreview({ config }: XmlPreviewProps) {
<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}>
<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}>
@@ -63,28 +77,45 @@ export function XmlPreview({ config }: XmlPreviewProps) {
{/* 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}>
<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ă'}
{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ă. -->'}
{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}>
<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ă'}
{copied === "xpath" ? "Copiat!" : "Copiază"}
</Button>
</div>
<pre className="max-h-80 overflow-auto rounded-lg border bg-muted/30 p-3 text-xs">
{xpaths || ''}
{xpaths || ""}
</pre>
</div>
</div>

View File

@@ -1,23 +1,22 @@
'use client';
"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';
import type { XmlGeneratorMode } from "../types";
import { Input } from "@/shared/components/ui/input";
import { Label } from "@/shared/components/ui/label";
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,
baseNamespace,
mode,
onSetBaseNamespace,
onSetMode,
}: XmlSettingsProps) {
return (
<div className="space-y-4">
@@ -38,31 +37,28 @@ export function XmlSettings({
<div>
<Label className="mb-1.5 block">Mod generare</Label>
<div className="flex gap-1.5">
{(['simple', 'advanced'] as XmlGeneratorMode[]).map((m) => (
{(["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',
"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'
? "border-primary bg-primary text-primary-foreground"
: "border-border hover:bg-accent",
)}
>
{m === 'simple' ? 'Simple' : 'Advanced'}
{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.'}
{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>
);

View File

@@ -1,20 +1,19 @@
'use client';
"use client";
import { useState, useCallback, useMemo } from 'react';
import type { XmlGeneratorConfig, XmlGeneratorMode } from '../types';
import { DEFAULT_PRESETS } from '../services/category-presets';
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') };
categories[name] = { name, fieldsText: fields.join("\n") };
}
return {
baseNamespace: 'http://schemas.beletage.ro/contract',
mode: 'advanced',
computeMetrics: true,
baseNamespace: "http://schemas.beletage.ro/contract",
mode: "advanced",
categories,
currentCategory: 'Beneficiar',
currentCategory: "Beneficiar",
};
}
@@ -29,34 +28,33 @@ export function useXmlConfig() {
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 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: '' } },
categories: { ...prev.categories, [name]: { name, fieldsText: "" } },
currentCategory: name,
};
});
@@ -70,7 +68,9 @@ export function useXmlConfig() {
return {
...prev,
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,
categories: {
...prev.categories,
[name]: { name, fieldsText: preset.join('\n') },
[name]: { name, fieldsText: preset.join("\n") },
},
}));
}, []);
@@ -95,7 +95,7 @@ export function useXmlConfig() {
...prev,
categories: {
...prev.categories,
[name]: { name: existing.name, fieldsText: '' },
[name]: { name: existing.name, fieldsText: "" },
},
};
});
@@ -109,20 +109,32 @@ export function useXmlConfig() {
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]);
return useMemo(
() => ({
config,
setMode,
setBaseNamespace,
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 {
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;
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;
return baseNs.replace(/\/+$/, "") + "/" + safeCat;
}
function getCategoryRoot(category: string): string {
const safeCat = sanitizeName(category) || category;
return safeCat + 'Data';
return safeCat + "Data";
}
interface FieldEntry {
@@ -29,14 +29,13 @@ export function generateCategoryXml(
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: '' };
if (raw.length === 0) return { xml: "", xpaths: "" };
const ns = getCategoryNamespace(baseNamespace, category);
const root = getCategoryRoot(category);
@@ -51,19 +50,19 @@ export function generateCategoryXml(
let baseName = base;
let idx = 2;
while (usedNames.has(baseName)) {
baseName = base + '_' + idx;
baseName = base + "_" + idx;
idx++;
}
usedNames.add(baseName);
const variants = [baseName];
if (mode === 'advanced') {
const suffixes = ['Short', 'Upper', 'Lower', 'Initials', 'First'];
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;
vn = baseName + suffix + "_" + k;
k++;
}
usedNames.add(vn);
@@ -74,24 +73,7 @@ export function generateCategoryXml(
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);
const allFields = fields;
// Build XML
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
@@ -110,18 +92,8 @@ export function generateCategoryXml(
for (const v of f.variants) {
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 };
}
@@ -129,20 +101,19 @@ 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);
results[cat] = generateCategoryXml(cat, catData, baseNamespace, mode);
}
return results;
}
export function downloadXmlFile(xml: string, filename: string): void {
const blob = new Blob([xml], { type: 'application/xml' });
const a = document.createElement('a');
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);
@@ -155,13 +126,12 @@ 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 JSZip = (await import("jszip")).default;
const results = generateAllCategories(categories, baseNamespace, mode);
const zip = new JSZip();
const folder = zip.folder('customXmlParts')!;
const folder = zip.folder("customXmlParts")!;
let hasAny = false;
for (const cat of Object.keys(results)) {
@@ -175,10 +145,10 @@ export async function downloadZipAll(
if (!hasAny) return;
const content = await zip.generateAsync({ type: 'blob' });
const a = document.createElement('a');
const content = await zip.generateAsync({ type: "blob" });
const a = document.createElement("a");
a.href = URL.createObjectURL(content);
a.download = 'beletage_custom_xml_parts.zip';
a.download = "beletage_custom_xml_parts.zip";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

View File

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