feat(pos): add pos-slip-layout-service for reading/writing per-letterhead slip layouts
This commit is contained in:
parent
e0ce1310e0
commit
3a29ad6a6a
2 changed files with 174 additions and 0 deletions
85
src/lib/server/pos-slip-layout-service.test.ts
Normal file
85
src/lib/server/pos-slip-layout-service.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { getSlipLayout, saveSlipLayout } from "./pos-slip-layout-service";
|
||||
|
||||
const mockGetAdminPocketBase = vi.fn();
|
||||
vi.mock("./pocketbase-admin", () => ({
|
||||
getAdminPocketBase: () => mockGetAdminPocketBase(),
|
||||
}));
|
||||
|
||||
describe("getSlipLayout", () => {
|
||||
it("returns null when the letterhead has no saved layout", async () => {
|
||||
mockGetAdminPocketBase.mockResolvedValue({
|
||||
collection: () => ({
|
||||
getList: async () => ({ items: [] }),
|
||||
}),
|
||||
});
|
||||
const result = await getSlipLayout("lh-1");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the saved layout, defaulting delivery_show_pricing to true when unset", async () => {
|
||||
mockGetAdminPocketBase.mockResolvedValue({
|
||||
collection: () => ({
|
||||
getList: async () => ({
|
||||
items: [{
|
||||
id: "row-1",
|
||||
letterhead: "lh-1",
|
||||
sale_blocks: [{ id: "logo", enabled: false }],
|
||||
delivery_blocks: [],
|
||||
cashup_blocks: [],
|
||||
delivery_show_pricing: undefined,
|
||||
}],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const result = await getSlipLayout("lh-1");
|
||||
expect(result).toEqual({
|
||||
sale_blocks: [{ id: "logo", enabled: false }],
|
||||
delivery_blocks: [],
|
||||
cashup_blocks: [],
|
||||
delivery_show_pricing: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveSlipLayout", () => {
|
||||
it("creates a new row when none exists yet, only writing the fields given", async () => {
|
||||
const create = vi.fn().mockResolvedValue({
|
||||
id: "row-1",
|
||||
letterhead: "lh-1",
|
||||
sale_blocks: [{ id: "logo", enabled: false }],
|
||||
delivery_blocks: [],
|
||||
cashup_blocks: [],
|
||||
delivery_show_pricing: true,
|
||||
});
|
||||
mockGetAdminPocketBase.mockResolvedValue({
|
||||
collection: () => ({
|
||||
getList: async () => ({ items: [] }),
|
||||
create,
|
||||
}),
|
||||
});
|
||||
const result = await saveSlipLayout("lh-1", { sale_blocks: [{ id: "logo", enabled: false }] });
|
||||
expect(create).toHaveBeenCalledWith({ letterhead: "lh-1", sale_blocks: [{ id: "logo", enabled: false }] });
|
||||
expect(result.sale_blocks).toEqual([{ id: "logo", enabled: false }]);
|
||||
});
|
||||
|
||||
it("updates the existing row when one exists", async () => {
|
||||
const update = vi.fn().mockResolvedValue({
|
||||
id: "row-1",
|
||||
letterhead: "lh-1",
|
||||
sale_blocks: [],
|
||||
delivery_blocks: [],
|
||||
cashup_blocks: [],
|
||||
delivery_show_pricing: false,
|
||||
});
|
||||
mockGetAdminPocketBase.mockResolvedValue({
|
||||
collection: () => ({
|
||||
getList: async () => ({ items: [{ id: "row-1", letterhead: "lh-1" }] }),
|
||||
update,
|
||||
}),
|
||||
});
|
||||
const result = await saveSlipLayout("lh-1", { delivery_show_pricing: false });
|
||||
expect(update).toHaveBeenCalledWith("row-1", { letterhead: "lh-1", delivery_show_pricing: false });
|
||||
expect(result.delivery_show_pricing).toBe(false);
|
||||
});
|
||||
});
|
||||
89
src/lib/server/pos-slip-layout-service.ts
Normal file
89
src/lib/server/pos-slip-layout-service.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { getAdminPocketBase } from "./pocketbase-admin";
|
||||
|
||||
/**
|
||||
* A single row in a slip's block list. `text` is present only on
|
||||
* blocks that carry editable text (header_title, footer); it's
|
||||
* null/absent to mean "use the default text".
|
||||
*/
|
||||
export type PosSlipBlock = {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
text?: string | null;
|
||||
};
|
||||
|
||||
type PosSlipLayoutRow = {
|
||||
id: string;
|
||||
letterhead: string;
|
||||
sale_blocks: PosSlipBlock[];
|
||||
delivery_blocks: PosSlipBlock[];
|
||||
cashup_blocks: PosSlipBlock[];
|
||||
delivery_show_pricing?: boolean;
|
||||
};
|
||||
|
||||
export type PosSlipLayout = {
|
||||
sale_blocks: PosSlipBlock[];
|
||||
delivery_blocks: PosSlipBlock[];
|
||||
cashup_blocks: PosSlipBlock[];
|
||||
delivery_show_pricing: boolean;
|
||||
};
|
||||
|
||||
export type PosSlipLayoutInput = Partial<PosSlipLayout>;
|
||||
|
||||
function escapePbValue(value: string): string {
|
||||
return value.replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
async function findRow(
|
||||
pb: Awaited<ReturnType<typeof getAdminPocketBase>>,
|
||||
letterheadId: string,
|
||||
): Promise<PosSlipLayoutRow | null> {
|
||||
const page = await pb.collection("pos_slip_layouts").getList<PosSlipLayoutRow>(1, 1, {
|
||||
filter: `letterhead = '${escapePbValue(letterheadId)}'`,
|
||||
});
|
||||
return page.items[0] || null;
|
||||
}
|
||||
|
||||
function toLayout(row: PosSlipLayoutRow): PosSlipLayout {
|
||||
return {
|
||||
sale_blocks: row.sale_blocks || [],
|
||||
delivery_blocks: row.delivery_blocks || [],
|
||||
cashup_blocks: row.cashup_blocks || [],
|
||||
delivery_show_pricing: row.delivery_show_pricing !== false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the saved slip layout for a letterhead. Returns null when
|
||||
* nothing has been saved yet — callers fall back to the hardcoded
|
||||
* defaults in `public/pos/slip-layout.js` (kept there, not here,
|
||||
* since the till needs the same defaults available when it's offline
|
||||
* and can't reach this endpoint at all).
|
||||
*/
|
||||
export async function getSlipLayout(letterheadId: string): Promise<PosSlipLayout | null> {
|
||||
const pb = await getAdminPocketBase();
|
||||
const row = await findRow(pb, letterheadId);
|
||||
return row ? toLayout(row) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create-or-update the slip layout row for a letterhead. Only the
|
||||
* fields present in `input` are written — saving the sale slip's
|
||||
* blocks from the editor doesn't touch delivery_blocks/cashup_blocks.
|
||||
*/
|
||||
export async function saveSlipLayout(
|
||||
letterheadId: string,
|
||||
input: PosSlipLayoutInput,
|
||||
): Promise<PosSlipLayout> {
|
||||
const pb = await getAdminPocketBase();
|
||||
const existing = await findRow(pb, letterheadId);
|
||||
const data: Record<string, unknown> = { letterhead: letterheadId };
|
||||
if (input.sale_blocks !== undefined) data.sale_blocks = input.sale_blocks;
|
||||
if (input.delivery_blocks !== undefined) data.delivery_blocks = input.delivery_blocks;
|
||||
if (input.cashup_blocks !== undefined) data.cashup_blocks = input.cashup_blocks;
|
||||
if (input.delivery_show_pricing !== undefined) data.delivery_show_pricing = input.delivery_show_pricing;
|
||||
|
||||
const row = existing
|
||||
? ((await pb.collection("pos_slip_layouts").update(existing.id, data)) as PosSlipLayoutRow)
|
||||
: ((await pb.collection("pos_slip_layouts").create(data)) as PosSlipLayoutRow);
|
||||
return toLayout(row);
|
||||
}
|
||||
Loading…
Reference in a new issue