Compare commits

7 Commits

Author SHA1 Message Date
BOHA
cde560a2c3 1.3.3 2026-03-27 10:47:46 +01:00
BOHA
e6198e1b67 fix: file viewers blocked on mobile — open blank window before async fetch
Mobile browsers block window.open() after async operations. Changed all
file viewers to open a blank window synchronously in the click handler,
then set location.href after fetch completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:47:45 +01:00
BOHA
495fdf6da2 1.3.2 2026-03-27 10:42:29 +01:00
BOHA
7d29f40ab2 fix: offers table PDF button opens blob from NAS instead of print page
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:42:29 +01:00
BOHA
6b9f1dee87 1.3.1 2026-03-27 10:33:15 +01:00
BOHA
687dcb9371 fix: OfferDetail uses default currency from system settings
The useEffect checked prev.currency === "EUR" but initial default was
changed to "CZK", so the settings default was never applied.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:33:15 +01:00
BOHA
9c49015968 1.3.0 2026-03-27 10:25:40 +01:00
7 changed files with 50 additions and 32 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "1.2.0", "version": "1.3.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "app-ts", "name": "app-ts",
"version": "1.2.0", "version": "1.3.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "app-ts", "name": "app-ts",
"version": "1.2.0", "version": "1.3.3",
"description": "", "description": "",
"main": "dist/server.js", "main": "dist/server.js",
"scripts": { "scripts": {

View File

@@ -975,18 +975,21 @@ export default function InvoiceDetail() {
// ─── Edit mode: PDF export ─── // ─── Edit mode: PDF export ───
const handleViewPdf = async (_lang = "cs") => { const handleViewPdf = async (_lang = "cs") => {
const newWindow = window.open("", "_blank");
setPdfLoading(true); setPdfLoading(true);
try { try {
const response = await apiFetch(`${API_BASE}/invoices/${id}/file`); const response = await apiFetch(`${API_BASE}/invoices/${id}/file`);
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("PDF soubor nenalezen — uložte fakturu pro vygenerování"); alert.error("PDF soubor nenalezen — uložte fakturu pro vygenerování");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba připojení"); alert.error("Chyba připojení");
} finally { } finally {
setPdfLoading(false); setPdfLoading(false);

View File

@@ -356,7 +356,7 @@ export default function OfferDetail() {
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
currency: currency:
prev.currency === "EUR" prev.currency === "CZK"
? companySettings.default_currency || "CZK" ? companySettings.default_currency || "CZK"
: prev.currency, : prev.currency,
vat_rate: vat_rate:
@@ -768,19 +768,25 @@ export default function OfferDetail() {
const handlePdf = async () => { const handlePdf = async () => {
if (!isEdit || pdfLoading) return; if (!isEdit || pdfLoading) return;
const newWindow = window.open("", "_blank");
setPdfLoading(true); setPdfLoading(true);
try { try {
const response = await apiFetch(`${API_BASE}/offers/${id}/file`); const response = await apiFetch(`${API_BASE}/offers/${id}/file`);
if (response.status === 401) return; if (response.status === 401) {
newWindow?.close();
return;
}
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování"); alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba při generování PDF"); alert.error("Chyba při generování PDF");
} finally { } finally {
setPdfLoading(false); setPdfLoading(false);

View File

@@ -221,26 +221,28 @@ export default function Offers() {
const handlePdf = async (quotation: Quotation) => { const handlePdf = async (quotation: Quotation) => {
if (pdfLoading) return; if (pdfLoading) return;
const newWindow = window.open("", "_blank");
setPdfLoading(quotation.id); setPdfLoading(quotation.id);
try { try {
const response = await apiFetch(`${API_BASE}/offers-pdf/${quotation.id}`); const response = await apiFetch(
if (response.status === 401) return; `${API_BASE}/offers/${quotation.id}/file`,
if (!response.ok) { );
alert.error("Nepodařilo se vygenerovat PDF"); if (response.status === 401) {
newWindow?.close();
return; return;
} }
const html = await response.text(); if (!response.ok) {
const w = window.open("", "_blank"); newWindow?.close();
if (w) { alert.error("PDF soubor nenalezen — otevřete nabídku a uložte ji");
w.document.open(); return;
w.document.write(html);
w.document.close();
w.onload = () => w.print();
} else {
alert.error("Prohlížeč zablokoval vyskakovací okno");
} }
const blob = await response.blob();
const url = URL.createObjectURL(blob);
if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
alert.error("Chyba při generování PDF"); newWindow?.close();
alert.error("Chyba připojení");
} finally { } finally {
setPdfLoading(null); setPdfLoading(null);
} }
@@ -753,7 +755,7 @@ export default function Offers() {
<button <button
onClick={() => handlePdf(q)} onClick={() => handlePdf(q)}
className="admin-btn-icon" className="admin-btn-icon"
title="PDF" title="Zobrazit nabídku"
disabled={pdfLoading === q.id} disabled={pdfLoading === q.id}
> >
{pdfLoading === q.id ? ( {pdfLoading === q.id ? (

View File

@@ -503,19 +503,22 @@ export default function ReceivedInvoices({
}; };
const openFile = async (inv: ReceivedInvoice) => { const openFile = async (inv: ReceivedInvoice) => {
const newWindow = window.open("", "_blank");
try { try {
const response = await apiFetch( const response = await apiFetch(
`${API_BASE}/received-invoices/${inv.id}/file`, `${API_BASE}/received-invoices/${inv.id}/file`,
); );
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("Nepodařilo se načíst soubor"); alert.error("Nepodařilo se načíst soubor");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba připojení"); alert.error("Chyba připojení");
} }
}; };

View File

@@ -68,7 +68,7 @@ export default async function companySettingsRoutes(
const settings = await prisma.company_settings.findFirst({ const settings = await prisma.company_settings.findFirst({
select: { [column]: true }, select: { [column]: true },
}); });
const buf = settings?.[column] as Buffer | null; const buf = settings?.[column] as unknown as Buffer | null;
if (!buf) return error(reply, "Logo nenalezeno", 404); if (!buf) return error(reply, "Logo nenalezeno", 404);
let mime = "image/png"; let mime = "image/png";
@@ -203,9 +203,15 @@ export default async function companySettingsRoutes(
max_requests_per_minute: true, max_requests_per_minute: true,
available_vat_rates: true, available_vat_rates: true,
available_currencies: true, available_currencies: true,
smtp_from: true,
smtp_from_name: true,
offer_number_pattern: true,
order_number_pattern: true,
invoice_number_pattern: true,
}, },
}); });
} }
if (!settings) return error(reply, "Nastavení nenalezeno", 500);
// Check if logo exists // Check if logo exists
const logoCheck = await prisma.company_settings.findFirst({ const logoCheck = await prisma.company_settings.findFirst({
@@ -219,9 +225,8 @@ export default async function companySettingsRoutes(
settings.custom_fields as string | null, settings.custom_fields as string | null,
); );
const pkg = await import("../../../package.json", { // eslint-disable-next-line @typescript-eslint/no-var-requires
assert: { type: "json" }, const pkg = require("../../../package.json") as { version: string };
});
let available_vat_rates: number[] = [0, 10, 12, 15, 21]; let available_vat_rates: number[] = [0, 10, 12, 15, 21];
try { try {
@@ -255,7 +260,7 @@ export default async function companySettingsRoutes(
available_currencies, available_currencies,
has_logo, has_logo,
has_logo_dark, has_logo_dark,
app_version: pkg.default.version, app_version: pkg.version,
}); });
}); });
@@ -264,9 +269,8 @@ export default async function companySettingsRoutes(
"/system-info", "/system-info",
{ preHandler: requirePermission("settings.manage") }, { preHandler: requirePermission("settings.manage") },
async (request, reply) => { async (request, reply) => {
const pkg = await import("../../../package.json", { // eslint-disable-next-line @typescript-eslint/no-var-requires
assert: { type: "json" }, const pkg = require("../../../package.json") as { version: string };
});
const uptimeSec = process.uptime(); const uptimeSec = process.uptime();
const days = Math.floor(uptimeSec / 86400); const days = Math.floor(uptimeSec / 86400);
const hours = Math.floor((uptimeSec % 86400) / 3600); const hours = Math.floor((uptimeSec % 86400) / 3600);
@@ -299,7 +303,7 @@ export default async function companySettingsRoutes(
const projectNas = new NasFileManager(); const projectNas = new NasFileManager();
return success(reply, { return success(reply, {
app_version: pkg.default.version, app_version: pkg.version,
node_version: process.version, node_version: process.version,
platform: `${os.type()} ${os.release()}`, platform: `${os.type()} ${os.release()}`,
uptime: uptimeStr, uptime: uptimeStr,