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:
BOHA
2026-03-26 10:36:39 +01:00
parent 0317ba3168
commit baceb88347
60 changed files with 2475 additions and 563 deletions

View File

@@ -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);
},
);