From bcb6d9df99439aadb89b76452c7b67513a360206 Mon Sep 17 00:00:00 2001 From: odweta Date: Thu, 10 Sep 2026 18:06:38 +0200 Subject: [PATCH] Store a single signature per user, like supplier profile Replace the signature preset dropdown (multiple named signatures) with a single saved signature per user, mirroring the supplier profile behavior: Save/Load buttons, auto-loaded on the new invoice screen. - New signature_profiles table (one row per user), migrated from any existing signature_presets data. - New /api/signature GET/PUT/DELETE endpoints replacing /api/signature-presets. - Updated frontend to save/load the single signature instead of a named preset dropdown. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- app/src/app.js | 36 +++++--------- app/src/db.js | 11 +++++ app/src/static/index.html | 17 ++----- app/src/static/script.js | 101 ++++++++++++-------------------------- app/src/static/style.css | 54 -------------------- 5 files changed, 59 insertions(+), 160 deletions(-) diff --git a/app/src/app.js b/app/src/app.js index 6d80740..efc8609 100644 --- a/app/src/app.js +++ b/app/src/app.js @@ -362,51 +362,37 @@ app.delete('/api/payment-presets/:id', requireAuth, async (req, res, next) => { } }); -const signaturePresetResponse = (row) => ({ - id: row.id, - name: row.name, - dataUrl: row.data_url -}); - -app.get('/api/signature-presets', requireAuth, async (req, res, next) => { +app.get('/api/signature', requireAuth, async (req, res, next) => { try { - const result = await pool.query( - 'SELECT * FROM signature_presets WHERE user_id = $1 ORDER BY name', - [req.userId] - ); - res.json({ presets: result.rows.map(signaturePresetResponse) }); + const result = await pool.query('SELECT * FROM signature_profiles WHERE user_id = $1', [req.userId]); + res.json({ signature: result.rowCount ? result.rows[0].data_url : null }); } catch (error) { next(error); } }); -app.post('/api/signature-presets', requireAuth, async (req, res, next) => { +app.put('/api/signature', requireAuth, async (req, res, next) => { try { - const name = String(req.body.name || '').trim(); const dataUrl = req.body.dataUrl; - if (!name) { - res.status(400).json({ error: 'Zadejte název podpisu.' }); - return; - } if (!isValidSignatureDataUrl(dataUrl)) { res.status(400).json({ error: 'Podpis musí být platný obrázek JPG nebo PNG.' }); return; } const result = await pool.query(` - INSERT INTO signature_presets (user_id, name, data_url) - VALUES ($1, $2, $3) - ON CONFLICT (user_id, name) DO UPDATE SET data_url = EXCLUDED.data_url + INSERT INTO signature_profiles (user_id, data_url) + VALUES ($1, $2) + ON CONFLICT (user_id) DO UPDATE SET data_url = EXCLUDED.data_url RETURNING * - `, [req.userId, name, dataUrl]); - res.status(201).json({ preset: signaturePresetResponse(result.rows[0]) }); + `, [req.userId, dataUrl]); + res.json({ signature: result.rows[0].data_url }); } catch (error) { next(error); } }); -app.delete('/api/signature-presets/:id', requireAuth, async (req, res, next) => { +app.delete('/api/signature', requireAuth, async (req, res, next) => { try { - await pool.query('DELETE FROM signature_presets WHERE id = $1 AND user_id = $2', [req.params.id, req.userId]); + await pool.query('DELETE FROM signature_profiles WHERE user_id = $1', [req.userId]); res.status(204).end(); } catch (error) { next(error); diff --git a/app/src/db.js b/app/src/db.js index 4789f26..6042db5 100644 --- a/app/src/db.js +++ b/app/src/db.js @@ -90,10 +90,21 @@ const initializeDatabase = () => { UNIQUE (user_id, name) ); + CREATE TABLE IF NOT EXISTS signature_profiles ( + user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + data_url TEXT NOT NULL + ); + ALTER TABLE supplier_profiles ADD COLUMN IF NOT EXISTS nazev_spolecnosti TEXT NOT NULL DEFAULT ''; ALTER TABLE supplier_profiles ADD COLUMN IF NOT EXISTS typ_subjektu TEXT NOT NULL DEFAULT 'osoba'; ALTER TABLE clients ADD COLUMN IF NOT EXISTS nazev_spolecnosti TEXT NOT NULL DEFAULT ''; ALTER TABLE clients ADD COLUMN IF NOT EXISTS typ_subjektu TEXT NOT NULL DEFAULT 'osoba'; + + INSERT INTO signature_profiles (user_id, data_url) + SELECT DISTINCT ON (user_id) user_id, data_url + FROM signature_presets + ORDER BY user_id, created_at DESC + ON CONFLICT (user_id) DO NOTHING; `).catch((error) => { schemaPromise = undefined; throw error; diff --git a/app/src/static/index.html b/app/src/static/index.html index a0f47e1..7a9503b 100644 --- a/app/src/static/index.html +++ b/app/src/static/index.html @@ -338,26 +338,19 @@
-
- - -

Přetáhněte sem obrázek podpisu (420×210 px, JPG nebo PNG) nebo klikněte pro výběr souboru

-
- - - -
-

+
+ + + +
diff --git a/app/src/static/script.js b/app/src/static/script.js index fc75601..7185abf 100644 --- a/app/src/static/script.js +++ b/app/src/static/script.js @@ -57,10 +57,8 @@ const signaturePreview = document.getElementById("signaturePreview"); const signatureDropzoneText = document.getElementById("signatureDropzoneText"); const removeSignatureButton = document.getElementById("removeSignatureButton"); const signatureStatus = document.getElementById("signatureStatus"); -const signaturePresetSelect = document.getElementById("signaturePresetSelect"); -const signaturePresetName = document.getElementById("signaturePresetName"); -const saveSignaturePresetButton = document.getElementById("saveSignaturePresetButton"); -const deleteSignaturePresetButton = document.getElementById("deleteSignaturePresetButton"); +const saveSignatureButton = document.getElementById("saveSignatureButton"); +const loadSignatureButton = document.getElementById("loadSignatureButton"); const loadPaymentPresetButton = document.getElementById("loadPaymentPresetButton"); const deletePaymentPresetButton = document.getElementById("deletePaymentPresetButton"); @@ -82,7 +80,6 @@ let itemIndex = 0; let clients = []; let invoices = []; let paymentPresets = []; -let signaturePresets = []; let currentInvoiceId = null; let isRegistrationMode = false; let signatureDataUrl = null; @@ -110,22 +107,31 @@ const applySignaturePreview = (dataUrl) => { } }; -const renderSignaturePresetOptions = (selectedId = signaturePresetSelect.value) => { - signaturePresetSelect.innerHTML = ""; - signaturePresets.forEach((preset) => { - const option = document.createElement("option"); - option.value = preset.id; - option.textContent = preset.name; - signaturePresetSelect.appendChild(option); - }); - signaturePresetSelect.value = selectedId; - deleteSignaturePresetButton.disabled = !signaturePresetSelect.value; +const saveSignature = async () => { + if (!signatureDataUrl) { + setSignatureStatus("Nejprve nahrajte podpis.", true); + return; + } + try { + await apiRequest("/api/signature", { + method: "PUT", + body: JSON.stringify({ dataUrl: signatureDataUrl }) + }); + setSignatureStatus("Podpis byl uložen."); + } catch (error) { + setSignatureStatus(error.message, true); + } }; -const loadSignaturePresets = async () => { - const response = await apiRequest("/api/signature-presets"); - signaturePresets = response.presets; - renderSignaturePresetOptions(); +const loadSignature = async () => { + const { signature } = await apiRequest("/api/signature"); + if (signature) { + applySignaturePreview(signature); + setSignatureStatus("Uloženo na serveru"); + return true; + } + setSignatureStatus("Zatím není uložený podpis"); + return false; }; const readImageDimensions = (dataUrl) => new Promise((resolve, reject) => { @@ -185,49 +191,11 @@ signatureInput.addEventListener("change", () => { handleSignatureFile(signatureInput.files[0]); signatureInput.value = ""; }); -signaturePresetSelect.addEventListener("change", () => { - const preset = signaturePresets.find(({ id }) => String(id) === signaturePresetSelect.value); - deleteSignaturePresetButton.disabled = !preset; - if (preset) { - signaturePresetName.value = preset.name; - applySignaturePreview(preset.dataUrl); - setSignatureStatus("Uložený podpis načten."); - } -}); -saveSignaturePresetButton.addEventListener("click", async () => { - if (!signatureDataUrl) { - setSignatureStatus("Nejprve nahrajte nebo vyberte podpis.", true); - return; - } +saveSignatureButton.addEventListener("click", saveSignature); +loadSignatureButton.addEventListener("click", async () => { try { - const response = await apiRequest("/api/signature-presets", { - method: "POST", - body: JSON.stringify({ - name: signaturePresetName.value, - dataUrl: signatureDataUrl - }) - }); - const preset = response.preset; - signaturePresets = [ - ...signaturePresets.filter((entry) => entry.id !== preset.id && entry.name !== preset.name), - preset - ].sort((left, right) => left.name.localeCompare(right.name)); - renderSignaturePresetOptions(String(preset.id)); - signaturePresetName.value = preset.name; - setSignatureStatus("Podpis byl uložen."); - } catch (error) { - setSignatureStatus(error.message, true); - } -}); -deleteSignaturePresetButton.addEventListener("click", async () => { - if (!signaturePresetSelect.value) { - return; - } - try { - await apiRequest(`/api/signature-presets/${signaturePresetSelect.value}`, { method: "DELETE" }); - signaturePresets = signaturePresets.filter(({ id }) => String(id) !== signaturePresetSelect.value); - signaturePresetName.value = ""; - renderSignaturePresetOptions(); + await loadSignature(); + setSignatureStatus("Podpis načten."); } catch (error) { setSignatureStatus(error.message, true); } @@ -448,9 +416,6 @@ const clearInvoiceEditor = () => { paymentPresetSelect.value = ""; paymentPresetName.value = ""; deletePaymentPresetButton.disabled = true; - signaturePresetSelect.value = ""; - signaturePresetName.value = ""; - deleteSignaturePresetButton.disabled = true; ["cisloUctu", "iban", "swift"].forEach((id) => { document.getElementById(id).value = ""; }); @@ -480,9 +445,6 @@ const loadInvoiceIntoEditor = (invoice) => { document.getElementById(id).value = invoice.data.payment?.[id] || ""; }); applySignaturePreview(invoice.data.signature || null); - signaturePresetSelect.value = ""; - signaturePresetName.value = ""; - deleteSignaturePresetButton.disabled = true; setSignatureStatus(""); itemsContainer.replaceChildren(); itemIndex = 0; @@ -521,8 +483,9 @@ const loadInvoices = async () => { renderInvoiceList(); }; -const openNewInvoice = () => { +const openNewInvoice = async () => { clearInvoiceEditor(); + await loadSignature(); showInvoiceScreen("editor"); }; @@ -709,7 +672,7 @@ deleteClientButton.addEventListener("click", async () => { const showInvoiceApp = async () => { authScreen.hidden = true; document.getElementById("main-container").hidden = false; - await Promise.all([loadSupplier(), loadClients(), loadInvoices(), loadPaymentPresets(), loadSignaturePresets(), loadNumberingSettings()]); + await Promise.all([loadSupplier(), loadClients(), loadInvoices(), loadPaymentPresets(), loadSignature(), loadNumberingSettings()]); showInvoiceScreen("list"); }; diff --git a/app/src/static/style.css b/app/src/static/style.css index 256aa51..9344a13 100644 --- a/app/src/static/style.css +++ b/app/src/static/style.css @@ -682,56 +682,6 @@ body { margin-top: 0.5rem; } -.signature-save-row { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - gap: 0.5rem; - margin-top: 0.75rem; -} - -.signature-save-row input { - min-width: 0; - padding: 0.55rem 0.7rem; - color: var(--ink); - background: #fff; - border: 1px solid #c8d0d5; - border-radius: 0.25rem; - font: inherit; -} - -.signature-save-row button { - padding: 0.55rem 0.7rem; - color: var(--navy); - background: transparent; - border: 1px solid var(--blue); - border-radius: 0.25rem; - cursor: pointer; - font: inherit; - font-size: 0.85rem; - font-weight: 700; - white-space: nowrap; -} - -.signature-save-row button:hover:not(:disabled) { - color: #fff; - background: var(--blue); -} - -.signature-save-row .secondary-button { - color: #8c3c36; - border-color: #c47a72; -} - -.signature-save-row .secondary-button:hover:not(:disabled) { - color: #fff; - background: #a53c36; -} - -.signature-save-row button:disabled { - cursor: not-allowed; - opacity: 0.45; -} - .signature-status { margin: 0.4rem 0 0; min-height: 1.1rem; @@ -1013,10 +963,6 @@ select:focus { grid-template-columns: 1fr; } - .signature-save-row { - grid-template-columns: 1fr; - } - .delete-item-button { justify-self: end; } -- 2.54.0