style: run prettier on entire codebase

This commit is contained in:
BOHA
2026-03-24 19:59:14 +01:00
parent 872be42107
commit 3c167cf5c4
148 changed files with 26740 additions and 13990 deletions

View File

@@ -1,300 +1,503 @@
import { FastifyInstance } from 'fastify';
import multipart from '@fastify/multipart';
import { received_invoices_status } from '@prisma/client';
import prisma from '../../config/database';
import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateReceivedInvoiceSchema, UpdateReceivedInvoiceSchema } from '../../schemas/received-invoices.schema';
import { FastifyInstance } from "fastify";
import multipart from "@fastify/multipart";
import { received_invoices_status } from "@prisma/client";
import prisma from "../../config/database";
import { requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error, parseId } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
import {
CreateReceivedInvoiceSchema,
UpdateReceivedInvoiceSchema,
} from "../../schemas/received-invoices.schema";
const VALID_STATUSES = ['unpaid', 'paid'] as const;
const ALLOWED_SORT_FIELDS = ['id', 'supplier_name', 'amount', 'issue_date', 'due_date', 'status', 'created_at'];
const VALID_STATUSES = ["unpaid", "paid"] as const;
const ALLOWED_SORT_FIELDS = [
"id",
"supplier_name",
"amount",
"issue_date",
"due_date",
"status",
"created_at",
];
export default async function receivedInvoicesRoutes(fastify: FastifyInstance): Promise<void> {
export default async function receivedInvoicesRoutes(
fastify: FastifyInstance,
): Promise<void> {
await fastify.register(multipart, { limits: { fileSize: 50 * 1024 * 1024 } });
fastify.get('/', { preHandler: requirePermission('invoices.view') }, async (request, reply) => {
const query = request.query as Record<string, unknown>;
const { page, limit, skip, order } = parsePagination(query);
fastify.get(
"/",
{ preHandler: requirePermission("invoices.view") },
async (request, reply) => {
const query = request.query as Record<string, unknown>;
const { page, limit, skip, order } = parsePagination(query);
const where: Record<string, unknown> = {};
if (query.year) where.year = Number(query.year);
if (query.month) where.month = Number(query.month);
if (query.status) where.status = String(query.status);
if (query.supplier_name) where.supplier_name = { contains: String(query.supplier_name) };
const where: Record<string, unknown> = {};
if (query.year) where.year = Number(query.year);
if (query.month) where.month = Number(query.month);
if (query.status) where.status = String(query.status);
if (query.supplier_name)
where.supplier_name = { contains: String(query.supplier_name) };
// Search across supplier_name, invoice_number, description
if (query.search) {
const search = String(query.search);
where.OR = [
{ supplier_name: { contains: search } },
{ invoice_number: { contains: search } },
{ description: { contains: search } },
];
}
// Search across supplier_name, invoice_number, description
if (query.search) {
const search = String(query.search);
where.OR = [
{ supplier_name: { contains: search } },
{ invoice_number: { contains: search } },
{ description: { contains: search } },
];
}
// Sort field whitelisting
const sortField = query.sort && ALLOWED_SORT_FIELDS.includes(String(query.sort)) ? String(query.sort) : 'id';
// Sort field whitelisting
const sortField =
query.sort && ALLOWED_SORT_FIELDS.includes(String(query.sort))
? String(query.sort)
: "id";
const [invoices, total] = await Promise.all([
prisma.received_invoices.findMany({ where, skip, take: limit, orderBy: { [sortField]: order } }),
prisma.received_invoices.count({ where }),
]);
const [invoices, total] = await Promise.all([
prisma.received_invoices.findMany({
where,
skip,
take: limit,
orderBy: { [sortField]: order },
}),
prisma.received_invoices.count({ where }),
]);
return reply.send({ success: true, data: invoices, pagination: buildPaginationMeta(total, page, limit) });
});
return reply.send({
success: true,
data: invoices,
pagination: buildPaginationMeta(total, page, limit),
});
},
);
// GET /api/admin/received-invoices/stats
fastify.get('/stats', { preHandler: requirePermission('invoices.view') }, async (request, reply) => {
const query = request.query as Record<string, unknown>;
const now = new Date();
const year = Number(query.year) || now.getFullYear();
const month = Number(query.month) || (now.getMonth() + 1);
fastify.get(
"/stats",
{ preHandler: requirePermission("invoices.view") },
async (request, reply) => {
const query = request.query as Record<string, unknown>;
const now = new Date();
const year = Number(query.year) || now.getFullYear();
const month = Number(query.month) || now.getMonth() + 1;
const where: Record<string, unknown> = { year, month };
const monthInvoices = await prisma.received_invoices.findMany({ where });
const where: Record<string, unknown> = { year, month };
const monthInvoices = await prisma.received_invoices.findMany({ where });
// Aggregate by currency → CurrencyAmount[] format
const aggregateByCurrency = (invs: typeof monthInvoices, field: 'amount' | 'vat_amount') => {
const map: Record<string, number> = {};
for (const inv of invs) {
const cur = inv.currency || 'CZK';
map[cur] = (map[cur] || 0) + (Number(inv[field]) || 0);
}
return Object.entries(map).filter(([, v]) => v > 0).map(([currency, amount]) => ({ amount: Math.round(amount * 100) / 100, currency }));
};
// Aggregate by currency → CurrencyAmount[] format
const aggregateByCurrency = (
invs: typeof monthInvoices,
field: "amount" | "vat_amount",
) => {
const map: Record<string, number> = {};
for (const inv of invs) {
const cur = inv.currency || "CZK";
map[cur] = (map[cur] || 0) + (Number(inv[field]) || 0);
}
return Object.entries(map)
.filter(([, v]) => v > 0)
.map(([currency, amount]) => ({
amount: Math.round(amount * 100) / 100,
currency,
}));
};
const sumCzk = (invs: typeof monthInvoices, field: 'amount' | 'vat_amount') => {
let total = 0;
for (const inv of invs) total += Number(inv[field]) || 0;
return Math.round(total * 100) / 100;
};
const sumCzk = (
invs: typeof monthInvoices,
field: "amount" | "vat_amount",
) => {
let total = 0;
for (const inv of invs) total += Number(inv[field]) || 0;
return Math.round(total * 100) / 100;
};
// Also get all-time unpaid
const allUnpaid = await prisma.received_invoices.findMany({ where: { status: { not: 'paid' } } });
// Also get all-time unpaid
const allUnpaid = await prisma.received_invoices.findMany({
where: { status: { not: "paid" } },
});
return success(reply, {
total_month: aggregateByCurrency(monthInvoices, 'amount'),
total_month_czk: sumCzk(monthInvoices, 'amount'),
vat_month: aggregateByCurrency(monthInvoices, 'vat_amount'),
vat_month_czk: sumCzk(monthInvoices, 'vat_amount'),
unpaid: aggregateByCurrency(allUnpaid, 'amount'),
unpaid_czk: sumCzk(allUnpaid, 'amount'),
unpaid_count: allUnpaid.length,
month_count: monthInvoices.length,
});
});
return success(reply, {
total_month: aggregateByCurrency(monthInvoices, "amount"),
total_month_czk: sumCzk(monthInvoices, "amount"),
vat_month: aggregateByCurrency(monthInvoices, "vat_amount"),
vat_month_czk: sumCzk(monthInvoices, "vat_amount"),
unpaid: aggregateByCurrency(allUnpaid, "amount"),
unpaid_czk: sumCzk(allUnpaid, "amount"),
unpaid_count: allUnpaid.length,
month_count: monthInvoices.length,
});
},
);
// GET /api/admin/received-invoices/suppliers — distinct supplier names for autocomplete
fastify.get('/suppliers', { preHandler: requirePermission('invoices.view') }, async (_request, reply) => {
const results = await prisma.received_invoices.findMany({
select: { supplier_name: true },
distinct: ['supplier_name'],
orderBy: { supplier_name: 'asc' },
});
return success(reply, results.map(r => r.supplier_name));
});
fastify.get(
"/suppliers",
{ preHandler: requirePermission("invoices.view") },
async (_request, reply) => {
const results = await prisma.received_invoices.findMany({
select: { supplier_name: true },
distinct: ["supplier_name"],
orderBy: { supplier_name: "asc" },
});
return success(
reply,
results.map((r) => r.supplier_name),
);
},
);
// GET /api/admin/received-invoices/:id/file
fastify.get<{ Params: { id: string } }>('/:id/file', { preHandler: requirePermission('invoices.view') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const invoice = await prisma.received_invoices.findUnique({
where: { id },
select: { file_data: true, file_name: true, file_mime: true },
});
if (!invoice?.file_data) return error(reply, 'Soubor nenalezen', 404);
fastify.get<{ Params: { id: string } }>(
"/:id/file",
{ preHandler: requirePermission("invoices.view") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const invoice = await prisma.received_invoices.findUnique({
where: { id },
select: { file_data: true, file_name: true, file_mime: true },
});
if (!invoice?.file_data) return error(reply, "Soubor 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));
});
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));
},
);
fastify.get<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('invoices.view') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const invoice = await prisma.received_invoices.findUnique({ 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);
});
fastify.get<{ Params: { id: string } }>(
"/:id",
{ preHandler: requirePermission("invoices.view") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const invoice = await prisma.received_invoices.findUnique({
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);
},
);
fastify.post('/', { preHandler: requirePermission('invoices.create') }, async (request, reply) => {
const contentType = request.headers['content-type'] || '';
fastify.post(
"/",
{ preHandler: requirePermission("invoices.create") },
async (request, reply) => {
const contentType = request.headers["content-type"] || "";
// Multipart upload: files[] + invoices JSON metadata
if (contentType.includes('multipart/form-data')) {
const parts = request.parts();
const files: Array<{ data: Buffer; name: string; mime: string; size: number }> = [];
let invoicesMeta: Array<Record<string, unknown>> = [];
// Multipart upload: files[] + invoices JSON metadata
if (contentType.includes("multipart/form-data")) {
const parts = request.parts();
const files: Array<{
data: Buffer;
name: string;
mime: string;
size: number;
}> = [];
let invoicesMeta: Array<Record<string, unknown>> = [];
for await (const part of parts) {
if (part.type === 'file') {
const buf = await part.toBuffer();
files.push({ data: buf, name: part.filename || 'file', mime: part.mimetype || 'application/octet-stream', size: buf.length });
} else if (part.fieldname === 'invoices') {
try { invoicesMeta = JSON.parse(part.value as string); } catch { /* ignore parse error */ }
for await (const part of parts) {
if (part.type === "file") {
const buf = await part.toBuffer();
files.push({
data: buf,
name: part.filename || "file",
mime: part.mimetype || "application/octet-stream",
size: buf.length,
});
} else if (part.fieldname === "invoices") {
try {
invoicesMeta = JSON.parse(part.value as string);
} catch {
/* ignore parse error */
}
}
}
if (files.length === 0)
return error(reply, "Vyberte alespoň jeden soubor", 400);
const now = new Date();
const createdIds: number[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
const meta = invoicesMeta[i] || {};
const amount = Number(meta.amount ?? 0);
const vatRate = Number(meta.vat_rate ?? 21);
// Amount includes VAT — extract VAT portion: amount - amount/(1 + rate/100)
const vatAmount =
vatRate > 0
? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100
: 0;
const invoice = await prisma.received_invoices.create({
data: {
month: Number(meta.month) || now.getMonth() + 1,
year: Number(meta.year) || now.getFullYear(),
supplier_name: meta.supplier_name
? String(meta.supplier_name)
: file.name,
invoice_number: meta.invoice_number
? String(meta.invoice_number)
: null,
description: meta.description ? String(meta.description) : null,
amount,
currency: meta.currency ? String(meta.currency) : "CZK",
vat_rate: vatRate,
vat_amount: vatAmount,
issue_date: meta.issue_date
? new Date(String(meta.issue_date))
: null,
due_date: meta.due_date ? new Date(String(meta.due_date)) : null,
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,
},
});
createdIds.push(invoice.id);
}
await logAudit({
request,
authData: request.authData,
action: "create",
entityType: "invoice",
entityId: createdIds[0],
description: `Nahráno ${createdIds.length} přijatých faktur`,
});
return success(
reply,
{ ids: createdIds, count: createdIds.length },
201,
`Nahráno ${createdIds.length} faktur`,
);
}
// JSON body: single invoice creation (no file)
const parsed = parseBody(CreateReceivedInvoiceSchema, request.body);
if ("error" in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const status = body.status;
if (!VALID_STATUSES.includes(status as (typeof VALID_STATUSES)[number])) {
return error(reply, "Neplatný stav", 400);
}
const amount = body.amount;
const vatRate = body.vat_rate;
const invoice = await prisma.received_invoices.create({
data: {
month: Number(body.month),
year: Number(body.year),
supplier_name: String(body.supplier_name),
invoice_number: body.invoice_number
? String(body.invoice_number)
: null,
description: body.description ? String(body.description) : null,
amount,
currency: body.currency ? String(body.currency) : "CZK",
vat_rate: vatRate,
vat_amount:
vatRate > 0
? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100
: 0,
issue_date: body.issue_date
? new Date(String(body.issue_date))
: null,
due_date: body.due_date ? new Date(String(body.due_date)) : null,
status: status as received_invoices_status,
notes: body.notes ? String(body.notes) : null,
uploaded_by: request.authData?.userId,
},
});
await logAudit({
request,
authData: request.authData,
action: "create",
entityType: "invoice",
entityId: invoice.id,
description: `Vytvořena přijatá faktura od ${invoice.supplier_name}`,
});
return success(reply, { id: invoice.id }, 201, "Faktura byla vytvořena");
},
);
fastify.put<{ Params: { id: string } }>(
"/:id",
{ preHandler: requirePermission("invoices.edit") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const parsed = parseBody(UpdateReceivedInvoiceSchema, request.body);
if ("error" in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.received_invoices.findUnique({
where: { id },
});
if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);
if (body.status !== undefined) {
const status = String(body.status);
if (
!VALID_STATUSES.includes(status as (typeof VALID_STATUSES)[number])
) {
return error(reply, "Neplatný stav", 400);
}
// Prevent reverting paid status (matching PHP)
if (String(existing.status) === "paid" && status !== "paid") {
return error(reply, "Nelze vrátit stav uhrazené faktury", 400);
}
}
if (files.length === 0) return error(reply, 'Vyberte alespoň jeden soubor', 400);
// Recalculate vat_amount when amount or vat_rate changes (matching PHP)
const finalAmount =
body.amount !== undefined
? Number(body.amount)
: Number(existing.amount);
const finalVatRate =
body.vat_rate !== undefined
? Number(body.vat_rate)
: Number(existing.vat_rate);
// Amount includes VAT — extract VAT portion: amount - amount/(1 + rate/100)
const computedVat =
finalVatRate > 0
? Math.round(
(finalAmount - finalAmount / (1 + finalVatRate / 100)) * 100,
) / 100
: 0;
const now = new Date();
const createdIds: number[] = [];
// Auto-set paid_date when status transitions to paid (matching PHP)
const newStatus =
body.status !== undefined
? String(body.status)
: String(existing.status);
const paidDate =
newStatus === "paid" && String(existing.status) !== "paid"
? new Date()
: body.paid_date !== undefined
? body.paid_date
? new Date(String(body.paid_date))
: null
: undefined;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const meta = invoicesMeta[i] || {};
const amount = Number(meta.amount ?? 0);
const vatRate = Number(meta.vat_rate ?? 21);
// Amount includes VAT — extract VAT portion: amount - amount/(1 + rate/100)
const vatAmount = vatRate > 0 ? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100 : 0;
const invoice = await prisma.received_invoices.create({
data: {
month: Number(meta.month) || (now.getMonth() + 1),
year: Number(meta.year) || now.getFullYear(),
supplier_name: meta.supplier_name ? String(meta.supplier_name) : file.name,
invoice_number: meta.invoice_number ? String(meta.invoice_number) : null,
description: meta.description ? String(meta.description) : null,
amount,
currency: meta.currency ? String(meta.currency) : 'CZK',
vat_rate: vatRate,
vat_amount: vatAmount,
issue_date: meta.issue_date ? new Date(String(meta.issue_date)) : null,
due_date: meta.due_date ? new Date(String(meta.due_date)) : null,
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,
},
});
createdIds.push(invoice.id);
// Auto-update month/year from issue_date if issue_date changes (matching PHP)
let autoMonth = body.month !== undefined ? Number(body.month) : undefined;
let autoYear = body.year !== undefined ? Number(body.year) : undefined;
if (body.issue_date && !body.month && !body.year) {
const issueDate = new Date(String(body.issue_date));
if (!isNaN(issueDate.getTime())) {
autoMonth = issueDate.getMonth() + 1;
autoYear = issueDate.getFullYear();
}
}
await logAudit({ request, authData: request.authData, action: 'create', entityType: 'invoice', entityId: createdIds[0], description: `Nahráno ${createdIds.length} přijatých faktur` });
return success(reply, { ids: createdIds, count: createdIds.length }, 201, `Nahráno ${createdIds.length} faktur`);
}
await prisma.received_invoices.update({
where: { id },
data: {
supplier_name:
body.supplier_name !== undefined
? String(body.supplier_name)
: undefined,
invoice_number:
body.invoice_number !== undefined
? body.invoice_number
? String(body.invoice_number)
: null
: undefined,
description:
body.description !== undefined
? body.description
? String(body.description)
: null
: undefined,
amount: body.amount !== undefined ? Number(body.amount) : undefined,
currency:
body.currency !== undefined ? String(body.currency) : undefined,
vat_rate:
body.vat_rate !== undefined ? Number(body.vat_rate) : undefined,
vat_amount:
body.amount !== undefined || body.vat_rate !== undefined
? computedVat
: body.vat_amount !== undefined
? Number(body.vat_amount)
: undefined,
issue_date:
body.issue_date !== undefined
? body.issue_date
? new Date(String(body.issue_date))
: null
: undefined,
due_date:
body.due_date !== undefined
? body.due_date
? new Date(String(body.due_date))
: null
: undefined,
paid_date: paidDate,
status:
body.status !== undefined
? (String(body.status) as received_invoices_status)
: undefined,
notes:
body.notes !== undefined
? body.notes
? String(body.notes)
: null
: undefined,
month: autoMonth,
year: autoYear,
modified_at: new Date(),
},
});
await logAudit({
request,
authData: request.authData,
action: "update",
entityType: "invoice",
entityId: id,
description: `Upravena přijatá faktura`,
});
return success(reply, { id }, 200, "Faktura byla uložena");
},
);
// JSON body: single invoice creation (no file)
const parsed = parseBody(CreateReceivedInvoiceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const status = body.status;
if (!VALID_STATUSES.includes(status as typeof VALID_STATUSES[number])) {
return error(reply, 'Neplatný stav', 400);
}
fastify.delete<{ Params: { id: string } }>(
"/:id",
{ preHandler: requirePermission("invoices.delete") },
async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const existing = await prisma.received_invoices.findUnique({
where: { id },
});
if (!existing) return error(reply, "Přijatá faktura nenalezena", 404);
const amount = body.amount;
const vatRate = body.vat_rate;
const invoice = await prisma.received_invoices.create({
data: {
month: Number(body.month),
year: Number(body.year),
supplier_name: String(body.supplier_name),
invoice_number: body.invoice_number ? String(body.invoice_number) : null,
description: body.description ? String(body.description) : null,
amount,
currency: body.currency ? String(body.currency) : 'CZK',
vat_rate: vatRate,
vat_amount: vatRate > 0 ? Math.round((amount - amount / (1 + vatRate / 100)) * 100) / 100 : 0,
issue_date: body.issue_date ? new Date(String(body.issue_date)) : null,
due_date: body.due_date ? new Date(String(body.due_date)) : null,
status: status as received_invoices_status,
notes: body.notes ? String(body.notes) : null,
uploaded_by: request.authData?.userId,
},
});
await logAudit({ request, authData: request.authData, action: 'create', entityType: 'invoice', entityId: invoice.id, description: `Vytvořena přijatá faktura od ${invoice.supplier_name}` });
return success(reply, { id: invoice.id }, 201, 'Faktura byla vytvořena');
});
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('invoices.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const parsed = parseBody(UpdateReceivedInvoiceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.received_invoices.findUnique({ where: { id } });
if (!existing) return error(reply, 'Přijatá faktura nenalezena', 404);
if (body.status !== undefined) {
const status = String(body.status);
if (!VALID_STATUSES.includes(status as typeof VALID_STATUSES[number])) {
return error(reply, 'Neplatný stav', 400);
}
// Prevent reverting paid status (matching PHP)
if (String(existing.status) === 'paid' && status !== 'paid') {
return error(reply, 'Nelze vrátit stav uhrazené faktury', 400);
}
}
// Recalculate vat_amount when amount or vat_rate changes (matching PHP)
const finalAmount = body.amount !== undefined ? Number(body.amount) : Number(existing.amount);
const finalVatRate = body.vat_rate !== undefined ? Number(body.vat_rate) : Number(existing.vat_rate);
// Amount includes VAT — extract VAT portion: amount - amount/(1 + rate/100)
const computedVat = finalVatRate > 0 ? Math.round((finalAmount - finalAmount / (1 + finalVatRate / 100)) * 100) / 100 : 0;
// Auto-set paid_date when status transitions to paid (matching PHP)
const newStatus = body.status !== undefined ? String(body.status) : String(existing.status);
const paidDate = newStatus === 'paid' && String(existing.status) !== 'paid'
? new Date()
: (body.paid_date !== undefined ? (body.paid_date ? new Date(String(body.paid_date)) : null) : undefined);
// Auto-update month/year from issue_date if issue_date changes (matching PHP)
let autoMonth = body.month !== undefined ? Number(body.month) : undefined;
let autoYear = body.year !== undefined ? Number(body.year) : undefined;
if (body.issue_date && !body.month && !body.year) {
const issueDate = new Date(String(body.issue_date));
if (!isNaN(issueDate.getTime())) {
autoMonth = issueDate.getMonth() + 1;
autoYear = issueDate.getFullYear();
}
}
await prisma.received_invoices.update({
where: { id },
data: {
supplier_name: body.supplier_name !== undefined ? String(body.supplier_name) : undefined,
invoice_number: body.invoice_number !== undefined ? (body.invoice_number ? String(body.invoice_number) : null) : undefined,
description: body.description !== undefined ? (body.description ? String(body.description) : null) : undefined,
amount: body.amount !== undefined ? Number(body.amount) : undefined,
currency: body.currency !== undefined ? String(body.currency) : undefined,
vat_rate: body.vat_rate !== undefined ? Number(body.vat_rate) : undefined,
vat_amount: (body.amount !== undefined || body.vat_rate !== undefined) ? computedVat : (body.vat_amount !== undefined ? Number(body.vat_amount) : undefined),
issue_date: body.issue_date !== undefined ? (body.issue_date ? new Date(String(body.issue_date)) : null) : undefined,
due_date: body.due_date !== undefined ? (body.due_date ? new Date(String(body.due_date)) : null) : undefined,
paid_date: paidDate,
status: body.status !== undefined ? String(body.status) as received_invoices_status : undefined,
notes: body.notes !== undefined ? (body.notes ? String(body.notes) : null) : undefined,
month: autoMonth,
year: autoYear,
modified_at: new Date(),
},
});
await logAudit({ request, authData: request.authData, action: 'update', entityType: 'invoice', entityId: id, description: `Upravena přijatá faktura` });
return success(reply, { id }, 200, 'Faktura byla uložena');
});
fastify.delete<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('invoices.delete') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const existing = await prisma.received_invoices.findUnique({ where: { id } });
if (!existing) return error(reply, 'Přijatá faktura nenalezena', 404);
await prisma.received_invoices.delete({ where: { id } });
await logAudit({ request, authData: request.authData, action: 'delete', entityType: 'invoice', entityId: id, description: `Smazána přijatá faktura` });
return success(reply, null, 200, 'Přijatá faktura smazána');
});
await prisma.received_invoices.delete({ where: { id } });
await logAudit({
request,
authData: request.authData,
action: "delete",
entityType: "invoice",
entityId: id,
description: `Smazána přijatá faktura`,
});
return success(reply, null, 200, "Přijatá faktura smazána");
},
);
}