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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user