feat: NAS storage for invoices/offers, code cleanup, date/time fixes
- NAS storage for created invoices (PDF via puppeteer), received invoices, and offers with auto-save on create/edit - Deterministic file paths derived from DB fields (no file_path column needed) - Separate NAS mount points: NAS_FINANCIALS_PATH, NAS_OFFERS_PATH - Invoice language field (cs/en) stored per invoice, replaces lang modal - Invoices list filtered by month/year matching KPI card selection - Centralized date helpers (src/utils/date.ts) replacing all .toISOString() calls that returned UTC instead of local time - Attendance project switching uses exact time (not rounded) - Comment cleanup: removed ~100 unnecessary/Czech comments - Removed as-any casts in orders and attendance - Prisma migrations: add invoice language, drop received_invoices BLOB columns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ import { FastifyInstance } from "fastify";
|
||||
import QRCode from "qrcode";
|
||||
import prisma from "../../config/database";
|
||||
import { requirePermission } from "../../middleware/auth";
|
||||
import { localDateCzStr } from "../../utils/date";
|
||||
import { nasFinancialsManager } from "../../services/nas-financials-manager";
|
||||
import { htmlToPdf } from "../../utils/html-to-pdf";
|
||||
|
||||
/* ── Helpers ─────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -9,7 +12,7 @@ function formatDate(date: Date | string | null | undefined): string {
|
||||
if (!date) return "";
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return String(date);
|
||||
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.${d.getFullYear()}`;
|
||||
return localDateCzStr(d);
|
||||
}
|
||||
|
||||
function formatNum(n: number, decimals = 2): string {
|
||||
@@ -278,7 +281,6 @@ export default async function invoicesPdfRoutes(
|
||||
unknown
|
||||
> | null;
|
||||
|
||||
// Order number lookup
|
||||
let orderNumber = "";
|
||||
if (invoice.order_id) {
|
||||
const orderRow = await prisma.orders.findUnique({
|
||||
@@ -298,7 +300,6 @@ export default async function invoicesPdfRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
// Logo
|
||||
let logoImg = "";
|
||||
if (settings?.logo_data) {
|
||||
const buf = Buffer.from(settings.logo_data as Buffer);
|
||||
@@ -313,7 +314,6 @@ export default async function invoicesPdfRoutes(
|
||||
const currency = invoice.currency || "CZK";
|
||||
const applyVat = !!invoice.apply_vat;
|
||||
|
||||
// Calculations
|
||||
const vatSummary: Record<string, { base: number; vat: number }> = {};
|
||||
let subtotal = 0;
|
||||
|
||||
@@ -380,7 +380,6 @@ export default async function invoicesPdfRoutes(
|
||||
});
|
||||
}
|
||||
|
||||
// Address lines
|
||||
const supp = buildAddressLines(settings, true, t);
|
||||
const cust = buildAddressLines(customer, false, t);
|
||||
|
||||
@@ -410,7 +409,6 @@ export default async function invoicesPdfRoutes(
|
||||
|
||||
const invoiceNumber = escapeHtml(invoice.invoice_number);
|
||||
|
||||
// Items HTML
|
||||
const itemsHtml = items
|
||||
.map((item, i) => {
|
||||
const qty = Number(item.quantity);
|
||||
@@ -434,7 +432,6 @@ export default async function invoicesPdfRoutes(
|
||||
})
|
||||
.join("");
|
||||
|
||||
// VAT recap rows
|
||||
const vatRecapHtml = vatRecap
|
||||
.map(
|
||||
(vr) => `<tr>
|
||||
@@ -446,7 +443,6 @@ export default async function invoicesPdfRoutes(
|
||||
)
|
||||
.join("");
|
||||
|
||||
// VAT detail rows for totals section
|
||||
let vatDetailHtml = "";
|
||||
if (applyVat) {
|
||||
for (const [rate, data] of Object.entries(vatSummary)) {
|
||||
@@ -460,7 +456,6 @@ export default async function invoicesPdfRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
// Notes section
|
||||
const notesRaw = invoice.notes ?? "";
|
||||
const notesStripped = notesRaw.replace(/<[^>]*>/g, "").trim();
|
||||
const notesHtml = notesStripped
|
||||
@@ -1027,6 +1022,31 @@ ${indentCSS}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Save PDF to NAS
|
||||
if (nasFinancialsManager.isConfigured() && invoice.invoice_number) {
|
||||
const issueDate = invoice.issue_date
|
||||
? new Date(invoice.issue_date)
|
||||
: new Date();
|
||||
const saveMode = query.save === "1";
|
||||
const pdfPromise = htmlToPdf(html)
|
||||
.then((pdfBuffer) => {
|
||||
nasFinancialsManager.saveIssuedInvoicePdf(
|
||||
invoice.invoice_number!,
|
||||
issueDate.getFullYear(),
|
||||
issueDate.getMonth() + 1,
|
||||
pdfBuffer,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
request.log.error(err, "Failed to save invoice PDF to NAS");
|
||||
});
|
||||
|
||||
if (saveMode) {
|
||||
await pdfPromise;
|
||||
return reply.send({ success: true, message: "PDF uloženo" });
|
||||
}
|
||||
}
|
||||
|
||||
return reply.type("text/html").send(html);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user