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