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

@@ -1,7 +1,5 @@
import prisma from "../config/database";
// Re-export for convenience
// Status transition rules matching PHP
const VALID_TRANSITIONS: Record<string, string[]> = {
issued: ["paid"],
@@ -36,6 +34,8 @@ interface ListInvoicesParams {
search: string;
status?: string;
customer_id?: number;
month?: number;
year?: number;
}
function computeInvoiceTotals(
@@ -75,13 +75,28 @@ export async function markOverdueInvoices() {
}
export async function listInvoices(params: ListInvoicesParams) {
const { page, limit, skip, sort, order, search, status, customer_id } =
params;
const {
page,
limit,
skip,
sort,
order,
search,
status,
customer_id,
month,
year,
} = params;
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";
const where: Record<string, unknown> = {};
if (status) where.status = status;
if (customer_id) where.customer_id = customer_id;
if (month && year) {
const from = new Date(year, month - 1, 1);
const to = new Date(year, month, 1);
where.issue_date = { gte: from, lt: to };
}
if (search) {
where.OR = [
{ invoice_number: { contains: search } },
@@ -159,7 +174,6 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
include: { invoice_items: true },
});
// Helper: compute invoice total WITH VAT (matching PHP)
const invoiceTotalWithVat = (inv: (typeof allInvoices)[0]) => {
const sub = inv.invoice_items.reduce(
(s, i) => s + (Number(i.quantity) || 0) * (Number(i.unit_price) || 0),
@@ -177,7 +191,6 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
return sub + vat;
};
// Helper: aggregate by currency
const aggregateByCurrency = (invoices: typeof allInvoices) => {
const map: Record<string, number> = {};
for (const inv of invoices) {
@@ -209,7 +222,6 @@ export async function getInvoiceStats(queryMonth?: number, queryYear?: number) {
const awaitingInvoices = allInvoices.filter((i) => i.status === "issued");
const overdueInvoices = allInvoices.filter((i) => i.status === "overdue");
// VAT by currency
const vatMap: Record<string, number> = {};
for (const inv of monthInvoices) {
if (!inv.apply_vat) continue;
@@ -309,6 +321,7 @@ export async function createInvoice(body: Record<string, any>) {
tax_date: body.tax_date ? new Date(String(body.tax_date)) : null,
issued_by: body.issued_by ? String(body.issued_by) : null,
billing_text: body.billing_text ? String(body.billing_text) : null,
language: body.language ? String(body.language) : "cs",
notes: body.notes ? String(body.notes) : null,
internal_notes: body.internal_notes ? String(body.internal_notes) : null,
},
@@ -361,6 +374,7 @@ export async function updateInvoice(id: number, body: Record<string, any>) {
"bank_account",
"issued_by",
"billing_text",
"language",
];
for (const f of strFields) {
if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;