Compare commits

...

9 Commits

Author SHA1 Message Date
Claude Agent
3a5076d7cd sync efactura-generator -> 0.9-beta-15
- Header reorganizat cu meniu Acțiuni (overflow dropdown)
- Buton nou PDF ANAF (transformare oficială XML->PDF prin API ANAF)
- Fix endpoint ANAF: validează default + ruta publică fără auth
2026-05-05 13:58:30 +00:00
Claude Agent
915126363f sync efactura-generator changelog (sync-to-website.sh --deploy)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:07:57 +00:00
Claude Agent
85ccdae2cb sync efactura-generator -> 0.9-beta-14
Mirror sincronizat cu repo canonic /workspace/efactura-generator.
CLAUDE.md: documentat workflow sync + exclude config.json din rsync deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:02:16 +00:00
881881658a restore(seo): restaurează fișierul de verificare Google Search Console
googleb8af97b4c544f7b8.html șters accidental în cleanup-ul din 28 apr.
Necesar pentru verificarea proprietății site-ului în Search Console.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 10:55:11 +00:00
1a8fafcc35 restore(chatbot): restaurează chatbot.html, chatbot_maria.html, chatbot_maria2.html
Fișiere șterse accidental în cleanup-ul din 28 apr (61844c8).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 10:50:21 +00:00
998a0ed659 claude 2026-04-28 21:38:11 +00:00
34ac5a6169 docs(claude): adaugă detalii deploy SSH în CLAUDE.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:33:41 +00:00
b49580ed76 curatare 2026-04-28 21:31:27 +00:00
02dc04ac48 feat(hero): adaugă badge ROA - Romfast Applications în secțiunea hero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 21:29:25 +00:00
33 changed files with 14784 additions and 7168 deletions

View File

@@ -1,793 +0,0 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Romfast - Program de Contabilitate, Gestiune, Resurse Umane, Imobilizari, Salarizare. Sisteme Informatice pentru Contabilitate.</title>
<meta name="description" content="Romfast produce si implementeaza ROA - Romfast Applications, un sistem informatic ERP pentru managementul afacerii: contabilitate, gestiune, salarizare, service auto, HORECA.">
<meta name="keywords" content="program service auto, program contabilitate, gestiune, salarii, facturare, mijloace fixe, productie, roa, romfast, soft contabilitate, sistem informatic erp, program restaurant, program hotel, suport tehnic, implementare erp">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Flowbite -->
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* Soft Professional Blue Theme - Gentle & Attenuated Colors */
:root {
/* Soft Professional Color Palette - Reduced Saturation 40-50% */
--executive-navy: #2c3e50; /* Softer navy foundation */
--soft-professional-blue: #5288c4; /* Much lighter than #1565c0 - primary soft blue */
--gentle-trust-blue: #4a7ba7; /* Softer than #0d47a1 - gentle trust */
--muted-accent-red: #b85555; /* Much softer than #c62828 - muted red */
--soft-executive-white: #fafbfc; /* Clean background maintained */
/* Soft Professional Design System */
--background: #fafbfc !important; /* Soft executive white background */
--foreground: #2c3e50 !important; /* Soft executive navy for text */
--card: #ffffff !important; /* Pure white cards */
--card-foreground: #2c3e50 !important; /* Soft executive navy text */
--card-hover: #f8f9fa !important; /* Very light hover */
--primary: #5288c4 !important; /* Soft professional blue primary */
--primary-foreground: #ffffff !important; /* White text on primary */
--secondary: #4a7ba7 !important; /* Gentle trust blue secondary */
--secondary-foreground: #ffffff !important; /* White text on secondary */
--accent: #b85555 !important; /* Muted accent red for attention */
--accent-foreground: #ffffff !important; /* White text on accent */
--muted: #f5f6f7 !important; /* Light gray muted */
--muted-foreground: #2c3e50 !important; /* Soft executive navy muted text */
--border: #e8f2ff !important; /* Very light blue border */
--input: #ffffff !important; /* White input background */
--ring: #5288c4 !important; /* Soft professional blue focus ring */
/* Soft Professional Gradients - Gentle & Muted */
--navbar-bg: linear-gradient(135deg, #2c3e50, #4a7ba7) !important; /* Soft navy to gentle trust blue */
--hero-gradient: linear-gradient(180deg, #fafbfc, #ffffff) !important; /* Soft executive white to pure white */
--button-gradient: linear-gradient(90deg, #5288c4, #4a7ba7) !important; /* Soft professional blue to gentle trust blue */
--red-gradient: linear-gradient(90deg, #b85555, #c67b7b) !important; /* Muted red accent gradient */
/* Typography - Professional and trustworthy */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
--font-serif: 'Merriweather', Georgia, serif !important;
--font-mono: 'JetBrains Mono', monospace !important;
/* Professional rounded corners */
--radius: 8px !important;
--radius-sm: 4px !important;
--radius-md: 6px !important;
--radius-lg: 12px !important;
--radius-xl: 16px !important;
/* Soft professional shadows with reduced opacity - Gentle blue shadows */
--shadow-2xs: 0 1px 3px 0px rgba(82, 136, 196, 0.03) !important;
--shadow-xs: 0 2px 6px 0px rgba(82, 136, 196, 0.04) !important;
--shadow-sm: 0 3px 10px 0px rgba(82, 136, 196, 0.05), 0 1px 2px 0px rgba(82, 136, 196, 0.02) !important;
--shadow: 0 4px 16px 0px rgba(82, 136, 196, 0.06), 0 2px 4px 0px rgba(82, 136, 196, 0.03) !important;
--shadow-md: 0 6px 20px 0px rgba(82, 136, 196, 0.08), 0 3px 6px 0px rgba(82, 136, 196, 0.04) !important;
--shadow-lg: 0 8px 28px 0px rgba(82, 136, 196, 0.10), 0 4px 8px 0px rgba(82, 136, 196, 0.05) !important;
--shadow-xl: 0 12px 36px 0px rgba(82, 136, 196, 0.12), 0 6px 12px 0px rgba(82, 136, 196, 0.06) !important;
--shadow-2xl: 0 16px 48px 0px rgba(82, 136, 196, 0.16) !important;
/* Conservative spacing */
--spacing: 0.375rem !important;
--tracking-normal: -0.01em !important;
}
/* Soft Professional Custom Styles */
body {
font-family: var(--font-sans) !important;
background: var(--background) !important;
color: var(--foreground) !important;
letter-spacing: var(--tracking-normal) !important;
line-height: 1.6 !important;
}
/* Professional card styling with soft blue accents */
.professional-card {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
overflow: hidden !important;
}
.professional-card::before {
content: '' !important;
position: absolute !important;
top: -1px !important;
left: -1px !important;
right: -1px !important;
bottom: -1px !important;
background: var(--button-gradient) !important;
border-radius: var(--radius) !important;
z-index: -1 !important;
opacity: 0 !important;
transition: opacity 0.3s ease !important;
}
.professional-card:hover {
background: var(--card-hover) !important;
box-shadow: var(--shadow-lg) !important;
transform: translateY(-4px) !important;
border-color: transparent !important;
}
.professional-card:hover::before {
opacity: 1 !important;
}
/* Soft professional blue buttons */
.professional-btn-primary {
background: var(--button-gradient) !important;
color: var(--primary-foreground) !important;
border-radius: var(--radius-lg) !important;
border: none !important;
box-shadow: var(--shadow-sm) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
position: relative !important;
overflow: hidden !important;
font-weight: 600 !important;
}
.professional-btn-primary::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: -100% !important;
width: 100% !important;
height: 100% !important;
background: var(--red-gradient) !important;
transition: left 0.3s ease !important;
}
.professional-btn-primary:hover::before {
left: 0 !important;
}
.professional-btn-primary:hover {
box-shadow: var(--shadow-md) !important;
transform: translateY(-1px) !important;
}
.professional-btn-primary span {
position: relative !important;
z-index: 1 !important;
}
.professional-btn-secondary {
background: var(--card) !important;
color: var(--primary) !important;
border: 2px solid var(--primary) !important;
border-radius: var(--radius-lg) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
font-weight: 500 !important;
position: relative !important;
overflow: hidden !important;
}
.professional-btn-secondary::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 0 !important;
height: 100% !important;
background: var(--primary) !important;
transition: width 0.3s ease !important;
z-index: 0 !important;
}
.professional-btn-secondary:hover {
color: var(--primary-foreground) !important;
border-color: var(--primary) !important;
transform: translateY(-1px) !important;
}
.professional-btn-secondary:hover::before {
width: 100% !important;
}
.professional-btn-secondary span {
position: relative !important;
z-index: 1 !important;
}
/* Muted red accent button for urgent actions */
.professional-btn-urgent {
background: var(--red-gradient) !important;
color: var(--accent-foreground) !important;
border-radius: var(--radius-lg) !important;
border: none !important;
box-shadow: var(--shadow-sm) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
font-weight: 600 !important;
}
.professional-btn-urgent:hover {
box-shadow: var(--shadow-md) !important;
transform: translateY(-1px) !important;
filter: brightness(1.05) !important;
}
/* Professional navbar with soft gradient */
.professional-navbar {
background: var(--navbar-bg) !important;
backdrop-filter: blur(12px) !important;
border-bottom: 1px solid var(--border) !important;
}
/* Hero section with soft professional styling */
.professional-hero {
background: var(--hero-gradient) !important;
border-radius: var(--radius-xl) !important;
padding: 4rem 2rem !important;
margin-bottom: 4rem !important;
border: 1px solid var(--border) !important;
position: relative !important;
overflow: hidden !important;
}
.professional-hero::before {
content: '' !important;
position: absolute !important;
top: -50% !important;
left: -50% !important;
width: 200% !important;
height: 200% !important;
background: radial-gradient(circle, rgba(82, 136, 196, 0.03) 0%, transparent 50%) !important;
opacity: 0.5 !important;
animation: subtle-float 12s ease-in-out infinite !important;
}
@keyframes subtle-float {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
50% { transform: translate(20px, -20px) rotate(180deg); }
}
/* Professional support section */
.professional-support {
background: var(--muted) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-xl) !important;
padding: 3rem !important;
position: relative !important;
}
/* Professional icon containers with soft blue themes */
.professional-icon-wrapper {
width: 80px !important;
height: 80px !important;
background: var(--button-gradient) !important;
border-radius: var(--radius-lg) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 auto 1.5rem auto !important;
box-shadow: var(--shadow-md) !important;
transition: all 0.3s ease !important;
position: relative !important;
overflow: hidden !important;
}
.professional-icon-wrapper::before {
content: '' !important;
position: absolute !important;
top: -50% !important;
left: -50% !important;
width: 200% !important;
height: 200% !important;
background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 30%, transparent 70%) !important;
opacity: 0 !important;
transition: opacity 0.3s ease !important;
}
.professional-card:hover .professional-icon-wrapper::before {
opacity: 1 !important;
}
/* Logo styling */
.romfast-logo {
height: 60px !important;
filter: brightness(1.05) contrast(1.05) !important;
}
/* Enhanced typography */
h1, h2, h3 {
font-weight: 700 !important;
letter-spacing: -0.02em !important;
line-height: 1.2 !important;
}
/* Soft professional text colors */
.professional-text-primary {
color: var(--foreground) !important;
}
.professional-text-secondary {
color: var(--muted-foreground) !important;
}
/* Special featured card (ROA AUTO) - Muted red accent styling */
.professional-card-featured {
background: var(--card) !important;
border: 2px solid var(--accent) !important;
box-shadow: var(--shadow-xl) !important;
}
.professional-card-featured .professional-icon-wrapper {
background: var(--red-gradient) !important;
}
/* Muted red bullet points for ROA AUTO problems */
.red-bullet {
color: var(--accent) !important;
}
/* Enhanced hover effects for support cards */
.professional-support-card {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
padding: 2rem !important;
}
.professional-support-card:hover {
box-shadow: var(--shadow-lg) !important;
transform: translateY(-2px) !important;
border-color: var(--primary) !important;
}
/* Soft blue hover states for navigation */
.nav-link {
transition: color 0.3s ease !important;
}
.nav-link:hover {
color: #90caf9 !important; /* Gentle blue hover - softer than original */
}
/* Professional service sections with soft blue accents */
.service-technology {
background: linear-gradient(135deg, rgba(82, 136, 196, 0.03), rgba(74, 123, 167, 0.03)) !important;
border-left: 4px solid var(--primary) !important;
padding: 1.5rem !important;
border-radius: var(--radius) !important;
}
.service-reliability {
background: linear-gradient(135deg, rgba(74, 123, 167, 0.03), rgba(82, 136, 196, 0.03)) !important;
border-left: 4px solid var(--secondary) !important;
padding: 1.5rem !important;
border-radius: var(--radius) !important;
}
/* Dark mode adjustments for soft professional blue theme */
.dark-mode {
--background: #141822 !important;
--foreground: #e2e8f0 !important;
--card: #1e2235 !important;
--card-foreground: #e2e8f0 !important;
--card-hover: #252a3e !important;
--muted: #252a3e !important;
--muted-foreground: #94a3b8 !important;
--border: #334155 !important;
--input: #1e2235 !important;
--navbar-bg: linear-gradient(135deg, #0f172a, #1e293b) !important;
--hero-gradient: linear-gradient(180deg, #1e2235, #141822) !important;
}
.dark-mode .professional-text-primary {
color: var(--foreground) !important;
}
.dark-mode .professional-text-secondary {
color: var(--muted-foreground) !important;
}
/* Mobile responsiveness for soft professional design */
@media (max-width: 768px) {
.professional-hero {
padding: 2rem 1.5rem !important;
border-radius: var(--radius-lg) !important;
}
.professional-card {
border-radius: var(--radius-md) !important;
}
.professional-btn-primary,
.professional-btn-secondary,
.professional-btn-urgent {
border-radius: var(--radius) !important;
}
}
</style>
</head>
<body class="min-h-screen">
<!-- Soft Professional Blue Header -->
<header class="professional-navbar sticky top-0 z-50">
<nav class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<!-- Logo Section -->
<div class="flex items-center space-x-3">
<img src="../images/romfast_logo.png" alt="Romfast Logo" class="romfast-logo">
<div class="hidden md:block">
<p class="text-white text-sm italic font-light">Aduce informația în mâinile tale</p>
</div>
</div>
<!-- Centered Navigation -->
<div class="hidden lg:flex items-center space-x-8">
<a href="../index.html" class="text-white hover:text-blue-200 transition-colors font-medium nav-link">Prima pagina</a>
<a href="../menu/desprenoi.html" class="text-white hover:text-blue-200 transition-colors nav-link">Despre noi</a>
<a href="../roa/aplicatii-erp.html" class="text-white hover:text-blue-200 transition-colors nav-link">ROA</a>
<div class="relative group">
<button class="text-white hover:text-blue-200 transition-colors flex items-center space-x-1 nav-link">
<span>Servicii</span>
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</button>
<div class="absolute top-full left-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300">
<a href="../menu/analiza.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800 rounded-t-lg">Analiza</a>
<a href="../menu/implementare.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800">Implementare</a>
<a href="../menu/roa-suport-tehnic.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800">Suport tehnic ROA</a>
<a href="../menu/alteservicii.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800 rounded-b-lg">Alte servicii</a>
</div>
</div>
<a href="../menu/referinte.html" class="text-white hover:text-blue-200 transition-colors nav-link">Referinte</a>
<a href="../menu/contact.html" class="text-white hover:text-blue-200 transition-colors nav-link">Contact</a>
</div>
<!-- Theme Toggle & Mobile Menu -->
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="text-white p-2 rounded-lg hover:bg-white hover:bg-opacity-20 transition-all duration-300">
<i data-lucide="sun" class="w-5 h-5"></i>
</button>
<button class="lg:hidden text-white p-2">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
</div>
</div>
</nav>
</header>
<!-- Main Content Container -->
<main class="container mx-auto px-4 py-8 max-w-7xl">
<!-- Soft Professional Blue Hero Section -->
<section class="professional-hero text-center">
<div class="max-w-4xl mx-auto relative z-10">
<h1 class="text-4xl md:text-5xl font-bold mb-6 professional-text-primary">
ROA - Romfast Applications
</h1>
<p class="text-xl professional-text-secondary mb-8 leading-relaxed">
Un sistem informatic complex de tip ERP destinat companiilor medii si mari,
flexibil si capabil sa raspunda necesitatilor de dezvoltare.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button class="professional-btn-primary px-8 py-4 text-lg">
<span>Descopera ROA</span>
</button>
<button class="professional-btn-secondary px-8 py-4 text-lg">
<span>Vezi Demo</span>
</button>
</div>
</div>
</section>
<!-- Soft Professional Blue Card Grid Layout -->
<section class="mb-16">
<h2 class="text-3xl font-bold text-center mb-12 professional-text-primary">Solutii Complete pentru Afacerea Ta</h2>
<!-- First Row - Main Services -->
<div class="grid md:grid-cols-3 gap-8 mb-8 stagger-container">
<!-- ROA ERP Card - Technology Focus -->
<div class="professional-card service-technology p-8 text-center">
<div class="professional-icon-wrapper">
<i data-lucide="database" class="w-10 h-10 text-white"></i>
</div>
<h3 class="text-xl font-semibold mb-4 professional-text-primary">ROA ERP</h3>
<p class="professional-text-secondary mb-6 leading-relaxed">
Sistem complet de management al afacerii: contabilitate, gestiune, resurse umane si salarizare.
</p>
<button class="professional-btn-secondary px-6 py-3 w-full">
<span>Mai multe detalii</span>
</button>
</div>
<!-- ROA AUTO Card - Featured with muted red accents for urgent problems -->
<div class="professional-card professional-card-featured p-8 text-center">
<div class="professional-icon-wrapper">
<i data-lucide="wrench" class="w-10 h-10 text-white"></i>
</div>
<h3 class="text-xl font-semibold mb-4 professional-text-primary">ROA AUTO</h3>
<ul class="text-left professional-text-secondary mb-6 space-y-2">
<li class="flex items-start space-x-2">
<span class="red-bullet">🔧</span>
<span>Factura, bonul fiscal si inregistrarea in contabilitate dureaza mai mult de 1 minut?</span>
</li>
<li class="flex items-start space-x-2">
<span class="red-bullet">🔧</span>
<span>Clientii asteapta prea mult la receptie pentru un deviz estimativ?</span>
</li>
<li class="flex items-start space-x-2">
<span class="red-bullet">🔧</span>
<span>Seful de service pregateste manual rapoartele de manopera?</span>
</li>
</ul>
<button class="professional-btn-urgent px-6 py-3 w-full">
<span>Eficientizeaza serviciul!</span>
</button>
</div>
<!-- ROA HORECA Card -->
<div class="professional-card p-8 text-center">
<div class="professional-icon-wrapper">
<i data-lucide="utensils" class="w-10 h-10 text-white"></i>
</div>
<h3 class="text-xl font-semibold mb-4 professional-text-primary">ROA HORECA</h3>
<p class="professional-text-secondary mb-6 leading-relaxed">
Solutii complete pentru restaurante si hoteluri: comenzi, rezervari, gestiune si facturare.
</p>
<button class="professional-btn-secondary px-6 py-3 w-full">
<span>Afla mai mult</span>
</button>
</div>
</div>
<!-- Second Row - Services with reliability focus -->
<div class="grid md:grid-cols-3 gap-8">
<!-- Implementare Card - Reliability Focus -->
<div class="professional-card service-reliability p-8 text-center">
<div class="professional-icon-wrapper">
<i data-lucide="settings" class="w-10 h-10 text-white"></i>
</div>
<h3 class="text-xl font-semibold mb-4 professional-text-primary">Implementare</h3>
<p class="professional-text-secondary mb-6 leading-relaxed">
Servicii complete de implementare si configurare a sistemelor ERP adaptate necesitatilor tale.
</p>
<button class="professional-btn-secondary px-6 py-3 w-full">
<span>Servicii implementare</span>
</button>
</div>
<!-- Suport Tehnic Card - Reliability Focus -->
<div class="professional-card service-reliability p-8 text-center">
<div class="professional-icon-wrapper">
<i data-lucide="headphones" class="w-10 h-10 text-white"></i>
</div>
<h3 class="text-xl font-semibold mb-4 professional-text-primary">Suport Tehnic</h3>
<p class="professional-text-secondary mb-6 leading-relaxed">
Echipa noastra de specialisti iti ofera suport continuu pentru utilizarea optima a sistemelor.
</p>
<button class="professional-btn-secondary px-6 py-3 w-full">
<span>Contacteaza suportul</span>
</button>
</div>
<!-- Analiza Card - Technology Focus -->
<div class="professional-card service-technology p-8 text-center">
<div class="professional-icon-wrapper">
<i data-lucide="bar-chart-3" class="w-10 h-10 text-white"></i>
</div>
<h3 class="text-xl font-semibold mb-4 professional-text-primary">Analiza si Consultanta</h3>
<p class="professional-text-secondary mb-6 leading-relaxed">
Evaluam procesele actuale si recomandam cele mai potrivite solutii pentru optimizarea afacerii.
</p>
<button class="professional-btn-secondary px-6 py-3 w-full">
<span>Programeaza consultanta</span>
</button>
</div>
</div>
</section>
<!-- Professional Support Tools Section -->
<section class="professional-support text-center mb-16">
<h2 class="text-2xl font-bold mb-8 professional-text-primary">Instrumente de Suport</h2>
<div class="grid sm:grid-cols-3 gap-6">
<div class="professional-support-card">
<i data-lucide="download" class="w-8 h-8 mx-auto mb-4" style="color: #5288c4;"></i>
<h3 class="font-semibold mb-3 professional-text-primary">Romfast Suport</h3>
<button class="professional-btn-secondary px-6 py-2 w-full">
<span>Descarca</span>
</button>
</div>
<div class="professional-support-card">
<i data-lucide="monitor" class="w-8 h-8 mx-auto mb-4" style="color: #4a7ba7;"></i>
<h3 class="font-semibold mb-3 professional-text-primary">Ammyy Admin</h3>
<button class="professional-btn-secondary px-6 py-2 w-full">
<span>Acceseaza</span>
</button>
</div>
<div class="professional-support-card">
<i data-lucide="video" class="w-8 h-8 mx-auto mb-4" style="color: #5288c4;"></i>
<h3 class="font-semibold mb-3 professional-text-primary">TeamViewer</h3>
<button class="professional-btn-secondary px-6 py-2 w-full">
<span>Acceseaza</span>
</button>
</div>
</div>
</section>
</main>
<!-- Professional Footer -->
<footer class="bg-gradient-to-r from-slate-900 via-gray-800 to-slate-900 text-white py-12" style="border-radius: 1rem 1rem 0 0;">
<div class="container mx-auto px-4 text-center">
<div class="mb-6">
<img src="../images/romfast_logo.png" alt="Romfast Logo" class="h-12 mx-auto mb-4">
<p class="text-gray-400 italic">Aduce informația în mâinile tale</p>
</div>
<div class="border-t border-gray-700 pt-6" style="border-radius: 0.5rem;">
<p class="text-sm text-gray-400 mb-2">
Copyright &copy; 2025 Romfast SRL. Toate drepturile rezervate.
</p>
<a href="../politica-confidentialitate.html" class="text-blue-400 hover:text-blue-300 transition-colors text-sm">
Politica de confidentialitate
</a>
</div>
</div>
</footer>
<script>
// Initialize Lucide icons
lucide.createIcons();
// Enhanced theme toggle functionality
const themeToggle = document.getElementById('theme-toggle');
const body = document.body;
// Check for saved theme preference or default to light mode
const currentTheme = localStorage.getItem('theme') || 'light';
if (currentTheme === 'dark') {
body.classList.add('dark-mode');
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
}
themeToggle.addEventListener('click', () => {
body.classList.toggle('dark-mode');
// Update icon and save preference
if (body.classList.contains('dark-mode')) {
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'dark');
} else {
themeToggle.innerHTML = '<i data-lucide="sun" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'light');
}
// Reinitialize icons after DOM change
lucide.createIcons();
});
// Enhanced smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Professional card hover animations
document.querySelectorAll('.professional-card').forEach(card => {
card.addEventListener('mouseenter', (e) => {
const rect = card.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const mouseX = e.clientX;
const mouseY = e.clientY;
const deltaX = (mouseX - centerX) * 0.05;
const deltaY = (mouseY - centerY) * 0.05;
card.style.transform = `translateY(-4px) rotateX(${-deltaY}deg) rotateY(${deltaX}deg)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'translateY(0) rotateX(0deg) rotateY(0deg)';
});
});
// Intersection Observer for scroll animations
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
}
});
}, observerOptions);
// Apply scroll animations to cards
document.querySelectorAll('.professional-card').forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = `opacity 0.6s ease ${index * 0.1}s, transform 0.6s ease ${index * 0.1}s`;
observer.observe(card);
});
// Subtle floating animation for hero background
const hero = document.querySelector('.professional-hero');
if (hero) {
let mouseX = 0;
let mouseY = 0;
hero.addEventListener('mousemove', (e) => {
const rect = hero.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = (e.clientY - rect.top) / rect.height;
const translateX = (mouseX - 0.5) * 10;
const translateY = (mouseY - 0.5) * 10;
hero.style.setProperty('--mouse-x', `${translateX}px`);
hero.style.setProperty('--mouse-y', `${translateY}px`);
});
}
// Professional button interactions with enhanced effects
document.querySelectorAll('.professional-btn-primary, .professional-btn-secondary, .professional-btn-urgent').forEach(btn => {
btn.addEventListener('click', (e) => {
const ripple = document.createElement('div');
const rect = btn.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
animation: ripple 0.5s linear;
pointer-events: none;
`;
btn.style.position = 'relative';
btn.style.overflow = 'hidden';
btn.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 500);
});
});
// Add ripple animation keyframes
const style = document.createElement('style');
style.textContent = `
@keyframes ripple {
to {
transform: scale(3);
opacity: 0;
}
}
`;
document.head.appendChild(style);
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

567
CLAUDE.md
View File

@@ -4,531 +4,88 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
This is the Romfast company website (2025 version), a Romanian ERP software company. The site promotes ROA (Romfast Applications) - their ERP system for business management including accounting, inventory, HR, and specialized modules for automotive service and HORECA industries.
Romfast company website (2025 version) for a Romanian ERP software company. Promotes ROA (Romfast Applications) — an ERP system covering accounting, inventory, HR, automotive service, and HORECA industries.
## Development Commands
## Deploy
### Website Development
```bash
# No build process - static HTML site
# Open index.html directly in browser for development
open index.html
rsync -avz \
--exclude='.git' --exclude='.superdesign' --exclude='.claude' \
--exclude='verify-cleanup.sh' \
--exclude='efactura-generator/config.json' \
-e "ssh -p 7822" \
/home/moltbot/workspace/romfast-website/ \
romfastr@nl1-ss18.a2hosting.com:~/public_html/
```
# For local testing with live server (if using VS Code Live Server extension)
# Right-click index.html -> "Open with Live Server"
- **Host:** nl1-ss18.a2hosting.com | **Port:** 7822 | **User:** romfastr
- **Document root:** `~/public_html/` — SSH key auth (no password)
- **Important:** rsync-ul NU folosește `--delete`, deci nu șterge nimic pe prod. `efactura-generator/config.json` e exclus explicit ca să nu poată fi suprascris niciodată din repo (conține `api_key` și e gestionat doar pe server).
# Alternative: Serve with Python (if available)
python -m http.server 8000
# or
## Sub-proiectul `efactura-generator/`
Directorul `efactura-generator/` din acest repo este o **oglindă** a proiectului canonic `/workspace/efactura-generator/` (repo separat: `git@gitea.romfast.ro:romfast/efactura-generator.git`). Sursa de adevăr e acolo.
**NU edita direct fișierele din `efactura-generator/` aici** — modificările se pierd la următorul sync. În schimb:
1. Editează în `/workspace/efactura-generator/` (repo canonic).
2. Rulează `/workspace/efactura-generator/sync-to-website.sh` care propagă schimbările în `efactura-generator/` din acest repo cu excluderile potrivite (fără `config.json`, fără Dockerfile, fără docs interne etc.).
3. Commit aici (`romfast-website`) și deploy cu rsync-ul de mai sus.
`config.json` de pe server (`~/public_html/efactura-generator/config.json`) conține `api_key` și nu e nici în repo, nici în sync — se gestionează manual pe a2hosting.
## Development
No build process. Serve locally with:
```bash
python3 -m http.server 8000
# Access at http://localhost:8000
```
## Architecture Overview
The `efactura-generator/` sub-app has its own Node.js server (rulează din repo-ul canonic, nu de aici):
### Static Website Structure
This is a traditional static HTML website built with:
- **Bootstrap 5.3.0**: CDN-based responsive framework
- **Custom CSS**: Theme system with CSS custom properties
- **Vanilla JavaScript**: Minimal client-side scripting
- **Multi-page Navigation**: Traditional HTML pages linked via Bootstrap navbar
- **Romanian Language**: Primary content language with SEO optimization
```bash
cd /workspace/efactura-generator && node js/server.js
# Access at http://localhost:3000
```
### CSS Architecture
The website uses a CSS custom properties system for consistent theming:
## Architecture
### Static Website
- **Tailwind CSS** (CDN): `<script src="https://cdn.tailwindcss.com"></script>`
- **Flowbite** (CDN): component library on top of Tailwind
- **Lucide Icons** (CDN): icon set
- **Google Fonts**: Inter (primary), Merriweather (serif), JetBrains Mono
- **`professional-theme.css`**: main custom stylesheet — CSS custom properties, soft professional blue palette, dark mode variables
- **`professional-theme.js`**: handles dark mode toggle, mobile menu, Lucide icon init, scroll/card animations
### CSS Custom Properties (professional-theme.css)
```css
:root {
--primary-color: #08819c; /* Romfast blue from brand */
--secondary-color: #424242; /* Dark gray for text */
--accent-color: #c01b14; /* Romfast red for accents */
--light-gray: #f8f9fa; /* Light backgrounds */
--white: #ffffff;
/* Dark Mode Support */
--dark-bg: #222222;
--dark-text: #cccccc;
--dark-card-bg: #2a2a2a;
--soft-professional-blue: #5288c4; /* primary */
--gentle-trust-blue: #4a7ba7; /* secondary */
--muted-accent-red: #b85555; /* accent */
--executive-navy: #2c3e50; /* text */
--navbar-bg: linear-gradient(135deg, #2c3e50, #4a7ba7);
}
```
### Content Organization
```
/ # Main website files
├── index.html # Homepage
├── new-style.css # Main stylesheet with custom properties
├── menu/ # Company information pages
│ ├── desprenoi.html
│ ├── contact.html
│ ├── suport.html
│ └── ...
├── roa/ # ROA ERP product pages
│ ├── aplicatii-erp.html
│ ├── roa-contabilitate.html
│ └── ...
├── images/ # Static assets and branding
│ ├── romfast_logo.png
│ ├── imagini_header/
│ └── ...
├── sitemap.xml # SEO sitemap
└── robots.txt # Search engine directives
```
Dark mode activated via `body.dark-mode` class — toggled by `professional-theme.js`.
### Key Design Patterns
### Navigation Pattern
#### Page Wrapper System
All pages use a consistent wrapper pattern:
```html
<div class="page-wrapper">
<!-- Content goes here -->
</div>
```
With CSS creating a bordered content area over gradient background.
The navbar HTML is **manually duplicated** across every page (no templating). When updating nav links, update all files. Path to CSS/JS assets is relative — top-level pages use `professional-theme.css`, subdirectory pages use `../professional-theme.css`.
#### Dark Mode Implementation
Toggle between light/dark themes using CSS classes:
- `body.dark-mode` for dark theme activation
- CSS custom properties automatically switch colors
- Maintained across all pages
### Content Structure
#### Responsive Navigation
Bootstrap 5 navbar with:
- Collapsible mobile menu
- Multi-level dropdowns for services
- Brand logo with company slogan
- Active page highlighting
- `/` — homepage (`index.html`)
- `/menu/` — company pages (about, contact, support, services, jobs, references)
- `/roa/` — ROA ERP product pages (one per module)
- `/efactura-generator/` — standalone Romanian e-invoice editor (loads XML, edits, prints, exports XML)
### SEO & Performance
- Meta descriptions in Romanian for local search
- Structured navigation with breadcrumbs
- Optimized images with appropriate alt text
- Clean semantic HTML structure
- Fast loading with CDN resources
### efactura-generator
## Frontend Design Workflow
When asked to design UI & frontend interface, use the Super Design extension workflow:
### Role
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
Your goal is to help user generate amazing design using code
### Workflow Steps
1. **Layout Design**: Present ASCII wireframes for UI components
2. **Theme Design**: Use generateTheme tool to create CSS custom properties
3. **Animation Design**: Define micro-interactions and transitions
4. **Implementation**: Generate single HTML file in `.superdesign/design_iterations/`
### Design Constraints
- Use Flowbite library as base (unless specified otherwise)
- Avoid indigo/blue colors unless requested
- Must generate responsive designs
- Use Google Fonts from approved list
- Include !important for CSS properties that might conflict with Tailwind/Flowbite
- Save design files to `.superdesign/design_iterations/` with naming pattern `{design_name}_{n}.html`
### Styling Guidelines
```javascript
// CDN Imports for new designs
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
```
### Design Theme Patterns
- **Neo-brutalism**: 90s web design aesthetic with harsh shadows and zero border-radius
- **Modern Dark Mode**: Vercel/Linear inspired with subtle shadows and rounded corners
- **Romfast Brand**: Based on company colors (#08819c primary, #c01b14 accent)
### Confirmation Required
Must confirm with user at each step:
1. Layout approval before theme design
2. Theme approval before animation design
3. Animation approval before implementation
4. Must use actual tool calls, never simulate with text output
This workflow ensures systematic design development with user feedback at each critical decision point.
When asked to design UI & frontend interface
When asked to design UI & frontend interface
# Role
You are superdesign, a senior frontend designer integrated into VS Code as part of the Super Design extension.
Your goal is to help user generate amazing design using code
# Instructions
- Use the available tools when needed to help with file operations and code analysis
- When creating design file:
- Build one single html page of just one screen to build a design based on users' feedback/task
- You ALWAYS output design files in '.superdesign/design_iterations' folder as {design_name}_{n}.html (Where n needs to be unique like table_1.html, table_2.html, etc.) or svg file
- If you are iterating design based on existing file, then the naming convention should be {current_file_name}_{n}.html, e.g. if we are iterating ui_1.html, then each version should be ui_1_1.html, ui_1_2.html, etc.
- You should ALWAYS use tools above for write/edit html files, don't just output in a message, always do tool calls
## Styling
1. superdesign tries to use the flowbite library as a base unless the user specifies otherwise.
2. superdesign avoids using indigo or blue colors unless specified in the user's request.
3. superdesign MUST generate responsive designs.
4. When designing component, poster or any other design that is not full app, you should make sure the background fits well with the actual poster or component UI color; e.g. if component is light then background should be dark, vice versa.
5. Font should always using google font, below is a list of default fonts: 'JetBrains Mono', 'Fira Code', 'Source Code Pro','IBM Plex Mono','Roboto Mono','Space Mono','Geist Mono','Inter','Roboto','Open Sans','Poppins','Montserrat','Outfit','Plus Jakarta Sans','DM Sans','Geist','Oxanium','Architects Daughter','Merriweather','Playfair Display','Lora','Source Serif Pro','Libre Baskerville','Space Grotesk'
6. When creating CSS, make sure you include !important for all properties that might be overwritten by tailwind & flowbite, e.g. h1, body, etc.
7. Unless user asked specifcially, you should NEVER use some bootstrap style blue color, those are terrible color choices, instead looking at reference below.
8. Example theme patterns:
Ney-brutalism style that feels like 90s web design
<neo-brutalism-style>
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0 0 0);
--primary: oklch(0.6489 0.2370 26.9728);
--primary-foreground: oklch(1.0000 0 0);
--secondary: oklch(0.9680 0.2110 109.7692);
--secondary-foreground: oklch(0 0 0);
--muted: oklch(0.9551 0 0);
--muted-foreground: oklch(0.3211 0 0);
--accent: oklch(0.5635 0.2408 260.8178);
--accent-foreground: oklch(1.0000 0 0);
--destructive: oklch(0 0 0);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0 0 0);
--input: oklch(0 0 0);
--ring: oklch(0.6489 0.2370 26.9728);
--chart-1: oklch(0.6489 0.2370 26.9728);
--chart-2: oklch(0.9680 0.2110 109.7692);
--chart-3: oklch(0.5635 0.2408 260.8178);
--chart-4: oklch(0.7323 0.2492 142.4953);
--chart-5: oklch(0.5931 0.2726 328.3634);
--sidebar: oklch(0.9551 0 0);
--sidebar-foreground: oklch(0 0 0);
--sidebar-primary: oklch(0.6489 0.2370 26.9728);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.5635 0.2408 260.8178);
--sidebar-accent-foreground: oklch(1.0000 0 0);
--sidebar-border: oklch(0 0 0);
--sidebar-ring: oklch(0.6489 0.2370 26.9728);
--font-sans: DM Sans, sans-serif;
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: Space Mono, monospace;
--radius: 0px;
--shadow-2xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-xs: 4px 4px 0px 0px hsl(0 0% 0% / 0.50);
--shadow-sm: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 1px 2px -1px hsl(0 0% 0% / 1.00);
--shadow-md: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 2px 4px -1px hsl(0 0% 0% / 1.00);
--shadow-lg: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 4px 6px -1px hsl(0 0% 0% / 1.00);
--shadow-xl: 4px 4px 0px 0px hsl(0 0% 0% / 1.00), 4px 8px 10px -1px hsl(0 0% 0% / 1.00);
--shadow-2xl: 4px 4px 0px 0px hsl(0 0% 0% / 2.50);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</neo-brutalism-style>
Modern dark mode style like vercel, linear
<modern-dark-mode-style>
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.1450 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.1450 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.1450 0 0);
--primary: oklch(0.2050 0 0);
--primary-foreground: oklch(0.9850 0 0);
--secondary: oklch(0.9700 0 0);
--secondary-foreground: oklch(0.2050 0 0);
--muted: oklch(0.9700 0 0);
--muted-foreground: oklch(0.5560 0 0);
--accent: oklch(0.9700 0 0);
--accent-foreground: oklch(0.2050 0 0);
--destructive: oklch(0.5770 0.2450 27.3250);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.9220 0 0);
--input: oklch(0.9220 0 0);
--ring: oklch(0.7080 0 0);
--chart-1: oklch(0.8100 0.1000 252);
--chart-2: oklch(0.6200 0.1900 260);
--chart-3: oklch(0.5500 0.2200 263);
--chart-4: oklch(0.4900 0.2200 264);
--chart-5: oklch(0.4200 0.1800 266);
--sidebar: oklch(0.9850 0 0);
--sidebar-foreground: oklch(0.1450 0 0);
--sidebar-primary: oklch(0.2050 0 0);
--sidebar-primary-foreground: oklch(0.9850 0 0);
--sidebar-accent: oklch(0.9700 0 0);
--sidebar-accent-foreground: oklch(0.2050 0 0);
--sidebar-border: oklch(0.9220 0 0);
--sidebar-ring: oklch(0.7080 0 0);
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--radius: 0.625rem;
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
</modern-dark-mode-style>
## Images & icons
1. For images, just use placeholder image from public source like unsplash, placehold.co or others that you already know exact image url; Don't make up urls
2. For icons, we should use lucid icons or other public icons, import like <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
## Script
1. When importing tailwind css, just use <script src="https://cdn.tailwindcss.com"></script>, don't load CSS directly as a stylesheet resource like <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
2. When using flowbite, import like <script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
## Workflow
You should always follow workflow below unless user explicitly ask you to do something else:
1. Layout design
2. Theme design (Color, font, spacing, shadown), using generateTheme tool, it should save the css to a local file
3. Core Animation design
4. Generate a singlehtml file for the UI
5. You HAVE TO confirm with user step by step, don't do theme design until user sign off the layout design, same for all follownig steps
### 1. Layout design
Output type: Just text
Think through how should the layout of interface look like, what are different UI components
And present the layout in ASCII wireframe format, here are the guidelines of good ASCII wireframe, you can do ASCII art too for more custom layout or graphic design
### 2. Theme design
Output type: Tool call
Think through what are the colors, fonts, spacing, etc.
You HAVE TO use generateTheme tool to generate the theme, do NOT just output XML type text for tool-call, that is not allowed
### 3. Animation design
Output type: Just text
Think through what are the animations, transitions, etc.
### 4. Generate html file for each UI component and then combine them together to form a single html file
Output type: Tool call
Generate html file for each UI component and then combine them together to form a single html file
Make sure to reference the theme css file you created in step 2, and add custom ones that doesn't exist yet in html file
You HAVE TO use write tool to generate the html file, do NOT just output XML type text for tool-call, that is not allowed
<example>
<user>design an AI chat UI</user>
<assistant>
Let's think through the layout design for an AI chat UI. Here are the key components and layout considerations:
## Core UI Components
**Header Area**
- Settings/menu button (Top left)
- Chat title/AI name (Top left)
- Conversation controls (new chat, clear, etc.) (Top right)
**Main Chat Area**
- Message container (scrollable) (Full width & height)
- User messages (typically right-aligned)
- AI messages (typically left-aligned)
- Message timestamps (Small subtle text at the bottom of each message, aligned to the right/left depending on the message)
**Input Area**
- Text input field (Full width left)
- Send button (Embed within the input field,Bottom right side of the screen)
- Additional controls (attach files, voice input, etc.) (Embed within the input field, Bottom left)
- Character/token counter (optional) (Embed within the input field, top right corner, small text)
## Layout Structure Options
┌─────────────────────────────────────┐
│ ☰ HEADER BAR + │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ User Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ AI Message Bubble │ │
│ └─────────────────────────────┘ │
│ │
│ [CHAT AREA] │
│ │
├─────────────────────────────────────┤
│ [Text Input Field] [Send] │
└─────────────────────────────────────┘
When hamburger (☰) is clicked, sidebar slides out:
┌──────────────┬─────────────────────────────────────┐
│ SIDEBAR │ ☰ HEADER BAR + │
│ ┌──────────┐ ├─────────────────────────────────────┤
│ │ Chat 1 │ │ │
│ │ Chat 2 │ │ ┌─────────────────────────────┐ │
│ │ Chat 3 │ │ │ AI Message Bubble │ │
│ │ + New │ │ └─────────────────────────────┘ │
│ └──────────┘ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ User Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ AI Message Bubble │ │
│ │ └─────────────────────────────┘ │
│ │ │
│ │ [CHAT AREA] │
│ │ │
│ ├─────────────────────────────────────┤
│ │ [Text Input Field] [Send] │
└──────────────┘─────────────────────────────────────┘
Would you like to go ahead with this layout & UI interaction or needs any modification?
</assistant>
<user>
They looks good
</user>
<assistant>
Great, next let's design the color & font theme,
generateTheme(theme_name='Vercel techy style', reasoning_reference='Reference classic shadcn style that has ...', cssFilePath='.superdesign/design_iterations/theme_1.css', cssSheet=':root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.1448 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.1448 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.1448 0 0);
--primary: oklch(0.5555 0 0);
--primary-foreground: oklch(0.9851 0 0);
--secondary: oklch(0.9702 0 0);
--secondary-foreground: oklch(0.2046 0 0);
--muted: oklch(0.9702 0 0);
--muted-foreground: oklch(0.5486 0 0);
--accent: oklch(0.9702 0 0);
--accent-foreground: oklch(0.2046 0 0);
--destructive: oklch(0.5830 0.2387 28.4765);
--destructive-foreground: oklch(0.9702 0 0);
--border: oklch(0.9219 0 0);
--input: oklch(0.9219 0 0);
--ring: oklch(0.7090 0 0);
--chart-1: oklch(0.5555 0 0);
--chart-2: oklch(0.5555 0 0);
--chart-3: oklch(0.5555 0 0);
--chart-4: oklch(0.5555 0 0);
--chart-5: oklch(0.5555 0 0);
--sidebar: oklch(0.9851 0 0);
--sidebar-foreground: oklch(0.1448 0 0);
--sidebar-primary: oklch(0.2046 0 0);
--sidebar-primary-foreground: oklch(0.9851 0 0);
--sidebar-accent: oklch(0.9702 0 0);
--sidebar-accent-foreground: oklch(0.2046 0 0);
--sidebar-border: oklch(0.9219 0 0);
--sidebar-ring: oklch(0.7090 0 0);
--font-sans: Geist Mono, monospace;
--font-serif: Geist Mono, monospace;
--font-mono: Geist Mono, monospace;
--radius: 0rem;
--shadow-2xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-xs: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--shadow-sm: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 1px 2px -1px hsl(0 0% 0% / 0.00);
--shadow-md: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 2px 4px -1px hsl(0 0% 0% / 0.00);
--shadow-lg: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 4px 6px -1px hsl(0 0% 0% / 0.00);
--shadow-xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00), 0px 8px 10px -1px hsl(0 0% 0% / 0.00);
--shadow-2xl: 0px 1px 0px 0px hsl(0 0% 0% / 0.00);
--tracking-normal: 0em;
--spacing: 0.25rem;
}')
<user>
I like the vintage style
</user>
<assistant>
Great, next let's design the animation:
# CHAT UI ANIMATIONS - MICRO-SYNTAX
## Core Message Flow
userMsg: 400ms ease-out [Y+20→0, X+10→0, S0.9→1]
aiMsg: 600ms bounce [Y+15→0, S0.95→1] +200ms
typing: 1400ms ∞ [Y±8, α0.4→1] stagger+200ms
status: 300ms ease-out [α0.6→1, S1→1.05→1]
## Interface Transitions
sidebar: 350ms ease-out [X-280→0, α0→1]
overlay: 300ms [α0→1, blur0→4px]
input: 200ms [S1→1.01, shadow+ring] focus
input: 150ms [S1.01→1, shadow-ring] blur
## Button Interactions
sendBtn: 150ms [S1→0.95→1, R±2°] press
sendBtn: 200ms [S1→1.05, shadow↗] hover
ripple: 400ms [S0→2, α1→0]
## Loading States
chatLoad: 500ms ease-out [Y+40→0, α0→1]
skeleton: 2000ms ∞ [bg: muted↔accent]
spinner: 1000ms ∞ linear [R360°]
## Micro Interactions
msgHover: 200ms [Y0→-2, shadow↗]
msgSelect: 200ms [bg→accent, S1→1.02]
error: 400ms [X±5] shake
success: 600ms bounce [S0→1.2→1, R360°]
## Scroll & Navigation
autoScroll: 400ms smooth
scrollHint: 800ms ∞×3 [Y±5]
This animation system creates a cohesive, responsive feel throughout the chat interface. Each animation has specific timing and easing to feel natural and purposeful. Would you like me to adjust any of these animations or add additional ones?
</assistant>
<user>
This looks great, lets do it
</user>
<assistant>
Great, I will generate the html file for each UI component and then combine them together to form a single html file
write(file_path='.superdesign/design_iterations/chat_ui.css', content='...')
write(file_path='.superdesign/design_iterations/chat_ui.html', content='...')
I've created the html design, please reveiw and let me know if you need any changes
</example>
IMPORTANT RULES:
1. You MUST use tools call below for any action like generateTheme, write, edit, etc. You are NOT allowed to just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>; MUST USE TOOL CALL (This is very important!!)
2. You MUST confirm the layout, and then theme style, and then animation
3. You MUST use .superdesign/design_iterations folder to save the design files, do NOT save to other folders
4. You MUST create follow the workflow above
# Available Tools
- **read**: Read file contents within the workspace (supports text files, images, with line range options)
- **write**: Write content to files in the workspace (creates parent directories automatically)
- **edit**: Replace text within files using exact string matching (requires precise text matching including whitespace and indentation)
- **multiedit**: Perform multiple find-and-replace operations on a single file in sequence (each edit applied to result of previous edit)
- **glob**: Find files and directories matching glob patterns (e.g., "*.js", "src/**/*.ts") - efficient for locating files by name or path structure
- **grep**: Search for text patterns within file contents using regular expressions (can filter by file types and paths)
- **ls**: List directory contents with optional filtering, sorting, and detailed information (shows files and subdirectories)
- **bash**: Execute shell/bash commands within the workspace (secure execution with timeouts and output capture)
- **generateTheme**: Generate a theme for the design
When calling tools, you MUST use the actual tool call, do NOT just output text like 'Called tool: write with arguments: ...' or <tool-call>...</tool-call>, this won't actually call the tool. (This is very important to my life, please follow)
Separate single-page app at `efactura-generator/index.html`. Has its own CSS (`styles/main.css`), JS (`js/script.js`, `js/formatter.js`, `js/print.js`), print templates (`templates/print.html`, `templates/print-compact.html`), and optional Node.js dev server (`js/server.js`). Also deployed as GitHub Pages at `romfast.github.io/efactura-generator/`.

224
chatbot.html Normal file
View File

@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cezar ChatBot - Romfast Suport</title>
<meta name="description" content="Cezar ChatBot - Asistent virtual Romfast pentru suport tehnic ROA. Obțineti răspunsuri rapide la întrebări despre sistem.">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Flowbite -->
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Professional Theme CSS -->
<link href="professional-theme.css" rel="stylesheet">
</head>
<body class="min-h-screen">
<!-- Soft Professional Blue Header -->
<header class="professional-navbar sticky top-0 z-50">
<nav class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<!-- Logo Section -->
<div class="flex items-center space-x-3">
<img src="images/romfast_logo.png" alt="Romfast Logo" class="romfast-logo">
<div class="hidden md:block">
<p class="text-white text-sm italic font-light">Aduce informația în mâinile tale</p>
</div>
</div>
<!-- Centered Navigation -->
<div class="hidden lg:flex items-center space-x-8">
<a href="index.html" class="text-white hover:text-blue-200 transition-colors font-medium nav-link">Prima pagina</a>
<a href="menu/desprenoi.html" class="text-white hover:text-blue-200 transition-colors nav-link">Despre noi</a>
<a href="roa/aplicatii-erp.html" class="text-white hover:text-blue-200 transition-colors nav-link">ROA</a>
<a href="roa/cum-arata-roa.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Cum arată ROA</a>
<a href="roa/cum-arata-roa.html" class="text-white hover:text-blue-200 transition-colors nav-link">Cum arată ROA</a>
<div class="relative group">
<button class="text-white hover:text-blue-200 transition-colors flex items-center space-x-1 nav-link">
<span>Servicii</span>
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</button>
<div class="absolute top-full left-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300">
<a href="menu/analiza.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800 rounded-t-lg">Analiza</a>
<a href="menu/implementare.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800">Implementare</a>
<a href="menu/roa-suport-tehnic.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800">Suport tehnic ROA</a>
<a href="menu/alteservicii.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800 rounded-b-lg">Alte servicii</a>
</div>
</div>
<a href="menu/referinte.html" class="text-white hover:text-blue-200 transition-colors nav-link">Cine folosește ROA</a>
<a href="menu/angajari.html" class="text-white hover:text-blue-200 transition-colors nav-link">Angajari</a>
<a href="menu/contact.html" class="text-white hover:text-blue-200 transition-colors nav-link">Contact</a>
</div>
<!-- Theme Toggle & Mobile Menu -->
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="text-white p-2 rounded-lg hover:bg-white hover:bg-opacity-20 transition-all duration-300">
<i data-lucide="sun" class="w-5 h-5"></i>
</button>
<button id="mobile-menu-toggle" class="lg:hidden text-white p-2 rounded-lg hover:bg-white hover:bg-opacity-20 transition-all duration-300">
<i data-lucide="menu" class="w-6 h-6" id="menu-icon"></i>
<i data-lucide="x" class="w-6 h-6 hidden" id="close-icon"></i>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="lg:hidden hidden bg-white border-t border-gray-200 shadow-lg">
<div class="px-4 py-4 space-y-3">
<a href="index.html" class="block text-gray-700 hover:text-blue-600 transition-colors font-medium py-2">Prima pagina</a>
<a href="menu/desprenoi.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Despre noi</a>
<a href="roa/aplicatii-erp.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">ROA</a>
<div class="border-l-2 border-gray-300 pl-4 space-y-2">
<p class="text-gray-600 font-medium text-sm">Servicii</p>
<a href="menu/analiza.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Analiza</a>
<a href="menu/implementare.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Implementare</a>
<a href="menu/roa-suport-tehnic.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Suport tehnic ROA</a>
<a href="menu/alteservicii.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Alte servicii</a>
</div>
<a href="menu/referinte.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Cine folosește ROA</a>
<a href="menu/angajari.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Angajari</a>
<a href="menu/contact.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Contact</a>
</div>
</div>
</nav>
</header>
<!-- Main Content Container -->
<main class="container mx-auto px-4 py-8 max-w-7xl">
<!-- Chatbot Container -->
<div class="professional-card p-8 max-w-4xl mx-auto">
<!-- Flowise Chatbot -->
<div class="flex justify-center">
<flowise-fullchatbot></flowise-fullchatbot>
</div>
</div>
</main>
<!-- Professional Footer -->
<footer class="professional-navbar mt-16">
<div class="container mx-auto px-4 py-8 text-center">
<div class="text-white">
<p class="mb-2">&copy; 2025 Romfast. Toate drepturile rezervate.</p>
<p class="text-sm opacity-80">ROA - Romfast Applications - Sisteme informatice pentru managementul afacerii</p>
</div>
</div>
</footer>
<script type="module">
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js"
Chatbot.initFull({
chatflowid: "462dabb9-6995-4f7e-ad8e-30624ae56be5",
apiHost: "https://mutual-special-koala.ngrok-free.app",
theme: {
chatWindow: {
showTitle: true,
title: 'Cezar ChatBot - Romfast Suport',
titleAvatarSrc: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg',
showAgentMessages: true,
welcomeMessage: 'Buna! Cu ce te pot ajuta?',
errorMessage: 'Am o eroare! Revino mai tarziu',
backgroundColor: "#ffffff",
backgroundImage: 'enter image path or link', // If set, this will overlap the background color of the chat window.
height: 700,
width: 500,
fontSize: 16,
starterPrompts: ['Cum se actualizeaza tokenul eFactura?', 'Cand se completeaza codul de plata pentru declaratia SAFT?'], // It overrides the starter prompts set by the chat flow passed
starterPromptFontSize: 15,
clearChatOnReload: true, // If set to true, the chat will be cleared when the page reloads.
botMessage: {
backgroundColor: "#f7f8ff",
textColor: "#303235",
showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
},
userMessage: {
backgroundColor: "#3B81F6",
textColor: "#ffffff",
showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
},
textInput: {
placeholder: 'Type your question',
backgroundColor: '#ffffff',
textColor: '#303235',
sendButtonColor: '#3B81F6',
maxChars: 2000,
maxCharsWarningMessage: 'You exceeded the characters limit. Please input less than 2000 characters.',
autoFocus: true, // If not used, autofocus is disabled on mobile and enabled on desktop. true enables it on both, false disables it on both.
sendMessageSound: false,
// sendSoundLocation: "send_message.mp3", // If this is not used, the default sound effect will be played if sendSoundMessage is true.
receiveMessageSound: false,
// receiveSoundLocation: "receive_message.mp3", // If this is not used, the default sound effect will be played if receiveSoundMessage is true.
},
feedback: {
color: '#303235',
},
footer: {
textColor: '#303235',
text: 'Powered by',
company: 'Romfast',
companyLink: 'https://www.romfast.ro',
}
}
}
})
</script>
<!-- Professional Theme JavaScript -->
<script>
// Theme toggle functionality
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
lucide.createIcons();
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
const body = document.body;
const html = document.documentElement;
// Check for saved theme preference or default to 'light'
const currentTheme = localStorage.getItem('theme') || 'light';
if (currentTheme === 'dark') {
body.classList.add('dark-mode');
html.classList.add('dark-mode');
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
}
themeToggle.addEventListener('click', function() {
body.classList.toggle('dark-mode');
html.classList.toggle('dark-mode');
// Update icon
if (body.classList.contains('dark-mode')) {
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'dark');
} else {
themeToggle.innerHTML = '<i data-lucide="sun" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'light');
}
// Reinitialize icons
lucide.createIcons();
});
// Mobile menu toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const menuIcon = document.getElementById('menu-icon');
const closeIcon = document.getElementById('close-icon');
mobileMenuToggle.addEventListener('click', function() {
mobileMenu.classList.toggle('hidden');
menuIcon.classList.toggle('hidden');
closeIcon.classList.toggle('hidden');
});
});
</script>
</body>
</html>

1264
chatbot_maria.html Normal file

File diff suppressed because it is too large Load Diff

778
chatbot_maria2.html Normal file
View File

@@ -0,0 +1,778 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maria ChatBot Advanced - Romfast Suport</title>
<meta name="description" content="Maria ChatBot Advanced - Asistent AI Romfast cu injectare automată de mesaje din URL și simulare avansată de interacțiuni pentru ROA.">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Flowbite -->
<script src="https://cdn.jsdelivr.net/npm/flowbite@2.0.0/dist/flowbite.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<!-- Professional Theme CSS -->
<link href="professional-theme.css" rel="stylesheet">
<style>
/* Advanced professional styling for Maria's enhanced features */
body {
font-family: var(--font-sans) !important;
background: var(--background) !important;
color: var(--foreground) !important;
letter-spacing: var(--tracking-normal) !important;
line-height: 1.6 !important;
}
/* Advanced professional styling for link generation button */
.link-button-small {
background: transparent !important;
border: none !important;
padding: 0 !important;
margin: 0 0 0 8px !important;
height: 56px !important;
width: 56px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
border-radius: var(--radius) !important;
font-size: 18px !important;
color: var(--primary) !important;
opacity: 0.8 !important;
flex-shrink: 0 !important;
}
.link-button-small:hover {
background-color: var(--card-hover) !important;
opacity: 1 !important;
transform: scale(1.05) !important;
color: var(--secondary) !important;
}
.link-button-small:active {
transform: scale(0.95) !important;
}
.link-button-small.success {
color: var(--accent) !important;
background-color: rgba(184, 85, 85, 0.1) !important;
}
/* Professional toast notifications with enhanced styling */
.toast {
position: fixed;
top: 20px;
right: 20px;
background: var(--primary) !important;
color: var(--primary-foreground) !important;
padding: 12px 20px;
border-radius: var(--radius-lg) !important;
z-index: 10000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
box-shadow: var(--shadow-lg) !important;
font-size: 14px;
font-family: var(--font-sans) !important;
border: 1px solid var(--border) !important;
}
.toast.show {
opacity: 1;
transform: translateX(0);
}
.toast.error {
background: var(--accent) !important;
color: var(--accent-foreground) !important;
}
/* Professional loading states and simulation indicators */
.simulation-indicator {
position: fixed;
bottom: 20px;
left: 20px;
background: var(--secondary) !important;
color: var(--secondary-foreground) !important;
padding: 8px 16px;
border-radius: var(--radius-lg) !important;
font-size: 12px;
font-weight: 500;
z-index: 9999;
opacity: 0;
transform: translateY(100%);
transition: all 0.3s ease;
box-shadow: var(--shadow-md) !important;
font-family: var(--font-mono) !important;
}
.simulation-indicator.show {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body class="min-h-screen">
<!-- Soft Professional Blue Header -->
<header class="professional-navbar sticky top-0 z-50">
<nav class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<!-- Logo Section -->
<div class="flex items-center space-x-3">
<img src="images/romfast_logo.png" alt="Romfast Logo" class="romfast-logo">
<div class="hidden md:block">
<p class="text-white text-sm italic font-light">Aduce informația în mâinile tale</p>
</div>
</div>
<!-- Centered Navigation -->
<div class="hidden lg:flex items-center space-x-8">
<a href="index.html" class="text-white hover:text-blue-200 transition-colors font-medium nav-link">Prima pagina</a>
<a href="menu/desprenoi.html" class="text-white hover:text-blue-200 transition-colors nav-link">Despre noi</a>
<a href="roa/aplicatii-erp.html" class="text-white hover:text-blue-200 transition-colors nav-link">ROA</a>
<a href="roa/cum-arata-roa.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Cum arată ROA</a>
<a href="roa/cum-arata-roa.html" class="text-white hover:text-blue-200 transition-colors nav-link">Cum arată ROA</a>
<div class="relative group">
<button class="text-white hover:text-blue-200 transition-colors flex items-center space-x-1 nav-link">
<span>Servicii</span>
<i data-lucide="chevron-down" class="w-4 h-4"></i>
</button>
<div class="absolute top-full left-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300">
<a href="menu/analiza.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800 rounded-t-lg">Analiza</a>
<a href="menu/implementare.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800">Implementare</a>
<a href="menu/roa-suport-tehnic.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800">Suport tehnic ROA</a>
<a href="menu/alteservicii.html" class="block px-4 py-3 text-gray-700 hover:bg-blue-50 hover:text-blue-800 rounded-b-lg">Alte servicii</a>
</div>
</div>
<a href="menu/referinte.html" class="text-white hover:text-blue-200 transition-colors nav-link">Cine folosește ROA</a>
<a href="menu/angajari.html" class="text-white hover:text-blue-200 transition-colors nav-link">Angajari</a>
<a href="menu/contact.html" class="text-white hover:text-blue-200 transition-colors nav-link">Contact</a>
</div>
<!-- Theme Toggle & Mobile Menu -->
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="text-white p-2 rounded-lg hover:bg-white hover:bg-opacity-20 transition-all duration-300">
<i data-lucide="sun" class="w-5 h-5"></i>
</button>
<button id="mobile-menu-toggle" class="lg:hidden text-white p-2 rounded-lg hover:bg-white hover:bg-opacity-20 transition-all duration-300">
<i data-lucide="menu" class="w-6 h-6" id="menu-icon"></i>
<i data-lucide="x" class="w-6 h-6 hidden" id="close-icon"></i>
</button>
</div>
</div>
<!-- Mobile Menu -->
<div id="mobile-menu" class="lg:hidden hidden bg-white border-t border-gray-200 shadow-lg">
<div class="px-4 py-4 space-y-3">
<a href="index.html" class="block text-gray-700 hover:text-blue-600 transition-colors font-medium py-2">Prima pagina</a>
<a href="menu/desprenoi.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Despre noi</a>
<a href="roa/aplicatii-erp.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">ROA</a>
<div class="border-l-2 border-gray-300 pl-4 space-y-2">
<p class="text-gray-600 font-medium text-sm">Servicii</p>
<a href="menu/analiza.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Analiza</a>
<a href="menu/implementare.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Implementare</a>
<a href="menu/roa-suport-tehnic.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Suport tehnic ROA</a>
<a href="menu/alteservicii.html" class="block text-gray-600 hover:text-blue-600 transition-colors py-1 text-sm">Alte servicii</a>
</div>
<a href="menu/referinte.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Cine folosește ROA</a>
<a href="menu/angajari.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Angajari</a>
<a href="menu/contact.html" class="block text-gray-700 hover:text-blue-600 transition-colors py-2">Contact</a>
</div>
</div>
</nav>
</header>
<!-- Main Content Container -->
<main class="container mx-auto px-4 py-8 max-w-7xl">
<!-- Maria Advanced Chatbot Section with Professional Styling -->
<section class="professional-hero text-center mb-8">
<div class="max-w-4xl mx-auto relative z-10">
<h1 class="text-4xl md:text-5xl font-bold mb-6 professional-text-primary">
Maria ChatBot Advanced
</h1>
<p class="text-xl professional-text-secondary mb-8 leading-relaxed">
Asistent AI avansat Romfast cu injectare automată de mesaje din URL și simulare ultra-realistă de interacțiuni pentru sistemul ROA.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<div class="professional-btn-secondary inline-flex items-center space-x-2">
<i data-lucide="zap" class="w-4 h-4"></i>
<span>Injectare automată mesaje</span>
</div>
<div class="professional-btn-secondary inline-flex items-center space-x-2">
<i data-lucide="bot" class="w-4 h-4"></i>
<span>Simulare interacțiuni utilizator</span>
</div>
<div class="professional-btn-secondary inline-flex items-center space-x-2">
<i data-lucide="link" class="w-4 h-4"></i>
<span>Link-uri partajabile</span>
</div>
</div>
</div>
</section>
<!-- Chatbot Container -->
<div class="professional-card p-8 max-w-4xl mx-auto">
<div class="text-center mb-6">
<h2 class="text-2xl font-semibold professional-text-primary mb-4">AI Suport Tehnic Avansat ROA</h2>
<p class="professional-text-secondary">Experiență chatbot avansată cu procesare automată de mesaje din URL-uri, simulare ultra-realistă de tastare și generare de link-uri partajabile pentru răspunsuri complexe.</p>
<!-- Advanced Features Info -->
<div class="grid md:grid-cols-3 gap-4 mt-6 text-sm">
<div class="bg-blue-50 p-3 rounded-lg">
<i data-lucide="download" class="w-5 h-5 mx-auto mb-2 text-blue-600"></i>
<p class="font-medium text-blue-800">URL Message Injection</p>
<p class="text-blue-600">Procesare automată întrebări din URL</p>
</div>
<div class="bg-green-50 p-3 rounded-lg">
<i data-lucide="keyboard" class="w-5 h-5 mx-auto mb-2 text-green-600"></i>
<p class="font-medium text-green-800">Real User Simulation</p>
<p class="text-green-600">Simulare realistă tastare caracter cu caracter</p>
</div>
<div class="bg-purple-50 p-3 rounded-lg">
<i data-lucide="share" class="w-5 h-5 mx-auto mb-2 text-purple-600"></i>
<p class="font-medium text-purple-800">Smart Link Generation</p>
<p class="text-purple-600">Generare link-uri partajabile pentru întrebări</p>
</div>
</div>
</div>
<!-- Flowise Chatbot -->
<div class="flex justify-center">
<flowise-fullchatbot></flowise-fullchatbot>
</div>
</div>
</main>
<!-- Professional Footer -->
<footer class="professional-navbar mt-16">
<div class="container mx-auto px-4 py-8 text-center">
<div class="text-white">
<p class="mb-2">&copy; 2025 Romfast. Toate drepturile rezervate.</p>
<p class="text-sm opacity-80">ROA - Romfast Applications - Sisteme informatice pentru managementul afacerii</p>
</div>
</div>
</footer>
<script type="module">
import Chatbot from "https://cdn.jsdelivr.net/npm/flowise-embed/dist/web.js"
// Funcție pentru a obține parametrii din URL
function getUrlParameter(name) {
try {
const urlParams = new URLSearchParams(window.location.search);
const value = urlParams.get(name);
if (value === null) {
return '';
}
return value;
} catch (error) {
console.error("Eroare cu URLSearchParams:", error);
try {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(location.search);
if (results === null) {
return '';
}
let rawValue = results[1];
function safeDecodeURIComponent(str) {
str = str.replace(/\+/g, ' ');
const chunks = str.split('%');
let result = chunks[0];
for (let i = 1; i < chunks.length; i++) {
const chunk = chunks[i];
if (chunk.length >= 2) {
const hexCode = chunk.substring(0, 2);
const rest = chunk.substring(2);
if (/^[0-9A-Fa-f]{2}$/.test(hexCode)) {
try {
result += decodeURIComponent('%' + hexCode) + rest;
} catch (e) {
result += '%' + chunk;
}
} else {
result += '%' + chunk;
}
} else {
result += '%' + chunk;
}
}
return result;
}
const decoded = safeDecodeURIComponent(rawValue);
return decoded;
} catch (fallbackError) {
console.error("Eroare și la fallback sigur:", fallbackError);
return '';
}
}
}
// Obține mesajul inițial din parametrul URL (dacă există)
const initialMessage = getUrlParameter('message');
// Definește prompt-urile inițiale standard
const defaultStarterPrompts = [
'Cum se actualizeaza tokenul eFactura?',
'Cand se completeaza codul de plata pentru declaratia SAFT?',
'Cum se configurează o politică de prețuri? Afișeaza pașii complet',
'Care sunt pasii pentru emiterea unei facturi service auto?',
'Cum se valideaza o comanda service auto?',
'Cum se deschide o comanda service auto?',
'Cum se modifica datele initiale ale unei comenzi service auto (ex: nr. km)?'
];
// Determină dacă să afișeze prompt-urile inițiale sau nu
const starterPrompts = initialMessage ? [] : defaultStarterPrompts;
// Inițializează chatbot-ul
const chatbotInstance = Chatbot.initFull({
chatflowid: "d4911620-07fe-41f8-adb4-f2f52d6ec766",
apiHost: "https://mutual-special-koala.ngrok-free.app",
theme: {
chatWindow: {
showTitle: true,
title: 'Maria ChatBot - Romfast Suport',
titleAvatarSrc: 'https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/svg/google-messages.svg',
showAgentMessages: true,
welcomeMessage: 'Bună! Eu sunt Maria, specialistul dvs. de suport tehnic Romfast. Sunt aici pentru a vă ajuta cu orice întrebări sau probleme tehnice legate de utilizarea sistemului ROA. Cum vă pot ajuta astăzi?',
errorMessage: 'Am o eroare! Revino mai tarziu',
backgroundColor: "#ffffff",
height: 700,
width: 500,
fontSize: 16,
starterPrompts: starterPrompts,
starterPromptFontSize: 15,
clearChatOnReload: true,
botMessage: {
backgroundColor: "#f7f8ff",
textColor: "#303235",
showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
},
userMessage: {
backgroundColor: "#3B81F6",
textColor: "#ffffff",
showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
},
textInput: {
placeholder: 'Scrieți întrebarea dvs.',
backgroundColor: '#ffffff',
textColor: '#303235',
sendButtonColor: '#3B81F6',
maxChars: 2000,
maxCharsWarningMessage: 'Ați depășit limita de caractere. Vă rugăm să introduceți mai puțin de 2000 de caractere.',
autoFocus: true,
sendMessageSound: false,
receiveMessageSound: false,
},
feedback: {
color: '#303235',
},
footer: {
textColor: '#303235',
text: 'Powered by',
company: 'Romfast',
companyLink: 'https://www.romfast.ro',
}
}
}
});
// Enhanced professional toast function with simulation indicators
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
if (type === 'error') {
toast.classList.add('error');
}
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 3000);
}
// Professional simulation indicator
function showSimulationIndicator(message) {
const indicator = document.createElement('div');
indicator.className = 'simulation-indicator';
indicator.textContent = `${message}`;
document.body.appendChild(indicator);
setTimeout(() => {
indicator.classList.add('show');
}, 100);
return indicator;
}
function hideSimulationIndicator(indicator) {
if (indicator && document.body.contains(indicator)) {
indicator.classList.remove('show');
setTimeout(() => {
if (document.body.contains(indicator)) {
document.body.removeChild(indicator);
}
}, 300);
}
}
// All the advanced message injection and simulation functions from the original file...
function waitForChatbotInput() {
return new Promise((resolve) => {
const selectors = [
'textarea',
'input[type="text"]',
'input',
'textarea[placeholder*="Scrieți întrebarea"]',
'textarea[placeholder*="Type your message"]',
'.text-input',
'[contenteditable="true"]',
'[class*="text-input"]',
'[class*="textarea"]',
'[class*="input"]'
];
function searchInShadowDOM() {
const chatbotElement = document.querySelector('flowise-fullchatbot');
if (chatbotElement && chatbotElement.shadowRoot) {
for (const selector of selectors) {
const elements = chatbotElement.shadowRoot.querySelectorAll(selector);
for (const element of elements) {
if (element.offsetParent !== null && !element.disabled && !element.readOnly) {
return element;
}
}
}
}
return null;
}
for (const selector of selectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
if (element.offsetParent !== null && !element.disabled && !element.readOnly) {
resolve(element);
return;
}
}
}
const shadowElement = searchInShadowDOM();
if (shadowElement) {
resolve(shadowElement);
return;
}
const observer = new MutationObserver((mutations) => {
for (const selector of selectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
if (element.offsetParent !== null && !element.disabled && !element.readOnly) {
observer.disconnect();
resolve(element);
return;
}
}
}
const shadowElement = searchInShadowDOM();
if (shadowElement) {
observer.disconnect();
resolve(shadowElement);
return;
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
console.error('❌ Nu s-a găsit textarea-ul chatbot-ului în 15 secunde');
resolve(null);
}, 15000);
});
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getKeyCode(char) {
if (char === ' ') return 'Space';
if (/[a-zA-Z]/.test(char)) return `Key${char.toUpperCase()}`;
if (/[0-9]/.test(char)) return `Digit${char}`;
if (char === '?') return 'Slash';
if (char === '.') return 'Period';
if (char === ',') return 'Comma';
return 'Key' + char.toUpperCase();
}
async function simulateRealUserInteraction(textarea, message) {
console.log('🎭 Simulez interacțiunea reală a utilizatorului...');
textarea.focus();
await sleep(100);
const rect = textarea.getBoundingClientRect();
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
clientX: rect.left + 20,
clientY: rect.top + rect.height / 2
});
textarea.dispatchEvent(clickEvent);
await sleep(100);
textarea.value = '';
const clearInputEvent = new InputEvent('input', {
inputType: 'deleteContentBackward',
bubbles: true,
cancelable: true
});
textarea.dispatchEvent(clearInputEvent);
await sleep(100);
for (let i = 0; i < message.length; i++) {
const char = message[i];
const keydownEvent = new KeyboardEvent('keydown', {
key: char,
code: getKeyCode(char),
keyCode: char.charCodeAt(0),
which: char.charCodeAt(0),
bubbles: true,
cancelable: true,
composed: true
});
textarea.dispatchEvent(keydownEvent);
textarea.value += char;
const inputEvent = new InputEvent('input', {
inputType: 'insertText',
data: char,
bubbles: true,
cancelable: true,
composed: true
});
textarea.dispatchEvent(inputEvent);
const keyupEvent = new KeyboardEvent('keyup', {
key: char,
code: getKeyCode(char),
keyCode: char.charCodeAt(0),
which: char.charCodeAt(0),
bubbles: true,
cancelable: true,
composed: true
});
textarea.dispatchEvent(keyupEvent);
await sleep(char === ' ' ? 40 : 60);
}
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
textarea.dispatchEvent(changeEvent);
const blurEvent = new FocusEvent('blur', { bubbles: true, cancelable: true });
textarea.dispatchEvent(blurEvent);
await sleep(50);
const focusEvent = new FocusEvent('focus', { bubbles: true, cancelable: true });
textarea.dispatchEvent(focusEvent);
console.log('✅ Simularea interacțiunii s-a terminat');
}
async function sendWithEnter(textarea) {
console.log('⌨️ Încerc trimiterea cu Enter...');
textarea.focus();
await sleep(100);
const enterKeydown = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true,
composed: true
});
textarea.dispatchEvent(enterKeydown);
await sleep(50);
console.log('✅ Enter trimis');
}
// Advanced professional message injection function
async function injectMessageIntoChat(message) {
let simulationIndicator;
try {
console.log('🚀 Începe injectarea avansată a mesajului:', message);
showToast('Se încarcă întrebarea din link...', 'success');
simulationIndicator = showSimulationIndicator('Initializing AI injection...');
await sleep(3000);
if (simulationIndicator) {
simulationIndicator.textContent = '⚡ Loading chatbot interface...';
}
if (simulationIndicator) {
simulationIndicator.textContent = '⚡ Locating chat interface...';
}
const textarea = await waitForChatbotInput();
if (!textarea) {
console.error('❌ Nu s-a găsit textarea-ul');
showToast('Nu s-a putut găsi casuta de chat.', 'error');
hideSimulationIndicator(simulationIndicator);
return;
}
console.log('✅ Textarea găsit:', textarea);
if (simulationIndicator) {
simulationIndicator.textContent = '⚡ Simulating real user typing...';
}
await simulateRealUserInteraction(textarea, message);
if (simulationIndicator) {
simulationIndicator.textContent = '⚡ Sending message...';
}
await sleep(1000);
await sendWithEnter(textarea);
if (simulationIndicator) {
simulationIndicator.textContent = '⚡ Cleaning up URL parameters...';
}
const url = new URL(window.location.href);
url.searchParams.delete('message');
window.history.replaceState({}, document.title, url.toString());
setTimeout(() => {
hideSimulationIndicator(simulationIndicator);
showToast('Mesaj injectat cu succes și trimis!', 'success');
}, 1000);
} catch (error) {
console.error('❌ Eroare:', error);
hideSimulationIndicator(simulationIndicator);
showToast('A apărut o eroare. Introduceți manual întrebarea.', 'error');
}
}
// Professional message injection with enhanced logging
if (initialMessage && initialMessage.trim() !== '') {
console.log('📨 Mesaj inițial detectat:', initialMessage);
const earlyIndicator = showSimulationIndicator('Preparing URL message injection...');
setTimeout(() => {
hideSimulationIndicator(earlyIndicator);
injectMessageIntoChat(initialMessage);
}, 5000);
}
</script>
<!-- Professional Theme JavaScript -->
<script>
// Advanced theme toggle functionality with enhanced features
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
lucide.createIcons();
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
const body = document.body;
const html = document.documentElement;
// Check for saved theme preference or default to 'light'
const currentTheme = localStorage.getItem('theme') || 'light';
if (currentTheme === 'dark') {
body.classList.add('dark-mode');
html.classList.add('dark-mode');
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
}
themeToggle.addEventListener('click', function() {
body.classList.toggle('dark-mode');
html.classList.toggle('dark-mode');
// Update icon
if (body.classList.contains('dark-mode')) {
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'dark');
} else {
themeToggle.innerHTML = '<i data-lucide="sun" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'light');
}
// Reinitialize icons
lucide.createIcons();
});
// Mobile menu toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
const menuIcon = document.getElementById('menu-icon');
const closeIcon = document.getElementById('close-icon');
mobileMenuToggle.addEventListener('click', function() {
mobileMenu.classList.toggle('hidden');
menuIcon.classList.toggle('hidden');
closeIcon.classList.toggle('hidden');
});
// Enhanced feature detection logging
console.log('🎨 Maria ChatBot Advanced - Professional Theme Loaded');
console.log('🔧 Features: URL Injection, User Simulation, Link Generation');
console.log('⚡ Advanced UI interactions initialized');
});
</script>
</body>
</html>

2
efactura-generator/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@@ -1,4 +1,9 @@
<FilesMatch "\.(html|php|css|js|ico|xml)$">
Order Allow,Deny
Allow from all
</FilesMatch>
<FilesMatch "\.(html|php|css|js|mjs|ico|xml)$">
Order Allow,Deny
Allow from all
</FilesMatch>
# Native ESM (.mjs) require JavaScript MIME type pe Apache.
<IfModule mod_mime.c>
AddType text/javascript .mjs
</IfModule>

View File

@@ -1,35 +1,134 @@
# Changelog
## 0.9-beta-4 - 07.02.2025
### Bugfixes
- Fixed: Al doilea tag TaxTotal se generează doar când valuta documentului diferă de valuta TVA.
## 0.9-beta-3 - 14.01.2025
### Bugfixes
- Fixed: Discounturile de pe articole nu se mai cumulează în discount global pe factură.
### New Features
- Added: Se completează valoare reducere și cod reducere pe fiecare articol.
## 0.9-beta-2 - 07.01.2025
### Bugfixes
- Fixed: Selecția judet București și afișare oraș.
### New Features
- Added: Se completează codul și motivul scutirii în secțiunea "Defalcare TVA" pentru articole cu Tip TVA "E" Neimpozabil.
- Added: S-a tratat cazul în care furnizorul nu este plătitor de TVA (codul fiscal nu are atributul fiscal "RO").
- Added: Citire din xml coduri identificare articole (vânzător, cumpărător, cod de bare, CPV, NC8, vamal).
- Added: Editare, adăugare, ștergere coduri identificare articole.
### Modifications
- Modified: Afișare responsive pentru ecrane de diferite dimensiuni.
### TODO
- Implement: citire și editare referință factura originală pentru factura storno.
- Implement: citire și editare modalități de plată.
## 0.9-beta-1 - 06.01.2025
- Initial beta release.
# Changelog
## 0.9-beta-15 - 05.05.2026
### New Features
- Buton nou „PDF ANAF" în meniul Acțiuni: descarcă PDF-ul oficial al facturii generat direct de ANAF din XML-ul curent. Înainte de generare, ANAF validează factura — dacă apar erori, sunt afișate în loc să descarce un PDF cu informații greșite. Disponibil când serverul are configurat suport pentru API-ul ANAF.
### Modifications
- Reorganizat header-ul aplicației: butoanele secundare (Printează, Descarcă PDF, PDF ANAF, Validare ANAF) sunt grupate într-un meniu „Acțiuni ▾" pentru reducerea aglomerării. În header rămân vizibile permanent doar acțiunile principale: Alege Fișier, Factură Nouă, Stornează, Salvează XML.
- Eliminat selectorul Standard/Compact și butonul „Printează" injectate dinamic în header — opțiunile sunt acum directe în meniul Acțiuni („Printează — Standard" și „Printează — Compact"), un singur click pentru orice variantă.
- Pe mobil meniul Acțiuni se deschide pe toată lățimea ecranului sub header.
### Bugfixes
- Fixed: butonul „Validare ANAF" (existent în versiuni anterioare) și endpoint-ul folosit pentru transformarea XML → PDF foloseau ruta cu autentificare OAuth (`api.anaf.ro`) chiar și fără un token configurat — acum, în lipsa token-ului, se folosește ruta publică ANAF (`webservicesp.anaf.ro`) care nu necesită autentificare. Astfel funcționalitățile ANAF merg și pe servere fără token.
- Fixed: apelul anterior pentru transformarea în PDF folosea forma „skip validare" — în versiuni viitoare, când ar fi fost cablată, ar fi descărcat un PDF chiar și pentru XML-uri cu probleme. Acum validarea e implicită și erorile blochează descărcarea.
## 0.9-beta-14 - 05.05.2026
### Bugfixes
- Fixed: pe găzduirea cu PHP 7.3 (a2hosting), `receiver.php` returna 500 Internal Server Error la upload XML din cauza unei sintaxe noi (arrow function `fn() =>`) introduse în beta-13. Înlocuită cu funcție anonimă clasică, compatibilă cu PHP 7.3+.
### Modifications
- Adăugat `sync-to-website.sh` — script pentru propagarea modificărilor către sub-directorul `efactura-generator/` din repo-ul `romfast-website` (de unde se face deploy către `https://romfast.ro/efactura-generator/`). Scriptul exclude automat fișierele care nu au ce căuta pe shared hosting (Dockerfile, web.config, docs interne) și `config.json` cu `api_key` (gestionat doar pe server).
- `sync-to-website.sh` poate face și deploy direct pe găzduirea de producție: `--deploy` (mirror local + deploy a2hosting) și `--deploy-only` (sare peste mirror, deploy direct canonic → server). Funcționează cu `--dry-run` pentru previzualizare și verifică SSH înainte să încerce rsync-ul.
- Documentate cele trei ținte de deploy în `CLAUDE.md` (GitHub Pages, romfast.ro via romfast-website, Docker/Dokploy) și avertizare PHP 7.3 pentru găzduirea a2hosting.
## 0.9-beta-13 - 04.05.2026
### Bugfixes
- Fixed: la încărcarea unui XML cu furnizor sau client neplătitor de TVA, codul fiscal apărea în câmpul „Nr. înregistrare" iar „Cod TVA" rămânea gol. Acum, dacă firma nu e plătitoare TVA, CIF-ul se completează în „Cod TVA" și numărul de la Registrul Comerțului în „Nr. înregistrare" (simetric cu modul în care se salvează XML-ul).
- Fixed: deploy în Docker/Dokploy returna „Acces interzis" la căutare CIF și validare ANAF, deoarece request-urile veneau prin reverse proxy și apăreau ca venind dintr-un IP intern, nu de la utilizator. Lista de IP-uri permise se poate dezactiva acum (gol sau `*`).
### Modifications
- Configurare receiver prin variabile de mediu (`ANAF_API_KEY`, `ANAF_ALLOWED_IPS`, `ANAF_TOKEN`, `ANAF_TEMP_LIFETIME`) — suprascriu valorile din `config.json`. Util pentru deploy în container fără rebuild la schimbare configurație.
- Dockerfile: setează implicit `ANAF_ALLOWED_IPS=*` (verificare IP dezactivată), potrivit pentru deploy behind reverse proxy unde same-origin asigură deja protecția.
## 0.9-beta-12 - 04.05.2026
### Bugfixes
- Fixed: eroare BR-CO-15 falsă după click pe „Recalculează Totaluri" — defalcarea TVA părea să nu corespundă cu Total TVA, deși matematica era corectă.
- Fixed: eroare BR-16 falsă pe linii cu cantitate fracționară (ex. 1,000 buc) — totalul liniei era marcat greșit ca incorect.
- Fixed: la salvare apărea „completați toate câmpurile obligatorii" chiar când nu se referențiază altă factură. Câmpul „Data factură referită" e opțional și nu mai blochează salvarea când e gol.
- Fixed: „Factură Nouă" — eroare XML declaration duplicată la parsare (`XMLSerializer` include deja `<?xml?>`, codul o prefixa din nou). Strip declaration înainte de concatenare.
### Modifications
- ANAF lookup CIF: completare automată oraș, județ, telefon și prefix `RO` la CIF-ul plătitorilor TVA. Toast indică acum statusul `Plătitor/Neplătitor TVA · Înregistrat eFactura`. Câmpurile `CountrySubentity` (SELECT RO-XX) și `Country` se populează automat.
- Număr factură: pre-populare din secvența localStorage la deschiderea aplicației (fără incrementare contor).
- Număr factură: format configurabil — serie + spațiu + an opțional + contor cu N cifre (18). Exemple: `RFT 20260001` (cu an) sau `RFT 0001` (fără an). Modal „Factură Nouă" extins cu checkbox „Include an în număr" și input „Cifre contor".
## 0.9-beta-11 - 04.05.2026
### Bugfixes
- Fixed: Lookup CIF ANAF nu mai funcționa — migrat la API v9 (PlatitorTvaRest) care a înlocuit v8-ul async.
### Modifications
- Added: Documentație rulare locală și Docker în README.
- Added: Script `start.sh` pentru pornire dev (Node :3000 + PHP :8000) cu auto-stop al proceselor existente pe aceste porturi. Banner-ul indică explicit `:8000` pentru testare cu ANAF/receiver, `:3000` pentru testare statică.
## 0.9-beta-10 - 30.04.2026
### New Features
- Added: Import bulk — se pot încărca mai multe fișiere XML (sau ZIP) simultan. Un sidebar afișează lista fișierelor deschise; fișierele modificate sunt marcate vizual. Limită 50 fișiere.
## 0.9-beta-9 - 30.04.2026
### New Features
- Added: Catalog produse local — articolele pot fi salvate în catalogul browserului și refolosite prin autocomplete la câmpul „Denumire" pe orice linie factură.
## 0.9-beta-8 - 30.04.2026
### New Features
- Added: Numerotare automată facturi — buton „Factură Nouă" generează numărul următor din serie configurată (ex. `RFT2026-0042`). La trecerea anului, aplicația întreabă dacă se continuă seria sau se resetează contorul la 1.
## 0.9-beta-7 - 30.04.2026
### New Features
- Added: Descărcare PDF direct din browser, fără server — buton „Descarcă PDF" în header și în paginile de printare.
## 0.9-beta-6 - 30.04.2026
### New Features
- Added: Validare XML la ANAF — buton „Validare ANAF" trimite factura la serverul ANAF și afișează erorile returnate (disponibil doar când receiver.php este activ).
- Added: Lookup CIF ANAF — buton „Caută CIF" lângă câmpurile cod TVA completează automat numele, adresa și numărul de înregistrare al firmei.
## 0.9-beta-5 - 30.04.2026
### New Features
- Added: Încărcare ZIP — fișierele ZIP cu XML eFactura pot fi încărcate direct (prin buton sau drag-and-drop); primul XML din arhivă este extras automat.
- Added: Redesign vizual complet — font Geist, paletă warm-paper cu header slate, spațiere și contrast îmbunătățite.
- Added: Profil furnizor — datele furnizorului pot fi salvate în browser și refolosite la facturi noi cu un singur click.
- Added: Tip factură — câmp nou pentru tipul documentului: factură comercială, notă de credit, factură corectată sau autofactură.
- Added: Modalități de plată — secțiune nouă cu unul sau mai multe rânduri cod plată + IBAN.
- Added: Referință factură originală — câmpuri pentru numărul și data facturii la care se referă storno-ul; completate automat la apăsarea „Stornează".
- Added: Validare matematică inline — badge verde/roșu pe fiecare linie și la total, arată dacă valorile sunt consistente. La salvare apare un avertisment dacă există diferențe.
- Added: Validare CIF/CUI pe blur — eroare inline dacă cifra de control nu este corectă.
- Added: Validare IBAN pe blur — eroare inline dacă IBAN-ul are lungime sau check digits incorecte.
- Added: Panel reguli CIUS-RO — panou flotant cu lista erorilor de conformitate față de standardul eFactura, actualizat în timp real. Click pe o eroare navighează la câmpul problematic.
### Bugfixes
- Fixed: Funcția „Stornează" lăsa valorile interne inconsistente față de ce era afișat în formular.
## 0.9-beta-4 - 07.02.2025
### Bugfixes
- Fixed: Al doilea tag TaxTotal se generează doar când valuta documentului diferă de valuta TVA.
## 0.9-beta-3 - 14.01.2025
### Bugfixes
- Fixed: Discounturile de pe articole nu se mai cumulează în discount global pe factură.
### New Features
- Added: Se completează valoare reducere și cod reducere pe fiecare articol.
## 0.9-beta-2 - 07.01.2025
### Bugfixes
- Fixed: Selecția judet București și afișare oraș.
### New Features
- Added: Se completează codul și motivul scutirii în secțiunea "Defalcare TVA" pentru articole cu Tip TVA "E" Neimpozabil.
- Added: S-a tratat cazul în care furnizorul nu este plătitor de TVA (codul fiscal nu are atributul fiscal "RO").
- Added: Citire din xml coduri identificare articole (vânzător, cumpărător, cod de bare, CPV, NC8, vamal).
- Added: Editare, adăugare, ștergere coduri identificare articole.
### Modifications
- Modified: Afișare responsive pentru ecrane de diferite dimensiuni.
### TODO
- Implement: citire și editare referință factura originală pentru factura storno.
- Implement: citire și editare modalități de plată.
## 0.9-beta-1 - 06.01.2025
- Initial beta release.

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +1,172 @@
# eFactura Editor / Editor Facturi Electronice
## Demo
https://romfast.github.io/efactura-generator/
## Overview / Prezentare
Romanian electronic invoice (eFactura) editor - loads XML, allows editing, printing invoices and generates new XML files.
Editor pentru facturi electronice (eFactura) - încarcă fișiere XML, permite editarea, printarea facturilor și generează fișiere XML noi.
![Factura 1](screenshot1.jpg)
![Factura 2](screenshot2.jpg)
![Factura 3](screenshot3.jpg)
## Installation & Usage / Instalare & Utilizare
### Option 1: Web Server / Opțiunea 1: Server Web
- Copy all project files to your web server maintaining the directory structure.
- Access through your web server URL.
- Use "Printează" button to print the invoice.
- Copiați toate fișierele pe server păstrând structura directoarelor.
- Accesați prin URL-ul serverului.
- Folosiți butonul "Printează" pentru a printa factura.
### Option 2: Local Development / Opțiunea 2: Dezvoltare Locală
1. Install Node.js / Instalați Node.js
2. Clone/download repository / Clonați/descărcați repository-ul
3. Run / Rulați: `node server.js`
4. Open / Deschideți: http://localhost:3000
## Project Structure / Structura Proiect
```
project/
├── index.html
├── styles/
│ ├── main.css
├── js/
│ ├── script.js
│ ├── formatter.js
│ └── print.js
├── templates/
│ └── print.html
└── server.js
```
## License / Licență
AGPL-3.0-or-later
If you use this software, even as a web service, you must:
1. Give credit to the original project
2. Share all your modifications
3. Use the same AGPL-3 license
Dacă folosiți acest software, chiar și ca serviciu web, trebuie să:
1. Menționați proiectul original
2. Partajați toate modificările făcute
3. Folosiți aceeași licență AGPL-3
## Changelog
[Istoric modificări](CHANGELOG.md)
# Editor eFactura
Editor static în browser pentru fișiere XML eFactura (UBL 2.1) conforme cu standardul ANAF.
## Demo
- GitHub Pages: https://romfast.github.io/efactura-generator/
## Scop
Aplicația este intenționat **statică** (HTML + CSS + JavaScript, fără build, fără backend obligatoriu) astfel încât să poată fi găzduită direct pe GitHub Pages sau pe orice server web simplu.
Cazurile principale de utilizare:
- **Corecții minore** într-un XML eFactura existent (date factură, furnizor, client, articole, TVA).
- **Generare factură storno** pornind de la o factură existentă, prin butonul „Stornează”.
Nu este un sistem complet de facturare — lucrează cu câte un singur XML, încărcat și salvat local.
![Factura 1](screenshot1.jpg)
![Factura 2](screenshot2.jpg)
![Factura 3](screenshot3.jpg)
## Funcționalități
### Încărcare XML
- Încărcare fișier XML local prin butonul „Alege Fișier XML”.
- Încărcare automată dintr-un fișier temporar prin parametru URL (`index.html?xml=<nume_fisier>`), folosit împreună cu `receiver.php` pentru integrare cu sisteme externe.
### Detalii factură
- Număr factură, data emiterii, data scadentă (cu calendar Pikaday și format `dd.mm.yyyy`).
- Monedă document și monedă TVA opțională (cu generarea automată a celui de-al doilea tag `TaxTotal` când monedele diferă).
- Câmp text adițional (notă) cu limită de 900 de caractere și împărțire automată.
### Furnizor și client
- Nume, cod TVA, număr de înregistrare, adresă, oraș, județ, țară, telefon, persoană de contact, email.
- Selector de țară conform ISO 3166-1 și selector de județ conform codurilor `RO-XX`.
- Tratare specială pentru București și pentru furnizori neplătitori de TVA (cod fiscal fără atributul `RO`).
### Articole factură
- Adăugare, editare și ștergere articole.
- Cantitate, preț, cotă TVA, unitate de măsură (EA, XPP, H87, KGM, MTR, LTR, MTQ).
- Reduceri pe linie cu cod și motiv reducere.
- Coduri de identificare articole multiple per linie: cod vânzător, cod cumpărător, cod de bare, CPV, NC8, cod vamal.
### TVA
- Tipuri TVA suportate: `S` (Standard), `AE` (Taxare Inversă), `O` (Neplătitor TVA), `Z` (Cotă 0%), `E` (Neimpozabil).
- Coduri de scutire `VATEX-EU-*` cu motiv corespunzător, completate automat în funcție de tipul TVA și editabile manual.
- Defalcare TVA cu mai multe cote, editabilă inline (bază impozabilă și valoare TVA).
### Reduceri și taxe la nivel de factură
- Adăugare/editare/ștergere reduceri și taxe suplimentare la nivel de document.
- Coduri motiv pentru reduceri (95, 41, 42, 60, 62 etc.) și pentru taxe (TV, FC, ZZZ).
### Totaluri
- Recalculare automată: subtotal, total reduceri, total taxe, valoare netă, TVA, total.
- Editare inline a totalurilor (click pe valoare) cu păstrarea totalurilor originale din XML-ul încărcat.
- Buton „Recalculează Totaluri” pentru regenerare din articole.
### Storno
- Buton „Stornează” care convertește factura curentă într-o factură storno (cantități și valori negative) gata de salvat.
### Salvare și printare
- Buton „Salvează XML” care generează și descarcă un XML UBL conform.
- Vizualizare printabilă în două formate (standard și compact) prin șabloanele din `templates/`.
### Formatare
- Numere, cantități și sume formatate conform locale-ului browserului, cu conversie automată la punct decimal pentru XML.
## Instalare și utilizare
### Opțiunea 1: Server web static
Copiați toate fișierele pe serverul web păstrând structura directoarelor și accesați `index.html` prin URL-ul serverului. Funcționează pe orice server static (Apache, nginx, GitHub Pages etc.).
### Opțiunea 2: Dezvoltare locală cu Node.js
```bash
node js/server.js
```
Apoi deschideți http://localhost:3000
Serverul Node este minimal (fără dependințe) și servește doar fișierele statice.
### Opțiunea 3: Script `start.sh` (Node + PHP simultan)
Pentru testare vizuală manuală cu receiver-ul PHP activ:
```bash
./start.sh
```
Pornește în paralel:
- Node static server pe `http://localhost:3000` (frontend, fără PHP — simulează GitHub Pages)
- PHP built-in server pe `http://localhost:8000` (servește atât frontend cât și `receiver.php`, `test-config.php`)
**Pentru testarea funcționalităților ANAF (lookup CIF, validare, PDF) deschideți `http://localhost:8000/` — frontend-ul apelează `./receiver.php` pe același origin, deci pe `:3000` (Node nu execută PHP) veți primi 404.** Folosiți `:3000` doar pentru a verifica comportamentul fără PHP.
Dacă porturile sunt deja ocupate, scriptul oprește procesele existente înainte de pornire. `Ctrl+C` oprește ambele servere. Loguri în `logs/dev-node.log` și `logs/dev-php.log`. Override porturi: `NODE_PORT=4000 PHP_PORT=9000 ./start.sh`.
### Opțiunea 4: Docker (local sau CI)
Imaginea include Apache + PHP 8.2 cu `mod_rewrite` și `mod_headers`, identic cu un hosting de producție.
**Build și run:**
```bash
docker build -t efactura-generator .
docker run --rm -p 8080:80 efactura-generator
```
Apoi deschideți http://localhost:8080
**Cu PHP receiver activ** (opțional — doar dacă integrați cu un sistem extern):
```bash
# Copiați și editați config.json înainte de build
cp config.json config.local.json
# Editați config.local.json: api_key, allowed_ips
docker build -t efactura-generator .
docker run --rm -p 8080:80 \
-v "$(pwd)/config.local.json:/var/www/html/config.json:ro" \
efactura-generator
```
**Testare rapidă (fără PHP):**
```bash
# Nici o dependință — server Node minimal, doar fișiere statice
node js/server.js # http://localhost:3000
```
> **Notă:** Directorul `temp/` din container este volatil — se șterge la `docker run --rm`. Dacă vreți persistență, montați un volum: `-v "$(pwd)/temp:/var/www/html/temp"`.
### Opțiunea 5: Integrare cu sistem extern (PHP)
Pentru a primi un XML dintr-o aplicație externă și a-l deschide direct în editor:
1. Configurați `config.json` (cheie API, IP-uri permise, durată de viață fișiere temporare).
2. Sistemul extern face POST cu conținutul XML către `receiver.php`, transmițând antetul `X-Api-Key`.
3. `receiver.php` validează XML-ul și namespace-urile UBL, salvează în `temp/` și returnează numele fișierului.
4. Utilizatorul este redirecționat către `index.html?xml=<nume_fisier>`, care încarcă XML-ul automat.
5. Fișierul temporar este șters după încărcare; fișierele mai vechi decât `temp_file_lifetime` ore sunt curățate automat.
Pentru diagnosticare există pagina `test-config.php`.
## Structura proiectului
```
efactura-generator/
├── index.html # Pagina principală
├── styles/main.css
├── js/
│ ├── script.js # Logica completă: parsare/generare XML, formular, totaluri
│ ├── formatter.js # Formatare numere/sume/cantități
│ ├── print.js # Generare vizualizare printabilă
│ └── server.js # Server static minimal pentru dezvoltare locală
├── templates/
│ ├── print.html # Șablon printare standard
│ └── print-compact.html # Șablon printare compact
├── receiver.php # Endpoint opțional pentru încărcare XML din sisteme externe
├── test-config.php # Pagina de diagnosticare pentru receiver
├── config.json # Configurație receiver (API key, IP-uri permise)
└── .htaccess.template # Configurație Apache pentru hosting în producție
```
## Licență
[AGPL-3.0-or-later](LICENSE.md)
Dacă folosiți acest software, chiar și ca serviciu web, trebuie să:
1. Menționați proiectul original.
2. Partajați toate modificările făcute.
3. Folosiți aceeași licență AGPL-3.
## Linkuri
- [Istoric modificări](CHANGELOG.md)
- [De făcut](TODO.md)
- [www.romfast.ro](https://www.romfast.ro)

View File

@@ -1,242 +1,305 @@
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor Factură Electronică</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.8.0/css/pikaday.min.css">
<link rel="stylesheet" href="./styles/main.css">
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
</head>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>Editor eFactura</h1>
<div class="app-author">Romfast SRL</div>
</div>
<div class="button-group">
<input type="file" id="fileInput" class="file-input" accept=".xml">
<button onclick="document.getElementById('fileInput').click()" class="button">Alege Fișier XML</button>
<button onclick="handleStorno()" class="button button-danger">Stornează</button>
<button onclick="saveXML()" class="button button-secondary">Salvează XML</button>
</div>
</div>
<form id="invoiceForm">
<!-- Details Grid -->
<div class="details-grid">
<!-- Invoice Details -->
<div class="form-section invoice-details">
<h2 class="section-title">Detalii Factură</h2>
<div class="compact-grid">
<div class="form-group">
<label class="form-label">Număr Factură</label>
<input type="text" class="form-input" name="invoiceNumber">
</div>
<div class="form-group">
<label class="form-label">Data Emiterii</label>
<div class="date-input-container">
<input type="text" class="form-input date-input" name="issueDate" placeholder="dd.mm.yyyy" maxlength="10" pattern="\d{2}.\d{2}.\d{4}">
<button type="button" class="calendar-button" tabindex="-1">📅</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Data Scadentă</label>
<div class="date-input-container">
<input type="text" class="form-input date-input" name="dueDate" placeholder="dd.mm.yyyy" maxlength="10" pattern="\d{2}.\d{2}.\d{4}">
<button type="button" class="calendar-button" tabindex="-1">📅</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Monedă Factură</label>
<input type="text" class="form-input" name="documentCurrencyCode" value="RON" maxlength="3">
</div>
<div class="form-group">
<label class="form-label">Monedă TVA (opțional)</label>
<input type="text" class="form-input" name="taxCurrencyCode" maxlength="3">
</div>
</div>
</div>
<!-- Supplier Details -->
<div class="form-section party-details">
<h2 class="section-title">Detalii Furnizor</h2>
<div class="compact-grid">
<div class="form-group">
<label class="form-label">Nume</label>
<input type="text" class="form-input" name="supplierName">
</div>
<div class="form-group">
<label class="form-label">Cod TVA</label>
<input type="text" class="form-input" name="supplierVAT">
</div>
<div class="form-group">
<label class="form-label">Nr. înregistrare</label>
<input type="text" class="form-input" name="supplierCompanyId">
</div>
<div class="form-group">
<label class="form-label">Adresă</label>
<input type="text" class="form-input" name="supplierAddress">
</div>
<div class="form-group">
<label class="form-label">Oraș</label>
<input type="text" class="form-input" name="supplierCity">
</div>
<div class="form-group">
<label class="form-label">Județ</label>
<input type="text" class="form-input" name="supplierCountrySubentity">
</div>
<div class="form-group">
<label class="form-label">Țară</label>
<input type="text" class="form-input" name="supplierCountry">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="tel" class="form-input" name="supplierPhone" pattern="[0-9]*">
</div>
<div class="form-group">
<label class="form-label">Persoană Contact</label>
<input type="text" class="form-input" name="supplierContactName">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" name="supplierEmail">
</div>
</div>
</div>
<!-- Customer Details -->
<div class="form-section party-details">
<h2 class="section-title">Detalii Client</h2>
<div class="compact-grid">
<div class="form-group">
<label class="form-label">Nume</label>
<input type="text" class="form-input" name="customerName">
</div>
<div class="form-group">
<label class="form-label">Cod TVA</label>
<input type="text" class="form-input" name="customerVAT">
</div>
<div class="form-group">
<label class="form-label">Nr. înregistrare</label>
<input type="text" class="form-input" name="customerCompanyId">
</div>
<div class="form-group">
<label class="form-label">Adresă</label>
<input type="text" class="form-input" name="customerAddress">
</div>
<div class="form-group">
<label class="form-label">Oraș</label>
<input type="text" class="form-input" name="customerCity">
</div>
<div class="form-group">
<label class="form-label">Județ</label>
<input type="text" class="form-input" name="customerCountrySubentity">
</div>
<div class="form-group">
<label class="form-label">Țară</label>
<input type="text" class="form-input" name="customerCountry">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="tel" class="form-input" name="customerPhone" pattern="[0-9]*">
</div>
<div class="form-group">
<label class="form-label">Persoană Contact</label>
<input type="text" class="form-input" name="customerContactName">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" name="customerEmail">
</div>
</div>
</div>
</div>
<!-- Note field -->
<div class="form-section">
<h2 class="section-title">Text Adițional</h2>
<div class="form-group">
<textarea class="form-input note-input" name="invoiceNote" rows="4" maxlength="900"></textarea>
<div class="note-counter">0/900 caractere</div>
</div>
</div>
<!-- Allowances and Charges -->
<div class="allowance-charges" id="allowanceCharges">
<h2 class="section-title">
Reduceri și Taxe Suplimentare
<button type="button" class="button button-small" onclick="addAllowanceCharge()">
Adaugă Reducere/Taxă
</button>
</h2>
</div>
<!-- Line Items -->
<div class="line-items" id="lineItems">
<h2 class="section-title">
Articole Factură
<button type="button" class="button button-small" onclick="addLineItem()">
Adaugă Articol
</button>
</h2>
</div>
<!-- Totals Section -->
<div class="totals">
<div class="total-row">
<span>Subtotal:</span>
<span id="subtotal" class="editable-total">0.00</span>
</div>
<div class="total-row">
<span>Total Reduceri:</span>
<span id="totalAllowances" class="editable-total">0.00</span>
</div>
<div class="total-row">
<span>Total Taxe:</span>
<span id="totalCharges" class="editable-total">0.00</span>
</div>
<div class="total-row">
<span>Valoare Netă:</span>
<span id="netAmount" class="editable-total">0.00</span>
</div>
<!-- VAT Breakdown -->
<div id="vatBreakdown">
<h3 class="section-title">Defalcare TVA</h3>
<div id="vatBreakdownRows"></div>
<button type="button" class="button button-small" onclick="window.addVATRate()">
Adaugă Cotă TVA
</button>
<div class="vat-total-group">
<div class="total-row">
<span>Total TVA:</span>
<span id="vat" class="editable-total">0.00</span>
</div>
</div>
</div>
<div class="total-row total-row-final">
<span>Total:</span>
<span id="total" class="editable-total">0.00</span>
</div>
<div class="total-row">
<button type="button" class="button" onclick="refreshTotals()">Recalculează Totaluri</button>
</div>
</div>
</form>
<footer class="app-footer">
<span id="app-version">v0.9-beta-4</span>
<a href="https://www.romfast.ro">www.romfast.ro</a>
</footer>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.8.0/pikaday.min.js"></script>
<script type="module" src="./js/script.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Editor Factură Electronică</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=geist:400,500,600,700|geist-mono:400,500,600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.8.0/css/pikaday.min.css">
<link rel="stylesheet" href="./styles/main.css">
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon" />
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>Editor eFactura</h1>
<div class="app-author">Romfast SRL</div>
</div>
<div class="button-group">
<input type="file" id="fileInput" class="file-input" accept=".xml,.zip,application/zip,application/x-zip-compressed" multiple>
<button onclick="document.getElementById('fileInput').click()" class="button">Alege Fișier XML / ZIP</button>
<button onclick="window.openNewInvoiceModal()" class="button" title="Factură nouă cu numerotare automată">Factură Nouă</button>
<button onclick="handleStorno()" class="button button-danger">Stornează</button>
<button onclick="saveXML()" class="button button-secondary">Salvează XML</button>
<div class="actions-menu-wrapper">
<button id="btnActionsMenu" type="button" class="button button-secondary" aria-haspopup="menu" aria-expanded="false" aria-controls="actionsMenu">Acțiuni <span class="actions-menu-chevron" aria-hidden="true"></span></button>
<div id="actionsMenu" class="actions-menu" role="menu" aria-labelledby="btnActionsMenu" hidden>
<button type="button" role="menuitem" class="actions-menu-item" data-action="print" data-template="standard">Printează — Standard</button>
<button type="button" role="menuitem" class="actions-menu-item" data-action="print" data-template="compact">Printează — Compact</button>
<div class="actions-menu-divider" role="separator"></div>
<button id="btnDownloadPDF" type="button" role="menuitem" class="actions-menu-item" data-action="downloadPdf">Descarcă PDF</button>
<div class="actions-menu-anaf" hidden>
<button id="btnPdfAnaf" type="button" role="menuitem" class="actions-menu-item is-success" data-action="pdfAnaf" title="PDF oficial generat de ANAF din XML (necesită receiver.php)">PDF ANAF</button>
<button id="btnValidateAnaf" type="button" role="menuitem" class="actions-menu-item is-success" data-action="validateAnaf" title="Validare prin API ANAF (necesită receiver.php)">Validare ANAF</button>
</div>
</div>
</div>
</div>
</div>
<form id="invoiceForm">
<!-- Details Grid -->
<div class="details-grid">
<!-- Invoice Details -->
<div class="form-section invoice-details">
<h2 class="section-title">Detalii Factură</h2>
<div class="compact-grid">
<div class="form-group">
<label class="form-label">Număr Factură</label>
<input type="text" class="form-input mono" name="invoiceNumber" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Tip Factură</label>
<select class="form-input" name="invoiceTypeCode">
<option value="380">380 — Factură comercială</option>
<option value="381">381 — Notă de credit</option>
<option value="384">384 — Factură corectată</option>
<option value="389">389 — Autofactură</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Data Emiterii</label>
<div class="date-input-container">
<input type="text" class="form-input date-input num" name="issueDate" placeholder="dd.mm.yyyy" maxlength="10" pattern="\d{2}.\d{2}.\d{4}">
<button type="button" class="calendar-button" tabindex="-1">📅</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Data Scadentă</label>
<div class="date-input-container">
<input type="text" class="form-input date-input num" name="dueDate" placeholder="dd.mm.yyyy" maxlength="10" pattern="\d{2}.\d{2}.\d{4}">
<button type="button" class="calendar-button" tabindex="-1">📅</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Monedă Factură</label>
<input type="text" class="form-input mono" name="documentCurrencyCode" value="RON" maxlength="3" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Monedă TVA (opțional)</label>
<input type="text" class="form-input mono" name="taxCurrencyCode" maxlength="3" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Factură Referită (nr.)</label>
<input type="text" class="form-input mono" name="billingRefId" placeholder="Nr. factură originală" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Data Factură Referită</label>
<div class="date-input-container">
<input type="text" class="form-input date-input num" name="billingRefDate" placeholder="dd.mm.yyyy" maxlength="10" pattern="\d{2}.\d{2}.\d{4}">
<button type="button" class="calendar-button" tabindex="-1">📅</button>
</div>
</div>
</div>
</div>
<!-- Supplier Details -->
<div class="form-section party-details">
<h2 class="section-title">
Detalii Furnizor
<span class="card-actions" id="supplierProfileActions">
<button type="button" class="button button-secondary button-small"
id="btnSaveProfile" onclick="window.saveSupplierProfile()">Salvează profil</button>
<button type="button" class="button button-secondary button-small"
id="btnUseProfile" onclick="window.useSupplierProfile()">Folosește profil</button>
<button type="button" class="button button-secondary button-small"
id="btnDeleteProfile" onclick="window.deleteSupplierProfile()">Șterge profil</button>
</span>
</h2>
<div class="compact-grid">
<div class="form-group">
<label class="form-label">Nume</label>
<input type="text" class="form-input" name="supplierName">
</div>
<div class="form-group">
<label class="form-label">Cod TVA</label>
<div class="input-with-action">
<input type="text" class="form-input mono" name="supplierVAT" style="text-align:left">
<button type="button" id="btnLookupSupplierCif" class="button button-secondary button-small anaf-cif-btn" style="display:none" onclick="window.lookupCif('supplier')" title="Caută date firmă din ANAF">Caută</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Nr. înregistrare</label>
<input type="text" class="form-input mono" name="supplierCompanyId" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Adresă</label>
<input type="text" class="form-input" name="supplierAddress">
</div>
<div class="form-group">
<label class="form-label">Oraș</label>
<input type="text" class="form-input" name="supplierCity">
</div>
<div class="form-group">
<label class="form-label">Județ</label>
<input type="text" class="form-input" name="supplierCountrySubentity">
</div>
<div class="form-group">
<label class="form-label">Țară</label>
<input type="text" class="form-input" name="supplierCountry">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="tel" class="form-input mono" name="supplierPhone" pattern="[0-9]*" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Persoană Contact</label>
<input type="text" class="form-input" name="supplierContactName">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" name="supplierEmail">
</div>
</div>
</div>
<!-- Customer Details -->
<div class="form-section party-details">
<h2 class="section-title">Detalii Client</h2>
<div class="compact-grid">
<div class="form-group">
<label class="form-label">Nume</label>
<input type="text" class="form-input" name="customerName">
</div>
<div class="form-group">
<label class="form-label">Cod TVA</label>
<div class="input-with-action">
<input type="text" class="form-input mono" name="customerVAT" style="text-align:left">
<button type="button" id="btnLookupCustomerCif" class="button button-secondary button-small anaf-cif-btn" style="display:none" onclick="window.lookupCif('customer')" title="Caută date firmă din ANAF">Caută</button>
</div>
</div>
<div class="form-group">
<label class="form-label">Nr. înregistrare</label>
<input type="text" class="form-input mono" name="customerCompanyId" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Adresă</label>
<input type="text" class="form-input" name="customerAddress">
</div>
<div class="form-group">
<label class="form-label">Oraș</label>
<input type="text" class="form-input" name="customerCity">
</div>
<div class="form-group">
<label class="form-label">Județ</label>
<input type="text" class="form-input" name="customerCountrySubentity">
</div>
<div class="form-group">
<label class="form-label">Țară</label>
<input type="text" class="form-input" name="customerCountry">
</div>
<div class="form-group">
<label class="form-label">Telefon</label>
<input type="tel" class="form-input mono" name="customerPhone" pattern="[0-9]*" style="text-align:left">
</div>
<div class="form-group">
<label class="form-label">Persoană Contact</label>
<input type="text" class="form-input" name="customerContactName">
</div>
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" name="customerEmail">
</div>
</div>
</div>
</div>
<!-- Note field -->
<div class="form-section">
<h2 class="section-title">Text Adițional</h2>
<div class="form-group">
<textarea class="form-input note-input" name="invoiceNote" rows="4" maxlength="900"></textarea>
<div class="note-counter">0/900 caractere</div>
</div>
</div>
<!-- Payment Means (A5) -->
<div class="form-section" id="paymentMeansSection">
<h2 class="section-title">
Modalități de Plată
<button type="button" class="button button-small" onclick="window.addPaymentMeansRow()">+ Adaugă</button>
</h2>
<div id="paymentMeansRows"></div>
</div>
<!-- Allowances and Charges -->
<div class="allowance-charges" id="allowanceCharges">
<h2 class="section-title">
Reduceri și Taxe Suplimentare
<button type="button" class="button button-small" onclick="addAllowanceCharge()">
Adaugă Reducere/Taxă
</button>
</h2>
</div>
<!-- Line Items -->
<div class="line-items" id="lineItems">
<h2 class="section-title">
Articole Factură
<button type="button" class="button button-small" onclick="addLineItem()">
Adaugă Articol
</button>
</h2>
</div>
<!-- Totals Section -->
<div class="totals">
<div class="total-row">
<span>Subtotal:</span>
<span id="subtotal" class="editable-total">0.00</span>
</div>
<div class="total-row">
<span>Total Reduceri:</span>
<span id="totalAllowances" class="editable-total">0.00</span>
</div>
<div class="total-row">
<span>Total Taxe:</span>
<span id="totalCharges" class="editable-total">0.00</span>
</div>
<div class="total-row">
<span>Valoare Netă:</span>
<span id="netAmount" class="editable-total">0.00</span>
</div>
<!-- VAT Breakdown -->
<div id="vatBreakdown">
<h3 class="section-title">Defalcare TVA</h3>
<div id="vatBreakdownRows"></div>
<button type="button" class="button button-small" onclick="window.addVATRate()">
Adaugă Cotă TVA
</button>
<div class="vat-total-group">
<div class="total-row">
<span>Total TVA:</span>
<span id="vat" class="editable-total">0.00</span>
</div>
</div>
</div>
<div class="total-row total-row-final">
<span>Total:</span>
<span class="total-with-badge">
<span id="total" class="editable-total">0.00</span>
<span id="total-badge" class="badge"></span>
</span>
</div>
<div class="total-row">
<button type="button" class="button" onclick="refreshTotals()">Recalculează Totaluri</button>
</div>
</div>
</form>
<footer class="app-footer">
<span id="app-version">v0.9-beta-15</span>
<a href="https://www.romfast.ro">www.romfast.ro</a>
</footer>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pikaday/1.8.0/pikaday.min.js"></script>
<script type="module" src="./js/script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,152 @@
/**
* js/anaf.js — Proxy ANAF APIs prin receiver.php
*
* Toate apelurile merg prin receiver.php (CORS proxy server-side).
* Pe hosting static (GitHub Pages, fără PHP), apelurile vor eșua cu eroare
* "receiver indisponibil" — verificați cu probeReceiver() la inițializare.
*
* Configurare opțională în config.json (server-side):
* "anaf_token": "<Bearer token OAuth ANAF>" — folosește ruta OAuth (api.anaf.ro)
* Fără token: receiver folosește ruta publică webservicesp.anaf.ro (fără auth).
*
* Endpoints ANAF (proxied):
* Validate : POST /FCTEL/rest/validare/FACT1
* XmlToPdf : POST /FCTEL/rest/transformare/FACT1 (validează default; întoarce PDF)
* CIF info : POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
*/
const RECEIVER = './receiver.php';
/**
* Verifică dacă receiver.php este disponibil pe server.
* Returnează true dacă poate răspunde la ?action=ping.
* @returns {Promise<boolean>}
*/
export async function probeReceiver() {
try {
const res = await fetch(`${RECEIVER}?action=ping`, { method: 'GET' });
if (!res.ok) return false;
const json = await res.json().catch(() => null);
return json?.pong === true;
} catch {
return false;
}
}
/**
* Validează un XML eFactura prin API-ul ANAF (necesită Bearer token în config.json).
* @param {string} xmlContent - XML ca string UTF-8
* @returns {Promise<{valid: boolean, messages: Array<{message:string, severity:string, xpathLocation?:string}>}>}
*/
export async function anafValidate(xmlContent) {
let res;
try {
res = await fetch(`${RECEIVER}?action=validate`, {
method: 'POST',
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: xmlContent
});
} catch (e) {
throw new Error('Receiver.php indisponibil — ' + e.message);
}
if (!res.ok) {
let msg = `ANAF validare: HTTP ${res.status}`;
try { const t = await res.text(); if (t) msg += ' — ' + t.slice(0, 200); } catch { /* ok */ }
throw new Error(msg);
}
const data = await res.json();
// Normalizare răspuns ANAF: { Messages: [{message, severity, xpathLocation}] }
const messages = (data.Messages || data.messages || []).map(m => ({
message: m.message || m.Message || String(m),
severity: (m.severity || m.Severity || 'ERROR').toUpperCase(),
xpathLocation: m.xpathLocation || m.XpathLocation || ''
}));
const valid = messages.filter(m => m.severity === 'ERROR' || m.severity === 'FATAL').length === 0;
return { valid, messages };
}
/**
* Obține PDF-ul oficial ANAF al facturii (transformare XML → PDF).
* ANAF /transformare/FACT1 validează XML-ul și întoarce direct PDF binary.
* Dacă XML-ul nu trece validarea, ANAF întoarce JSON cu erori (status 400).
*
* @param {string} xmlContent - XML ca string UTF-8
* @returns {Promise<{pdf: Blob}|{errors: Array<{message:string,severity:string}>}>}
* - pdf: Blob `application/pdf` la succes
* - errors: listă mesaje validare la eșec
*/
export async function anafPdf(xmlContent) {
let res;
try {
res = await fetch(`${RECEIVER}?action=pdf`, {
method: 'POST',
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: xmlContent
});
} catch (e) {
throw new Error('Receiver.php indisponibil — ' + e.message);
}
const ct = (res.headers.get('Content-Type') || '').toLowerCase();
if (res.ok && ct.includes('application/pdf')) {
return { pdf: await res.blob() };
}
// ANAF validation errors come back as JSON (HTTP 400 or 200 with JSON body)
if (ct.includes('application/json')) {
const data = await res.json().catch(() => null);
const messages = (data?.Messages || data?.messages || []).map(m => ({
message: m.message || m.Message || String(m),
severity: (m.severity || m.Severity || 'ERROR').toUpperCase()
}));
if (messages.length) return { errors: messages };
if (data?.error) throw new Error(data.error);
}
if (!res.ok) {
const txt = await res.text().catch(() => '');
throw new Error(`ANAF transformare: HTTP ${res.status}` + (txt ? ' — ' + txt.slice(0, 200) : ''));
}
throw new Error(`ANAF transformare: răspuns neașteptat (${ct || 'fără content-type'})`);
}
/**
* Caută informații contribuabil după CIF prin ANAF.
* Folosește API-ul sincron ANAF v9 (webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva).
* Nu necesită token OAuth.
* @param {string|number} cif - CIF/CUI (cu sau fără prefix RO)
* @returns {Promise<{
* found: boolean,
* denumire?: string,
* adresa?: string,
* nrRegCom?: string,
* cui?: number,
* tvaActiv?: boolean,
* strada?: string,
* oras?: string,
* judetCod?: string,
* codPostal?: string,
* telefon?: string,
* statusEFactura?: boolean
* }>}
* @property {string} strada - Strada + număr din adresa_sediu_social ANAF
* @property {string} oras - Localitatea (fără prefix MUN./ORS./COM.)
* @property {string} judetCod - Cod județ ISO format RO-XX (ex: RO-B, RO-CJ)
* @property {string} codPostal - Cod poștal
* @property {string} telefon - Număr telefon din date_generale ANAF
* @property {boolean} statusEFactura - Înregistrat în sistemul eFactura
*/
export async function anafCifLookup(cif) {
const cifNum = String(cif).replace(/^RO\s*/i, '').trim();
let res;
try {
res = await fetch(`${RECEIVER}?action=cif&cif=${encodeURIComponent(cifNum)}`);
} catch (e) {
throw new Error('Receiver.php indisponibil — ' + e.message);
}
if (!res.ok) {
throw new Error(`ANAF CIF lookup: HTTP ${res.status}`);
}
return res.json();
}

View File

@@ -0,0 +1,129 @@
/**
* js/catalog.js — Catalog produse/servicii în IndexedDB (PR-A13)
*
* Folosește `openCatalog()` din storage.js (DB `efactura` v1, store `products`,
* indexes: `name`, `sellerItemID`, `cpvCode`).
*
* Schema produs (v1):
* id: string (UUID v4)
* name: string — denumire produs/serviciu (indexed, searched by prefix)
* unit: string — cod UM (EA, KGM, etc.)
* price: string — preț unitar canonical decimal
* vatType: string — cod tip TVA (S, AE, O, Z, E)
* vatRate: string — cotă TVA (19, 9, 5, 0)
* description: string — descriere detaliată (opțional)
* sellerItemID: string — cod articol furnizor (opțional, indexed)
* cpvCode: string — cod CPV (opțional, indexed)
*/
import { openCatalog } from './storage.js';
/**
* Adaugă sau actualizează un produs în catalog.
* Dacă `product.id` lipsește, generează UUID nou.
* @param {Object} product
* @returns {Promise<string>} ID-ul produsului salvat
*/
export async function catalogAdd(product) {
const db = await openCatalog();
const entry = {
id: product.id || _uuid(),
name: (product.name || '').trim(),
unit: (product.unit || 'EA').trim(),
price: (product.price || '0').trim(),
vatType: (product.vatType || 'S').trim(),
vatRate: (product.vatRate || '19').trim(),
description: (product.description || '').trim(),
sellerItemID: (product.sellerItemID || '').trim(),
cpvCode: (product.cpvCode || '').trim(),
};
return new Promise((resolve, reject) => {
const tx = db.transaction('products', 'readwrite');
const store = tx.objectStore('products');
const req = store.put(entry);
req.onsuccess = () => resolve(entry.id);
req.onerror = () => reject(req.error);
});
}
/**
* Caută produse după prefix de denumire (case-insensitive prefix match).
* Returnează max `limit` rezultate, sortate alfabetic.
* @param {string} prefix
* @param {number} limit
* @returns {Promise<Array>}
*/
export async function catalogSearch(prefix, limit = 8) {
if (!prefix || !prefix.trim()) return [];
const db = await openCatalog();
const lower = prefix.trim().toLowerCase();
const results = [];
return new Promise((resolve, reject) => {
const tx = db.transaction('products', 'readonly');
const store = tx.objectStore('products');
const index = store.index('name');
// Interval IDB: [lower, lower + '￿') pentru prefix match
const range = IDBKeyRange.bound(lower, lower + '￿', false, false);
// Scanăm cu cursor pe index name (lowercase nu e direct în IDB —
// folosim open cursor pe tot și filtrăm client-side pentru robustețe)
const allReq = index.openCursor();
allReq.onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor || results.length >= limit) {
resolve(results);
return;
}
const name = (cursor.value.name || '').toLowerCase();
if (name.startsWith(lower)) {
results.push(cursor.value);
}
cursor.continue();
};
allReq.onerror = () => reject(allReq.error);
});
}
/**
* Șterge un produs din catalog după ID.
* @param {string} id
* @returns {Promise<void>}
*/
export async function catalogDelete(id) {
const db = await openCatalog();
return new Promise((resolve, reject) => {
const tx = db.transaction('products', 'readwrite');
const store = tx.objectStore('products');
const req = store.delete(id);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
});
}
/**
* Listează toate produsele (pentru management catalog).
* @returns {Promise<Array>}
*/
export async function catalogList() {
const db = await openCatalog();
return new Promise((resolve, reject) => {
const tx = db.transaction('products', 'readonly');
const store = tx.objectStore('products');
const req = store.getAll();
req.onsuccess = () => resolve(req.result || []);
req.onerror = () => reject(req.error);
});
}
/** Generează un UUID v4 simplu (crypto.randomUUID dacă disponibil, fallback manual). */
function _uuid() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}

View File

@@ -1,71 +1,71 @@
export class InvoiceFormatter {
constructor() {
this.locale = navigator.language;
this.currencyFormatter = new Intl.NumberFormat(this.locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: true
});
this.quantityFormatter = new Intl.NumberFormat(this.locale, {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
useGrouping: true
});
this.numberFormatter = new Intl.NumberFormat(this.locale, {
minimumFractionDigits: 4,
maximumFractionDigits: 4,
useGrouping: true
});
}
formatCurrency(value) {
const numValue = parseFloat(value);
return isNaN(numValue) ? '0,00' : this.currencyFormatter.format(numValue);
}
formatQuantity(value) {
const numValue = parseFloat(value);
return isNaN(numValue) ? '0,000' : this.quantityFormatter.format(numValue);
}
formatNumber(value) {
const numValue = parseFloat(value);
return isNaN(numValue) ? '0,0000' : this.numberFormatter.format(numValue);
}
parseCurrency(value) {
if (typeof value !== 'string') {
value = value.toString();
}
// Remove all non-digit characters except decimal and minus
const normalized = value.replace(/[^\d\-.,]/g, '')
// Replace thousands separator
.replace(/[.,](?=.*[.,])/g, '')
// Last dot/comma is decimal separator
.replace(/[.,]/, '.');
return parseFloat(normalized) || 0;
}
parseQuantity(value) {
if (typeof value !== 'string') {
value = value.toString();
}
const normalized = value.replace(/[^\d\-.,]/g, '')
.replace(/[.,](?=.*[.,])/g, '')
.replace(/[.,]/, '.');
return parseFloat(normalized) || 0;
}
parseNumber(value) {
if (typeof value !== 'string') {
value = value.toString();
}
const normalized = value.replace(/[^\d\-.,]/g, '')
.replace(/[.,](?=.*[.,])/g, '')
.replace(/[.,]/, '.');
return parseFloat(normalized) || 0;
}
}
// js/formatter.js
//
// Compatibility-layer formatter folosit de print template + script.js
// pentru afișare. Internal delegate la js/numeric.js (PR-E E1+E3+E4).
//
// E2: locale hardcoded "ro-RO" (înlocuit `navigator.language`). Audiența
// țintă e RO; print PDF / display formular trebuie să fie consistent
// între browsere și OS-uri.
import { RO_LOCALE, parseStrict, parseStrictOr, format2, format3, format4 } from './numeric.js';
export class InvoiceFormatter {
constructor() {
this.locale = RO_LOCALE;
this.currencyFormatter = new Intl.NumberFormat(this.locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
useGrouping: true
});
this.quantityFormatter = new Intl.NumberFormat(this.locale, {
minimumFractionDigits: 3,
maximumFractionDigits: 3,
useGrouping: true
});
this.numberFormatter = new Intl.NumberFormat(this.locale, {
minimumFractionDigits: 4,
maximumFractionDigits: 4,
useGrouping: true
});
}
formatCurrency(value) {
const big = (value === '' || value === null || value === undefined)
? null
: parseStrict(value);
return big === null ? '0,00' : format2(big);
}
formatQuantity(value) {
const big = (value === '' || value === null || value === undefined)
? null
: parseStrict(value);
return big === null ? '0,000' : format3(big);
}
formatNumber(value) {
const big = (value === '' || value === null || value === undefined)
? null
: parseStrict(value);
return big === null ? '0,0000' : format4(big);
}
/**
* Strict-but-pragmatic parsing → number (pentru consumatorii vechi).
* Pentru cod nou, preferă `parseStrict` din numeric.js (returnează Big).
*/
parseCurrency(value) {
return Number(parseStrictOr(value, '0').toString());
}
parseQuantity(value) {
return Number(parseStrictOr(value, '0').toString());
}
parseNumber(value) {
return Number(parseStrictOr(value, '0').toString());
}
}

View File

@@ -0,0 +1,273 @@
// js/numeric.js
//
// Numeric pipeline canonică pentru editor eFactura (PR-E / Track 1).
//
// Trei reguli fundamentale:
//
// 1. `input.dataset.raw` este unica sursă de adevăr numerică (canonical
// decimal-dot string). `input.value` este display-only — locale
// "ro-RO" cu virgulă decimală.
//
// 2. Toate calculele folosesc Big.js (precizie arbitrară), niciodată
// Number. Rounding mode: HALF_UP (standard fiscal RO).
//
// 3. Parserul este strict-but-pragmatic: acceptă atât canonicul XML
// ("1234.56") cât și displayul RO ("1234,56" / "1.234,56"). Refuză
// formele EN ambigue ("1,234.56").
//
// Prefix module exports:
// - `Big` re-export pentru consumeri (single source of truth pentru
// pin-ul vendored).
// - `parseStrict(value)` → Big | null. null pentru NaN / empty / format
// ambiguu.
// - `parseStrictOr(value, fallback)` → Big. Fallback la "0" dacă invalid.
// - `format2`, `format3`, `format4` → string ro-RO display cu zecimale fix.
// - `formatRaw(big, decimals)` → string canonical decimal-dot pentru XML.
// - `setRaw(input, value)` → setează dataset.raw + input.value formatted.
// - `getRaw(input)` → Big citit din dataset.raw, fallback la parseStrict
// pe input.value.
// - `lineTotal(qty, price, discount, vatRate)` → { net, vat, gross } cu Big.
import Big from './vendor/big.mjs';
// HALF_UP = 1 în big.js. (HALF_EVEN = 2, HALF_DOWN = 3 — vezi big.mjs).
Big.RM = 1;
// Default decimal places pentru division (suficient pentru calcul intermediar).
Big.DP = 20;
export { Big };
// Locale hardcoded pentru proiectul RO. NU folosim navigator.language —
// vezi DESIGN.md / E2.
export const RO_LOCALE = 'ro-RO';
const _displayFmt = {
2: new Intl.NumberFormat(RO_LOCALE, { minimumFractionDigits: 2, maximumFractionDigits: 2, useGrouping: true }),
3: new Intl.NumberFormat(RO_LOCALE, { minimumFractionDigits: 3, maximumFractionDigits: 3, useGrouping: true }),
4: new Intl.NumberFormat(RO_LOCALE, { minimumFractionDigits: 4, maximumFractionDigits: 4, useGrouping: true }),
};
/**
* Parser strict-but-pragmatic.
*
* Acceptă:
* - canonical XML / număr cu zecimală pe punct: "1234.56", "0.001"
* - RO display cu zecimală pe virgulă: "1234,56", "1.234,56", "1.234.567,89"
* - integer: "0", "-12", " 42 "
* - Big sau Number: returnate direct (Number → Big via toString).
*
* Refuză (returnează null):
* - empty string / null / undefined
* - NaN (după ce s-a încercat normalizarea)
* - format EN cu thousands separator pe virgulă: "1,234.56" (ambiguu pentru RO)
* - alte caractere non-numerice: "abc", "1.2.3" cu mai multe puncte și fără virgulă
*
* @param {string|number|Big|null|undefined} value
* @returns {Big|null}
*/
export function parseStrict(value) {
if (value === null || value === undefined) return null;
if (value instanceof Big) return value;
if (typeof value === 'number') {
if (!Number.isFinite(value)) return null;
return new Big(value.toString());
}
if (typeof value !== 'string') return null;
let s = value.trim();
if (s === '') return null;
// Optional leading minus.
let sign = '';
if (s.startsWith('-')) { sign = '-'; s = s.slice(1); }
else if (s.startsWith('+')) { s = s.slice(1); }
if (s === '') return null;
const dotCount = (s.match(/\./g) || []).length;
const commaCount = (s.match(/,/g) || []).length;
let canonical;
if (commaCount === 0 && dotCount === 0) {
// integer
if (!/^\d+$/.test(s)) return null;
canonical = s;
} else if (commaCount === 0 && dotCount === 1) {
// canonical decimal-dot: "1234.56"
if (!/^\d+\.\d+$/.test(s)) return null;
canonical = s;
} else if (commaCount === 0 && dotCount > 1) {
// ambigu: "1.2.3" — refuz
return null;
} else if (commaCount === 1) {
// RO: virgula = decimală; punctele = thousands.
// Forma așteptată: cifre[.cifre[.cifre]]*,cifre+
if (!/^\d{1,3}(?:\.\d{3})*,\d+$/.test(s) && !/^\d+,\d+$/.test(s)) {
return null;
}
canonical = s.replace(/\./g, '').replace(',', '.');
} else {
// commaCount > 1 — nu e RO valid. Refuz (ar putea fi EN "1,234,567.89"
// dar asta e ambiguu pentru audiența RO).
return null;
}
try {
return new Big(sign + canonical);
} catch (_) {
return null;
}
}
/**
* Variantă "or fallback" pentru cazurile unde un fallback la zero e
* acceptabil (display, sumare). NU folosi pentru validare.
*
* @param {*} value
* @param {string|number|Big} fallback
* @returns {Big}
*/
export function parseStrictOr(value, fallback = '0') {
const parsed = parseStrict(value);
if (parsed !== null) return parsed;
if (fallback instanceof Big) return fallback;
return new Big(fallback);
}
/** Format Big → string display ro-RO cu N zecimale fixe. */
function _format(value, decimals) {
const big = (value instanceof Big) ? value : parseStrictOr(value);
const fmt = _displayFmt[decimals] || _displayFmt[2];
// Big.toFixed(decimals) → canonical decimal-dot. Convert la Number
// doar pentru Intl format (number passes through cu precizie suficientă
// pentru valori fiscale practice).
return fmt.format(Number(big.toFixed(decimals)));
}
export function format2(value) { return _format(value, 2); }
export function format3(value) { return _format(value, 3); }
export function format4(value) { return _format(value, 4); }
/**
* Format pentru ieșirea XML: canonical decimal-dot, fix N zecimale,
* fără thousands separator. Folosit la serializare UBL.
*
* @param {*} value
* @param {number} decimals
* @returns {string}
*/
export function formatRaw(value, decimals = 2) {
const big = (value instanceof Big) ? value : parseStrictOr(value);
return big.toFixed(decimals);
}
/**
* Setează valoarea unui input numeric:
* - `dataset.raw` ← canonical decimal-dot (sursa de adevăr)
* - `input.value` ← display ro-RO cu N zecimale
*
* Folosit la populare din XML și la commit-ul user-editat (post-blur).
*
* @param {HTMLInputElement} input
* @param {*} value Big | string | number
* @param {number} decimals decimale display (2 = currency, 3 = qty, 4 = price)
*/
export function setRaw(input, value, decimals = 2) {
const big = (value instanceof Big) ? value : parseStrictOr(value);
input.dataset.raw = big.toFixed(decimals);
// type="number" acceptă doar punct decimal; type="text" primește display ro-RO
input.value = (input.type === 'number') ? big.toFixed(decimals) : _format(big, decimals);
}
/**
* Citește valoarea numerică canonică a unui input.
* - Preferă `dataset.raw` (set de noi pe populate / blur).
* - Fallback la `parseStrict(input.value)` dacă raw absent.
* - Fallback final la Big("0").
*
* @param {HTMLInputElement} input
* @returns {Big}
*/
export function getRaw(input) {
if (!input) return new Big('0');
if (input.dataset && input.dataset.raw !== undefined && input.dataset.raw !== '') {
const parsed = parseStrict(input.dataset.raw);
if (parsed !== null) return parsed;
}
return parseStrictOr(input.value, '0');
}
/**
* Marchează un input ca dirty (editat de user). PR-A11 va folosi acest
* flag pentru tolerance switching (zero pe row dirty, ±0.01 RON pe row
* loaded).
*/
export function markDirty(input) {
if (input && input.dataset) input.dataset.dirty = '1';
}
/**
* Atașează handler-ul de blur care:
* 1. parseStrict pe input.value
* 2. setRaw cu valoarea normalizată (sau lasă raw existent dacă parse eșuează
* și marchează vizual ca invalid).
* 3. markDirty.
*
* @param {HTMLInputElement} input
* @param {number} decimals
*/
export function wireDatasetRaw(input, decimals = 2) {
if (!input || input.dataset.rawWired === '1') return;
input.addEventListener('blur', () => {
const parsed = parseStrict(input.value);
if (parsed === null && input.value.trim() !== '') {
input.classList.add('invalid');
return;
}
input.classList.remove('invalid');
if (parsed !== null) {
setRaw(input, parsed, decimals);
markDirty(input);
}
});
// La input change, marchează dirty (dar nu reformatează — lasă user să tasteze).
input.addEventListener('input', () => markDirty(input));
input.dataset.rawWired = '1';
}
/**
* Calculează totalul pe linia de factură.
*
* net = (qty * price) - lineDiscount
* vat = round2(net * vatRate / 100)
* gross = net + vat
*
* @param {*} qty
* @param {*} price
* @param {*} discount
* @param {*} vatRate procent (ex. 19 pentru 19%)
* @returns {{net: Big, vat: Big, gross: Big}}
*/
export function lineTotal(qty, price, discount, vatRate) {
const q = parseStrictOr(qty, '0');
const p = parseStrictOr(price, '0');
const d = parseStrictOr(discount, '0');
const r = parseStrictOr(vatRate, '0');
const gross = q.times(p);
const net = gross.minus(d);
const vat = net.times(r).div(100).round(2, 1); // HALF_UP
const total = net.plus(vat);
return { net, vat, gross: total };
}
/**
* Helper: a.eq(b) cu toleranță. Returnează true dacă |a - b| ≤ epsilon.
* Pentru A11 reconciliation legacy: ±0.01 RON.
*/
export function withinTolerance(a, b, epsilon) {
const aB = (a instanceof Big) ? a : parseStrictOr(a);
const bB = (b instanceof Big) ? b : parseStrictOr(b);
const eB = (epsilon instanceof Big) ? epsilon : parseStrictOr(epsilon);
return aB.minus(bB).abs().lte(eB);
}

View File

@@ -1,251 +1,251 @@
import { InvoiceFormatter } from './formatter.js';
export class InvoicePrintHandler {
constructor() {
this.printWindow = null;
this.formatter = new InvoiceFormatter();
this.templates = {
standard: './templates/print.html',
compact: './templates/print-compact.html'
};
this.currentTemplate = 'standard';
}
setTemplate(templateName) {
if (this.templates[templateName]) {
this.currentTemplate = templateName;
}
}
collectInvoiceData() {
return {
// Basic details
invoiceNumber: document.querySelector('[name="invoiceNumber"]').value,
issueDate: document.querySelector('[name="issueDate"]').value,
dueDate: document.querySelector('[name="dueDate"]').value,
documentCurrencyCode: document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON',
taxCurrencyCode: document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(),
exchangeRate: parseFloat(document.querySelector('[name="exchangeRate"]')?.value || 1),
// Supplier details
supplier: {
name: document.querySelector('[name="supplierName"]').value,
vat: document.querySelector('[name="supplierVAT"]').value,
companyId: document.querySelector('[name="supplierCompanyId"]').value,
address: document.querySelector('[name="supplierAddress"]').value,
city: document.querySelector('[name="supplierCity"]').value,
county: document.querySelector('[name="supplierCountrySubentity"]').value,
country: document.querySelector('[name="supplierCountry"]').value,
phone: document.querySelector('[name="supplierPhone"]').value,
contactName: document.querySelector('[name="supplierContactName"]').value,
email: document.querySelector('[name="supplierEmail"]').value
},
// Customer details
customer: {
name: document.querySelector('[name="customerName"]').value,
vat: document.querySelector('[name="customerVAT"]').value,
companyId: document.querySelector('[name="customerCompanyId"]').value,
address: document.querySelector('[name="customerAddress"]').value,
city: document.querySelector('[name="customerCity"]').value,
county: document.querySelector('[name="customerCountrySubentity"]').value,
country: document.querySelector('[name="customerCountry"]').value,
phone: document.querySelector('[name="customerPhone"]').value,
contactName: document.querySelector('[name="customerContactName"]').value,
email: document.querySelector('[name="customerEmail"]').value
},
// Line items with formatted values
items: Array.from(document.querySelectorAll('.line-item')).map((item, index) => ({
number: index + 1,
description: item.querySelector('[name^="description"]').value,
quantity: this.formatter.formatQuantity(item.querySelector('[name^="quantity"]').value),
unit: item.querySelector('[name^="unit"]').value,
price: this.formatter.formatCurrency(item.querySelector('[name^="price"]').value),
vatRate: this.formatter.formatCurrency(item.querySelector('[name^="vatRate"]').value),
totalAmount: this.formatter.formatCurrency(
this.formatter.parseQuantity(item.querySelector('[name^="quantity"]').value) *
this.formatter.parseCurrency(item.querySelector('[name^="price"]').value)
)
})),
// Note
note: document.querySelector('[name="invoiceNote"]')?.value,
// Get totals directly from the display elements
totals: {
subtotal: document.getElementById('subtotal').textContent,
allowances: document.getElementById('totalAllowances').textContent,
charges: document.getElementById('totalCharges').textContent,
netAmount: document.getElementById('netAmount').textContent,
vat: document.getElementById('vat').textContent,
total: document.getElementById('total').textContent
},
// VAT Breakdown
vatBreakdown: Array.from(document.querySelectorAll('.vat-row')).map(row => ({
type: row.querySelector('.vat-type').value,
rate: row.querySelector('.vat-rate').value,
base: row.querySelector('.vat-base').value,
amount: row.querySelector('.vat-amount').value
}))
};
}
createPartyHTML(party) {
return `
<p><strong>${party.name}</strong></p>
<p>CUI: ${party.vat}</p>
<p>Nr. Reg. Com.: ${party.companyId}</p>
<p>${party.address}</p>
<p>${party.city}${party.county ? ', ' + party.county : ''}</p>
<p>${party.country}</p>
${party.phone ? `<p>Tel: ${party.phone}</p>` : ''}
${party.contactName ? `<p>Contact: ${party.contactName}</p>` : ''}
${party.email ? `<p>Email: ${party.email}</p>` : ''}
`;
}
getVATTypeLabel(type) {
const labels = {
'S': 'Standard',
'AE': 'Taxare Inversă',
'O': 'Neplătitor TVA',
'Z': 'Cotă 0%',
'E': 'Scutit'
};
return labels[type] || type;
}
async print() {
try {
// Collect all the data
const invoiceData = this.collectInvoiceData();
// Open new window and load the selected print template
this.printWindow = window.open(
this.templates[this.currentTemplate],
'_blank',
'width=800,height=600'
);
// Wait for the window to load
await new Promise(resolve => {
this.printWindow.onload = resolve;
});
// Generate QR code
const qrData = {
invoiceNumber: invoiceData.invoiceNumber,
issueDate: invoiceData.issueDate,
supplier: invoiceData.supplier.name,
customer: invoiceData.customer.name,
total: this.formatter.parseCurrency(invoiceData.totals.total)
};
const qrElement = this.printWindow.document.getElementById('qrcode');
if (qrElement) {
new this.printWindow.QRCode(qrElement, {
text: JSON.stringify(qrData),
width: 100,
height: 100,
colorDark: "#2563eb",
colorLight: "#ffffff",
correctLevel: this.printWindow.QRCode.CorrectLevel.L
});
}
// Populate the template with data
this.populatePrintWindow(invoiceData);
// Print the window
this.printWindow.print();
// Clean up
this.printWindow.onafterprint = () => {
this.printWindow.close();
this.printWindow = null;
};
} catch (error) {
console.error('Print failed:', error);
if (this.printWindow) {
this.printWindow.close();
this.printWindow = null;
}
alert('A apărut o eroare la printare. Vă rugăm să încercați din nou.');
}
}
populatePrintWindow(data) {
if (!this.printWindow) return;
const doc = this.printWindow.document;
// Basic details
doc.getElementById('print-invoice-number').textContent = data.invoiceNumber;
doc.getElementById('print-issue-date').textContent = data.issueDate;
doc.getElementById('print-due-date').textContent = data.dueDate;
doc.getElementById('print-document-currency').textContent = data.documentCurrencyCode;
// Currency information
const taxCurrencyContainer = doc.getElementById('print-tax-currency-container');
if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) {
taxCurrencyContainer.style.display = 'block';
doc.getElementById('print-tax-currency').textContent = data.taxCurrencyCode;
doc.getElementById('print-exchange-rate').textContent = this.formatter.formatNumber(data.exchangeRate);
}
// Party details
doc.getElementById('print-supplier-details').innerHTML = this.createPartyHTML(data.supplier);
doc.getElementById('print-customer-details').innerHTML = this.createPartyHTML(data.customer);
// Note
if (data.note) {
const noteSection = doc.getElementById('print-note');
noteSection.style.display = 'block';
noteSection.querySelector('div').textContent = data.note;
}
// Line items - use formatted values from data
doc.getElementById('print-items').innerHTML = data.items.map(item => `
<tr>
<td>${item.number}</td>
<td>${item.description}</td>
<td>${item.unit}</td>
<td class="number-cell">${item.quantity}</td>
<td class="number-cell">${item.price}</td>
<td class="number-cell">${item.vatRate}%</td>
<td class="number-cell">${item.totalAmount}</td>
</tr>
`).join('');
// Totals - use values directly from display
doc.getElementById('print-subtotal').textContent = data.totals.subtotal;
doc.getElementById('print-allowances').textContent = data.totals.allowances;
doc.getElementById('print-charges').textContent = data.totals.charges;
doc.getElementById('print-net-amount').textContent = data.totals.netAmount;
doc.getElementById('print-total').textContent = data.totals.total;
// VAT Breakdown - use values directly from display
doc.getElementById('print-vat-breakdown').innerHTML = data.vatBreakdown.map(vat => `
<div>${this.getVATTypeLabel(vat.type)}</div>
<div>${vat.rate}%</div>
<div>${vat.base}</div>
<div>${vat.amount}</div>
`).join('');
// VAT totals
doc.getElementById('print-vat-currency-main').textContent = data.documentCurrencyCode;
doc.getElementById('print-vat-main').textContent = data.totals.vat;
const secondaryVatRow = doc.getElementById('print-vat-secondary');
if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) {
secondaryVatRow.style.display = 'flex';
doc.getElementById('print-vat-currency-secondary').textContent = data.taxCurrencyCode;
const vatInTaxCurrency = this.formatter.parseCurrency(data.totals.vat) * data.exchangeRate;
doc.getElementById('print-vat-secondary-amount').textContent =
this.formatter.formatCurrency(vatInTaxCurrency);
}
}
import { InvoiceFormatter } from './formatter.js';
export class InvoicePrintHandler {
constructor() {
this.printWindow = null;
this.formatter = new InvoiceFormatter();
this.templates = {
standard: './templates/print.html',
compact: './templates/print-compact.html'
};
this.currentTemplate = 'standard';
}
setTemplate(templateName) {
if (this.templates[templateName]) {
this.currentTemplate = templateName;
}
}
collectInvoiceData() {
return {
// Basic details
invoiceNumber: document.querySelector('[name="invoiceNumber"]').value,
issueDate: document.querySelector('[name="issueDate"]').value,
dueDate: document.querySelector('[name="dueDate"]').value,
documentCurrencyCode: document.querySelector('[name="documentCurrencyCode"]').value.toUpperCase() || 'RON',
taxCurrencyCode: document.querySelector('[name="taxCurrencyCode"]').value.toUpperCase(),
exchangeRate: parseFloat(document.querySelector('[name="exchangeRate"]')?.value || 1),
// Supplier details
supplier: {
name: document.querySelector('[name="supplierName"]').value,
vat: document.querySelector('[name="supplierVAT"]').value,
companyId: document.querySelector('[name="supplierCompanyId"]').value,
address: document.querySelector('[name="supplierAddress"]').value,
city: document.querySelector('[name="supplierCity"]').value,
county: document.querySelector('[name="supplierCountrySubentity"]').value,
country: document.querySelector('[name="supplierCountry"]').value,
phone: document.querySelector('[name="supplierPhone"]').value,
contactName: document.querySelector('[name="supplierContactName"]').value,
email: document.querySelector('[name="supplierEmail"]').value
},
// Customer details
customer: {
name: document.querySelector('[name="customerName"]').value,
vat: document.querySelector('[name="customerVAT"]').value,
companyId: document.querySelector('[name="customerCompanyId"]').value,
address: document.querySelector('[name="customerAddress"]').value,
city: document.querySelector('[name="customerCity"]').value,
county: document.querySelector('[name="customerCountrySubentity"]').value,
country: document.querySelector('[name="customerCountry"]').value,
phone: document.querySelector('[name="customerPhone"]').value,
contactName: document.querySelector('[name="customerContactName"]').value,
email: document.querySelector('[name="customerEmail"]').value
},
// Line items with formatted values
items: Array.from(document.querySelectorAll('.line-item')).map((item, index) => ({
number: index + 1,
description: item.querySelector('[name^="description"]').value,
quantity: this.formatter.formatQuantity(item.querySelector('[name^="quantity"]').value),
unit: item.querySelector('[name^="unit"]').value,
price: this.formatter.formatCurrency(item.querySelector('[name^="price"]').value),
vatRate: this.formatter.formatCurrency(item.querySelector('[name^="vatRate"]').value),
totalAmount: this.formatter.formatCurrency(
this.formatter.parseQuantity(item.querySelector('[name^="quantity"]').value) *
this.formatter.parseCurrency(item.querySelector('[name^="price"]').value)
)
})),
// Note
note: document.querySelector('[name="invoiceNote"]')?.value,
// Get totals directly from the display elements
totals: {
subtotal: document.getElementById('subtotal').textContent,
allowances: document.getElementById('totalAllowances').textContent,
charges: document.getElementById('totalCharges').textContent,
netAmount: document.getElementById('netAmount').textContent,
vat: document.getElementById('vat').textContent,
total: document.getElementById('total').textContent
},
// VAT Breakdown
vatBreakdown: Array.from(document.querySelectorAll('.vat-row')).map(row => ({
type: row.querySelector('.vat-type').value,
rate: row.querySelector('.vat-rate').value,
base: row.querySelector('.vat-base').value,
amount: row.querySelector('.vat-amount').value
}))
};
}
createPartyHTML(party) {
return `
<p><strong>${party.name}</strong></p>
<p>CUI: ${party.vat}</p>
<p>Nr. Reg. Com.: ${party.companyId}</p>
<p>${party.address}</p>
<p>${party.city}${party.county ? ', ' + party.county : ''}</p>
<p>${party.country}</p>
${party.phone ? `<p>Tel: ${party.phone}</p>` : ''}
${party.contactName ? `<p>Contact: ${party.contactName}</p>` : ''}
${party.email ? `<p>Email: ${party.email}</p>` : ''}
`;
}
getVATTypeLabel(type) {
const labels = {
'S': 'Standard',
'AE': 'Taxare Inversă',
'O': 'Neplătitor TVA',
'Z': 'Cotă 0%',
'E': 'Scutit'
};
return labels[type] || type;
}
async print() {
try {
// Collect all the data
const invoiceData = this.collectInvoiceData();
// Open new window and load the selected print template
this.printWindow = window.open(
this.templates[this.currentTemplate],
'_blank',
'width=800,height=600'
);
// Wait for the window to load
await new Promise(resolve => {
this.printWindow.onload = resolve;
});
// Generate QR code
const qrData = {
invoiceNumber: invoiceData.invoiceNumber,
issueDate: invoiceData.issueDate,
supplier: invoiceData.supplier.name,
customer: invoiceData.customer.name,
total: this.formatter.parseCurrency(invoiceData.totals.total)
};
const qrElement = this.printWindow.document.getElementById('qrcode');
if (qrElement) {
new this.printWindow.QRCode(qrElement, {
text: JSON.stringify(qrData),
width: 100,
height: 100,
colorDark: "#2563eb",
colorLight: "#ffffff",
correctLevel: this.printWindow.QRCode.CorrectLevel.L
});
}
// Populate the template with data
this.populatePrintWindow(invoiceData);
// Print the window
this.printWindow.print();
// Clean up
this.printWindow.onafterprint = () => {
this.printWindow.close();
this.printWindow = null;
};
} catch (error) {
console.error('Print failed:', error);
if (this.printWindow) {
this.printWindow.close();
this.printWindow = null;
}
alert('A apărut o eroare la printare. Vă rugăm să încercați din nou.');
}
}
populatePrintWindow(data) {
if (!this.printWindow) return;
const doc = this.printWindow.document;
// Basic details
doc.getElementById('print-invoice-number').textContent = data.invoiceNumber;
doc.getElementById('print-issue-date').textContent = data.issueDate;
doc.getElementById('print-due-date').textContent = data.dueDate;
doc.getElementById('print-document-currency').textContent = data.documentCurrencyCode;
// Currency information
const taxCurrencyContainer = doc.getElementById('print-tax-currency-container');
if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) {
taxCurrencyContainer.style.display = 'block';
doc.getElementById('print-tax-currency').textContent = data.taxCurrencyCode;
doc.getElementById('print-exchange-rate').textContent = this.formatter.formatNumber(data.exchangeRate);
}
// Party details
doc.getElementById('print-supplier-details').innerHTML = this.createPartyHTML(data.supplier);
doc.getElementById('print-customer-details').innerHTML = this.createPartyHTML(data.customer);
// Note
if (data.note) {
const noteSection = doc.getElementById('print-note');
noteSection.style.display = 'block';
noteSection.querySelector('div').textContent = data.note;
}
// Line items - use formatted values from data
doc.getElementById('print-items').innerHTML = data.items.map(item => `
<tr>
<td>${item.number}</td>
<td>${item.description}</td>
<td>${item.unit}</td>
<td class="number-cell">${item.quantity}</td>
<td class="number-cell">${item.price}</td>
<td class="number-cell">${item.vatRate}%</td>
<td class="number-cell">${item.totalAmount}</td>
</tr>
`).join('');
// Totals - use values directly from display
doc.getElementById('print-subtotal').textContent = data.totals.subtotal;
doc.getElementById('print-allowances').textContent = data.totals.allowances;
doc.getElementById('print-charges').textContent = data.totals.charges;
doc.getElementById('print-net-amount').textContent = data.totals.netAmount;
doc.getElementById('print-total').textContent = data.totals.total;
// VAT Breakdown - use values directly from display
doc.getElementById('print-vat-breakdown').innerHTML = data.vatBreakdown.map(vat => `
<div>${this.getVATTypeLabel(vat.type)}</div>
<div>${vat.rate}%</div>
<div>${vat.base}</div>
<div>${vat.amount}</div>
`).join('');
// VAT totals
doc.getElementById('print-vat-currency-main').textContent = data.documentCurrencyCode;
doc.getElementById('print-vat-main').textContent = data.totals.vat;
const secondaryVatRow = doc.getElementById('print-vat-secondary');
if (data.taxCurrencyCode && data.taxCurrencyCode !== data.documentCurrencyCode) {
secondaryVatRow.style.display = 'flex';
doc.getElementById('print-vat-currency-secondary').textContent = data.taxCurrencyCode;
const vatInTaxCurrency = this.formatter.parseCurrency(data.totals.vat) * data.exchangeRate;
doc.getElementById('print-vat-secondary-amount').textContent =
this.formatter.formatCurrency(vatInTaxCurrency);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,43 @@
// server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 3000;
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.json': 'application/json',
'.xml': 'application/xml'
};
const server = http.createServer((req, res) => {
// Convert URL to file path, using index.html for root
let filePath = req.url === '/' ? './index.html' : '.' + req.url;
// Get file extension for MIME type
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'text/plain';
// Read and serve the file
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
res.writeHead(404);
res.end('File not found');
} else {
res.writeHead(500);
res.end('Server error: ' + err.code);
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
}
});
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
// server.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 3000;
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.mjs': 'text/javascript',
'.json': 'application/json',
'.xml': 'application/xml'
};
const server = http.createServer((req, res) => {
// Convert URL to file path, using index.html for root
let filePath = req.url === '/' ? './index.html' : '.' + req.url;
// Get file extension for MIME type
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'text/plain';
// Read and serve the file
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
res.writeHead(404);
res.end('File not found');
} else {
res.writeHead(500);
res.end('Server error: ' + err.code);
}
} else {
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
}
});
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});

View File

@@ -0,0 +1,185 @@
// js/storage.js
//
// Helpers de stocare pentru efactura-generator (PR-PROFIL / A12+A13).
//
// Reguli:
// 1. Toate cheile localStorage/sessionStorage încep cu "efactura." —
// enforced la setter; getJSON acceptă orice cheie pentru compatibilitate
// retroactivă, dar setJSON/cacheSet aruncă dacă prefixul lipsește.
// 2. Quota errors localStorage → toast vizibil "spațiu local plin".
// 3. Cheile convenționale: efactura.{tip}.v1
// Ex: efactura.profil.v1, efactura.catalog.v1, efactura.session.v1
//
// Exports:
// getJSON(key, default) → valoare parsată sau default
// setJSON(key, value) → salvează; toast error dacă QuotaExceeded
// cacheGet(key) → sessionStorage (ephemer, null dacă absent)
// cacheSet(key, value) → sessionStorage (silențios dacă eșuează)
// openCatalog() → Promise<IDBDatabase> pentru catalog produse (A13)
const KEY_PREFIX = 'efactura.';
/**
* Validează că cheia respectă prefixul obligatoriu.
* @param {string} key
*/
function _enforcePrefix(key) {
if (typeof key !== 'string' || !key.startsWith(KEY_PREFIX)) {
throw new Error(
`storage.js: cheia "${key}" trebuie să înceapă cu "${KEY_PREFIX}". ` +
`Convenție: efactura.{tip}.v1`
);
}
}
/**
* Afișează un toast (dacă window.showToast e disponibil) sau loghează.
* @param {string} msg
* @param {string} variant 'error'|'warning'|'info'|'success'
*/
function _toast(msg, variant = 'error') {
if (typeof window !== 'undefined' && typeof window.showToast === 'function') {
window.showToast(msg, variant);
} else {
console.warn('[storage]', msg);
}
}
/**
* Citește o valoare JSON din localStorage.
* Returnează `defaultValue` dacă cheia lipsește sau JSON e invalid.
*
* @param {string} key
* @param {*} defaultValue
* @returns {*}
*/
export function getJSON(key, defaultValue = null) {
_enforcePrefix(key);
try {
const raw = localStorage.getItem(key);
if (raw === null) return defaultValue;
return JSON.parse(raw);
} catch (_) {
return defaultValue;
}
}
/**
* Scrie o valoare JSON în localStorage.
* La QuotaExceededError → toast "spațiu local plin".
*
* @param {string} key
* @param {*} value
*/
export function setJSON(key, value) {
_enforcePrefix(key);
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (err) {
// QuotaExceededError: code 22 (Firefox/Chrome), 1014 (Firefox NS), sau name check.
const isQuota = err && (
err.name === 'QuotaExceededError' ||
err.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
err.code === 22 ||
err.code === 1014
);
if (isQuota) {
_toast(
'Spațiu local plin — datele nu au putut fi salvate.',
'error'
);
} else {
_toast(`Eroare la salvare locală: ${err && err.message ? err.message : err}`, 'error');
}
}
}
/**
* Citește din sessionStorage (cache ephemer, valabil doar pe durata sesiunii).
* Returnează null dacă absent sau invalid.
*
* @param {string} key
* @returns {*|null}
*/
export function cacheGet(key) {
_enforcePrefix(key);
try {
const raw = sessionStorage.getItem(key);
if (raw === null) return null;
return JSON.parse(raw);
} catch (_) {
return null;
}
}
/**
* Scrie în sessionStorage. Erorile sunt ignorate silențios (storage e
* ephemer și poate fi blocat de browser în incognito / iframe sandboxed).
*
* @param {string} key
* @param {*} value
*/
export function cacheSet(key, value) {
_enforcePrefix(key);
try {
sessionStorage.setItem(key, JSON.stringify(value));
} catch (_) {
// Ignorat: sessionStorage e ephemer, erorile nu sunt critice.
}
}
// IndexedDB pentru catalog produse (A13 lazy init).
let _catalogDb = null;
/**
* Deschide (sau returnează instanța cached a) bazei de date IndexedDB
* `efactura` v1. Crează object store `products` la prima rulare.
*
* Schema v1 (lock per eng review 14A):
* - DB name: `efactura`
* - store: `products`, keyPath: `id` (uuid v4 generat de caller)
* - indexes: `name`, `sellerItemID`, `cpvCode`
*
* Dacă IndexedDB lipsește (private browsing), Promise rejectează cu Error
* `indexeddb-unavailable` — caller-ul trebuie să degradeze la "feature
* disabled" cu toast (NU să crash-eze).
*
* @returns {Promise<IDBDatabase>}
*/
export function openCatalog() {
if (_catalogDb) return Promise.resolve(_catalogDb);
return new Promise((resolve, reject) => {
if (typeof indexedDB === 'undefined') {
reject(new Error('indexeddb-unavailable'));
return;
}
const req = indexedDB.open('efactura', 1);
req.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('products')) {
const store = db.createObjectStore('products', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
store.createIndex('sellerItemID', 'sellerItemID', { unique: false });
store.createIndex('cpvCode', 'cpvCode', { unique: false });
}
};
req.onsuccess = (event) => {
_catalogDb = event.target.result;
resolve(_catalogDb);
};
req.onerror = (event) => {
reject(event.target.error);
};
req.onblocked = () => {
reject(new Error('indexeddb-blocked'));
};
});
}
// Export prefix pentru tests / consumeri care vor să verifice convenția.
export const STORAGE_PREFIX = KEY_PREFIX;

View File

@@ -0,0 +1,481 @@
/**
* js/validation/br-ro.js — PR-BR (A2)
* Top 30 reguli BR din CIUS-RO Schematron + EN 16931-1.
* Selecție: severity fatal/error din Schematron + reguli care vizează
* câmpuri editabile (CIF, date, totale, coduri TVA, articole factură).
*
* Fiecare regulă:
* { code, severity ('fatal'|'error'|'warning'), message, fieldRef, check(invoiceData) }
*
* invoiceData = obiect snapshot din colectInvoiceDataForBR() în script.js.
* Toate funcțiile sunt pure — fără acces DOM, fără efecte secundare.
*/
import { validateCIF } from './cif.js';
import { validateIBAN } from './iban.js';
// Coduri TVA valide per CIUS-RO
const VALID_VAT_TYPES = ['S', 'AE', 'O', 'Z', 'E'];
// Coduri tip factură valide per CIUS-RO
const VALID_INVOICE_TYPES = ['380', '381', '384', '389'];
// Coduri țară ISO 3166-1 alfa-2 (set parțial — UE + țări comune)
const EU_COUNTRY_CODES = new Set([
'AT','BE','BG','CY','CZ','DE','DK','EE','ES','FI','FR','GR','HR','HU',
'IE','IT','LT','LU','LV','MT','NL','PL','PT','RO','SE','SI','SK',
'AD','AL','BA','BY','CH','GB','GE','IS','LI','ME','MK','MD','MN','NO',
'RS','TR','UA','US','CA','AU','JP','CN','KR','BR','IN','ZA','SG','AE',
'XK','SM','VA','MC','GI','FO','GL','IM','JE','GG'
]);
/**
* Parsează un număr din string ignorând formatare (punct sau virgulă drept separator mii).
* Returnează NaN dacă nu e un număr valid.
*/
function parseNum(val) {
if (val === null || val === undefined || val === '') return NaN;
const s = String(val).trim().replace(/\s/g, '');
// Format ro-RO are virgulă ca separator zecimal ("1.234,56" sau "1,5").
// Doar când există virgulă tratăm punctele drept separator de mii.
// Altfel: parse canonical decimal-dot (dataset.raw, XML) — "1.000" = 1, NU 1000.
if (s.includes(',')) {
return parseFloat(s.replace(/\./g, '').replace(',', '.'));
}
return parseFloat(s);
}
/** Parsează o dată din format dd.mm.yyyy → Date object (sau null) */
function parseRoDate(str) {
if (!str || !/^\d{2}\.\d{2}\.\d{4}$/.test(str.trim())) return null;
const [d, m, y] = str.trim().split('.').map(Number);
const dt = new Date(y, m - 1, d);
if (isNaN(dt.getTime())) return null;
return dt;
}
/** Compară două valori numerice cu toleranță ε */
function approxEqual(a, b, eps = 0.02) {
if (isNaN(a) || isNaN(b)) return false;
return Math.abs(a - b) <= eps;
}
// ============================================================================
// REGULILE BR — 30 reguli în ordinea: ID, date, furnizor, client,
// articole, TVA, totaluri, CIUS-RO specifice.
// ============================================================================
export const BR_RULES = [
// ── Identificare factură ─────────────────────────────────────────────────
{
code: 'BR-01',
severity: 'fatal',
message: 'Factura trebuie să aibă un număr de identificare (ID).',
fieldRef: '[name="invoiceNumber"]',
check: (d) => d.invoiceNumber !== '',
},
{
code: 'BR-02',
severity: 'fatal',
message: 'Factura trebuie să aibă o dată de emitere.',
fieldRef: '[name="issueDate"]',
check: (d) => d.issueDate !== '' && parseRoDate(d.issueDate) !== null,
},
{
code: 'BR-03',
severity: 'error',
message: 'Codul tipului de factură trebuie să fie 380, 381, 384 sau 389.',
fieldRef: '[name="invoiceTypeCode"]',
check: (d) => VALID_INVOICE_TYPES.includes(d.invoiceTypeCode),
},
{
code: 'BR-04',
severity: 'fatal',
message: 'Factura trebuie să specifice moneda (codul ISO 4217).',
fieldRef: '[name="documentCurrencyCode"]',
check: (d) => d.currencyCode !== '' && d.currencyCode.length === 3,
},
// ── Date scadență ────────────────────────────────────────────────────────
{
code: 'BR-DT-01',
severity: 'error',
message: 'Data emiterii nu poate fi în viitor cu mai mult de 30 zile.',
fieldRef: '[name="issueDate"]',
check: (d) => {
const issued = parseRoDate(d.issueDate);
if (!issued) return true; // BR-02 handles missing date
const limit = new Date();
limit.setDate(limit.getDate() + 30);
return issued <= limit;
},
},
{
code: 'BR-DT-02',
severity: 'warning',
message: 'Data scadenței (dueDate) nu trebuie să fie anterioară datei de emitere.',
fieldRef: '[name="dueDate"]',
check: (d) => {
if (!d.dueDate) return true;
const issued = parseRoDate(d.issueDate);
const due = parseRoDate(d.dueDate);
if (!issued || !due) return true;
return due >= issued;
},
},
// ── Furnizor ─────────────────────────────────────────────────────────────
{
code: 'BR-06',
severity: 'fatal',
message: 'Furnizorul trebuie să aibă un nume (RegistrationName).',
fieldRef: '[name="supplierName"]',
check: (d) => d.supplierName !== '',
},
{
code: 'BR-07',
severity: 'fatal',
message: 'Adresa furnizorului trebuie să includă orașul.',
fieldRef: '[name="supplierCity"]',
check: (d) => d.supplierCity !== '',
},
{
code: 'BR-08',
severity: 'fatal',
message: 'Țara furnizorului trebuie specificată (cod ISO 3166-1).',
fieldRef: '[name="supplierCountry"]',
check: (d) => d.supplierCountry !== '',
},
{
code: 'BR-RO-001',
severity: 'error',
message: 'CIF/CUI furnizor invalid: cifra de control nu se potrivește.',
fieldRef: '[name="supplierVAT"]',
check: (d) => {
if (!d.supplierVAT) return true; // gol = alt BR verifică
return validateCIF(d.supplierVAT).valid;
},
},
{
code: 'BR-RO-010',
severity: 'fatal',
message: 'Furnizorul trebuie să aibă un cod de identificare fiscală (CIF/VAT).',
fieldRef: '[name="supplierVAT"]',
check: (d) => d.supplierVAT !== '',
},
// ── Client ───────────────────────────────────────────────────────────────
{
code: 'BR-07-C',
severity: 'fatal',
message: 'Clientul trebuie să aibă un nume (RegistrationName).',
fieldRef: '[name="customerName"]',
check: (d) => d.customerName !== '',
},
{
code: 'BR-08-C',
severity: 'fatal',
message: 'Țara clientului trebuie specificată (cod ISO 3166-1).',
fieldRef: '[name="customerCountry"]',
check: (d) => d.customerCountry !== '',
},
{
code: 'BR-RO-002',
severity: 'error',
message: 'CIF/CUI client invalid: cifra de control nu se potrivește.',
fieldRef: '[name="customerVAT"]',
check: (d) => {
if (!d.customerVAT) return true;
return validateCIF(d.customerVAT).valid;
},
},
// ── Articole factură ─────────────────────────────────────────────────────
{
code: 'BR-21',
severity: 'fatal',
message: (d) => {
const bad = d.lineItems.filter(li => !li.description);
return bad.length === 1
? `Linia ${bad[0].index + 1} trebuie să aibă o denumire (descriere).`
: `${bad.length} linii fără denumire (liniile ${bad.map(l => l.index + 1).join(', ')}).`;
},
fieldRef: null, // dinamic — scroll la prima linie cu eroare
fieldRefDynamic: (d) => {
const bad = d.lineItems.find(li => !li.description);
return bad ? `[name="description${bad.index}"]` : null;
},
check: (d) => d.lineItems.every(li => li.description !== ''),
},
{
code: 'BR-22',
severity: 'fatal',
message: (d) => {
const bad = d.lineItems.filter(li => isNaN(parseNum(li.quantity)) || parseNum(li.quantity) === 0);
return bad.length === 1
? `Linia ${bad[0].index + 1} trebuie să aibă o cantitate validă (≠ 0).`
: `${bad.length} linii cu cantitate lipsă sau zero.`;
},
fieldRefDynamic: (d) => {
const bad = d.lineItems.find(li => isNaN(parseNum(li.quantity)) || parseNum(li.quantity) === 0);
return bad ? `[name="quantity${bad.index}"]` : null;
},
check: (d) => d.lineItems.every(li => !isNaN(parseNum(li.quantity)) && parseNum(li.quantity) !== 0),
},
{
code: 'BR-23',
severity: 'fatal',
message: (d) => {
const bad = d.lineItems.filter(li => isNaN(parseNum(li.unitPrice)));
return `Linia ${bad[0]?.index + 1 || '?'}: prețul unitar trebuie specificat.`;
},
fieldRefDynamic: (d) => {
const bad = d.lineItems.find(li => isNaN(parseNum(li.unitPrice)));
return bad ? `[name="price${bad.index}"]` : null;
},
check: (d) => d.lineItems.every(li => !isNaN(parseNum(li.unitPrice))),
},
{
code: 'BR-24',
severity: 'fatal',
message: (d) => {
const bad = d.lineItems.filter(li => !VALID_VAT_TYPES.includes(li.vatType));
return `Linia ${bad[0]?.index + 1 || '?'}: codul categoriei TVA trebuie să fie S/AE/O/Z/E.`;
},
fieldRefDynamic: (d) => {
const bad = d.lineItems.find(li => !VALID_VAT_TYPES.includes(li.vatType));
return bad ? `[name="vatType${bad.index}"]` : null;
},
check: (d) => d.lineItems.every(li => VALID_VAT_TYPES.includes(li.vatType)),
},
{
code: 'BR-16',
severity: 'error',
message: (d) => {
const bad = d.lineItems.find(li => {
const qty = parseNum(li.quantity);
const price = parseNum(li.unitPrice);
const disc = parseNum(li.discount) || 0;
const net = parseNum(li.lineTotal);
if (isNaN(qty) || isNaN(price) || isNaN(net)) return false;
return !approxEqual(qty * price - disc, net);
});
return bad
? `Linia ${bad.index + 1}: total net ≠ cantitate × preț discount.`
: 'Total net linie inconsistent.';
},
fieldRefDynamic: (d) => {
const bad = d.lineItems.find(li => {
const qty = parseNum(li.quantity);
const price = parseNum(li.unitPrice);
const disc = parseNum(li.discount) || 0;
const net = parseNum(li.lineTotal);
if (isNaN(qty) || isNaN(price) || isNaN(net)) return false;
return !approxEqual(qty * price - disc, net);
});
return bad ? `[data-line-total-index="${bad.index}"]` : null;
},
check: (d) => d.lineItems.every(li => {
const qty = parseNum(li.quantity);
const price = parseNum(li.unitPrice);
const disc = parseNum(li.discount) || 0;
const net = parseNum(li.lineTotal);
if (isNaN(qty) || isNaN(price) || isNaN(net)) return true; // BR-22/23 handles
return approxEqual(qty * price - disc, net);
}),
},
// ── Factură cu cel puțin un articol ────────────────────────────────────
{
code: 'BR-16-L',
severity: 'fatal',
message: 'Factura trebuie să conțină cel puțin un articol (linie factură).',
fieldRef: null,
check: (d) => d.lineItems.length > 0,
},
// ── TVA breakdown ────────────────────────────────────────────────────────
{
code: 'BR-31',
severity: 'fatal',
message: 'Defalcarea TVA (TaxTotal/TaxSubtotal) nu poate fi goală.',
fieldRef: '#vatBreakdownRows',
check: (d) => d.vatRows.length > 0,
},
{
code: 'BR-32',
severity: 'error',
message: (d) => {
const bad = d.vatRows.find(r => {
const rt = parseNum(r.rate);
return isNaN(rt) || rt < 0 || rt > 100;
});
return `Cota TVA ${bad?.rate ?? ''} este invalidă (trebuie 0100%).`;
},
fieldRef: '.vat-rate',
check: (d) => d.vatRows.every(r => {
const rt = parseNum(r.rate);
return !isNaN(rt) && rt >= 0 && rt <= 100;
}),
},
{
code: 'BR-45',
severity: 'error',
message: (d) => {
const bad = d.vatRows.find(r => !VALID_VAT_TYPES.includes(r.type));
return `Codul categoriei TVA "${bad?.type ?? ''}" este invalid. Valori acceptate: S, AE, O, Z, E.`;
},
fieldRef: '.vat-type',
check: (d) => d.vatRows.every(r => VALID_VAT_TYPES.includes(r.type)),
},
{
code: 'BR-AE-01',
severity: 'warning',
message: 'Categoria AE (Taxare Inversă) trebuie să aibă cota TVA 0%.',
fieldRef: '.vat-rate',
check: (d) => d.vatRows
.filter(r => r.type === 'AE')
.every(r => parseNum(r.rate) === 0),
},
{
code: 'BR-O-01',
severity: 'warning',
message: 'Categoria O (Neplătitor TVA) trebuie să aibă cota TVA 0%.',
fieldRef: '.vat-rate',
check: (d) => d.vatRows
.filter(r => r.type === 'O')
.every(r => parseNum(r.rate) === 0),
},
{
code: 'BR-E-01',
severity: 'warning',
message: 'Categoria E (Neimpozabil) trebuie să aibă cota TVA 0%.',
fieldRef: '.vat-rate',
check: (d) => d.vatRows
.filter(r => r.type === 'E')
.every(r => parseNum(r.rate) === 0),
},
// ── Consistență totaluri ─────────────────────────────────────────────────
{
code: 'BR-CO-15',
severity: 'fatal',
message: (d) => {
const sumRows = d.vatRows.reduce((s, r) => s + (parseNum(r.amount) || 0), 0);
const disp = parseNum(d.totalVat);
const diff = Math.abs(sumRows - disp).toFixed(2);
return `Total TVA afișat (${disp.toFixed(2)}) ≠ suma rândurilor TVA (${sumRows.toFixed(2)}). Diferență: ${diff} RON.`;
},
fieldRef: '#vat',
check: (d) => {
if (d.vatRows.length === 0) return true;
const sumRows = d.vatRows.reduce((s, r) => s + (parseNum(r.amount) || 0), 0);
const disp = parseNum(d.totalVat);
return approxEqual(sumRows, disp);
},
},
{
code: 'BR-CO-16',
severity: 'fatal',
message: (d) => {
const expected = parseNum(d.subtotal) - parseNum(d.allowances) + parseNum(d.charges) + parseNum(d.totalVat);
const actual = parseNum(d.grandTotal);
const diff = Math.abs(expected - actual).toFixed(2);
return `Total factură (${actual.toFixed(2)}) ≠ subtotal reduceri + adaosuri + TVA (${expected.toFixed(2)}). Diferență: ${diff} RON.`;
},
fieldRef: '#total',
check: (d) => {
const expected = parseNum(d.subtotal) - parseNum(d.allowances) + parseNum(d.charges) + parseNum(d.totalVat);
const actual = parseNum(d.grandTotal);
if (isNaN(expected) || isNaN(actual)) return true;
return approxEqual(expected, actual);
},
},
// ── CIUS-RO specifice ────────────────────────────────────────────────────
{
code: 'BR-RO-180',
severity: 'error',
message: 'CIUS-RO: codul tipului de factură trebuie să fie 380 (factură), 381 (credit note), 384 (corectată) sau 389 (autofactură).',
fieldRef: '[name="invoiceTypeCode"]',
check: (d) => d.invoiceTypeCode === '' || VALID_INVOICE_TYPES.includes(d.invoiceTypeCode),
},
{
code: 'BR-RO-003',
severity: 'warning',
message: 'CIUS-RO: numărul facturii (ID) nu trebuie să fie gol sau să conțină doar spații.',
fieldRef: '[name="invoiceNumber"]',
check: (d) => d.invoiceNumber.trim() !== '',
},
{
code: 'BR-IBAN-01',
severity: 'warning',
message: (d) => {
const badIdx = d.ibans.findIndex(ib => ib && !validateIBAN(ib).valid);
return `IBAN #${badIdx + 1} invalid: verificați lungimea și cifrele de control.`;
},
fieldRef: null,
fieldRefDynamic: (d) => {
const badIdx = d.ibans.findIndex(ib => ib && !validateIBAN(ib).valid);
return badIdx >= 0 ? `[name="paymentMeansIBAN${badIdx}"]` : null;
},
check: (d) => d.ibans.every(ib => !ib || validateIBAN(ib).valid),
},
];
/**
* Rulează toate regulile pe invoiceData și returnează lista de violări.
* @param {object} invoiceData — snapshot din collectInvoiceDataForBR()
* @returns {{ code, severity, message, fieldRef }[]}
*/
export function runBRRules(invoiceData) {
const violations = [];
for (const rule of BR_RULES) {
if (!rule.check(invoiceData)) {
const msg = typeof rule.message === 'function'
? rule.message(invoiceData)
: rule.message;
const fRef = rule.fieldRefDynamic
? rule.fieldRefDynamic(invoiceData)
: rule.fieldRef;
violations.push({
code: rule.code,
severity: rule.severity,
message: msg,
fieldRef: fRef,
});
}
}
return violations;
}

View File

@@ -0,0 +1,71 @@
/**
* js/validation/cif.js — PR-VALID-IDS (A9)
* Validare CIF/CUI românesc prin sumă ponderată (mod 11, mod 10).
* Funcție pură, fără efecte secundare, fără dependențe externe.
*/
// Greutățile pentru cifrele 1-9 (se aplică pe primele 9 cifre ale CIF-ului).
const WEIGHTS = [7, 5, 3, 2, 1, 7, 5, 3, 2];
/**
* Validează un CIF/CUI românesc.
*
* Algoritm:
* 1. Elimină prefixul "RO" dacă există (case-insensitive).
* 2. Elimină spații.
* 3. Verifică că are între 2 și 10 cifre.
* 4. Completează cu zerouri la stânga până la 10 cifre.
* 5. Calculează suma ponderată pe primele 9 cifre cu WEIGHTS.
* 6. (sumă * 10) % 11 % 10 trebuie să fie egal cu cifra de control (ultima).
*
* @param {string} value — valoarea brută din câmp (poate fi goală, poate conține "RO")
* @returns {{ valid: boolean, message: string }}
*/
export function validateCIF(value) {
if (!value || value.trim() === '') {
return { valid: true, message: '' }; // câmp gol — valid (nu e required check)
}
let normalized = value.trim().toUpperCase();
// Elimină prefixul RO
if (normalized.startsWith('RO')) {
normalized = normalized.slice(2).trim();
}
// Elimină spații și cratime rămase
normalized = normalized.replace(/[\s\-]/g, '');
// Trebuie să conțină doar cifre
if (!/^\d+$/.test(normalized)) {
return { valid: false, message: 'CIF invalid: conține caractere nepermise' };
}
// Lungime: minim 2, maxim 10 cifre
if (normalized.length < 2 || normalized.length > 10) {
return { valid: false, message: 'CIF invalid: lungimea trebuie să fie între 2 și 10 cifre' };
}
// Completează cu zerouri la stânga până la 10 cifre
const padded = normalized.padStart(10, '0');
// Extrage primele 9 cifre (pentru ponderare) și cifra de control (ultima)
const digits = padded.split('').map(Number);
const checkDigit = digits[9];
const controlDigits = digits.slice(0, 9);
// Calculează suma ponderată
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += controlDigits[i] * WEIGHTS[i];
}
// Cifra de control calculată
const computed = (sum * 10) % 11 % 10;
if (computed !== checkDigit) {
return { valid: false, message: 'CIF invalid: cifra de control nu se potrivește' };
}
return { valid: true, message: '' };
}

View File

@@ -0,0 +1,63 @@
/**
* js/validation/iban.js — PR-VALID-IDS (A10)
* Validare IBAN internațional prin algoritmul ISO 13616 (mod 97).
* Funcție pură, fără efecte secundare, fără dependențe externe.
*/
/**
* Validează un IBAN (orice țară, inclusiv RO).
*
* Algoritm ISO 13616:
* 1. Elimină spații și convertește la uppercase.
* 2. Verifică lungimea minimă (4 caractere).
* 3. Mută primele 4 caractere la sfârșitul șirului.
* 4. Înlocuiește fiecare literă cu echivalentul numeric: A=10, B=11, ..., Z=35.
* 5. Calculează numărul rezultat modulo 97 — trebuie să fie 1.
*
* Lungimi specifice per țară nu sunt forțate (validare structurală generică);
* IBAN-ul RO are 24 caractere, verificat separat cu mesaj specific.
*
* @param {string} value — valoarea brută din câmp
* @returns {{ valid: boolean, message: string }}
*/
export function validateIBAN(value) {
if (!value || value.trim() === '') {
return { valid: true, message: '' }; // câmp gol — valid (nu e required check)
}
// Normalizare: elimină spații, uppercase
const normalized = value.trim().toUpperCase().replace(/\s/g, '');
// Lungime minimă
if (normalized.length < 4) {
return { valid: false, message: 'IBAN invalid: lungime sau check digits' };
}
// Verifică că IBAN-ul conține doar litere și cifre
if (!/^[A-Z0-9]+$/.test(normalized)) {
return { valid: false, message: 'IBAN invalid: caractere nepermise' };
}
// IBAN RO trebuie să aibă exact 24 caractere
if (normalized.startsWith('RO') && normalized.length !== 24) {
return { valid: false, message: 'IBAN invalid: lungime sau check digits' };
}
// Rearanjare: primele 4 caractere la final
const rearranged = normalized.slice(4) + normalized.slice(0, 4);
// Înlocuiește literele cu cifre: A=10 ... Z=35
const numericString = rearranged.replace(/[A-Z]/g, ch => String(ch.charCodeAt(0) - 55));
// Calculează mod 97 pe un număr mare (string chunking pentru a evita overflow)
let remainder = 0;
for (let i = 0; i < numericString.length; i++) {
remainder = (remainder * 10 + parseInt(numericString[i], 10)) % 97;
}
if (remainder !== 1) {
return { valid: false, message: 'IBAN invalid: lungime sau check digits' };
}
return { valid: true, message: '' };
}

1027
efactura-generator/js/vendor/big.mjs vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
/**
* js/vendor/html2pdf.mjs — ESM wrapper pentru html2pdf.js 0.10.2
*
* Încarcă bundle-ul UMD via <script> injection la primul apel.
* Bundlul include html2canvas + jsPDF — ~900 KB, deci lazy loading.
*
* Utilizare:
* import getHtml2pdf from './vendor/html2pdf.mjs';
* const html2pdf = await getHtml2pdf();
* await html2pdf().set({ filename: 'factura.pdf' }).from(element).save();
*
* @see https://ekoopmans.github.io/html2pdf.js/
* @version 0.10.2
* @license MIT
*/
let _promise = null;
export default function getHtml2pdf() {
if (globalThis.html2pdf) {
return Promise.resolve(globalThis.html2pdf);
}
if (_promise) return _promise;
_promise = new Promise((resolve, reject) => {
const script = document.createElement('script');
// Rezolvă calea relativ la locația fișierului curent (ESM import.meta.url)
script.src = new URL('./html2pdf.bundle.min.js', import.meta.url).href;
script.onload = () => {
if (typeof globalThis.html2pdf === 'function') {
resolve(globalThis.html2pdf);
} else {
reject(new Error('html2pdf.js bundle încărcat dar globalThis.html2pdf este undefined'));
}
};
script.onerror = () => {
_promise = null; // permite retry
reject(new Error('Nu s-a putut încărca html2pdf.bundle.min.js'));
};
document.head.appendChild(script);
});
return _promise;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,156 +1,356 @@
<?php
// Încărcare configurație
$config = json_decode(file_get_contents(dirname(__FILE__) . '/config.json'), true);
if (!$config) {
header('HTTP/1.1 500 Internal Server Error');
die('Eroare la încărcarea configurației');
// ============================================================================
// receiver.php — Endpoint server-side pentru:
// 1. Primire XML eFactura (POST fără action) → salvare în temp/
// 2. Proxy ANAF APIs:
// ?action=ping — health check (no auth)
// ?action=validate — proxy validare ANAF (anaf_token opțional → ruta OAuth; altfel ruta publică)
// ?action=pdf — proxy transformare XmlToPdf ANAF (PDF binary; anaf_token opțional)
// ?action=cif — lookup contribuabil după CIF (nu necesită token OAuth)
// 3. Curățare fișiere temporare (?cleanup=xml_XXXX.xml)
// ============================================================================
// === 1. Configurație ========================================================
// Citește config.json (fallback) și suprascrie cu variabile de mediu dacă sunt prezente.
// Util pentru deploy în container (Docker/Dokploy): nu trebuie rebuild la schimbare config.
// ANAF_API_KEY — suprascrie api_key
// ANAF_ALLOWED_IPS — listă IP-uri separate prin virgulă; "*" sau gol = check dezactivat
// ANAF_TOKEN — Bearer token OAuth ANAF (opțional, doar pentru viitoare extensii)
// ANAF_TEMP_LIFETIME — ore păstrare fișiere temp (default 1)
$configPath = dirname(__FILE__) . '/config.json';
$config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : [];
if (!is_array($config)) $config = [];
if (($v = getenv('ANAF_API_KEY')) !== false && $v !== '') $config['api_key'] = $v;
if (($v = getenv('ANAF_TOKEN')) !== false && $v !== '') $config['anaf_token'] = $v;
if (($v = getenv('ANAF_TEMP_LIFETIME')) !== false && $v !== '') $config['temp_file_lifetime'] = intval($v);
if (($v = getenv('ANAF_ALLOWED_IPS')) !== false) {
$config['allowed_ips'] = array_values(array_filter(array_map('trim', explode(',', $v))));
}
// Funcție de validare XML
if (empty($config['api_key'])) {
header('HTTP/1.1 500 Internal Server Error');
header('Content-Type: application/json');
die(json_encode(['success' => false, 'error' => 'Eroare la încărcarea configurației']));
}
// === 2. Funcții helper ======================================================
/** Validează structura XML și namespace-urile UBL. */
function validateXML($xmlContent) {
// Dezactivează raportarea erorilor standard și folosește erori interne libxml
libxml_use_internal_errors(true);
// Elimină BOM (Byte Order Mark) dacă există
$xmlContent = preg_replace('/^\xEF\xBB\xBF/', '', $xmlContent);
// Curăță spațiile de la început și final
$xmlContent = trim($xmlContent);
// Încearcă să încarce XML-ul
$xml = simplexml_load_string($xmlContent);
if ($xml === false) {
$errors = libxml_get_errors();
$errorMessages = [];
foreach ($errors as $error) {
$errorMessages[] = [
'level' => $error->level,
'code' => $error->code,
'column' => $error->column,
'message' => $error->message,
'line' => $error->line
$msgs = array_map(function($e) {
return [
'level' => $e->level, 'code' => $e->code,
'column' => $e->column, 'message' => $e->message, 'line' => $e->line
];
}
}, $errors);
libxml_clear_errors();
return [
'valid' => false,
'errors' => $errorMessages
];
return ['valid' => false, 'errors' => $msgs];
}
// Verifică namespace-urile necesare
$namespaces = $xml->getNamespaces(true);
$requiredNamespaces = [
$required = [
'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'
];
foreach ($requiredNamespaces as $ns) {
$found = false;
foreach ($namespaces as $namespace) {
if ($namespace === $ns) {
$found = true;
break;
}
}
if (!$found) {
return [
'valid' => false,
'errors' => [
['message' => "Namespace lipsă: $ns"]
]
];
foreach ($required as $ns) {
if (!in_array($ns, array_values($namespaces))) {
return ['valid' => false, 'errors' => [['message' => "Namespace lipsă: $ns"]]];
}
}
return [
'valid' => true,
'errors' => []
];
return ['valid' => true, 'errors' => []];
}
// Verificare IP
/**
* Verifică IP-ul clientului față de allowed_ips.
* Returnează true dacă lista e goală sau conține "*" (check dezactivat — util în
* container behind reverse proxy, unde REMOTE_ADDR e IP-ul intern Traefik/nginx).
*/
function checkIP() {
global $config;
$clientIP = $_SERVER['REMOTE_ADDR'];
return in_array($clientIP, $config['allowed_ips']);
$allowed = $config['allowed_ips'] ?? [];
if (empty($allowed) || in_array('*', $allowed, true)) return true;
return in_array($_SERVER['REMOTE_ADDR'], $allowed, true);
}
// Verificare token
/** Verifică header-ul X-Api-Key față de api_key din config.json. */
function validateToken() {
global $config;
$headers = getallheaders();
$token = isset($headers['X-Api-Key']) ? $headers['X-Api-Key'] : '';
$token = $headers['X-Api-Key'] ?? '';
return hash_equals($config['api_key'], $token);
}
// Verificare origine request
if (false and !checkIP()) {
/**
* Execută un request cURL POST. Returnează ['body' => string, 'error' => string].
* @param string $url URL destinație
* @param string $body Corp request
* @param array $headers Headere suplimentare
*/
function curlPost($url, $body, $headers = []) {
if (!function_exists('curl_init')) {
return ['body' => '', 'error' => 'cURL nu este disponibil pe acest server'];
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
]);
$resp = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
return ['body' => $resp ?: '', 'error' => $error];
}
// === 3. Action routing (înainte de auth pentru ?action=ping) ================
$action = $_GET['action'] ?? '';
// Health check — nu necesită auth
if ($action === 'ping') {
header('Content-Type: application/json');
echo json_encode(['pong' => true]);
exit;
}
// === 4. Auth ================================================================
// IP check: dacă allowed_ips e gol sau conține "*", se sare peste (vezi checkIP()).
if (!checkIP()) {
header('HTTP/1.1 403 Forbidden');
header('Content-Type: application/json');
error_log("Acces interzis pentru IP: " . $_SERVER['REMOTE_ADDR']);
die(json_encode([
'success' => false,
'error' => 'Acces interzis',
'details' => 'IP-ul nu este autorizat'
]));
die(json_encode(['success' => false, 'error' => 'Acces interzis', 'details' => 'IP-ul nu este autorizat']));
}
// Verificare token
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && !validateToken()) {
header('HTTP/1.1 401 Unauthorized');
error_log("Token invalid de la IP: " . $_SERVER['REMOTE_ADDR']);
die(json_encode([
'success' => false,
'error' => 'Token invalid',
'details' => 'Autentificare eșuată'
]));
// ANAF proxy actions: nu necesită X-Api-Key (same-origin implicat prin IP check)
// Upload XML: necesită X-Api-Key
if (!in_array($action, ['validate', 'pdf', 'cif'])) {
if ($_SERVER['REQUEST_METHOD'] !== 'GET' && !validateToken()) {
header('HTTP/1.1 401 Unauthorized');
header('Content-Type: application/json');
error_log("Token invalid de la IP: " . $_SERVER['REMOTE_ADDR']);
die(json_encode(['success' => false, 'error' => 'Token invalid', 'details' => 'Autentificare eșuată']));
}
}
// Configurare director pentru fișiere temporare
// === 5. ANAF proxy handlers =================================================
/**
* Proxy validare ANAF eFactura.
* Necesită: "anaf_token": "Bearer XXX" în config.json
* POST https://api.anaf.ro/prod/FCTEL/rest/validare/FACT1
*/
function handleAnafValidate() {
global $config;
$xmlContent = file_get_contents('php://input');
$token = $config['anaf_token'] ?? '';
$headers = ['Content-Type: text/plain; charset=utf-8'];
if ($token) {
$headers[] = "Authorization: Bearer $token";
}
$result = curlPost('https://api.anaf.ro/prod/FCTEL/rest/validare/FACT1', $xmlContent, $headers);
header('Content-Type: application/json');
if ($result['error']) {
http_response_code(502);
echo json_encode(['error' => 'cURL error: ' . $result['error']]);
exit;
}
// Transmite răspunsul ANAF direct — structura: {"Messages": [...]}
echo $result['body'];
exit;
}
/**
* Proxy transformare ANAF XmlToPdf — întoarce PDF binary direct.
* Doc: https://api.anaf.ro/prod/FCTEL/rest/transformare/{standard}/{novld}
* - standard = FACT1 (UBL Invoice / Credit Note)
* - novld absent → ANAF validează XML-ul; dacă invalid, întoarce JSON cu erori
* - novld = "DA" → skip validare (NEFOLOSIT aici — vrem validare implicită)
*
* Endpoint:
* - cu anaf_token configurat: api.anaf.ro (OAuth2)
* - fără token: webservicesp.anaf.ro (rută publică, fără auth)
*
* Răspuns:
* - PDF (application/pdf) la succes — body începe cu "%PDF-"
* - JSON cu erori validare la eșec (HTTP 200 sau 400)
*
* Frontend (anaf.js) detectează tipul după Content-Type și afișează corespunzător.
*/
function handleAnafPdf() {
global $config;
$xmlContent = file_get_contents('php://input');
$token = $config['anaf_token'] ?? '';
$headers = ['Content-Type: text/plain; charset=utf-8'];
if ($token) {
$headers[] = "Authorization: Bearer $token";
$url = 'https://api.anaf.ro/prod/FCTEL/rest/transformare/FACT1';
} else {
$url = 'https://webservicesp.anaf.ro/prod/FCTEL/rest/transformare/FACT1';
}
$result = curlPost($url, $xmlContent, $headers);
if ($result['error']) {
header('Content-Type: application/json');
http_response_code(502);
echo json_encode(['error' => 'cURL error: ' . $result['error']]);
exit;
}
$body = $result['body'];
// Sniff response type — PDF binary începe cu "%PDF-", JSON cu "{".
if (substr($body, 0, 5) === '%PDF-') {
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="factura_anaf.pdf"');
echo $body;
exit;
}
// JSON (erori validare) sau alt răspuns text — pasează către client.
$trimmed = ltrim($body);
if ($trimmed !== '' && ($trimmed[0] === '{' || $trimmed[0] === '[')) {
header('Content-Type: application/json');
http_response_code(400);
echo $body;
exit;
}
// Unknown response (HTML error page, etc.)
header('Content-Type: application/json');
http_response_code(502);
echo json_encode(['error' => 'ANAF răspuns neașteptat', 'preview' => substr($body, 0, 200)]);
exit;
}
/**
* Proxy lookup contribuabil după CIF prin ANAF.
* Nu necesită token OAuth — API public ANAF.
*
* API utilizat: PlatitorTvaRest v9 (sincron)
* POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
* Body: [{"cui": <int>, "data": "YYYY-MM-DD"}]
* Răspuns direct: {found: [...], notFound: [...]}
* Doc: https://static.anaf.ro/static/10/Anaf/Informatii_R/Servicii_web/doc_WS_V9.txt
*
* Alternativă disponibilă: AsynchWebService v8 (async, batch până la 100 CUI-uri)
* Submit : POST https://webservicesp.anaf.ro/AsynchWebService/api/v8/ws/tva → correlationId
* Result : GET https://webservicesp.anaf.ro/AsynchWebService/api/v7/ws/tva?id={correlationId}
* (după min. 2s; rezultat disponibil max. 3 zile)
* Doc: https://static.anaf.ro/static/10/Anaf/Informatii_R/Servicii_web/doc_WS_Async_V8.txt
* Potrivit pentru bulk lookup (ex. import multiplu CIF-uri); pentru single CIF la click
* user, v9 sincron e preferabil (un singur request, fără polling).
*/
function handleAnafCif() {
$cif = intval($_GET['cif'] ?? '0');
if ($cif <= 0) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['error' => 'CIF invalid sau lipsă']);
exit;
}
$today = date('Y-m-d');
$payload = json_encode([['cui' => $cif, 'data' => $today]]);
$headers = ['Content-Type: application/json', 'Accept: application/json'];
$result = curlPost(
'https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva',
$payload, $headers
);
header('Content-Type: application/json');
if ($result['error']) {
http_response_code(502);
echo json_encode(['error' => 'ANAF indisponibil: ' . $result['error']]);
exit;
}
$data = json_decode($result['body'], true);
if (!$data || !array_key_exists('found', $data)) {
http_response_code(502);
echo json_encode(['error' => 'Răspuns neașteptat ANAF', 'raw' => $result['body']]);
exit;
}
echo json_encode(_normalizeCifResponse($data, $cif));
exit;
}
/** Normalizează răspunsul ANAF TVA v9 în formatul js/anaf.js. */
function _normalizeCifResponse($data, $cif) {
$found = $data['found'] ?? [];
if (empty($found)) {
return ['found' => false];
}
$c = $found[0];
$dg = $c['date_generale'] ?? [];
$as = $c['adresa_sediu_social'] ?? [];
$tv = $c['inregistrare_scop_Tva'] ?? [];
return [
'found' => true,
'denumire' => $dg['denumire'] ?? '',
'adresa' => $dg['adresa'] ?? '',
'nrRegCom' => $dg['nrRegCom'] ?? '',
'cui' => $dg['cui'] ?? $cif,
'tvaActiv' => !empty($tv['scpTVA']),
'strada' => trim(($as['sdenumire_Strada'] ?? '') . ' ' . ($as['snumar_Strada'] ?? '')),
'oras' => trim(preg_replace('/^(MUN\.|ORS\.|COM\.)\s+/iu', '', $as['sdenumire_Localitate'] ?? '')),
'judetCod' => 'RO-' . strtoupper($as['scod_JudetAuto'] ?? ''),
'codPostal' => $as['scod_Postal'] ?? '',
'telefon' => $dg['telefon'] ?? '',
'statusEFactura'=> !empty($dg['statusRO_e_Factura']),
];
}
// === 6. Rutare acțiuni ANAF =================================================
switch ($action) {
case 'validate': handleAnafValidate();
case 'pdf': handleAnafPdf();
case 'cif': handleAnafCif();
}
// === 7. Upload XML (comportament original) ==================================
$uploadDir = dirname(__FILE__) . '/temp/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
// Procesare request POST (primire XML)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
// Citește conținutul XML din request
$xmlContent = file_get_contents('php://input');
// Validare XML
$validationResult = validateXML($xmlContent);
if (!$validationResult['valid']) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode([
'success' => false,
'error' => 'XML invalid',
'details' => $validationResult['errors']
]);
echo json_encode(['success' => false, 'error' => 'XML invalid', 'details' => $validationResult['errors']]);
error_log("Validare XML eșuată: " . json_encode($validationResult['errors']));
exit;
}
// Generează nume unic pentru fișier
$fileName = uniqid('xml_') . '.xml';
$filePath = $uploadDir . $fileName;
// Salvează fișierul
if (file_put_contents($filePath, $xmlContent)) {
// Răspuns succes
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'fileName' => $fileName
]);
echo json_encode(['success' => true, 'fileName' => $fileName]);
} else {
throw new Exception('Eroare la salvarea fișierului');
}
@@ -158,19 +358,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
http_response_code(500);
error_log("Eroare procesare XML: " . $e->getMessage());
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
// Procesare request GET (curățare fișiere temporare)
// === 8. Curățare manuală fișiere temporare (?cleanup=xml_XXXX.xml) =========
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['cleanup'])) {
$fileName = basename($_GET['cleanup']); // Sanitizare nume fișier
if (preg_match('/^xml_[a-f0-9]+\.xml$/', $fileName)) { // Verifică formatul numelui
$fileName = basename($_GET['cleanup']);
if (preg_match('/^xml_[a-f0-9]+\.xml$/', $fileName)) {
$filePath = $uploadDir . $fileName;
if (file_exists($filePath)) {
if (unlink($filePath)) {
header('Content-Type: application/json');
@@ -178,35 +375,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['cleanup'])) {
} else {
header('Content-Type: application/json');
http_response_code(500);
error_log("Nu s-a putut șterge fișierul: " . $filePath);
echo json_encode([
'success' => false,
'error' => 'Nu s-a putut șterge fișierul'
]);
error_log("Nu s-a putut șterge: " . $filePath);
echo json_encode(['success' => false, 'error' => 'Nu s-a putut șterge fișierul']);
}
} else {
header('Content-Type: application/json');
http_response_code(404);
echo json_encode([
'success' => false,
'error' => 'Fișierul nu există'
]);
echo json_encode(['success' => false, 'error' => 'Fișierul nu există']);
}
} else {
header('Content-Type: application/json');
http_response_code(400);
error_log("Nume fișier invalid solicitat: " . $fileName);
echo json_encode([
'success' => false,
'error' => 'Nume fișier invalid'
]);
echo json_encode(['success' => false, 'error' => 'Nume fișier invalid']);
}
exit;
}
// Curățare automată a fișierelor vechi
$files = glob($uploadDir . 'xml_*.xml');
$now = time();
$maxAge = $config['temp_file_lifetime'] * 3600; // Conversie ore în secunde
// === 9. Curățare automată fișiere vechi =====================================
$files = glob($uploadDir . 'xml_*.xml');
$now = time();
$maxAge = ($config['temp_file_lifetime'] ?? 1) * 3600;
foreach ($files as $file) {
if ($now - filemtime($file) > $maxAge) {
@@ -214,4 +403,4 @@ foreach ($files as $file) {
error_log("Fișier vechi șters: " . basename($file));
}
}
?>
?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,331 +1,377 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Factură</title>
<style>
:root {
--primary-color: #2563eb;
--text-color: #1e293b;
--text-light: #64748b;
--border-color: #e2e8f0;
--background-light: #f8fafc;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.4;
color: var(--text-color);
font-size: 10px;
padding: 1cm;
}
.invoice-container {
max-width: 210mm;
margin: 0 auto;
}
.invoice-header {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary-color);
}
.invoice-title {
color: var(--primary-color);
font-size: 18px;
font-weight: bold;
}
.invoice-number, .invoice-dates {
font-size: 11px;
color: var(--text-light);
}
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
margin-left: -3rem;
margin-top: -0.5rem;
}
#qrcode {
width: 100px;
height: 100px;
margin-bottom: 0.5rem;
}
.e-invoice-info {
font-size: 10px;
color: var(--text-light);
text-align: center;
max-width: 120px;
}
.party-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
font-size: 10px;
}
.party-box {
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
}
.party-title {
font-weight: bold;
color: var(--primary-color);
margin-bottom: 0.25rem;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 9px;
}
.items-table th,
.items-table td {
padding: 4px;
border-bottom: 1px solid var(--border-color);
}
.items-table th:nth-child(1),
.items-table td:nth-child(1) {
text-align: center;
}
.items-table th:nth-child(4),
.items-table td:nth-child(4),
.items-table th:nth-child(5),
.items-table td:nth-child(5),
.items-table th:nth-child(6),
.items-table td:nth-child(6),
.items-table th:nth-child(7),
.items-table td:nth-child(7) {
text-align: right;
}
.note-section {
font-size: 9px;
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
}
.totals-container {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
margin-top: 1rem;
}
.currency-info {
font-size: 9px;
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
width: fit-content;
}
.currency-code {
font-weight: bold;
color: var(--primary-color);
}
.totals-section {
font-size: 10px;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
}
.total-row.final {
border-top: 1px solid var(--primary-color);
padding-top: 4px;
margin-top: 4px;
font-weight: bold;
color: var(--primary-color);
}
.vat-breakdown {
margin-top: 0.5rem;
font-size: 9px;
}
.vat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
margin: 4px 0;
}
.footer {
margin-top: 1rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
font-size: 8px;
color: var(--text-light);
text-align: center;
}
@media print {
@page {
size: A4;
margin: 1cm;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
</head>
<body>
<div class="invoice-container">
<div class="invoice-header">
<div>
<h1 class="invoice-title">FACTURĂ</h1>
<div class="invoice-number">Seria & Nr: <span id="print-invoice-number"></span></div>
<div class="invoice-dates">
Data emiterii: <span id="print-issue-date"></span> |
Data scadentă: <span id="print-due-date"></span>
</div>
</div>
<div class="qr-section">
<div id="qrcode"></div>
<div class="e-invoice-info"></div>
</div>
</div>
<div class="party-details">
<div class="party-box">
<div class="party-title">Furnizor</div>
<div id="print-supplier-details"></div>
</div>
<div class="party-box">
<div class="party-title">Client</div>
<div id="print-customer-details"></div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Nr.</th>
<th>Denumire</th>
<th>UM</th>
<th>Cant.</th>
<th>Preț</th>
<th>TVA</th>
<th>Total</th>
</tr>
</thead>
<tbody id="print-items"></tbody>
</table>
<div class="note-section" id="print-note" style="display: none;">
<h3>Text Adițional:</h3>
<div></div>
</div>
<div class="totals-container">
<div class="currency-info">
<p>Monedă Factură: <span class="currency-code" id="print-document-currency"></span></p>
<p id="print-tax-currency-container" style="display: none;">
Monedă TVA: <span class="currency-code" id="print-tax-currency"></span>
<br>
Curs valutar: <span id="print-exchange-rate"></span>
</p>
</div>
<div class="totals-section">
<div class="total-row">
<span>Subtotal:</span>
<span id="print-subtotal"></span>
</div>
<div id="print-allowances-row" style="display: none;">
<div class="total-row">
<span>Reduceri:</span>
<span id="print-allowances"></span>
</div>
</div>
<div id="print-charges-row" style="display: none;">
<div class="total-row">
<span>Taxe:</span>
<span id="print-charges"></span>
</div>
</div>
<div class="total-row">
<span>Valoare Netă:</span>
<span id="print-net-amount"></span>
</div>
<div class="vat-breakdown">
<div class="vat-grid">
<div>Tip TVA</div>
<div>Cotă</div>
<div>Bază</div>
<div>TVA</div>
</div>
<div class="vat-grid" id="print-vat-breakdown"></div>
<div class="total-row">
<span>Total TVA (<span id="print-vat-currency-main"></span>):</span>
<span id="print-vat-main"></span>
</div>
<div id="print-vat-secondary" class="total-row" style="display: none;">
<span>TVA (<span id="print-vat-currency-secondary"></span>):</span>
<span id="print-vat-secondary-amount"></span>
</div>
</div>
<div class="total-row final">
<span>Total cu TVA:</span>
<span id="print-total"></span>
</div>
</div>
</div>
<div class="footer">
</div>
</div>
<script>
function updateAllowancesChargesVisibility() {
const allowancesRow = document.getElementById('print-allowances-row');
const chargesRow = document.getElementById('print-charges-row');
const allowancesAmount = parseFloat(document.getElementById('print-allowances').textContent) || 0;
const chargesAmount = parseFloat(document.getElementById('print-charges').textContent) || 0;
allowancesRow.style.display = allowancesAmount > 0 ? 'block' : 'none';
chargesRow.style.display = chargesAmount > 0 ? 'block' : 'none';
}
window.addEventListener('load', updateAllowancesChargesVisibility);
</script>
</body>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Factură</title>
<style>
:root {
--primary-color: #2563eb;
--text-color: #1e293b;
--text-light: #64748b;
--border-color: #e2e8f0;
--background-light: #f8fafc;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.4;
color: var(--text-color);
font-size: 10px;
padding: 1cm;
}
.invoice-container {
max-width: 210mm;
margin: 0 auto;
}
.invoice-header {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary-color);
}
.invoice-title {
color: var(--primary-color);
font-size: 18px;
font-weight: bold;
}
.invoice-number, .invoice-dates {
font-size: 11px;
color: var(--text-light);
}
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
margin-left: -3rem;
margin-top: -0.5rem;
}
#qrcode {
width: 100px;
height: 100px;
margin-bottom: 0.5rem;
}
.e-invoice-info {
font-size: 10px;
color: var(--text-light);
text-align: center;
max-width: 120px;
}
.party-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
font-size: 10px;
}
.party-box {
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
}
.party-title {
font-weight: bold;
color: var(--primary-color);
margin-bottom: 0.25rem;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 9px;
}
.items-table th,
.items-table td {
padding: 4px;
border-bottom: 1px solid var(--border-color);
}
.items-table th:nth-child(1),
.items-table td:nth-child(1) {
text-align: center;
}
.items-table th:nth-child(4),
.items-table td:nth-child(4),
.items-table th:nth-child(5),
.items-table td:nth-child(5),
.items-table th:nth-child(6),
.items-table td:nth-child(6),
.items-table th:nth-child(7),
.items-table td:nth-child(7) {
text-align: right;
}
.note-section {
font-size: 9px;
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
}
.totals-container {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
margin-top: 1rem;
}
.currency-info {
font-size: 9px;
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
width: fit-content;
}
.currency-code {
font-weight: bold;
color: var(--primary-color);
}
.totals-section {
font-size: 10px;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
}
.total-row.final {
border-top: 1px solid var(--primary-color);
padding-top: 4px;
margin-top: 4px;
font-weight: bold;
color: var(--primary-color);
}
.vat-breakdown {
margin-top: 0.5rem;
font-size: 9px;
}
.vat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
margin: 4px 0;
}
.footer {
margin-top: 1rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
font-size: 8px;
color: var(--text-light);
text-align: center;
}
@media print {
@page {
size: A4;
margin: 1cm;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
.pdf-toolbar { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 10px; }
.pdf-toolbar button {
padding: 5px 12px; font-size: 12px; border-radius: 4px; cursor: pointer; border: 1px solid;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.pdf-toolbar .btn-pdf { background: #1e40af; color: #fff; border-color: #1e40af; }
.pdf-toolbar .btn-pdf:hover { background: #1e3a8a; }
.pdf-toolbar .btn-pdf:disabled { opacity: 0.6; cursor: not-allowed; }
.pdf-toolbar .btn-print { background: transparent; color: #57534e; border-color: #d6d3d1; }
.pdf-toolbar .btn-print:hover { background: #f5f5f4; }
@media print { .pdf-toolbar { display: none !important; } }
</style>
</head>
<body>
<div class="pdf-toolbar">
<button class="btn-print" onclick="window.print()">Printează</button>
<button id="btnSavePdf" class="btn-pdf" onclick="savePdf()">Descarcă PDF</button>
</div>
<div class="invoice-container" id="invoice-content">
<div class="invoice-header">
<div>
<h1 class="invoice-title">FACTURĂ</h1>
<div class="invoice-number">Seria & Nr: <span id="print-invoice-number"></span></div>
<div class="invoice-dates">
Data emiterii: <span id="print-issue-date"></span> |
Data scadentă: <span id="print-due-date"></span>
</div>
</div>
<div class="qr-section">
<div id="qrcode"></div>
<div class="e-invoice-info"></div>
</div>
</div>
<div class="party-details">
<div class="party-box">
<div class="party-title">Furnizor</div>
<div id="print-supplier-details"></div>
</div>
<div class="party-box">
<div class="party-title">Client</div>
<div id="print-customer-details"></div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Nr.</th>
<th>Denumire</th>
<th>UM</th>
<th>Cant.</th>
<th>Preț</th>
<th>TVA</th>
<th>Total</th>
</tr>
</thead>
<tbody id="print-items"></tbody>
</table>
<div class="note-section" id="print-note" style="display: none;">
<h3>Text Adițional:</h3>
<div></div>
</div>
<div class="totals-container">
<div class="currency-info">
<p>Monedă Factură: <span class="currency-code" id="print-document-currency"></span></p>
<p id="print-tax-currency-container" style="display: none;">
Monedă TVA: <span class="currency-code" id="print-tax-currency"></span>
<br>
Curs valutar: <span id="print-exchange-rate"></span>
</p>
</div>
<div class="totals-section">
<div class="total-row">
<span>Subtotal:</span>
<span id="print-subtotal"></span>
</div>
<div id="print-allowances-row" style="display: none;">
<div class="total-row">
<span>Reduceri:</span>
<span id="print-allowances"></span>
</div>
</div>
<div id="print-charges-row" style="display: none;">
<div class="total-row">
<span>Taxe:</span>
<span id="print-charges"></span>
</div>
</div>
<div class="total-row">
<span>Valoare Netă:</span>
<span id="print-net-amount"></span>
</div>
<div class="vat-breakdown">
<div class="vat-grid">
<div>Tip TVA</div>
<div>Cotă</div>
<div>Bază</div>
<div>TVA</div>
</div>
<div class="vat-grid" id="print-vat-breakdown"></div>
<div class="total-row">
<span>Total TVA (<span id="print-vat-currency-main"></span>):</span>
<span id="print-vat-main"></span>
</div>
<div id="print-vat-secondary" class="total-row" style="display: none;">
<span>TVA (<span id="print-vat-currency-secondary"></span>):</span>
<span id="print-vat-secondary-amount"></span>
</div>
</div>
<div class="total-row final">
<span>Total cu TVA:</span>
<span id="print-total"></span>
</div>
</div>
</div>
<div class="footer">
</div>
</div>
<script>
function updateAllowancesChargesVisibility() {
const allowancesRow = document.getElementById('print-allowances-row');
const chargesRow = document.getElementById('print-charges-row');
const allowancesAmount = parseFloat(document.getElementById('print-allowances').textContent) || 0;
const chargesAmount = parseFloat(document.getElementById('print-charges').textContent) || 0;
allowancesRow.style.display = allowancesAmount > 0 ? 'block' : 'none';
chargesRow.style.display = chargesAmount > 0 ? 'block' : 'none';
}
window.addEventListener('load', updateAllowancesChargesVisibility);
async function savePdf() {
const btn = document.getElementById('btnSavePdf');
if (btn) { btn.disabled = true; btn.textContent = 'Generare…'; }
try {
if (!window.html2pdf) {
await new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = '../js/vendor/html2pdf.bundle.min.js';
s.onload = resolve;
s.onerror = () => reject(new Error('Bundle html2pdf indisponibil'));
document.head.appendChild(s);
});
}
const element = document.getElementById('invoice-content');
const invoiceNumber = document.getElementById('print-invoice-number')?.textContent || 'factura';
await window.html2pdf().set({
margin: [8, 8, 8, 8],
filename: 'factura_' + invoiceNumber.replace(/[^a-zA-Z0-9_-]/g, '_') + '.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(element).save();
} catch (err) {
alert('Eroare la generarea PDF: ' + err.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'Descarcă PDF'; }
}
}
</script>
</body>
</html>

View File

@@ -1,378 +1,446 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Factură</title>
<style>
/* Variables - Keep original colors */
:root {
--primary-color: #2563eb;
--text-color: #1e293b;
--text-light: #64748b;
--border-color: #e2e8f0;
--background-light: #f8fafc;
}
/* Base Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.4;
color: var(--text-color);
background: white;
font-size: 11px;
padding: 1cm;
}
/* Layout */
.invoice-container {
max-width: 210mm;
margin: 0 auto;
}
/* Header */
.invoice-header {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--primary-color);
}
.invoice-title-section {
display: flex;
flex-direction: column;
}
.invoice-title {
color: var(--primary-color);
font-size: 18px;
font-weight: bold;
margin-bottom: 0.25rem;
}
.invoice-number {
font-size: 11px;
color: var(--text-light);
margin-bottom: 0.25rem;
}
.invoice-dates {
font-size: 10px;
color: var(--text-light);
}
/* QR Code */
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
margin-left: 2rem;
}
#qrcode {
width: 100px;
height: 100px;
margin-bottom: 0.5rem;
}
.e-invoice-info {
font-size: 8px;
color: var(--text-light);
text-align: center;
max-width: 100px;
}
/* Party Details */
.party-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.party-box {
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
}
.party-title {
font-weight: bold;
color: var(--primary-color);
margin-bottom: 0.25rem;
font-size: 11px;
}
.party-info p {
margin: 0.15rem 0;
font-size: 10px;
}
/* Items Table */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 9px;
}
.items-table th,
.items-table td {
padding: 4px;
border-bottom: 1px solid var(--border-color);
}
.items-table th:nth-child(1),
.items-table td:nth-child(1) {
text-align: center;
}
.items-table th:nth-child(4),
.items-table td:nth-child(4),
.items-table th:nth-child(5),
.items-table td:nth-child(5),
.items-table th:nth-child(6),
.items-table td:nth-child(6),
.items-table th:nth-child(7),
.items-table td:nth-child(7) {
text-align: right;
}
/* Note Section */
.note-section {
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
font-size: 9px;
}
.note-section h3 {
font-size: 10px;
color: var(--text-light);
margin-bottom: 0.25rem;
}
/* Totals Container */
.totals-container {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
margin-top: 1rem;
}
.currency-info {
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
font-size: 9px;
width: fit-content;
}
.currency-code {
font-weight: bold;
color: var(--primary-color);
}
.totals-section {
font-size: 10px;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
border-bottom: 1px solid var(--border-color);
}
.total-row.final {
border-bottom: 2px solid var(--primary-color);
font-weight: bold;
font-size: 11px;
color: var(--primary-color);
}
/* VAT Breakdown */
.vat-breakdown {
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--background-light);
border-radius: 4px;
font-size: 9px;
}
.vat-title {
font-weight: bold;
color: var(--text-light);
margin-bottom: 0.25rem;
}
.vat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
margin-bottom: 0.5rem;
}
.vat-grid-header {
font-weight: bold;
color: var(--text-light);
}
.vat-amount-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-top: 1px solid var(--border-color);
}
/* Footer */
.footer {
margin-top: 1rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
font-size: 8px;
color: var(--text-light);
text-align: center;
}
/* Print Styles */
@media print {
@page {
size: A4;
margin: 1cm;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.no-print {
display: none;
}
.show-in-print {
display: flex !important;
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
</head>
<body>
<div class="invoice-container">
<div class="invoice-header">
<div class="invoice-title-section">
<h1 class="invoice-title">FACTURĂ</h1>
<div class="invoice-number">Seria & Nr: <span id="print-invoice-number"></span></div>
<div class="invoice-dates">
Data emiterii: <span id="print-issue-date"></span> |
Data scadentă: <span id="print-due-date"></span>
</div>
</div>
<div class="qr-section">
<div id="qrcode"></div>
<div class="e-invoice-info"></div>
</div>
</div>
<div class="party-details">
<div class="party-box">
<div class="party-title">Furnizor</div>
<div class="party-info" id="print-supplier-details"></div>
</div>
<div class="party-box">
<div class="party-title">Client</div>
<div class="party-info" id="print-customer-details"></div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Nr.</th>
<th>Denumire</th>
<th>UM</th>
<th>Cant.</th>
<th>Preț</th>
<th>TVA</th>
<th>Total</th>
</tr>
</thead>
<tbody id="print-items"></tbody>
</table>
<div class="note-section" id="print-note" style="display: none;">
<h3>Text Adițional:</h3>
<div></div>
</div>
<div class="totals-container">
<div class="currency-info">
<p>Monedă Factură: <span class="currency-code" id="print-document-currency"></span></p>
<p id="print-tax-currency-container" style="display: none;">
Monedă TVA: <span class="currency-code" id="print-tax-currency"></span>
<br>
Curs valutar: <span id="print-exchange-rate"></span>
</p>
</div>
<div class="totals-section">
<div class="total-row">
<span>Subtotal:</span>
<span id="print-subtotal"></span>
</div>
<div class="total-row">
<span>Total Reduceri:</span>
<span id="print-allowances"></span>
</div>
<div class="total-row">
<span>Total Taxe:</span>
<span id="print-charges"></span>
</div>
<div class="total-row">
<span>Valoare Netă:</span>
<span id="print-net-amount"></span>
</div>
<div class="vat-breakdown">
<div class="vat-title">Defalcare TVA</div>
<div class="vat-grid">
<div class="vat-grid-header">Tip TVA</div>
<div class="vat-grid-header">Cotă</div>
<div class="vat-grid-header">Bază</div>
<div class="vat-grid-header">TVA</div>
</div>
<div class="vat-grid" id="print-vat-breakdown"></div>
<div id="print-vat-currencies" class="vat-amount-row">
<span>Total TVA (<span id="print-vat-currency-main"></span>):</span>
<span id="print-vat-main"></span>
</div>
<div id="print-vat-secondary" class="vat-amount-row" style="display: none;">
<span>Total TVA (<span id="print-vat-currency-secondary"></span>):</span>
<span id="print-vat-secondary-amount"></span>
</div>
</div>
<div class="total-row final">
<span>Total cu TVA:</span>
<span id="print-total"></span>
</div>
</div>
</div>
<div class="footer">
</div>
</div>
</body>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Factură</title>
<style>
/* Variables - Keep original colors */
:root {
--primary-color: #2563eb;
--text-color: #1e293b;
--text-light: #64748b;
--border-color: #e2e8f0;
--background-light: #f8fafc;
}
/* Base Styles */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.4;
color: var(--text-color);
background: white;
font-size: 11px;
padding: 1cm;
}
/* Layout */
.invoice-container {
max-width: 210mm;
margin: 0 auto;
}
/* Header */
.invoice-header {
display: grid;
grid-template-columns: 1fr auto;
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--primary-color);
}
.invoice-title-section {
display: flex;
flex-direction: column;
}
.invoice-title {
color: var(--primary-color);
font-size: 18px;
font-weight: bold;
margin-bottom: 0.25rem;
}
.invoice-number {
font-size: 11px;
color: var(--text-light);
margin-bottom: 0.25rem;
}
.invoice-dates {
font-size: 10px;
color: var(--text-light);
}
/* QR Code */
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
margin-left: 2rem;
}
#qrcode {
width: 100px;
height: 100px;
margin-bottom: 0.5rem;
}
.e-invoice-info {
font-size: 8px;
color: var(--text-light);
text-align: center;
max-width: 100px;
}
/* Party Details */
.party-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.party-box {
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
}
.party-title {
font-weight: bold;
color: var(--primary-color);
margin-bottom: 0.25rem;
font-size: 11px;
}
.party-info p {
margin: 0.15rem 0;
font-size: 10px;
}
/* Items Table */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1rem;
font-size: 9px;
}
.items-table th,
.items-table td {
padding: 4px;
border-bottom: 1px solid var(--border-color);
}
.items-table th:nth-child(1),
.items-table td:nth-child(1) {
text-align: center;
}
.items-table th:nth-child(4),
.items-table td:nth-child(4),
.items-table th:nth-child(5),
.items-table td:nth-child(5),
.items-table th:nth-child(6),
.items-table td:nth-child(6),
.items-table th:nth-child(7),
.items-table td:nth-child(7) {
text-align: right;
}
/* Note Section */
.note-section {
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
font-size: 9px;
}
.note-section h3 {
font-size: 10px;
color: var(--text-light);
margin-bottom: 0.25rem;
}
/* Totals Container */
.totals-container {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
margin-top: 1rem;
}
.currency-info {
padding: 0.5rem;
background: var(--background-light);
border: 1px solid var(--border-color);
font-size: 9px;
width: fit-content;
}
.currency-code {
font-weight: bold;
color: var(--primary-color);
}
.totals-section {
font-size: 10px;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 2px 0;
border-bottom: 1px solid var(--border-color);
}
.total-row.final {
border-bottom: 2px solid var(--primary-color);
font-weight: bold;
font-size: 11px;
color: var(--primary-color);
}
/* VAT Breakdown */
.vat-breakdown {
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--background-light);
border-radius: 4px;
font-size: 9px;
}
.vat-title {
font-weight: bold;
color: var(--text-light);
margin-bottom: 0.25rem;
}
.vat-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
margin-bottom: 0.5rem;
}
.vat-grid-header {
font-weight: bold;
color: var(--text-light);
}
.vat-amount-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-top: 1px solid var(--border-color);
}
/* Footer */
.footer {
margin-top: 1rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border-color);
font-size: 8px;
color: var(--text-light);
text-align: center;
}
/* Print Styles */
@media print {
@page {
size: A4;
margin: 1cm;
}
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.no-print {
display: none;
}
.show-in-print {
display: flex !important;
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
/* Toolbar PDF — ascuns la print */
.pdf-toolbar {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-bottom: 12px;
}
.pdf-toolbar button {
padding: 6px 14px;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
border-radius: 4px;
cursor: pointer;
border: 1px solid;
}
.pdf-toolbar .btn-pdf {
background: #1e40af;
color: #fff;
border-color: #1e40af;
}
.pdf-toolbar .btn-pdf:hover { background: #1e3a8a; }
.pdf-toolbar .btn-pdf:disabled { opacity: 0.6; cursor: not-allowed; }
.pdf-toolbar .btn-print {
background: transparent;
color: #57534e;
border-color: #d6d3d1;
}
.pdf-toolbar .btn-print:hover { background: #f5f5f4; }
@media print { .pdf-toolbar { display: none !important; } }
</style>
</head>
<body>
<div class="pdf-toolbar no-print">
<button class="btn-print" onclick="window.print()">Printează</button>
<button id="btnSavePdf" class="btn-pdf" onclick="savePdf()">Descarcă PDF</button>
</div>
<div class="invoice-container" id="invoice-content">
<div class="invoice-header">
<div class="invoice-title-section">
<h1 class="invoice-title">FACTURĂ</h1>
<div class="invoice-number">Seria & Nr: <span id="print-invoice-number"></span></div>
<div class="invoice-dates">
Data emiterii: <span id="print-issue-date"></span> |
Data scadentă: <span id="print-due-date"></span>
</div>
</div>
<div class="qr-section">
<div id="qrcode"></div>
<div class="e-invoice-info"></div>
</div>
</div>
<div class="party-details">
<div class="party-box">
<div class="party-title">Furnizor</div>
<div class="party-info" id="print-supplier-details"></div>
</div>
<div class="party-box">
<div class="party-title">Client</div>
<div class="party-info" id="print-customer-details"></div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Nr.</th>
<th>Denumire</th>
<th>UM</th>
<th>Cant.</th>
<th>Preț</th>
<th>TVA</th>
<th>Total</th>
</tr>
</thead>
<tbody id="print-items"></tbody>
</table>
<div class="note-section" id="print-note" style="display: none;">
<h3>Text Adițional:</h3>
<div></div>
</div>
<div class="totals-container">
<div class="currency-info">
<p>Monedă Factură: <span class="currency-code" id="print-document-currency"></span></p>
<p id="print-tax-currency-container" style="display: none;">
Monedă TVA: <span class="currency-code" id="print-tax-currency"></span>
<br>
Curs valutar: <span id="print-exchange-rate"></span>
</p>
</div>
<div class="totals-section">
<div class="total-row">
<span>Subtotal:</span>
<span id="print-subtotal"></span>
</div>
<div class="total-row">
<span>Total Reduceri:</span>
<span id="print-allowances"></span>
</div>
<div class="total-row">
<span>Total Taxe:</span>
<span id="print-charges"></span>
</div>
<div class="total-row">
<span>Valoare Netă:</span>
<span id="print-net-amount"></span>
</div>
<div class="vat-breakdown">
<div class="vat-title">Defalcare TVA</div>
<div class="vat-grid">
<div class="vat-grid-header">Tip TVA</div>
<div class="vat-grid-header">Cotă</div>
<div class="vat-grid-header">Bază</div>
<div class="vat-grid-header">TVA</div>
</div>
<div class="vat-grid" id="print-vat-breakdown"></div>
<div id="print-vat-currencies" class="vat-amount-row">
<span>Total TVA (<span id="print-vat-currency-main"></span>):</span>
<span id="print-vat-main"></span>
</div>
<div id="print-vat-secondary" class="vat-amount-row" style="display: none;">
<span>Total TVA (<span id="print-vat-currency-secondary"></span>):</span>
<span id="print-vat-secondary-amount"></span>
</div>
</div>
<div class="total-row final">
<span>Total cu TVA:</span>
<span id="print-total"></span>
</div>
</div>
</div>
<div class="footer">
</div>
</div>
<script>
// Descarcă PDF via html2pdf.js (bundle local)
async function savePdf() {
const btn = document.getElementById('btnSavePdf');
if (btn) { btn.disabled = true; btn.textContent = 'Generare…'; }
try {
// Încarcă bundle-ul UMD html2pdf.js (dacă nu e deja)
if (!window.html2pdf) {
await new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = '../js/vendor/html2pdf.bundle.min.js';
s.onload = resolve;
s.onerror = () => reject(new Error('Bundle html2pdf indisponibil'));
document.head.appendChild(s);
});
}
const element = document.getElementById('invoice-content');
const invoiceNumber = document.getElementById('print-invoice-number')?.textContent || 'factura';
await window.html2pdf().set({
margin: [10, 10, 10, 10],
filename: 'factura_' + invoiceNumber.replace(/[^a-zA-Z0-9_-]/g, '_') + '.pdf',
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(element).save();
} catch (err) {
alert('Eroare la generarea PDF: ' + err.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = 'Descarcă PDF'; }
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
google-site-verification: googleb8af97b4c544f7b8.html

View File

@@ -93,6 +93,9 @@
<!-- Soft Professional Blue Hero Section -->
<section class="professional-hero text-center">
<div class="max-w-4xl mx-auto relative z-10">
<div class="mb-4">
<span class="inline-block px-4 py-1 rounded-full text-sm font-semibold" style="background:#e8f4f7; color:#08819c; border:1px solid #b3d9e6;">ROA &mdash; Romfast Applications</span>
</div>
<h1 class="text-4xl md:text-5xl font-bold mb-6 max-w-3xl mx-auto leading-tight" style="color: #1e293b; letter-spacing: -0.02em">
ERP românesc și programe specializate pe industrii —<br>
<span style="color: #5288c4">cu validări și corelații care dau siguranță</span><br>