feat(TICKET-LOGO): completed feature

This commit is contained in:
chattie
2026-08-25 06:35:39 +02:00
parent 419f47ec1c
commit 7639c1ff42
16 changed files with 324 additions and 12 deletions

View File

@@ -57,6 +57,7 @@ interface ReceiptSettings {
contactPhone: string;
receiptHeader: string;
receiptFooter: string;
logoUrl: string;
prefix: string;
nextNumber: number;
padding: number;
@@ -72,6 +73,7 @@ const emptyReceipt: ReceiptSettings = {
contactPhone: '',
receiptHeader: '',
receiptFooter: '',
logoUrl: '',
prefix: 'TPV',
nextNumber: 1,
padding: 6,
@@ -823,6 +825,12 @@ export default function PosAdminPage() {
value={receipt.receiptFooter}
onChange={(value) => setReceipt({ ...receipt, receiptFooter: value })}
/>
<Field
label="URL del logo"
value={receipt.logoUrl}
onChange={(value) => setReceipt({ ...receipt, logoUrl: value })}
placeholder="https://ejemplo.com/logo.png"
/>
<Field
label="Prefijo de ticket"
value={receipt.prefix}

View File

@@ -72,7 +72,7 @@ export default function ReceiptModal({
{/* Logo */}
<div className="mb-3 flex justify-center">
<img
src="/images/logo-main.png"
src={receipt.logoUrl ?? '/images/logo-main.png'}
alt="Logo"
className="h-16 object-contain print:h-12"
/>

View File

@@ -47,6 +47,7 @@ export interface PosReceipt {
email: string | null;
phone: string | null;
};
logoUrl: string | null;
terminal: { id: string; name: string };
cashier: string;
sessionId: string;

View File

@@ -0,0 +1,18 @@
'use strict';
exports.shorthands = undefined;
exports.up = (pgm) => {
pgm.addColumns('pos_stores', {
logo_url: {
type: 'text',
notNull: false,
default: null,
},
});
pgm.addCommentOnColumn('pos_stores', 'logo_url', 'URL del logo custom para tickets TPV. Si es null, se usa el logo default /images/logo-main.png');
};
exports.down = (pgm) => {
pgm.dropColumns('pos_stores', ['logo_url']);
};

View File

@@ -136,12 +136,13 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
contactPhone: z.string().optional(),
receiptHeader: z.string().optional(),
receiptFooter: z.string().optional(),
logoUrl: z.string().url().max(500).optional(),
}),
request.body ?? {},
);
const result = await pool.query<{ id: string; name: string; slug: string; active: boolean }>(
`INSERT INTO pos_stores (name, slug, address, tax_id, contact_email, contact_phone, receipt_header, receipt_footer)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`INSERT INTO pos_stores (name, slug, address, tax_id, contact_email, contact_phone, receipt_header, receipt_footer, logo_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, name, slug, active`,
[
body.name,
@@ -152,6 +153,7 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
body.contactPhone,
body.receiptHeader,
body.receiptFooter,
body.logoUrl ?? null,
],
);
return reply.code(201).send(result.rows[0]);
@@ -1268,6 +1270,7 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
contactPhone: z.string().trim().max(64).optional(),
receiptHeader: z.string().trim().max(500).optional(),
receiptFooter: z.string().trim().max(1000).optional(),
logoUrl: z.string().url().max(500).optional(),
prefix: z
.string()
.trim()
@@ -1288,7 +1291,8 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
tax_id = COALESCE($4, tax_id), contact_email = COALESCE($5, contact_email),
contact_phone = COALESCE($6, contact_phone),
receipt_header = COALESCE($7, receipt_header),
receipt_footer = COALESCE($8, receipt_footer), updated_at = now()
receipt_footer = COALESCE($8, receipt_footer),
logo_url = $9, updated_at = now()
WHERE id = $1 RETURNING id`,
[
body.storeId,
@@ -1299,6 +1303,7 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps
body.contactPhone,
body.receiptHeader,
body.receiptFooter,
body.logoUrl ?? null,
],
);
if (!store.rows[0]) throw new AppError(404, 'STORE_NOT_FOUND', 'No se encontró la tienda');

View File

@@ -37,6 +37,7 @@ interface ReceiptOrderRow {
terminal_name: string;
cashier_email: string;
return_policy: string | null;
logo_url: string | null;
}
interface ReceiptItemRow {
@@ -61,7 +62,7 @@ export async function buildPosReceipt(queryable: Queryable, orderId: string): Pr
o.total_cents, o.created_at, o.cash_session_id, o.terminal_id,
customer.email AS customer_email,
store.name AS store_name, store.address, store.tax_id, store.contact_email,
store.contact_phone, store.receipt_header, store.receipt_footer,
store.contact_phone, store.receipt_header, store.receipt_footer, store.logo_url,
terminal.name AS terminal_name, cashier.email AS cashier_email,
receipt_settings.return_policy
FROM orders_orders o
@@ -117,6 +118,7 @@ export async function buildPosReceipt(queryable: Queryable, orderId: string): Pr
email: order.contact_email,
phone: order.contact_phone,
},
logoUrl: order.logo_url,
terminal: { id: order.terminal_id, name: order.terminal_name },
cashier: order.cashier_email,
sessionId: order.cash_session_id,
@@ -231,6 +233,7 @@ export async function buildPosReturnReceipt(
orderId: original.orderId,
issuedAt: original.issuedAt,
company: original.company,
logoUrl: original.logoUrl,
terminal: original.terminal,
cashier: original.cashier,
sessionId: original.sessionId,

View File

@@ -93,6 +93,8 @@ export interface PosReceipt {
email: string | null;
phone: string | null;
};
/** URL del logo custom para el ticket. Si es null, usar /images/logo-main.png */
logoUrl: string | null;
terminal: { id: string; name: string };
cashier: string;
sessionId: string;