feat: add campaign copying scheduling and residual handling
This commit is contained in:
@@ -7,6 +7,7 @@ import { Card } from "@govoplan/core-webui";
|
||||
import { PageTitle } from "@govoplan/core-webui";
|
||||
import { LoadingFrame } from "@govoplan/core-webui";
|
||||
import { MetricCard } from "@govoplan/core-webui";
|
||||
import { FormField } from "@govoplan/core-webui";
|
||||
import LockedVersionNotice from "./components/LockedVersionNotice";
|
||||
import VersionLine from "./components/VersionLine";
|
||||
import { ToggleSwitch } from "@govoplan/core-webui";
|
||||
@@ -56,6 +57,9 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
const basePaths = useMemo(() => normalizeAttachmentBasePaths(attachments.base_paths, attachments), [attachments]);
|
||||
const globalRules = useMemo(() => normalizeAttachmentRules(attachments.global), [attachments.global]);
|
||||
const zipConfig = useMemo(() => normalizeAttachmentZipCollection(attachments.zip), [attachments.zip]);
|
||||
const residualFiles = asRecord(attachments.residual_files);
|
||||
const residualMode = ["report", "attach"].includes(String(residualFiles.mode)) ? String(residualFiles.mode) : "none";
|
||||
const residualRecipient = asRecord(residualFiles.recipient);
|
||||
const filenameFieldOptions = useMemo(() => buildZipFilenameFieldOptions(displayDraft), [displayDraft]);
|
||||
const passwordFields = useMemo(() => getDraftFields(displayDraft).filter((field) => field.type === "password"), [displayDraft]);
|
||||
const zipArchiveNameValidation = useMemo(
|
||||
@@ -96,6 +100,18 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function patchResidualFiles(next: Record<string, unknown>) {
|
||||
if (locked) return;
|
||||
patch(["attachments", "residual_files"], {
|
||||
mode: "none",
|
||||
recipient: null,
|
||||
subject: "Unassigned files in campaign {{local:campaign_name}}",
|
||||
text: "The campaign build found {{local:residual_file_count}} file(s) that were not assigned to a recipient.\n\n{{local:residual_file_list}}",
|
||||
...residualFiles,
|
||||
...next
|
||||
});
|
||||
}
|
||||
|
||||
function patchBasePath(index: number, patch: Partial<AttachmentBasePath>) {
|
||||
patchBasePaths(basePaths.map((basePath, currentIndex) => currentIndex === index ? { ...basePath, ...patch } : basePath));
|
||||
}
|
||||
@@ -271,6 +287,48 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Unassigned file disposition" collapsible>
|
||||
<div className="campaign-residual-file-form">
|
||||
<FormField label="Action" help="Only sources with Unsent enabled are inspected. The existing warning policy remains active when no disposition is selected.">
|
||||
<select value={residualMode} disabled={locked} onChange={(event) => {
|
||||
const mode = event.target.value;
|
||||
patchResidualFiles({
|
||||
mode,
|
||||
recipient: mode === "none" ? null : {
|
||||
email: String(residualRecipient.email ?? ""),
|
||||
name: String(residualRecipient.name ?? "") || null
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<option value="none">Apply warning policy only</option>
|
||||
<option value="report">Prepare report message</option>
|
||||
<option value="attach">Prepare report with files attached</option>
|
||||
</select>
|
||||
</FormField>
|
||||
{residualMode !== "none" && <>
|
||||
<FormField label="Recipient email">
|
||||
<input type="email" value={String(residualRecipient.email ?? "")} disabled={locked} onChange={(event) => patchResidualFiles({ recipient: { ...residualRecipient, email: event.target.value } })} />
|
||||
</FormField>
|
||||
<FormField label="Recipient name">
|
||||
<input value={String(residualRecipient.name ?? "")} disabled={locked} onChange={(event) => patchResidualFiles({ recipient: { ...residualRecipient, name: event.target.value || null } })} />
|
||||
</FormField>
|
||||
<div className="campaign-residual-file-wide">
|
||||
<FormField label="Subject">
|
||||
<input value={String(residualFiles.subject ?? "Unassigned files in campaign {{local:campaign_name}}") } disabled={locked} onChange={(event) => patchResidualFiles({ subject: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
<div className="campaign-residual-file-wide">
|
||||
<FormField label="Report text" help="Available values: {{local:campaign_name}}, {{local:residual_file_count}}, and {{local:residual_file_list}}.">
|
||||
<textarea rows={5} value={String(residualFiles.text ?? "")} disabled={locked} onChange={(event) => patchResidualFiles({ text: event.target.value })} />
|
||||
</FormField>
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
{residualMode !== "none" && <DismissibleAlert tone="info" dismissible={false} compact>
|
||||
A residual-file message becomes a separate campaign row that always needs review. It uses the normal build, approval, queue, delivery, report, and audit lifecycle; saving this setting never sends it.
|
||||
</DismissibleAlert>}
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-campaign.zip_attachments.6b58ed68" collapsible>
|
||||
<div className="attachment-zip-master-toggle">
|
||||
<ToggleSwitch
|
||||
|
||||
Reference in New Issue
Block a user