feat(orders): PDF footer = issuer name+email, 'Zobrazit objednávku' button, per-currency list totals
- Issued-order PDF: drop the Vystavil/Schválil signature row; footer now shows 'Vystavil: <name>' + 'E-mail: <email>' from the logged-in user (authData). - IssuedOrderDetail PDF button 'Export PDF' -> 'Zobrazit objednávku' (already opens inline like invoices' 'Zobrazit fakturu'). - New /orders/stats + /issued-orders/stats endpoints sum order total (incl VAT) per currency across the active filter; rendered as 'Celkem: …' beneath both the received and issued lists. formatMultiCurrency lifted to a shared util. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,7 +187,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
delivery_terms: "Dodací podmínky:",
|
||||
payment_terms: "Platební podmínky:",
|
||||
issued_by: "Vystavil:",
|
||||
approved_by: "Schválil:",
|
||||
email: "E-mail:",
|
||||
ico: "IČ: ",
|
||||
dic: "DIČ: ",
|
||||
},
|
||||
@@ -215,7 +215,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
delivery_terms: "Delivery terms:",
|
||||
payment_terms: "Payment terms:",
|
||||
issued_by: "Issued by:",
|
||||
approved_by: "Approved by:",
|
||||
email: "E-mail:",
|
||||
ico: "Reg. No.: ",
|
||||
dic: "Tax ID: ",
|
||||
},
|
||||
@@ -249,6 +249,7 @@ export function renderIssuedOrderHtml(
|
||||
customer: Record<string, unknown> | null,
|
||||
settings: Record<string, unknown> | null,
|
||||
lang: Lang,
|
||||
issuer: { name: string; email: string },
|
||||
): string {
|
||||
const t = translations[lang];
|
||||
const applyVat = order.apply_vat !== false;
|
||||
@@ -642,21 +643,6 @@ export function renderIssuedOrderHtml(
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Prevzal / razitko */
|
||||
.footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 4mm;
|
||||
font-size: 9pt;
|
||||
border-top: 0.5pt solid #aaa;
|
||||
padding-top: 2mm;
|
||||
min-height: 15mm;
|
||||
}
|
||||
.footer-row .col {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Poznamky */
|
||||
.invoice-notes {
|
||||
margin-top: 4mm;
|
||||
@@ -798,18 +784,9 @@ ${indentCSS}
|
||||
</div><!-- /.invoice-content -->
|
||||
<div class="invoice-footer">
|
||||
|
||||
<!-- Vystavil -->
|
||||
${
|
||||
order.issued_by
|
||||
? `<div class="issued-by"><span class="lbl">${escapeHtml(t.issued_by)}</span> ${escapeHtml(order.issued_by)}</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
<!-- Vystavil / Schvalil -->
|
||||
<div class="footer-row">
|
||||
<div class="col">${escapeHtml(t.issued_by)}</div>
|
||||
<div class="col" style="text-align:right">${escapeHtml(t.approved_by)}</div>
|
||||
</div>
|
||||
<!-- Vystavil (jmeno + e-mail prihlaseneho uzivatele) -->
|
||||
<div class="issued-by"><span class="lbl">${escapeHtml(t.issued_by)}</span> ${escapeHtml(issuer.name)}</div>
|
||||
${issuer.email ? `<div class="issued-by"><span class="lbl">${escapeHtml(t.email)}</span> ${escapeHtml(issuer.email)}</div>` : ""}
|
||||
|
||||
</div><!-- /.invoice-footer -->
|
||||
</div><!-- /.invoice-page -->
|
||||
@@ -854,12 +831,21 @@ export default async function issuedOrdersPdfRoutes(fastify: FastifyInstance) {
|
||||
unknown
|
||||
> | null;
|
||||
|
||||
// Footer issuer = the logged-in user (there is no user FK on the
|
||||
// order). The route runs under requirePermission("orders.view") so
|
||||
// authData is always present here. Mirrors orders-pdf.ts userName.
|
||||
const issuer = {
|
||||
name: `${request.authData?.firstName ?? ""} ${request.authData?.lastName ?? ""}`.trim(),
|
||||
email: request.authData?.email ?? "",
|
||||
};
|
||||
|
||||
const html = renderIssuedOrderHtml(
|
||||
order,
|
||||
items,
|
||||
customer,
|
||||
settings,
|
||||
lang,
|
||||
issuer,
|
||||
);
|
||||
|
||||
// ?save=1 → archive the PDF to NAS instead of returning it (mirrors
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../../schemas/issued-orders.schema";
|
||||
import {
|
||||
listIssuedOrders,
|
||||
getIssuedOrderTotals,
|
||||
getIssuedOrder,
|
||||
createIssuedOrder,
|
||||
updateIssuedOrder,
|
||||
@@ -54,6 +55,25 @@ export default async function issuedOrdersRoutes(fastify: FastifyInstance) {
|
||||
},
|
||||
);
|
||||
|
||||
// GET /api/admin/issued-orders/stats — per-currency total (incl. VAT) summed
|
||||
// over the WHOLE filtered set (not one page). Registered BEFORE "/:id" so the
|
||||
// literal "stats" path isn't captured as an order id.
|
||||
fastify.get(
|
||||
"/stats",
|
||||
{ preHandler: requirePermission("orders.view") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const result = await getIssuedOrderTotals({
|
||||
search: query.search ? String(query.search) : undefined,
|
||||
status: query.status ? String(query.status) : undefined,
|
||||
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
});
|
||||
return success(reply, { totals: result.totals });
|
||||
},
|
||||
);
|
||||
|
||||
// GET /api/admin/issued-orders/:id
|
||||
fastify.get<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../../schemas/orders.schema";
|
||||
import {
|
||||
listOrders,
|
||||
getOrderTotals,
|
||||
getOrder,
|
||||
getOrderAttachment,
|
||||
createOrderFromQuotation,
|
||||
@@ -68,6 +69,25 @@ export default async function ordersRoutes(
|
||||
},
|
||||
);
|
||||
|
||||
// GET /api/admin/orders/stats — per-currency total (incl. VAT) summed over the
|
||||
// WHOLE filtered set (not one page). Must be registered BEFORE "/:id" so the
|
||||
// literal "stats" path isn't captured as an order id.
|
||||
fastify.get(
|
||||
"/stats",
|
||||
{ preHandler: requirePermission("orders.view") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const result = await getOrderTotals({
|
||||
search: query.search ? String(query.search) : undefined,
|
||||
status: query.status ? String(query.status) : undefined,
|
||||
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
|
||||
month: query.month ? Number(query.month) : undefined,
|
||||
year: query.year ? Number(query.year) : undefined,
|
||||
});
|
||||
return success(reply, { totals: result.totals });
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("orders.view") },
|
||||
|
||||
Reference in New Issue
Block a user