Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/modules/doing/controllers/PlanController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class PlanController extends DoingBaseController {
return result;
}

private async copyTimesAndPositions(churchId: string, sourcePlanId: string, targetPlan: Plan, oldPlan: Plan, copyAssignments: boolean): Promise<Map<string, string>> {
private async copyTimesAndPositions(churchId: string, sourcePlanId: string, targetPlan: Plan, oldPlan: Plan, copyAssignments: boolean): Promise<{ timeIdMap: Map<string, string>; positionIdMap: Map<string, string> }> {
const times: Time[] = await this.repos.time.loadByPlanId(churchId, sourcePlanId) as Time[];
const positions: Position[] = await this.repos.position.loadByPlanId(churchId, sourcePlanId) as Position[];
const positionIdMap = new Map<string, string>();
Expand Down Expand Up @@ -144,10 +144,10 @@ export class PlanController extends DoingBaseController {
}

await Promise.all(promises);
return timeIdMap;
return { timeIdMap, positionIdMap };
}

private async copyServiceOrderItems(churchId: string, sourcePlanId: string, targetPlanId: string): Promise<Map<string, string>> {
private async copyServiceOrderItems(churchId: string, sourcePlanId: string, targetPlanId: string, positionIdMap: Map<string, string>): Promise<Map<string, string>> {
const planItems: PlanItem[] = await this.repos.planItem.loadForPlan(churchId, sourcePlanId) as PlanItem[];
const planItemIdMap = new Map<string, string>();

Expand All @@ -156,6 +156,7 @@ export class PlanController extends DoingBaseController {
const oldId = item.id;
item.id = undefined;
item.planId = targetPlanId;
item.positionId = positionIdMap.get(item.positionId || "");
const savedItem = await this.repos.planItem.save(item);
if (oldId) planItemIdMap.set(oldId, savedItem.id || "");
}
Expand All @@ -168,6 +169,7 @@ export class PlanController extends DoingBaseController {
item.id = undefined;
item.planId = targetPlanId;
item.parentId = newParentId;
item.positionId = positionIdMap.get(item.positionId || "");
const savedItem = await this.repos.planItem.save(item);
if (oldId) planItemIdMap.set(oldId, savedItem.id || "");
}
Expand Down Expand Up @@ -265,14 +267,15 @@ export class PlanController extends DoingBaseController {
const plan = await this.repos.plan.save(p);

let timeIdMap = new Map<string, string>();
let positionIdMap = new Map<string, string>();
let planItemIdMap = new Map<string, string>();

if (copyMode !== "none") {
timeIdMap = await this.copyTimesAndPositions(au.churchId, id, plan, oldPlan, copyMode === "all");
({ timeIdMap, positionIdMap } = await this.copyTimesAndPositions(au.churchId, id, plan, oldPlan, copyMode === "all"));
}

if (copyServiceOrder) {
planItemIdMap = await this.copyServiceOrderItems(au.churchId, id, plan.id!);
planItemIdMap = await this.copyServiceOrderItems(au.churchId, id, plan.id!, positionIdMap);
}

if (timeIdMap.size > 0 && planItemIdMap.size > 0) {
Expand Down
1 change: 1 addition & 0 deletions src/modules/doing/models/PlanItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class PlanItem {
public sort?: number;
public itemType?: string;
public relatedId?: string;
public positionId?: string;
public label?: string;
public description?: string;
public seconds?: number;
Expand Down
4 changes: 2 additions & 2 deletions src/modules/doing/repositories/PlanItemRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ export class PlanItemRepo {

private async create(model: PlanItem): Promise<PlanItem> {
model.id = UniqueIdHelper.shortId();
await getDb().insertInto("planItems").values({ id: model.id, churchId: model.churchId, planId: model.planId, parentId: model.parentId, sort: model.sort, itemType: model.itemType, relatedId: model.relatedId, label: model.label, description: model.description, seconds: model.seconds, link: model.link, providerId: model.providerId, providerPath: model.providerPath, providerContentPath: model.providerContentPath, thumbnailUrl: model.thumbnailUrl }).execute();
await getDb().insertInto("planItems").values({ id: model.id, churchId: model.churchId, planId: model.planId, parentId: model.parentId, sort: model.sort, itemType: model.itemType, relatedId: model.relatedId, positionId: model.positionId, label: model.label, description: model.description, seconds: model.seconds, link: model.link, providerId: model.providerId, providerPath: model.providerPath, providerContentPath: model.providerContentPath, thumbnailUrl: model.thumbnailUrl }).execute();
return model;
}

private async update(model: PlanItem): Promise<PlanItem> {
await getDb().updateTable("planItems").set({ planId: model.planId, parentId: model.parentId, sort: model.sort, itemType: model.itemType, relatedId: model.relatedId, label: model.label, description: model.description, seconds: model.seconds, link: model.link, providerId: model.providerId, providerPath: model.providerPath, providerContentPath: model.providerContentPath, thumbnailUrl: model.thumbnailUrl }).where("id", "=", model.id).where("churchId", "=", model.churchId).execute();
await getDb().updateTable("planItems").set({ planId: model.planId, parentId: model.parentId, sort: model.sort, itemType: model.itemType, relatedId: model.relatedId, positionId: model.positionId, label: model.label, description: model.description, seconds: model.seconds, link: model.link, providerId: model.providerId, providerPath: model.providerPath, providerContentPath: model.providerContentPath, thumbnailUrl: model.thumbnailUrl }).where("id", "=", model.id).where("churchId", "=", model.churchId).execute();
return model;
}

Expand Down
43 changes: 43 additions & 0 deletions src/modules/doing/repositories/__tests__/PlanItemRepo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import "reflect-metadata";
jest.mock("../../db/index", () => ({ getDb: jest.fn() }));
jest.mock("@churchapps/apihelper", () => ({ UniqueIdHelper: { shortId: () => "gen_id" } }));

import { getDb } from "../../db/index.js";
import { PlanItemRepo } from "../PlanItemRepo.js";

function recordingDb() {
const calls: { method: string; args: any[] }[] = [];
const proxy: any = new Proxy({}, {
get(_t, prop) {
if (typeof prop === "symbol" || prop === "then") return undefined;
if (prop === "execute") return async () => [];
if (prop === "executeTakeFirst") return async () => null;
return (...args: any[]) => { calls.push({ method: prop as string, args }); return proxy; };
}
});
return { proxy, calls };
}

const firstArg = (calls: { method: string; args: any[] }[], method: string) => calls.find((c) => c.method === method)?.args[0];

describe("PlanItemRepo positionId", () => {
afterEach(() => jest.restoreAllMocks());

it("persists positionId on create", async () => {
const { proxy, calls } = recordingDb();
(getDb as jest.Mock).mockReturnValue(proxy);
const saved = await new PlanItemRepo().save({ churchId: "c1", planId: "pl1", label: "Sermon", positionId: "pos1" });
expect(saved.id).toBe("gen_id");
expect(firstArg(calls, "insertInto")).toBe("planItems");
expect(firstArg(calls, "values")).toMatchObject({ id: "gen_id", planId: "pl1", positionId: "pos1" });
});

it("persists positionId on update", async () => {
const { proxy, calls } = recordingDb();
(getDb as jest.Mock).mockReturnValue(proxy);
await new PlanItemRepo().save({ id: "pi1", churchId: "c1", planId: "pl1", label: "Sermon", positionId: "pos2" });
expect(calls.some((c) => c.method === "insertInto")).toBe(false);
expect(firstArg(calls, "updateTable")).toBe("planItems");
expect(firstArg(calls, "set")).toMatchObject({ positionId: "pos2" });
});
});
9 changes: 9 additions & 0 deletions tools/migrations/doing/2026-08-20_plan_item_position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type Kysely, sql } from "kysely";

export async function up(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE planItems ADD COLUMN positionId CHAR(11) NULL`.execute(db);
}

export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE planItems DROP COLUMN positionId`.execute(db);
}