diff --git a/efactura-generator/CHANGELOG.md b/efactura-generator/CHANGELOG.md
index 4fa6519..27a960d 100644
--- a/efactura-generator/CHANGELOG.md
+++ b/efactura-generator/CHANGELOG.md
@@ -1,5 +1,19 @@
# 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
diff --git a/efactura-generator/index.html b/efactura-generator/index.html
index 9281fa4..d231247 100644
--- a/efactura-generator/index.html
+++ b/efactura-generator/index.html
@@ -21,10 +21,21 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -281,7 +292,7 @@
diff --git a/efactura-generator/js/anaf.js b/efactura-generator/js/anaf.js
index 1c348f7..1ce34b3 100644
--- a/efactura-generator/js/anaf.js
+++ b/efactura-generator/js/anaf.js
@@ -5,13 +5,14 @@
* Pe hosting static (GitHub Pages, fără PHP), apelurile vor eșua cu eroare
* "receiver indisponibil" — verificați cu probeReceiver() la inițializare.
*
- * Configurare necesară în config.json (server-side):
- * "anaf_token": "" — necesar pentru validate + pdf
+ * Configurare opțională în config.json (server-side):
+ * "anaf_token": "" — 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 https://api.anaf.ro/prod/FCTEL/rest/validare/FACT1
- * PDF/HTML : POST https://api.anaf.ro/prod/FCTEL/rest/transformare/FACT1/DA
- * CIF info : POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
+ * 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';
@@ -65,11 +66,14 @@ export async function anafValidate(xmlContent) {
}
/**
- * Obține vizualizarea ANAF a facturii (ZIP cu HTML).
- * Notă: ANAF /transformare returnează ZIP+HTML, nu PDF direct.
- * PDF-ul real este generat client-side prin PR-PDF / html2pdf.js.
+ * 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} ZIP blob
+ * @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;
@@ -82,10 +86,29 @@ export async function anafPdf(xmlContent) {
} catch (e) {
throw new Error('Receiver.php indisponibil — ' + e.message);
}
- if (!res.ok) {
- throw new Error(`ANAF vizualizare: HTTP ${res.status}`);
+
+ const ct = (res.headers.get('Content-Type') || '').toLowerCase();
+
+ if (res.ok && ct.includes('application/pdf')) {
+ return { pdf: await res.blob() };
}
- return 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'})`);
}
/**
diff --git a/efactura-generator/js/script.js b/efactura-generator/js/script.js
index 1d3542e..0d2d6a1 100644
--- a/efactura-generator/js/script.js
+++ b/efactura-generator/js/script.js
@@ -8,7 +8,7 @@ import JSZip from './vendor/jszip.mjs';
import { validateCIF } from './validation/cif.js';
import { validateIBAN } from './validation/iban.js';
import { runBRRules } from './validation/br-ro.js';
-import { probeReceiver, anafValidate, anafCifLookup } from './anaf.js';
+import { probeReceiver, anafValidate, anafCifLookup, anafPdf } from './anaf.js';
import { catalogAdd, catalogSearch, catalogDelete } from './catalog.js';
// Constants
@@ -4572,6 +4572,63 @@ window.validateAnaf = async function() {
}
};
+/**
+ * Descarcă PDF-ul oficial generat de ANAF din XML-ul curent.
+ * Apelează endpoint-ul ANAF /transformare/FACT1 (validează implicit).
+ * Dacă XML-ul nu trece validarea ANAF, afișează erorile în loc să descarce.
+ * Necesită receiver.php disponibil pe server (proxy CORS).
+ */
+window.downloadPdfAnaf = async function() {
+ if (!currentInvoice) {
+ showToast('Nicio factură încărcată. Deschideți un XML eFactura mai întâi.', 'warning');
+ return;
+ }
+ const btn = document.getElementById('btnPdfAnaf');
+ if (btn) _btnLoading(btn, 'Generare PDF ANAF...');
+
+ try {
+ const xmlString = _currentXMLString();
+ if (!xmlString) throw new Error('Nu s-a putut genera XML-ul facturii.');
+
+ const result = await anafPdf(xmlString);
+
+ if (result.errors && result.errors.length) {
+ // ANAF a respins XML-ul la validare; afișează primele erori în toast.
+ const errs = result.errors.filter(m => m.severity === 'ERROR' || m.severity === 'FATAL');
+ const sub = result.errors.slice(0, 3).map(m => m.message).join(' | ');
+ showToast(
+ `ANAF a respins XML-ul: ${errs.length || result.errors.length} mesaje`,
+ 'error',
+ sub.slice(0, 220)
+ );
+ return;
+ }
+
+ if (!(result.pdf instanceof Blob)) {
+ throw new Error('Răspuns ANAF fără PDF.');
+ }
+
+ // Descarcă blob-ul ca fișier PDF.
+ const inv = printHandler.collectInvoiceData();
+ const safeNum = String(inv.invoiceNumber || 'factura').replace(/[^a-z0-9_-]+/gi, '_');
+ const url = URL.createObjectURL(result.pdf);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${safeNum}_ANAF.pdf`;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ URL.revokeObjectURL(url);
+
+ showToast('PDF ANAF descărcat.', 'success');
+ } catch (err) {
+ showToast('Eroare PDF ANAF: ' + err.message, 'error',
+ 'Verificați că receiver.php este disponibil pe server.');
+ } finally {
+ if (btn) _btnDone(btn);
+ }
+};
+
/**
* Caută datele firmei după CIF din câmpul supplierVAT / customerVAT.
* @param {'supplier'|'customer'} party
@@ -4647,11 +4704,12 @@ window.lookupCif = async function(party) {
}
};
-// Probează receiver.php la startup — dacă e disponibil, afișează butoanele ANAF.
+// Probează receiver.php la startup — dacă e disponibil, afișează item-urile ANAF.
(async () => {
const available = await probeReceiver().catch(() => false);
if (available) {
- document.getElementById('btnValidateAnaf')?.style?.setProperty('display', '');
+ // Item-urile ANAF din meniul "Acțiuni" sunt grupate într-un wrapper hidden.
+ document.querySelector('.actions-menu-anaf')?.removeAttribute('hidden');
document.querySelectorAll('.anaf-cif-btn').forEach(b => b.style.removeProperty('display'));
}
})();
@@ -5262,38 +5320,66 @@ const printHandler = new InvoicePrintHandler();
// Initialize when the document is ready
document.addEventListener('DOMContentLoaded', () => {
- // Add print controls to the UI
- const headerButtonGroup = document.querySelector('.button-group');
- if (headerButtonGroup) {
- const printControls = document.createElement('div');
- printControls.className = 'print-controls';
- printControls.style.display = 'flex';
- printControls.style.gap = '8px';
- printControls.style.alignItems = 'center';
-
- // Create template selector
- const templateSelect = document.createElement('select');
- templateSelect.className = 'form-input';
- templateSelect.style.width = 'auto';
- templateSelect.innerHTML = `
-
-
- `;
- templateSelect.addEventListener('change', (e) => {
- printHandler.setTemplate(e.target.value);
- });
+ setupActionsMenu();
+});
- // Create print button
- const printButton = document.createElement('button');
- printButton.className = 'button';
- printButton.onclick = () => printHandler.print();
- printButton.innerHTML = 'Printează';
+/**
+ * Cablează meniul "Acțiuni ▾" din header: toggle, click-outside, Esc, item handlers.
+ * Înlocuiește vechea injecție DOM pentru print controls — toate output-urile
+ * (Printează Standard/Compact, Descarcă PDF, PDF ANAF, Validare ANAF) sunt în meniu.
+ */
+function setupActionsMenu() {
+ const trigger = document.getElementById('btnActionsMenu');
+ const menu = document.getElementById('actionsMenu');
+ if (!trigger || !menu) return;
- // Add elements to controls
- printControls.appendChild(templateSelect);
- printControls.appendChild(printButton);
-
- // Add controls to header
- headerButtonGroup.appendChild(printControls);
- }
-});
\ No newline at end of file
+ const open = () => {
+ menu.hidden = false;
+ trigger.setAttribute('aria-expanded', 'true');
+ };
+ const close = () => {
+ menu.hidden = true;
+ trigger.setAttribute('aria-expanded', 'false');
+ };
+ const toggle = () => (menu.hidden ? open() : close());
+
+ trigger.addEventListener('click', (e) => {
+ e.stopPropagation();
+ toggle();
+ });
+
+ document.addEventListener('click', (e) => {
+ if (!menu.hidden && !menu.contains(e.target) && e.target !== trigger) close();
+ });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && !menu.hidden) {
+ close();
+ trigger.focus();
+ }
+ });
+
+ // Item dispatch — fiecare buton are data-action (+ data-template pentru print).
+ menu.addEventListener('click', (e) => {
+ const item = e.target.closest('[data-action]');
+ if (!item) return;
+ const action = item.dataset.action;
+ close();
+
+ switch (action) {
+ case 'print':
+ printHandler.setTemplate(item.dataset.template || 'standard');
+ printHandler.print();
+ break;
+ case 'downloadPdf':
+ window.downloadPDF?.();
+ break;
+ case 'pdfAnaf':
+ window.downloadPdfAnaf?.();
+ break;
+ case 'validateAnaf':
+ window.validateAnaf?.();
+ break;
+ }
+ });
+}
\ No newline at end of file
diff --git a/efactura-generator/receiver.php b/efactura-generator/receiver.php
index 0acd439..3738f99 100644
--- a/efactura-generator/receiver.php
+++ b/efactura-generator/receiver.php
@@ -4,8 +4,8 @@
// 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 (necesită anaf_token în config.json)
-// ?action=pdf — proxy transformare ANAF (ZIP+HTML, necesită anaf_token)
+// ?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)
// ============================================================================
@@ -175,10 +175,21 @@ function handleAnafValidate() {
}
/**
- * Proxy transformare ANAF (vizualizare ZIP+HTML).
- * Notă: ANAF returnează ZIP cu fișiere HTML, nu PDF direct.
- * Necesită: "anaf_token": "Bearer XXX" în config.json
- * POST https://api.anaf.ro/prod/FCTEL/rest/transformare/FACT1/DA
+ * 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;
@@ -188,9 +199,12 @@ function handleAnafPdf() {
$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('https://api.anaf.ro/prod/FCTEL/rest/transformare/FACT1/DA', $xmlContent, $headers);
+ $result = curlPost($url, $xmlContent, $headers);
if ($result['error']) {
header('Content-Type: application/json');
@@ -198,9 +212,30 @@ function handleAnafPdf() {
echo json_encode(['error' => 'cURL error: ' . $result['error']]);
exit;
}
- header('Content-Type: application/zip');
- header('Content-Disposition: attachment; filename="vizualizare_anaf.zip"');
- echo $result['body'];
+
+ $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;
}
diff --git a/efactura-generator/styles/main.css b/efactura-generator/styles/main.css
index ed0b827..4312f6a 100644
--- a/efactura-generator/styles/main.css
+++ b/efactura-generator/styles/main.css
@@ -152,6 +152,114 @@ input.date-input {
flex-wrap: wrap;
}
+/* ----------------------------------------------------------------------------
+ Actions overflow menu (header dropdown)
+ ---------------------------------------------------------------------------- */
+.actions-menu-wrapper {
+ position: relative;
+ display: inline-flex;
+}
+
+.actions-menu-chevron {
+ margin-left: 4px;
+ font-size: 10px;
+ line-height: 1;
+ transition: transform 120ms ease;
+ display: inline-block;
+}
+
+#btnActionsMenu[aria-expanded="true"] .actions-menu-chevron {
+ transform: rotate(180deg);
+}
+
+.actions-menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ right: 0;
+ min-width: 220px;
+ background-color: #1e293b; /* slate-800 — slightly lighter than header-bg slate-900 */
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.32);
+ padding: 4px 0;
+ z-index: 950;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ animation: actions-menu-in 120ms ease-out;
+}
+
+.actions-menu[hidden] {
+ display: none;
+}
+
+@keyframes actions-menu-in {
+ from { opacity: 0; transform: translateY(-4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.actions-menu-item {
+ appearance: none;
+ background: transparent;
+ border: none;
+ color: #f1f5f9;
+ text-align: left;
+ padding: 8px 14px;
+ font-family: inherit;
+ font-size: 13px;
+ line-height: 1.3;
+ cursor: pointer;
+ width: 100%;
+ transition: background-color 80ms ease, color 80ms ease;
+}
+
+.actions-menu-item:hover,
+.actions-menu-item:focus-visible {
+ background-color: rgba(255, 255, 255, 0.06);
+ outline: none;
+}
+
+.actions-menu-item:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.actions-menu-item.is-success {
+ color: #86efac; /* green ghost — ANAF actions */
+}
+
+.actions-menu-item.is-success:hover,
+.actions-menu-item.is-success:focus-visible {
+ background-color: rgba(134, 239, 172, 0.10);
+}
+
+.actions-menu-divider {
+ height: 1px;
+ background-color: rgba(255, 255, 255, 0.10);
+ margin: 4px 0;
+}
+
+.actions-menu-anaf {
+ display: flex;
+ flex-direction: column;
+}
+
+.actions-menu-anaf[hidden] {
+ display: none;
+}
+
+@media (max-width: 768px) {
+ .actions-menu-wrapper {
+ position: static;
+ }
+ .actions-menu {
+ left: var(--space-3);
+ right: var(--space-3);
+ top: auto;
+ margin-top: 6px;
+ }
+}
+
/* ----------------------------------------------------------------------------
Cards / form sections
---------------------------------------------------------------------------- */