feat: add campaign copying scheduling and residual handling

This commit is contained in:
2026-08-07 14:54:04 +02:00
parent 696f8f6385
commit c2efd6b7bd
35 changed files with 3359 additions and 39 deletions
+162 -2
View File
@@ -102,6 +102,66 @@ export type CampaignLifecyclePolicy = {
provenance: Record<string, unknown>;
};
export type CampaignCopyOptions = {
name?: string;
external_id?: string;
include_recipients: boolean;
include_files: boolean;
include_shares: boolean;
include_policies: boolean;
include_mail_profile: boolean;
};
export type CampaignScheduleOccurrence = {
id: string;
schedule_id: string;
scheduled_for: string;
status: "prepared" | "failed" | string;
generated_campaign_id?: string | null;
generated_version_id?: string | null;
error?: string | null;
created_at: string;
};
export type CampaignSchedule = {
id: string;
campaign_id: string;
source_version_id: string;
name: string;
recurrence_kind: "once" | "daily" | "weekly" | "monthly";
interval_count: number;
timezone: string;
starts_at: string;
next_fire_at?: string | null;
ends_at?: string | null;
max_occurrences: number;
occurrence_count: number;
active: boolean;
resource_revision: number;
last_fired_at?: string | null;
last_campaign_id?: string | null;
last_error?: string | null;
created_at: string;
updated_at: string;
occurrences: CampaignScheduleOccurrence[];
};
export type CampaignScheduleCreate = {
source_version_id: string;
name: string;
recurrence_kind: CampaignSchedule["recurrence_kind"];
interval_count: number;
timezone: string;
starts_at: string;
ends_at?: string | null;
max_occurrences: number;
include_recipients: boolean;
include_files: boolean;
include_shares: boolean;
include_policies: boolean;
include_mail_profile: boolean;
};
export type CampaignVersionDetail = CampaignVersionListItem & {
raw_json: Record<string, unknown>;
campaign_json?: Record<string, unknown>;
@@ -431,6 +491,50 @@ export type CampaignPrintTemplatesResponse = {
templates: CampaignPrintTemplate[];
};
export type CampaignContentLibraryTarget = "subject" | "text" | "html";
export type CampaignContentLibraryItem = {
id: string;
name: string;
description?: string | null;
template_type: string;
kind: "fragment" | "campaign_part";
status: string;
scope_type: "tenant" | "group" | "user";
scope_id?: string | null;
read_only: boolean;
current_revision: number;
revision: number;
revision_id: string;
locale?: string | null;
published: boolean;
targets: CampaignContentLibraryTarget[];
subject?: string | null;
text?: string | null;
html?: string | null;
body_mode: "text" | "html" | "both";
};
export type CampaignContentLibraryResponse = {
available: boolean;
writable: boolean;
reason?: string | null;
items: CampaignContentLibraryItem[];
};
export type CampaignContentLibrarySaveInput = {
name: string;
description?: string | null;
kind: "fragment" | "campaign_part";
target?: CampaignContentLibraryTarget | null;
subject?: string | null;
text?: string | null;
html?: string | null;
body_mode: "text" | "html" | "both";
locale: string;
visibility: "personal" | "tenant";
};
export type CampaignRecipientSnapshotItem = {
contact_id: string;
display_name: string;
@@ -991,6 +1095,26 @@ query = "")
return apiFetch<CampaignPrintTemplatesResponse>(settings, `/api/v1/campaigns/${campaignId}/print-templates${suffix}`);
}
export async function listCampaignContentLibrary(
settings: ApiSettings,
campaignId: string,
query = "")
: Promise<CampaignContentLibraryResponse> {
const suffix = query.trim() ? `?query=${encodeURIComponent(query.trim())}` : "";
return apiFetch<CampaignContentLibraryResponse>(settings, `/api/v1/campaigns/${campaignId}/content-library${suffix}`);
}
export async function saveCampaignContentLibraryItem(
settings: ApiSettings,
campaignId: string,
payload: CampaignContentLibrarySaveInput)
: Promise<{template: CampaignContentLibraryItem;}> {
return apiFetch<{template: CampaignContentLibraryItem;}>(settings, `/api/v1/campaigns/${campaignId}/content-library`, {
method: "POST",
body: JSON.stringify(payload)
});
}
export async function snapshotCampaignRecipientAddressSource(
settings: ApiSettings,
campaignId: string,
@@ -1052,17 +1176,53 @@ export async function copyCampaign(
settings: ApiSettings,
campaignId: string,
sourceVersionId: string,
expectedStateToken: string)
expectedStateToken: string,
options: CampaignCopyOptions)
: Promise<CampaignCreateResponse> {
return apiFetch<CampaignCreateResponse>(settings, `/api/v1/campaigns/${campaignId}/copies`, {
method: "POST",
body: JSON.stringify({
source_version_id: sourceVersionId,
expected_state_token: expectedStateToken
expected_state_token: expectedStateToken,
...options,
name: options.name?.trim() || undefined,
external_id: options.external_id?.trim() || undefined
})
});
}
export async function listCampaignSchedules(
settings: ApiSettings,
campaignId: string)
: Promise<CampaignSchedule[]> {
const response = await apiFetch<{items: CampaignSchedule[]}>(settings, `/api/v1/campaigns/${campaignId}/schedules`);
return response.items;
}
export async function createCampaignSchedule(
settings: ApiSettings,
campaignId: string,
payload: CampaignScheduleCreate)
: Promise<CampaignSchedule> {
return apiFetch<CampaignSchedule>(settings, `/api/v1/campaigns/${campaignId}/schedules`, {
method: "POST",
body: JSON.stringify(payload)
});
}
export async function setCampaignScheduleState(
settings: ApiSettings,
campaignId: string,
scheduleId: string,
active: boolean,
baseRevision: number)
: Promise<CampaignSchedule> {
return apiFetch<CampaignSchedule>(settings, `/api/v1/campaigns/${campaignId}/schedules/${scheduleId}`, {
method: "PATCH",
body: JSON.stringify({ active, base_revision: baseRevision })
});
}
export async function archiveCampaignVersion(
settings: ApiSettings,
campaignId: string,