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

@@ -11,6 +11,7 @@ import {
CreateReceivedInvoiceSchema,
UpdateReceivedInvoiceSchema,
} from "../../schemas/received-invoices.schema";
import { nasFinancialsManager } from "../../services/nas-financials-manager";
const VALID_STATUSES = ["unpaid", "paid"] as const;
const ALLOWED_SORT_FIELDS = [
@@ -160,16 +161,31 @@ export default async function receivedInvoicesRoutes(
if (id === null) return;
const invoice = await prisma.received_invoices.findUnique({
where: { id },
select: { file_data: true, file_name: true, file_mime: true },
select: {
file_name: true,
file_mime: true,
year: true,
month: true,
},
});
if (!invoice?.file_data) return error(reply, "Soubor nenalezen", 404);
if (!invoice?.file_name) return error(reply, "Soubor nenalezen", 404);
const relPath = nasFinancialsManager.buildReceivedPath(
invoice.file_name,
invoice.year,
invoice.month,
);
const nasFile = nasFinancialsManager.readReceivedInvoice(relPath);
if (!nasFile) return error(reply, "Soubor na NAS nenalezen", 404);
const mime = invoice.file_mime || "application/pdf";
const filename = invoice.file_name || `received-invoice-${id}.pdf`;
return reply
.type(mime)
.header("Content-Disposition", `inline; filename="${filename}"`)
.send(Buffer.from(invoice.file_data));
.header(
"Content-Disposition",
`inline; filename="${invoice.file_name}"`,
)
.send(nasFile.data);
},
);
@@ -183,9 +199,10 @@ export default async function receivedInvoicesRoutes(
where: { id },
});
if (!invoice) return error(reply, "Přijatá faktura nenalezena", 404);
// Don't send file_data in detail response (can be large)
const { file_data: _fileData, ...rest } = invoice;
return success(reply, rest);
return success(reply, {
...invoice,
has_file: !!invoice.file_name,
});
},
);
@@ -230,6 +247,8 @@ export default async function receivedInvoicesRoutes(
const now = new Date();
const createdIds: number[] = [];
const useNas = nasFinancialsManager.isConfigured();
for (let i = 0; i < files.length; i++) {
const file = files[i];
const meta = invoicesMeta[i] || {};
@@ -241,10 +260,34 @@ export default async function receivedInvoicesRoutes(
? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100
: 0;
const issueDate = meta.issue_date
? new Date(String(meta.issue_date))
: null;
const invoiceMonth = issueDate
? issueDate.getMonth() + 1
: Number(meta.month) || now.getMonth() + 1;
const invoiceYear = issueDate
? issueDate.getFullYear()
: Number(meta.year) || now.getFullYear();
if (!useNas) {
return error(reply, "NAS úložiště není nakonfigurováno", 503);
}
const nasResult = nasFinancialsManager.saveReceivedInvoice(
file.name,
invoiceYear,
invoiceMonth,
file.data,
);
if ("error" in nasResult) {
return error(reply, nasResult.error, 503);
}
const invoice = await prisma.received_invoices.create({
data: {
month: Number(meta.month) || now.getMonth() + 1,
year: Number(meta.year) || now.getFullYear(),
month: invoiceMonth,
year: invoiceYear,
supplier_name: meta.supplier_name
? String(meta.supplier_name)
: file.name,
@@ -263,7 +306,6 @@ export default async function receivedInvoicesRoutes(
status: "unpaid",
notes: meta.notes ? String(meta.notes) : null,
uploaded_by: request.authData?.userId,
file_data: Uint8Array.from(file.data),
file_name: file.name,
file_mime: file.mime,
file_size: file.size,
@@ -488,6 +530,15 @@ export default async function receivedInvoicesRoutes(
});
if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);
if (existing.file_name) {
const relPath = nasFinancialsManager.buildReceivedPath(
existing.file_name,
existing.year,
existing.month,
);
nasFinancialsManager.deleteReceivedInvoice(relPath);
}
await prisma.received_invoices.delete({ where: { id } });
await logAudit({
request,