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:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions

View File

@@ -0,0 +1,456 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configurator semnatura e-mail</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.no-select { -webkit-user-select: none; -ms-user-select: none; user-select: none; }
input[type=range] {
-webkit-appearance: none; appearance: none; width: 100%; height: 4px;
background: #e5e7eb; border-radius: 5px; outline: none; transition: background 0.2s ease;
}
input[type=range]:hover { background: #d1d5db; }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 12px; height: 20px;
background: #22B5AB; cursor: pointer; border-radius: 4px;
margin-top: -8px; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out;
}
input[type=range]::-webkit-slider-thumb:active { transform: scale(1.15); box-shadow: 0 2px 6px rgba(0,0,0,0.3); }
input[type=range]::-moz-range-thumb {
width: 12px; height: 20px; background: #22B5AB; cursor: pointer;
border-radius: 4px; border: none; box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
#preview-wrapper { transition: transform 0.2s ease-in-out; transform-origin: top left; }
.color-swatch {
width: 24px; height: 24px; border-radius: 9999px; cursor: pointer;
border: 2px solid transparent; transition: all 0.2s ease;
}
.color-swatch.active { border-color: #22B5AB; transform: scale(1.1); box-shadow: 0 0 0 2px white, 0 0 0 4px #22B5AB; }
.collapsible-content { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; }
.collapsible-content.open { max-height: 1000px; /* Valoare mare pentru a permite extinderea */ }
.collapsible-trigger svg { transition: transform 0.3s ease; }
.collapsible-trigger.open svg { transform: rotate(90deg); }
</style>
</head>
<body class="bg-gray-50 text-gray-800 no-select">
<div class="container mx-auto p-4 md:p-8">
<header class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Configurator semnatura e-mail</h1>
</header>
<div class="flex flex-col lg:flex-row gap-8">
<!-- Panoul de control -->
<aside class="lg:w-2/5 bg-white p-6 rounded-2xl shadow-lg border border-gray-200">
<div id="controls">
<!-- Secțiunea Date Personale -->
<div class="mb-4">
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4">Date Personale</h3>
<div class="space-y-3">
<div>
<label for="input-prefix" class="block text-sm font-medium text-gray-700 mb-1">Titulatură (prefix)</label>
<input type="text" id="input-prefix" value="arh." class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
</div>
<div>
<label for="input-name" class="block text-sm font-medium text-gray-700 mb-1">Nume și Prenume</label>
<input type="text" id="input-name" value="Marius TĂRĂU" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
</div>
<div>
<label for="input-title" class="block text-sm font-medium text-gray-700 mb-1">Funcția</label>
<input type="text" id="input-title" value="Arhitect • Beletage SRL" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
</div>
<div>
<label for="input-phone" class="block text-sm font-medium text-gray-700 mb-1">Telefon (format 07xxxxxxxx)</label>
<input type="tel" id="input-phone" value="0785123433" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-teal-500 focus:border-teal-500">
</div>
</div>
</div>
<!-- Culori Text (Collapsible) -->
<div class="mb-4">
<div class="collapsible-trigger flex justify-between items-center cursor-pointer border-b pb-2 mb-2">
<h3 class="text-lg font-semibold text-gray-800">Culori Text</h3>
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</div>
<div class="collapsible-content">
<div id="color-controls" class="space-y-2 pt-2"></div>
</div>
</div>
<!-- Secțiunea Stil & Aranjare (Collapsible) -->
<div class="mb-4">
<div class="collapsible-trigger flex justify-between items-center cursor-pointer border-b pb-2 mb-2">
<h3 class="text-lg font-semibold text-gray-800">Stil & Aranjare</h3>
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
</div>
<div class="collapsible-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3 pt-2">
<div>
<label for="green-line-width" class="block text-sm font-medium text-gray-700 mb-2">Lungime linie verde (<span id="green-line-value">97</span>px)</label>
<input id="green-line-width" type="range" min="50" max="300" value="97">
</div>
<div>
<label for="section-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. secțiuni (<span id="section-spacing-value">10</span>px)</label>
<input id="section-spacing" type="range" min="0" max="30" value="10">
</div>
<div>
<label for="logo-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. Logo (<span id="logo-spacing-value">10</span>px)</label>
<input id="logo-spacing" type="range" min="0" max="30" value="10">
</div>
<div>
<label for="title-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. funcție (<span id="title-spacing-value">2</span>px)</label>
<input id="title-spacing" type="range" min="0" max="20" value="2">
</div>
<div>
<label for="b-gutter-width" class="block text-sm font-medium text-gray-700 mb-2">Aliniere contact (<span id="b-gutter-value">13</span>px)</label>
<input id="b-gutter-width" type="range" min="0" max="150" value="13">
</div>
<div>
<label for="icon-text-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiu Icon-Text (<span id="icon-text-spacing-value">5</span>px)</label>
<input id="icon-text-spacing" type="range" min="-10" max="30" value="5">
</div>
<div>
<label for="icon-vertical-pos" class="block text-sm font-medium text-gray-700 mb-2">Aliniere vert. iconițe (<span id="icon-vertical-value">1</span>px)</label>
<input id="icon-vertical-pos" type="range" min="-10" max="10" value="1">
</div>
<div>
<label for="motto-spacing" class="block text-sm font-medium text-gray-700 mb-2">Spațiere vert. motto (<span id="motto-spacing-value">3</span>px)</label>
<input id="motto-spacing" type="range" min="0" max="20" value="3">
</div>
</div>
</div>
</div>
<!-- Opțiuni -->
<div class="mb-4">
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4">Opțiuni</h3>
<div class="space-y-2">
<label class="flex items-center space-x-3 cursor-pointer">
<input type="checkbox" id="reply-variant-checkbox" class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500">
<span class="text-sm font-medium text-gray-700">Variantă simplă (fără logo/adresă)</span>
</label>
<label class="flex items-center space-x-3 cursor-pointer">
<input type="checkbox" id="super-reply-variant-checkbox" class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500">
<span class="text-sm font-medium text-gray-700">Super-simplă (doar nume/telefon)</span>
</label>
<label class="flex items-center space-x-3 cursor-pointer">
<input type="checkbox" id="use-svg-checkbox" class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500">
<span class="text-sm font-medium text-gray-700">Folosește imagini SVG (calitate maximă)</span>
</label>
</div>
</div>
</div>
<!-- Buton de Export -->
<div class="mt-8 pt-6 border-t">
<button id="export-btn" class="w-full bg-teal-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 transition-all duration-300 ease-in-out transform hover:scale-105">
Descarcă HTML
</button>
</div>
</aside>
<!-- Previzualizare Live -->
<main class="lg:w-3/5 bg-white p-6 rounded-2xl shadow-lg border border-gray-200 overflow-hidden">
<div class="flex justify-between items-center border-b pb-3 mb-4">
<h2 class="text-2xl font-bold text-gray-900">Previzualizare Live</h2>
<button id="zoom-btn" class="text-sm bg-gray-200 text-gray-700 px-3 py-1 rounded-md hover:bg-gray-300">Zoom 100%</button>
</div>
<div id="preview-wrapper" class="overflow-auto">
<div id="preview-container">
<!-- Aici este inserat codul HTML al semnăturii -->
</div>
</div>
</main>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const controls = {
prefix: document.getElementById('input-prefix'),
name: document.getElementById('input-name'),
title: document.getElementById('input-title'),
phone: document.getElementById('input-phone'),
greenLine: document.getElementById('green-line-width'),
gutter: document.getElementById('b-gutter-width'),
iconTextSpacing: document.getElementById('icon-text-spacing'),
iconVertical: document.getElementById('icon-vertical-pos'),
mottoSpacing: document.getElementById('motto-spacing'),
sectionSpacing: document.getElementById('section-spacing'),
titleSpacing: document.getElementById('title-spacing'),
logoSpacing: document.getElementById('logo-spacing'),
replyCheckbox: document.getElementById('reply-variant-checkbox'),
superReplyCheckbox: document.getElementById('super-reply-variant-checkbox'),
useSvgCheckbox: document.getElementById('use-svg-checkbox'),
exportBtn: document.getElementById('export-btn'),
zoomBtn: document.getElementById('zoom-btn'),
colorControls: document.getElementById('color-controls')
};
const values = {
greenLine: document.getElementById('green-line-value'),
gutter: document.getElementById('b-gutter-value'),
iconTextSpacing: document.getElementById('icon-text-spacing-value'),
iconVertical: document.getElementById('icon-vertical-value'),
mottoSpacing: document.getElementById('motto-spacing-value'),
sectionSpacing: document.getElementById('section-spacing-value'),
titleSpacing: document.getElementById('title-spacing-value'),
logoSpacing: document.getElementById('logo-spacing-value')
};
const previewContainer = document.getElementById('preview-container');
const previewWrapper = document.getElementById('preview-wrapper');
const imageSets = {
png: {
logo: 'https://beletage.ro/img/Semnatura-Logo.png',
greySlash: 'https://beletage.ro/img/Grey-slash.png',
greenSlash: 'https://beletage.ro/img/Green-slash.png'
},
svg: {
logo: 'https://beletage.ro/img/Logo-Beletage.svg',
greySlash: 'https://beletage.ro/img/Grey-slash.svg',
greenSlash: 'https://beletage.ro/img/Green-slash.svg'
}
};
const beletageColors = {
verde: '#22B5AB',
griInchis: '#54504F',
griDeschis: '#A7A9AA',
negru: '#323232'
};
const colorConfig = {
prefix: { label: 'Titulatură', default: beletageColors.griInchis },
name: { label: 'Nume', default: beletageColors.griInchis },
title: { label: 'Funcție', default: beletageColors.griDeschis },
address: { label: 'Adresă', default: beletageColors.griDeschis },
phone: { label: 'Telefon', default: beletageColors.griInchis },
website: { label: 'Website', default: beletageColors.griInchis },
motto: { label: 'Motto', default: beletageColors.verde }
};
let currentColors = {};
function createColorPickers() {
for (const [key, config] of Object.entries(colorConfig)) {
currentColors[key] = config.default;
const controlRow = document.createElement('div');
controlRow.className = 'flex items-center justify-between';
const label = document.createElement('span');
label.className = 'text-sm font-medium text-gray-700';
label.textContent = config.label;
controlRow.appendChild(label);
const swatchesContainer = document.createElement('div');
swatchesContainer.className = 'flex items-center space-x-2';
swatchesContainer.dataset.controlKey = key;
for (const color of Object.values(beletageColors)) {
const swatch = document.createElement('div');
swatch.className = 'color-swatch';
swatch.style.backgroundColor = color;
swatch.dataset.color = color;
if (color === config.default) swatch.classList.add('active');
swatchesContainer.appendChild(swatch);
}
controlRow.appendChild(swatchesContainer);
controls.colorControls.appendChild(controlRow);
}
controls.colorControls.addEventListener('click', (e) => {
if (e.target.classList.contains('color-swatch')) {
const key = e.target.parentElement.dataset.controlKey;
currentColors[key] = e.target.dataset.color;
e.target.parentElement.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('active'));
e.target.classList.add('active');
updatePreview();
}
});
}
function generateSignatureHTML(data) {
const {
prefix, name, title, phone, phoneLink, greenLineWidth, gutterWidth,
iconTextSpacing, iconVerticalOffset, mottoSpacing, sectionSpacing, titleSpacing, logoSpacing,
isReply, isSuperReply, colors, images
} = data;
const hideTitle = isReply || isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
const hideLogoAddress = isReply || isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
const hideBottom = isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
const hidePhoneIcon = isSuperReply ? 'mso-hide:all;display:none!important;max-height:0;overflow:hidden;font-size:0;line-height:0;' : '';
const spacerWidth = Math.max(0, iconTextSpacing);
const textPaddingLeft = Math.max(0, -iconTextSpacing);
const prefixHTML = prefix ? `<span style="font-size:13px; color:${colors.prefix};">${prefix} </span>` : '';
const logoWidth = controls.useSvgCheckbox.checked ? 162 : 162;
const logoHeight = controls.useSvgCheckbox.checked ? 24 : 24;
return `
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540" style="font-family: Arial, Helvetica, sans-serif; color:#333333; font-size:14px; line-height:18px;">
<tbody>
<tr><td style="padding:0 0 ${titleSpacing}px 0;">${prefixHTML}<span style="font-size:15px; color:${colors.name}; font-weight:700;">${name}</span></td></tr>
<tr style="${hideTitle}"><td style="padding:0 0 8px 0;"><span style="font-size:12px; color:${colors.title};">${title}</span></td></tr>
<tr style="${hideBottom}">
<td style="padding:0; font-size:0; line-height:0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
<tr>
<td width="${greenLineWidth}" height="2" bgcolor="${beletageColors.verde}" style="font-size:0; line-height:0; height:2px;"></td>
<td width="${540 - greenLineWidth}" height="2" style="font-size:0; line-height:0; height:2px;"></td>
</tr>
</table>
</td>
</tr>
<tr style="${hideLogoAddress}"><td style="padding:${logoSpacing}px 0 ${parseInt(logoSpacing, 10) + 2}px 0;">
<a href="https://www.beletage.ro" style="text-decoration:none; border:0;">
<img src="${images.logo}" alt="Beletage" style="display:block; border:0; height:${logoHeight}px; width:${logoWidth}px;" height="${logoHeight}" width="${logoWidth}">
</a>
</td></tr>
<tr>
<td style="padding-top:${hideLogoAddress ? '0' : sectionSpacing}px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540" style="font-size:13px; line-height:18px;">
<tbody>
<tr style="${hideLogoAddress}">
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
<td width="11" style="width:11px; vertical-align:top; padding-top:${4 + iconVerticalOffset}px;">
<img src="${images.greySlash}" alt="" width="11" height="11" style="display: block; border:0;">
</td>
<td width="${spacerWidth}" style="width:${spacerWidth}px; font-size:0; line-height:0;"></td>
<td style="vertical-align:top; padding:0 0 0 ${textPaddingLeft}px;">
<a href="https://maps.google.com/?q=str.%20Unirii%203%2C%20ap.%2026%2C%20Cluj-Napoca%20400417%2C%20Rom%C3%A2nia" style="color:${colors.address}; text-decoration:none;"><span style="color:${colors.address}; text-decoration:none;">str. Unirii, nr. 3, ap. 26<br>Cluj-Napoca, Cluj 400417<br>România</span></a>
</td>
</tr>
<tr>
<td width="${gutterWidth}" style="width:${gutterWidth}px; font-size:0; line-height:0;"></td>
<td width="11" style="width:11px; vertical-align:top; padding-top:${12 + iconVerticalOffset}px; ${hidePhoneIcon}">
<img src="${images.greenSlash}" alt="" width="11" height="7" style="display: block; border:0;">
</td>
<td width="${isSuperReply ? 0 : spacerWidth}" style="width:${isSuperReply ? 0 : spacerWidth}px; font-size:0; line-height:0;"></td>
<td style="vertical-align:top; padding:8px 0 0 ${isSuperReply ? 0 : textPaddingLeft}px;">
<a href="${phoneLink}" style="color:${colors.phone}; text-decoration:none;"><span style="color:${colors.phone}; text-decoration:none;">${phone}</span></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr style="${hideBottom}"><td style="padding:${sectionSpacing}px 0 ${mottoSpacing}px 0;"><a href="https://www.beletage.ro" style="color:${colors.website}; text-decoration:none;"><span style="color:${colors.website}; text-decoration:none;">www.beletage.ro</span></a></td></tr>
<tr style="${hideBottom}">
<td style="padding:0; font-size:0; line-height:0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="540">
<tr>
<td width="${greenLineWidth}" height="1" bgcolor="${beletageColors.verde}" style="font-size:0; line-height:0; height:1px;"></td>
<td width="${540 - greenLineWidth}" height="1" style="font-size:0; line-height:0; height:1px;"></td>
</tr>
</table>
</td>
</tr>
<tr style="${hideBottom}"><td style="padding:${mottoSpacing}px 0 0 0;"><span style="font-size:12px; color:${colors.motto}; font-style:italic;">we make complex simple</span></td></tr>
</tbody>
</table>
`;
}
function updatePreview() {
const phoneRaw = controls.phone.value.replace(/\s/g, '');
let formattedPhone = controls.phone.value;
let phoneLink = `tel:${phoneRaw}`;
if (phoneRaw.length === 10 && phoneRaw.startsWith('07')) {
formattedPhone = `+40 ${phoneRaw.substring(1, 4)} ${phoneRaw.substring(4, 7)} ${phoneRaw.substring(7, 10)}`;
phoneLink = `tel:+40${phoneRaw.substring(1)}`;
}
if (controls.superReplyCheckbox.checked) {
controls.replyCheckbox.checked = true;
controls.replyCheckbox.disabled = true;
} else {
controls.replyCheckbox.disabled = false;
}
const data = {
prefix: controls.prefix.value,
name: controls.name.value,
title: controls.title.value,
phone: formattedPhone,
phoneLink: phoneLink,
greenLineWidth: controls.greenLine.value,
gutterWidth: controls.gutter.value,
iconTextSpacing: controls.iconTextSpacing.value,
iconVerticalOffset: parseInt(controls.iconVertical.value, 10),
mottoSpacing: controls.mottoSpacing.value,
sectionSpacing: controls.sectionSpacing.value,
titleSpacing: controls.titleSpacing.value,
logoSpacing: controls.logoSpacing.value,
isReply: controls.replyCheckbox.checked,
isSuperReply: controls.superReplyCheckbox.checked,
colors: { ...currentColors },
images: controls.useSvgCheckbox.checked ? imageSets.svg : imageSets.png
};
values.greenLine.textContent = data.greenLineWidth;
values.gutter.textContent = data.gutterWidth;
values.iconTextSpacing.textContent = data.iconTextSpacing;
values.iconVertical.textContent = data.iconVerticalOffset;
values.mottoSpacing.textContent = data.mottoSpacing;
values.sectionSpacing.textContent = data.sectionSpacing;
values.titleSpacing.textContent = data.titleSpacing;
values.logoSpacing.textContent = data.logoSpacing;
previewContainer.innerHTML = generateSignatureHTML(data);
}
// --- Inițializare ---
createColorPickers();
Object.values(controls).forEach(control => {
if (control.id !== 'export-btn' && control.id !== 'zoom-btn') {
control.addEventListener('input', updatePreview);
}
});
document.querySelectorAll('.collapsible-trigger').forEach(trigger => {
trigger.addEventListener('click', () => {
const content = trigger.nextElementSibling;
trigger.classList.toggle('open');
content.classList.toggle('open');
});
});
controls.zoomBtn.addEventListener('click', () => {
const isZoomed = previewWrapper.style.transform === 'scale(2)';
if (isZoomed) {
previewWrapper.style.transform = 'scale(1)';
controls.zoomBtn.textContent = 'Zoom 200%';
} else {
previewWrapper.style.transform = 'scale(2)';
controls.zoomBtn.textContent = 'Zoom 100%';
}
});
controls.exportBtn.addEventListener('click', () => {
const finalHTML = previewContainer.innerHTML;
const blob = new Blob([finalHTML], { type: 'text/html' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'semnatura-beletage.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
updatePreview();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,148 @@
Pauza de masa
Timp personal
Concediu
Compensare overtime
Beletage
Ofertare
Configurari
Organizare initiala
Pregatire Portofoliu
Website
Documentare
Design grafic
Design interior
Design exterior
Releveu
Reclama
000 Farmacie
002 Cladire birouri Stratec
003 PUZ Bellavista
007 Design Apartament Teodora
010 Casa Doinei
016 Duplex Eremia
024 Bloc Petofi
028 PUZ Borhanci-Sopor
033 Mansardare Branului
039 Cabinete Stoma Scala
041 Imobil mixt Progresului
045 Casa Andrei Muresanu
052 PUZ Carpenului
059 PUZ Nordului
064 Casa Salicea
066 Terasa Gherase
070 Bloc Fanatelor
073 Case Frumoasa
074 PUG Cosbuc
076 Casa Copernicus
077 PUZ Schimbare destinatie Brancusi
078 Service auto Linistei
079 Amenajare drum Servitute Eremia
080 Bloc Tribunul
081 Extindere casa Gherase
083 Modificari casa Zsigmund 18
084 Mansardare Petofi 21
085 Container CT Spital Tabacarilor
086 Imprejmuire casa sat Gheorgheni
087 Duplex Oasului fn
089 PUZ A-Liu Sopor
090 VR MedEvents
091 Reclama Caparol
092 Imobil birouri 13 Septembrie
093 Casa Salistea Noua
094 PUD Casa Rediu
095 Duplex Vanatorului
096 Design apartament Sopor
097 Cabana Gilau
101 PUZ Gilau
102 PUZ Ghimbav
103 Piscine Lunca Noua
104 PUZ REGHIN
105 CUT&Crust
106 PUZ Mihai Romanu Nord
108 Reabilitare Bloc Beiusului
109 Case Samboleni
110 Penny Crasna
111 Anexa Piscina Borhanci
112 PUZ Blocuri Bistrita
113 PUZ VARATEC-FIRIZA
114 PUG Husi
115 PUG Josenii Bargaului
116 PUG Monor
117 Schimbare Destinatie Mihai Viteazu 2
120 Anexa Brasov
121 Imprejurare imobil Mesterul Manole 9
122 Fastfood Bashar
123 PUD Rediu 2
127 Casa Socaciu Ciurila
128 Schimbare de destinatie Danubius
129 (re) Casa Sarca-Sorescu
130 Casa Suta-Wonderland
131 PUD Oasului Hufi
132 Reabilitare Camin Cultural Baciu
133 PUG Feldru
134 DALI Blocuri Murfatlar
135 Case de vacanta Dianei
136 PUG BROSTENI
139 Casa Turda
140 Releveu Bistrita (Morariu)
141 PUZ Janovic Jeno
142 Penny Borhanci
143 Pavilion Politie Radauti
149 Duplex Sorescu 31-33
150 DALI SF Scoala Baciu
151 Casa Alexandru Bohatiel 17
152 PUZ Penny Tautii Magheraus
153 PUG Banita
155 PT Scoala Floresti
156 Case Sorescu
157 Gradi-Cresa Baciu
158 Duplex Sorescu 21-23
159 Amenajare Spatiu Grenke PBC
160 Etajare Primaria Baciu
161 Extindere Ap Baciu
164 SD salon Aurel Vlaicu
165 Reclama Marasti
166 Catei Apahida
167 Apartament Mircea Zaciu 13-15
169 Casa PETRILA 37
170 Cabana Campeni AB
171 Camin Apahida
L089 PUZ TUSA-BOJAN
172 Design casa Iugoslaviei 18
173 Reabilitare spitale Sighetu
174 StudX UMFST
176 - 2025 - ReAC Ansamblu rezi Bibescu
CU
Schita
Avize
PUD
AO
PUZ
PUG
DTAD
DTAC
PT
Detalii de Executie
Studii de fundamentare
Regulament
Parte desenata
Parte scrisa
Consultanta client
Macheta
Consultanta receptie
Redactare
Depunere
Ridicare
Verificare proiect
Vizita santier
Master MATDR

View File

@@ -0,0 +1,694 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<title>Beletage Word XML Data Engine</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- JSZip pentru arhivă ZIP -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"
integrity="sha512-FGv7V3GpCr3C6wz6Q4z8F1v8y4mZohwPqhwKiPfz0btvAvOE0tfLOgvBcFQncn1C3KW0y5fN9c7v1sQW8vGfMQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<style>
:root {
color-scheme: dark;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 1.5rem;
background: #020617;
color: #e5e7eb;
}
h1 {
font-size: 1.7rem;
margin-bottom: .25rem;
}
.subtitle {
font-size: .9rem;
color: #9ca3af;
margin-bottom: 1rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.card {
background: #020617;
border-radius: 1rem;
padding: 1.1rem 1.3rem;
margin-bottom: 1rem;
border: 1px solid #1e293b;
box-shadow: 0 15px 35px rgba(0,0,0,.45);
}
label {
font-size: .8rem;
color: #9ca3af;
display: block;
margin-bottom: .2rem;
}
input, textarea, select {
width: 100%;
box-sizing: border-box;
padding: .5rem .6rem;
border-radius: .5rem;
border: 1px solid #334155;
background: #020617;
color: #e5e7eb;
font-family: inherit;
font-size: .9rem;
outline: none;
}
input:focus, textarea:focus, select:focus {
border-color: #38bdf8;
box-shadow: 0 0 0 1px #38bdf8;
}
textarea { min-height: 140px; resize: vertical; }
.row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.col-3 { flex: 1 1 220px; }
.col-6 { flex: 1 1 320px; }
.col-9 { flex: 3 1 420px; }
button {
padding: .55rem 1.1rem;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, #38bdf8, #6366f1);
color: #fff;
font-size: .9rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 12px 25px rgba(37,99,235,.4);
}
button:hover { filter: brightness(1.05); transform: translateY(-1px); }
button:active { transform: translateY(0); box-shadow: 0 8px 18px rgba(37,99,235,.6); }
.btn-secondary {
background: transparent;
border: 1px solid #4b5563;
box-shadow: none;
}
.btn-secondary:hover {
background: #020617;
box-shadow: 0 8px 20px rgba(0,0,0,.6);
}
.btn-small {
font-size: .8rem;
padding: .35rem .8rem;
box-shadow: none;
}
.toggle {
display: inline-flex;
align-items: center;
gap: .4rem;
font-size: .8rem;
color: #cbd5f5;
cursor: pointer;
user-select: none;
}
.toggle input { width: auto; }
.pill-row {
display: flex;
flex-wrap: wrap;
gap: .4rem;
margin-bottom: .4rem;
}
.pill {
padding: .25rem .7rem;
border-radius: 999px;
border: 1px solid #334155;
font-size: .8rem;
cursor: pointer;
background: #020617;
color: #e5e7eb;
display: inline-flex;
align-items: center;
gap: .35rem;
}
.pill.active {
background: linear-gradient(135deg, #38bdf8, #6366f1);
border-color: transparent;
color: #0f172a;
}
.pill span.remove {
font-size: .8rem;
opacity: .7;
}
.pill span.remove:hover { opacity: 1; }
.small {
font-size: .8rem;
color: #9ca3af;
margin-top: .25rem;
}
pre {
background: #020617;
border-radius: .75rem;
padding: .7rem .8rem;
border: 1px solid #1f2937;
overflow: auto;
font-size: .8rem;
max-height: 340px;
}
.badge {
display: inline-flex;
align-items: center;
padding: .15rem .45rem;
border-radius: 999px;
font-size: .7rem;
background: rgba(148,163,184,.18);
margin-right: .4rem;
margin-bottom: .25rem;
}
@media (max-width: 768px) {
body { padding: 1rem; }
.card { padding: 1rem; }
}
</style>
</head>
<body>
<div class="container">
<h1>Beletage Word XML Data Engine</h1>
<p class="subtitle">
Generator de <strong>Custom XML Parts</strong> pentru Word, pe categorii (Beneficiar, Proiect, Suprafete, Meta etc.),
cu mod <em>Simple</em> / <em>Advanced</em> și câmpuri derivate (Short, Upper, Initials) + POT/CUT pregătite.
</p>
<!-- SETĂRI GLOBALE -->
<div class="card">
<div class="row">
<div class="col-6">
<label for="baseNs">Bază Namespace (se completează automat cu /Categorie)</label>
<input id="baseNs" type="text" value="http://schemas.beletage.ro/contract">
<div class="small">Ex: <code>http://schemas.beletage.ro/contract</code> → pentru categoria „Proiect” devine
<code>http://schemas.beletage.ro/contract/Proiect</code>.
</div>
</div>
<div class="col-3">
<label>Mod generare câmpuri</label>
<div class="pill-row">
<div class="pill active" id="modeSimplePill" onclick="setMode('simple')">Simple</div>
<div class="pill" id="modeAdvancedPill" onclick="setMode('advanced')">Advanced</div>
</div>
<div class="small">
<strong>Simple</strong>: doar câmpurile tale.<br>
<strong>Advanced</strong>: + Short / Upper / Lower / Initials / First pentru fiecare câmp.
</div>
</div>
<div class="col-3">
<label>Opțiuni extra</label>
<div class="small" style="margin-top:.25rem;">
<label class="toggle">
<input type="checkbox" id="computeMetrics" checked>
<span>Adaugă câmpuri POT / CUT în categoria Suprafete</span>
</label>
</div>
</div>
</div>
</div>
<!-- CATEGORII -->
<div class="card">
<div class="row">
<div class="col-3">
<label>Categorii de date</label>
<div id="categoryPills" class="pill-row"></div>
<button class="btn-secondary btn-small" onclick="addCategoryPrompt()">+ Adaugă categorie</button>
<div class="small">
Exemple de organizare: <code>Beneficiar</code>, <code>Proiect</code>, <code>Suprafete</code>, <code>Meta</code>.
</div>
</div>
<div class="col-9">
<label>Câmpuri pentru categoria selectată</label>
<textarea id="fieldsArea"></textarea>
<div class="small">
Un câmp pe linie. Poți edita lista. Butonul „Reset categorie la preset” reîncarcă valorile default pentru
categoria curentă (dacă există).
</div>
<div style="margin-top:.5rem; display:flex; gap:.5rem; flex-wrap:wrap;">
<button class="btn-secondary btn-small" onclick="resetCategoryToPreset()">Reset categorie la preset</button>
<button class="btn-secondary btn-small" onclick="clearCategoryFields()">Curăță câmpurile</button>
</div>
<div class="small" id="nsRootInfo" style="margin-top:.6rem;"></div>
</div>
</div>
</div>
<!-- GENERARE & DOWNLOAD -->
<div class="card">
<div style="display:flex; flex-wrap:wrap; gap:.5rem; align-items:center; margin-bottom:.5rem;">
<button onclick="generateAll()">Generează XML pentru toate categoriile</button>
<button class="btn-secondary" onclick="downloadCurrentXml()">Descarcă XML categorie curentă</button>
<button class="btn-secondary" onclick="downloadZipAll()">Descarcă ZIP cu toate XML-urile</button>
</div>
<div class="small">
<span class="badge">Tip</span>
În Word, fiecare fișier generat devine un Custom XML Part separat (ex: <code>BeneficiarData.xml</code>,
<code>ProiectData.xml</code> etc.), perfect pentru organizarea mapping-urilor.
</div>
</div>
<!-- PREVIEW -->
<div class="card">
<h3 style="margin-top:0;">Preview XML & XPaths</h3>
<div class="small" style="margin-bottom:.4rem;">
Selectează o categorie pentru a vedea XML-ul și XPaths-urile aferente.
</div>
<div class="row">
<div class="col-6">
<div class="badge">XML categorie curentă</div>
<pre id="xmlPreview"></pre>
</div>
<div class="col-6">
<div class="badge">XPaths categorie curentă</div>
<pre id="xpathPreview"></pre>
</div>
</div>
</div>
</div>
<script>
// --- PRESETURI CATEGORII ---
const defaultPresets = {
"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"
]
};
// --- STATE ---
let categories = {}; // { Categorie: { fieldsText: "..." } }
let currentCategory = "Beneficiar";
let mode = "advanced"; // "simple" | "advanced"
const xmlParts = {}; // { Categorie: xmlString }
const xpathParts = {}; // { Categorie: xpathString }
// --- UTILITARE ---
function sanitizeName(name) {
if (!name) return null;
let n = name.trim();
if (!n) return null;
n = n.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_.-]/g, "");
if (!/^[A-Za-z_]/.test(n)) n = "_" + n;
return n;
}
function initialsFromLabel(label) {
if (!label) return "";
return label.trim().split(/\s+/).map(s => s.charAt(0).toUpperCase() + ".").join("");
}
function firstToken(label) {
if (!label) return "";
return label.trim().split(/\s+/)[0] || "";
}
function getBaseNamespace() {
const val = document.getElementById("baseNs").value.trim();
return val || "http://schemas.beletage.ro/contract";
}
function getCategoryNamespace(cat) {
const base = getBaseNamespace();
const safeCat = sanitizeName(cat) || cat;
return base.replace(/\/+$/,"") + "/" + safeCat;
}
function getCategoryRoot(cat) {
const safeCat = sanitizeName(cat) || cat;
return safeCat + "Data";
}
// --- MOD SIMPLE/ADVANCED ---
function setMode(m) {
mode = m === "advanced" ? "advanced" : "simple";
document.getElementById("modeSimplePill").classList.toggle("active", mode === "simple");
document.getElementById("modeAdvancedPill").classList.toggle("active", mode === "advanced");
// regenerăm previw dacă avem ceva
generateAll(false);
}
// --- CATEGORII: INIT, UI, STORAGE ---
function initCategories() {
// încarcă din localStorage, altfel default
const saved = window.localStorage.getItem("beletage_xml_categories");
if (saved) {
try {
const parsed = JSON.parse(saved);
categories = parsed.categories || {};
currentCategory = parsed.currentCategory || "Beneficiar";
} catch(e) {
Object.keys(defaultPresets).forEach(cat => {
categories[cat] = { fieldsText: defaultPresets[cat].join("\n") };
});
currentCategory = "Beneficiar";
}
} else {
Object.keys(defaultPresets).forEach(cat => {
categories[cat] = { fieldsText: defaultPresets[cat].join("\n") };
});
currentCategory = "Beneficiar";
}
renderCategoryPills();
loadCategoryToUI(currentCategory);
}
function persistCategories() {
try {
window.localStorage.setItem("beletage_xml_categories", JSON.stringify({
categories,
currentCategory
}));
} catch(e){}
}
function renderCategoryPills() {
const container = document.getElementById("categoryPills");
container.innerHTML = "";
Object.keys(categories).forEach(cat => {
const pill = document.createElement("div");
pill.className = "pill" + (cat === currentCategory ? " active" : "");
pill.onclick = () => switchCategory(cat);
pill.textContent = cat;
// nu permitem ștergerea preset-urilor de bază direct (doar la custom)
if (!defaultPresets[cat]) {
const remove = document.createElement("span");
remove.className = "remove";
remove.textContent = "×";
remove.onclick = (ev) => {
ev.stopPropagation();
deleteCategory(cat);
};
pill.appendChild(remove);
}
container.appendChild(pill);
});
}
function switchCategory(cat) {
saveCurrentCategoryFields();
currentCategory = cat;
renderCategoryPills();
loadCategoryToUI(cat);
updateNsRootInfo();
showPreview(cat);
persistCategories();
}
function loadCategoryToUI(cat) {
const area = document.getElementById("fieldsArea");
area.value = categories[cat]?.fieldsText || "";
updateNsRootInfo();
}
function saveCurrentCategoryFields() {
const area = document.getElementById("fieldsArea");
if (!categories[currentCategory]) {
categories[currentCategory] = { fieldsText: "" };
}
categories[currentCategory].fieldsText = area.value;
}
function deleteCategory(cat) {
if (!confirm(`Sigur ștergi categoria "${cat}"?`)) return;
delete categories[cat];
const keys = Object.keys(categories);
currentCategory = keys[0] || "Beneficiar";
renderCategoryPills();
loadCategoryToUI(currentCategory);
updateNsRootInfo();
persistCategories();
}
function addCategoryPrompt() {
const name = prompt("Nume categorie nouă (ex: Urbanism, Fiscal, Altele):");
if (!name) return;
const trimmed = name.trim();
if (!trimmed) return;
if (categories[trimmed]) {
alert("Categoria există deja.");
return;
}
categories[trimmed] = { fieldsText: "" };
currentCategory = trimmed;
renderCategoryPills();
loadCategoryToUI(currentCategory);
updateNsRootInfo();
persistCategories();
}
function resetCategoryToPreset() {
if (!defaultPresets[currentCategory]) {
alert("Categoria curentă nu are preset definit.");
return;
}
if (!confirm("Resetezi lista de câmpuri la presetul standard pentru această categorie?")) return;
categories[currentCategory].fieldsText = defaultPresets[currentCategory].join("\n");
loadCategoryToUI(currentCategory);
persistCategories();
}
function clearCategoryFields() {
categories[currentCategory].fieldsText = "";
loadCategoryToUI(currentCategory);
persistCategories();
}
function updateNsRootInfo() {
const ns = getCategoryNamespace(currentCategory);
const root = getCategoryRoot(currentCategory);
document.getElementById("nsRootInfo").innerHTML =
`<strong>Namespace:</strong> <code>${ns}</code><br>` +
`<strong>Root element:</strong> <code>&lt;${root}&gt;</code>`;
}
// --- GENERARE XML PENTRU O CATEGORIE ---
function generateCategory(cat) {
const entry = categories[cat];
if (!entry) return { xml: "", xpaths: "" };
const raw = (entry.fieldsText || "").split(/\r?\n/)
.map(l => l.trim())
.filter(l => l.length > 0);
if (raw.length === 0) {
return { xml: "", xpaths: "" };
}
const ns = getCategoryNamespace(cat);
const root = getCategoryRoot(cat);
const computeMetrics = document.getElementById("computeMetrics").checked;
const usedNames = new Set();
const fields = []; // { label, baseName, variants: [] }
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 advCandidates = [
baseName + "Short",
baseName + "Upper",
baseName + "Lower",
baseName + "Initials",
baseName + "First"
];
for (let v of advCandidates) {
let vn = v;
let k = 2;
while (usedNames.has(vn)) {
vn = v + "_" + k;
k++;
}
usedNames.add(vn);
variants.push(vn);
}
}
fields.push({ label, baseName, variants });
}
// detectăm câmpuri pentru metrici (în special categoria Suprafete)
const extraMetricFields = [];
if (computeMetrics && cat.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) {
if (!usedNames.has("POT")) {
usedNames.add("POT");
extraMetricFields.push({ label: "Procent Ocupare Teren", baseName: "POT", variants: ["POT"] });
}
}
if (hasTeren && hasDesf) {
if (!usedNames.has("CUT")) {
usedNames.add("CUT");
extraMetricFields.push({ label: "Coeficient Utilizare Teren", baseName: "CUT", variants: ["CUT"] });
}
}
}
// generăm XML
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += `<${root} xmlns="${ns}">\n`;
const allFieldEntries = fields.concat(extraMetricFields);
for (const f of allFieldEntries) {
for (const v of f.variants) {
xml += ` <${v}></${v}>\n`;
}
}
xml += `</${root}>\n`;
// generăm XPaths
let xp = `Categorie: ${cat}\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 };
}
// --- GENERARE PENTRU TOATE CATEGORIILE ---
function generateAll(showForCurrent = true) {
saveCurrentCategoryFields();
Object.keys(categories).forEach(cat => {
const { xml, xpaths } = generateCategory(cat);
xmlParts[cat] = xml;
xpathParts[cat] = xpaths;
});
if (showForCurrent) {
showPreview(currentCategory);
}
persistCategories();
}
// --- PREVIEW ---
function showPreview(cat) {
document.getElementById("xmlPreview").textContent = xmlParts[cat] || "<!-- Niciun XML generat încă pentru această categorie. -->";
document.getElementById("xpathPreview").textContent = xpathParts[cat] || "";
}
// --- DOWNLOAD: XML CATEGORIE ---
function downloadCurrentXml() {
generateAll(false);
const xml = xmlParts[currentCategory];
if (!xml) {
alert("Nu există XML generat pentru categoria curentă. Apasă întâi „Generează XML pentru toate categoriile”.");
return;
}
const root = getCategoryRoot(currentCategory);
const fileName = root + ".xml";
const blob = new Blob([xml], { type: "application/xml" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
a.click();
URL.revokeObjectURL(a.href);
}
// --- DOWNLOAD: ZIP CU TOATE XML-URILE ---
async function downloadZipAll() {
generateAll(false);
const cats = Object.keys(categories);
if (cats.length === 0) {
alert("Nu există categorii.");
return;
}
const zip = new JSZip();
const folder = zip.folder("customXmlParts");
let hasAny = false;
for (const cat of cats) {
const xml = xmlParts[cat];
if (!xml) continue;
hasAny = true;
const root = getCategoryRoot(cat);
const fileName = root + ".xml";
folder.file(fileName, xml);
}
if (!hasAny) {
alert("Nu există XML generat încă. Apasă întâi „Generează XML pentru toate categoriile”.");
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";
a.click();
URL.revokeObjectURL(a.href);
}
// --- INIT ---
window.addEventListener("DOMContentLoaded", () => {
initCategories();
updateNsRootInfo();
generateAll();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<title>Generator XML Word Versiune Extinsă</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: system-ui, sans-serif;
padding: 1.5rem;
background: #0f172a;
color: #e5e7eb;
}
.card {
background: #020617;
border-radius: 1rem;
padding: 1.25rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
border: 1px solid #1e293b;
margin-bottom: 1rem;
}
label { font-size: .85rem; color: #94a3b8; }
input, textarea {
width: 100%; padding: .55rem .7rem;
border-radius: .5rem; border: 1px solid #334155;
background: #020617; color: #e5e7eb;
}
textarea { min-height: 120px; }
button {
padding: .6rem 1.2rem; border-radius: 999px; border: none;
background: linear-gradient(135deg,#38bdf8,#6366f1);
font-weight: 600; color: white; cursor: pointer;
}
pre {
background: #000; padding: .8rem; border-radius: .7rem;
border: 1px solid #1e293b; max-height: 350px; overflow: auto;
font-size: .85rem;
}
</style>
</head>
<body>
<h1>Generator Word XML Varianta Extinsă (cu Short / Upper / Lower / Initials)</h1>
<div class="card">
<label>Namespace URI</label>
<input id="nsUri" value="http://schemas.beletage.ro/word/contract">
<label style="margin-top:1rem;">Element rădăcină</label>
<input id="rootElement" value="ContractData">
<label style="margin-top:1rem;">Lista de câmpuri (unul pe linie)</label>
<textarea id="fieldList">NumeClient
TitluProiect
Adresa</textarea>
<button onclick="generateXML()" style="margin-top:1rem;">Generează XML complet</button>
</div>
<div class="card">
<h3>Custom XML Part (item1.xml)</h3>
<pre id="xmlOutput"></pre>
<button onclick="downloadXML()">Descarcă XML</button>
</div>
<div class="card">
<h3>XPaths pentru mapping</h3>
<pre id="xpathOutput"></pre>
</div>
<script>
function sanitize(name) {
if (!name) return null;
let n = name.trim();
if (!n) return null;
n = n.replace(/\s+/g,"_").replace(/[^A-Za-z0-9_.-]/g,"");
if (!/^[A-Za-z_]/.test(n)) n = "_" + n;
return n;
}
function initials(str) {
return str.split(/\s+/).map(s => s[0]?.toUpperCase() + ".").join("");
}
function generateXML() {
const ns = document.getElementById("nsUri").value.trim();
const root = sanitize(document.getElementById("rootElement").value) || "Root";
const fieldRaw = document.getElementById("fieldList").value;
const lines = fieldRaw.split(/\r?\n/)
.map(l => l.trim()).filter(l => l.length);
const fields = [];
for (let l of lines) {
const base = sanitize(l);
if (!base) continue;
fields.push({
base,
variants: [
base, // original
base + "Short", // prescurtat
base + "Upper", // caps
base + "Lower", // lowercase
base + "Initials", // inițiale
base + "First" // primul cuvânt
]
});
}
// === GENERĂM XML ===
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += `<${root} xmlns="${ns}">\n`;
for (const f of fields) {
for (const v of f.variants) {
xml += ` <${v}></${v}>\n`;
}
}
xml += `</${root}>`;
document.getElementById("xmlOutput").textContent = xml;
// === GENERĂM XPATHS ===
let xp = `Namespace: ${ns}\nRoot: /${root}\n\n`;
for (const f of fields) {
xp += `# ${f.base}\n`;
xp += `/${root}/${f.base}\n`;
xp += `/${root}/${f.base}Short\n`;
xp += `/${root}/${f.base}Upper\n`;
xp += `/${root}/${f.base}Lower\n`;
xp += `/${root}/${f.base}Initials\n`;
xp += `/${root}/${f.base}First\n\n`;
}
document.getElementById("xpathOutput").textContent = xp;
}
function downloadXML() {
const text = document.getElementById("xmlOutput").textContent;
const blob = new Blob([text], { type: "application/xml" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "item1.xml";
a.click();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<title>Generator Word XML Custom Part</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 1.5rem;
background: #0f172a;
color: #e5e7eb;
}
h1 {
font-size: 1.6rem;
margin-bottom: 0.5rem;
}
.container {
max-width: 1100px;
margin: 0 auto;
}
.card {
background: #020617;
border-radius: 1rem;
padding: 1.25rem 1.5rem;
box-shadow: 0 15px 40px rgba(0,0,0,0.35);
border: 1px solid #1f2937;
margin-bottom: 1rem;
}
label {
display: block;
font-size: 0.85rem;
color: #9ca3af;
margin-bottom: 0.25rem;
}
input, textarea {
width: 100%;
box-sizing: border-box;
padding: 0.5rem 0.6rem;
border-radius: 0.5rem;
border: 1px solid #374151;
background: #020617;
color: #e5e7eb;
font-family: inherit;
font-size: 0.9rem;
outline: none;
}
input:focus, textarea:focus {
border-color: #38bdf8;
box-shadow: 0 0 0 1px #38bdf8;
}
textarea {
min-height: 140px;
resize: vertical;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.col-6 {
flex: 1 1 260px;
}
button {
padding: 0.6rem 1.2rem;
border-radius: 999px;
border: none;
font-size: 0.9rem;
cursor: pointer;
margin-top: 0.75rem;
background: linear-gradient(135deg, #38bdf8, #6366f1);
color: white;
font-weight: 600;
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.4);
}
button:hover {
filter: brightness(1.05);
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
box-shadow: 0 6px 18px rgba(37,99,235,0.6);
}
pre {
background: #020617;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
overflow: auto;
font-size: 0.8rem;
border: 1px solid #1f2937;
max-height: 360px;
}
.subtitle {
font-size: 0.85rem;
color: #9ca3af;
margin-bottom: 1rem;
}
.pill {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 999px;
background: rgba(148, 163, 184, 0.2);
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
.pill span {
opacity: 0.8;
}
.small {
font-size: 0.8rem;
color: #9ca3af;
margin-top: 0.4rem;
}
.btn-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
}
.btn-secondary {
background: transparent;
border: 1px solid #4b5563;
box-shadow: none;
color: #e5e7eb;
}
.btn-secondary:hover {
background: #111827;
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
}
@media (max-width: 640px) {
body {
padding: 1rem;
}
.card {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Generator XML pentru Word Custom XML Part</h1>
<p class="subtitle">
Introdu câmpurile (unul pe linie) și obții XML pentru <strong>Custom XML Part</strong>, plus XPaths pentru mapping în Word.
</p>
<div class="card">
<div class="row">
<div class="col-6">
<label for="nsUri">Namespace URI (obligatoriu)</label>
<input id="nsUri" type="text"
value="http://schemas.beletage.ro/word/data">
<div class="small">
Exemplu: <code>http://schemas.firma-ta.ro/word/contract</code>
</div>
</div>
<div class="col-6">
<label for="rootElement">Nume element rădăcină</label>
<input id="rootElement" type="text" value="Root">
<div class="small">
Exemplu: <code>ContractData</code>, <code>ClientInfo</code> etc.
</div>
</div>
</div>
<div style="margin-top:1rem;">
<label for="fieldList">Lista de câmpuri (unul pe linie)</label>
<textarea id="fieldList" placeholder="Exemplu:
NumeClient
Adresa
DataContract
ValoareTotala"></textarea>
<div class="small">
Numele va fi curățat automat pentru a fi valid ca nume de element XML
(spațiile devin <code>_</code>, caracterele ciudate se elimină).
</div>
</div>
<div class="btn-row">
<button type="button" onclick="generateXML()">Generează XML</button>
<button type="button" class="btn-secondary" onclick="fillDemo()">Exemplu demo</button>
</div>
</div>
<div class="card">
<div class="pill"><strong>1</strong><span>Custom XML Part (item1.xml)</span></div>
<pre id="xmlOutput"></pre>
<div class="btn-row">
<button type="button" class="btn-secondary" onclick="copyToClipboard('xmlOutput')">
Copiază XML
</button>
<button type="button" class="btn-secondary" onclick="downloadXML()">
Descarcă item1.xml
</button>
</div>
</div>
<div class="card">
<div class="pill"><strong>2</strong><span>XPaths pentru mapping în Word</span></div>
<pre id="xpathOutput"></pre>
<button type="button" class="btn-secondary" onclick="copyToClipboard('xpathOutput')">
Copiază XPaths
</button>
<p class="small">
În Word &rarr; <strong>Developer</strong> &rarr; <strong>XML Mapping Pane</strong> &rarr; alegi Custom XML Part-ul
&rarr; pentru fiecare câmp, click dreapta &rarr; <em>Insert Content Control</em> &rarr; tipul dorit.
</p>
</div>
</div>
<script>
function sanitizeXmlName(name) {
if (!name) return null;
let n = name.trim();
if (!n) return null;
// înlocuim spații cu underscore
n = n.replace(/\s+/g, "_");
// eliminăm caractere invalide pentru nume de element XML
n = n.replace(/[^A-Za-z0-9_.-]/g, "");
// numele XML nu are voie să înceapă cu cifră sau punct sau cratimă
if (!/^[A-Za-z_]/.test(n)) {
n = "_" + n;
}
return n || null;
}
function generateXML() {
const nsUri = document.getElementById("nsUri").value.trim();
const root = sanitizeXmlName(document.getElementById("rootElement").value) || "Root";
const fieldRaw = document.getElementById("fieldList").value;
const xmlOutput = document.getElementById("xmlOutput");
const xpathOutput = document.getElementById("xpathOutput");
if (!nsUri) {
alert("Te rog completează Namespace URI.");
return;
}
const lines = fieldRaw.split(/\r?\n/)
.map(l => l.trim())
.filter(l => l.length > 0);
const fields = [];
const used = new Set();
for (let line of lines) {
const clean = sanitizeXmlName(line);
if (!clean) continue;
let finalName = clean;
let idx = 2;
while (used.has(finalName)) {
finalName = clean + "_" + idx;
idx++;
}
used.add(finalName);
fields.push({ original: line, xmlName: finalName });
}
if (fields.length === 0) {
xmlOutput.textContent = "<!-- Niciun câmp valid. Completează lista de câmpuri. -->";
xpathOutput.textContent = "";
return;
}
// Generăm XML-ul pentru Custom XML Part
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += `<${root} xmlns="${nsUri}">\n`;
for (const f of fields) {
xml += ` <${f.xmlName}></${f.xmlName}>\n`;
}
xml += `</${root}>\n`;
xmlOutput.textContent = xml;
// Generăm lista de XPaths
let xpaths = `Namespace: ${nsUri}\nRoot: /${root}\n\nCâmpuri:\n`;
for (const f of fields) {
xpaths += `- ${f.original} => /${root}/${f.xmlName}\n`;
}
xpathOutput.textContent = xpaths;
}
function copyToClipboard(elementId) {
const el = document.getElementById(elementId);
if (!el || !el.textContent) return;
navigator.clipboard.writeText(el.textContent)
.then(() => alert("Copiat în clipboard."))
.catch(() => alert("Nu am reușit să copiez în clipboard."));
}
function downloadXML() {
const xmlText = document.getElementById("xmlOutput").textContent;
if (!xmlText || xmlText.startsWith("<!--")) {
alert("Nu există XML valid de descărcat.");
return;
}
const blob = new Blob([xmlText], { type: "application/xml" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "item1.xml";
a.click();
URL.revokeObjectURL(url);
}
function fillDemo() {
document.getElementById("nsUri").value = "http://schemas.beletage.ro/word/contract";
document.getElementById("rootElement").value = "ContractData";
document.getElementById("fieldList").value = [
"NumeClient",
"AdresaClient",
"Proiect",
"DataContract",
"ValoareTotala",
"Moneda",
"TermenExecutie"
].join("\n");
generateXML();
}
</script>
</body>
</html>