Merge pull request 'fix(po): keep export stamp clear of the signature (no overlap)' (#61) from fix/stamp-no-overlap into master
Reviewed-on: #61
This commit is contained in:
commit
e4c4c370f6
3 changed files with 79 additions and 9 deletions
|
|
@ -6,6 +6,7 @@ import { TC_FIXED_LINE, TC_DEFAULTS } from "@/lib/validations/po";
|
||||||
import { downloadBuffer } from "@/lib/storage";
|
import { downloadBuffer } from "@/lib/storage";
|
||||||
import { CANCELLED_WATERMARK_PNG_BASE64, CANCELLED_WATERMARK_W, CANCELLED_WATERMARK_H } from "@/lib/cancelled-watermark";
|
import { CANCELLED_WATERMARK_PNG_BASE64, CANCELLED_WATERMARK_W, CANCELLED_WATERMARK_H } from "@/lib/cancelled-watermark";
|
||||||
import { getImageSize, scaleToBox } from "@/lib/image-size";
|
import { getImageSize, scaleToBox } from "@/lib/image-size";
|
||||||
|
import { signatoryLayout } from "@/lib/po-export-layout";
|
||||||
|
|
||||||
// ── Company fallback constants (used when no company is linked to a PO) ──────
|
// ── Company fallback constants (used when no company is linked to a PO) ──────
|
||||||
|
|
||||||
|
|
@ -476,27 +477,25 @@ export async function GET(request: NextRequest, { params }: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sigExt = signatureBase64 ? scaleToBox(signatureSize ?? { width: 360, height: 96 }, 165, 44) : null;
|
const sigExt = signatureBase64 ? scaleToBox(signatureSize ?? { width: 360, height: 96 }, 165, 44) : null;
|
||||||
const sigLeft = sigExt ? Math.round((SIG_BLOCK_PX - sigExt.width) / 2) : 0; // centred over the name
|
const stampExt = stampImg ? scaleToBox(stampImg, 80, 66) : null;
|
||||||
|
// Signature centred over the name; stamp to its RIGHT with a gap (no overlap).
|
||||||
|
const { sigLeft, stampLeft } = signatoryLayout({ blockPx: SIG_BLOCK_PX, sig: sigExt, stamp: stampExt });
|
||||||
|
|
||||||
// Stamp / seal — drawn FIRST so it sits BEHIND the signature, tucked to its right.
|
// Stamp / seal — drawn FIRST so it layers BEHIND the signature if they ever touch.
|
||||||
if (stampImg) {
|
if (stampImg && stampExt && stampLeft != null) {
|
||||||
const stampExt = scaleToBox(stampImg, 80, 66);
|
|
||||||
const stampLeft = sigExt
|
|
||||||
? Math.min(SIG_BLOCK_PX - stampExt.width, sigLeft + sigExt.width - Math.round(stampExt.width * 0.35))
|
|
||||||
: SIG_BLOCK_PX - stampExt.width - 6;
|
|
||||||
const stampId = wb.addImage({
|
const stampId = wb.addImage({
|
||||||
base64: stampImg.base64,
|
base64: stampImg.base64,
|
||||||
extension: stampImg.mime === "image/jpeg" ? "jpeg" : "png",
|
extension: stampImg.mime === "image/jpeg" ? "jpeg" : "png",
|
||||||
});
|
});
|
||||||
ws.addImage(stampId, {
|
ws.addImage(stampId, {
|
||||||
tl: anchorAt(Math.max(0, stampLeft), SIG_ROW - 1),
|
tl: anchorAt(stampLeft, SIG_ROW - 1),
|
||||||
ext: stampExt,
|
ext: stampExt,
|
||||||
editAs: "oneCell",
|
editAs: "oneCell",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Approver signature — drawn AFTER the stamp (on top), centred over the name.
|
// Approver signature — drawn AFTER the stamp (on top), centred over the name.
|
||||||
if (signatureBase64 && sigExt) {
|
if (signatureBase64 && sigExt && sigLeft != null) {
|
||||||
const imgType = signatureMime === "image/jpeg" ? "jpeg" : "png";
|
const imgType = signatureMime === "image/jpeg" ? "jpeg" : "png";
|
||||||
const imgId = wb.addImage({ base64: signatureBase64, extension: imgType });
|
const imgId = wb.addImage({ base64: signatureBase64, extension: imgType });
|
||||||
ws.addImage(imgId, {
|
ws.addImage(imgId, {
|
||||||
|
|
|
||||||
32
App/lib/po-export-layout.ts
Normal file
32
App/lib/po-export-layout.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Geometry for the exported PO's left signatory block (cols A-D).
|
||||||
|
// The approver signature is centred over the name; the company stamp/seal sits to
|
||||||
|
// its RIGHT with a gap so it never overlays the signature or name — important
|
||||||
|
// because uploaded signatures/stamps aren't always transparent PNGs.
|
||||||
|
|
||||||
|
export interface Size { width: number; height: number }
|
||||||
|
|
||||||
|
export interface SignatoryLayout {
|
||||||
|
sigLeft: number | null; // px from the block's left edge, or null when no signature
|
||||||
|
stampLeft: number | null; // px from the block's left edge, or null when no stamp
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signatoryLayout(opts: {
|
||||||
|
blockPx: number;
|
||||||
|
sig: Size | null;
|
||||||
|
stamp: Size | null;
|
||||||
|
gap?: number;
|
||||||
|
}): SignatoryLayout {
|
||||||
|
const gap = opts.gap ?? 10;
|
||||||
|
const sigLeft = opts.sig ? Math.round((opts.blockPx - opts.sig.width) / 2) : null; // centred
|
||||||
|
|
||||||
|
let stampLeft: number | null = null;
|
||||||
|
if (opts.stamp) {
|
||||||
|
stampLeft =
|
||||||
|
sigLeft != null && opts.sig
|
||||||
|
? Math.min(opts.blockPx - opts.stamp.width, sigLeft + opts.sig.width + gap) // clear of the signature
|
||||||
|
: opts.blockPx - opts.stamp.width - 6; // no signature → right-align in the block
|
||||||
|
stampLeft = Math.max(0, stampLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sigLeft, stampLeft };
|
||||||
|
}
|
||||||
39
App/tests/unit/po-export-layout.test.ts
Normal file
39
App/tests/unit/po-export-layout.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { signatoryLayout } from "@/lib/po-export-layout";
|
||||||
|
|
||||||
|
const BLOCK = 503; // px width of the A-D signatory block
|
||||||
|
|
||||||
|
describe("signatoryLayout", () => {
|
||||||
|
it("centres the signature in the block", () => {
|
||||||
|
const { sigLeft } = signatoryLayout({ blockPx: BLOCK, sig: { width: 153, height: 44 }, stamp: null });
|
||||||
|
expect(sigLeft).not.toBeNull();
|
||||||
|
expect(sigLeft! + 153 / 2).toBeCloseTo(BLOCK / 2, 0); // centre ≈ block centre
|
||||||
|
});
|
||||||
|
|
||||||
|
it("places the stamp to the RIGHT of the signature with no overlap", () => {
|
||||||
|
const sig = { width: 153, height: 44 };
|
||||||
|
const stamp = { width: 67, height: 66 };
|
||||||
|
const { sigLeft, stampLeft } = signatoryLayout({ blockPx: BLOCK, sig, stamp, gap: 10 });
|
||||||
|
expect(stampLeft! ).toBeGreaterThanOrEqual(sigLeft! + sig.width); // starts at/after signature ends
|
||||||
|
expect(stampLeft! + stamp.width).toBeLessThanOrEqual(BLOCK); // stays inside the block
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never overlaps even with the widest signature + stamp", () => {
|
||||||
|
const sig = { width: 165, height: 44 }; // scaleToBox caps
|
||||||
|
const stamp = { width: 80, height: 66 }; // scaleToBox caps
|
||||||
|
const { sigLeft, stampLeft } = signatoryLayout({ blockPx: BLOCK, sig, stamp });
|
||||||
|
expect(stampLeft!).toBeGreaterThanOrEqual(sigLeft! + sig.width);
|
||||||
|
expect(stampLeft! + stamp.width).toBeLessThanOrEqual(BLOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("right-aligns the stamp when there is no signature", () => {
|
||||||
|
const { sigLeft, stampLeft } = signatoryLayout({ blockPx: BLOCK, sig: null, stamp: { width: 67, height: 66 } });
|
||||||
|
expect(sigLeft).toBeNull();
|
||||||
|
expect(stampLeft! + 67).toBeLessThanOrEqual(BLOCK);
|
||||||
|
expect(stampLeft!).toBeGreaterThan(BLOCK / 2); // on the right side
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns nulls when there are no images", () => {
|
||||||
|
expect(signatoryLayout({ blockPx: BLOCK, sig: null, stamp: null })).toEqual({ sigLeft: null, stampLeft: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue