Add bulk calendar invitation delivery
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ApiSettings, AuthInfo } from "../../types";
|
||||
import {
|
||||
getCampaignCalendarInvitationCatalog,
|
||||
getCampaignPostboxCatalog,
|
||||
type CampaignCalendarCatalog,
|
||||
type CampaignPostboxCatalog
|
||||
} from "../../api/campaigns";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
@@ -49,8 +51,14 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
||||
templates: [],
|
||||
organization_units: []
|
||||
});
|
||||
const [calendarCatalog, setCalendarCatalog] = useState<CampaignCalendarCatalog>({
|
||||
available: false,
|
||||
calendars: []
|
||||
});
|
||||
const [postboxTargetsOpen, setPostboxTargetsOpen] = useState(false);
|
||||
const postboxModuleInstalled = usePlatformModuleInstalled("postbox");
|
||||
const calendarModuleInstalled = usePlatformModuleInstalled("calendar");
|
||||
const mailModuleInstalled = usePlatformModuleInstalled("mail");
|
||||
const isPolicyView = view === "policy";
|
||||
|
||||
const version = data.currentVersion;
|
||||
@@ -77,6 +85,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
||||
const rateLimit = asRecord(delivery.rate_limit);
|
||||
const retry = asRecord(delivery.retry);
|
||||
const postboxDelivery = asRecord(delivery.postbox);
|
||||
const calendarInvitation = asRecord(delivery.calendar_invitation);
|
||||
const fieldDefinitions = useMemo(
|
||||
() => getDraftFields(displayDraft),
|
||||
[displayDraft]
|
||||
@@ -123,6 +132,40 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
||||
settings.apiKey
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!calendarModuleInstalled) {
|
||||
setCalendarCatalog({
|
||||
available: false,
|
||||
reason: "The Calendar module is not active.",
|
||||
calendars: []
|
||||
});
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void getCampaignCalendarInvitationCatalog(settings, campaignId)
|
||||
.then((catalog) => {
|
||||
if (!cancelled) setCalendarCatalog(catalog);
|
||||
})
|
||||
.catch((cause) => {
|
||||
if (!cancelled) {
|
||||
setCalendarCatalog({
|
||||
available: false,
|
||||
reason: cause instanceof Error ? cause.message : String(cause),
|
||||
calendars: []
|
||||
});
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
campaignId,
|
||||
calendarModuleInstalled,
|
||||
settings.accessToken,
|
||||
settings.apiBaseUrl,
|
||||
settings.apiKey
|
||||
]);
|
||||
|
||||
function patchEditor(path: string[], value: unknown) {
|
||||
if (locked) return;
|
||||
setEditorState((current) => updateNested(current, path, value));
|
||||
@@ -329,6 +372,107 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card title="Calendar invitations" collapsible>
|
||||
<div className="form-grid compact responsive-form-grid">
|
||||
<ToggleSwitch
|
||||
label="Send individualized invitations"
|
||||
checked={getBool(calendarInvitation, "enabled")}
|
||||
disabled={locked || !calendarCatalog.available || !mailModuleInstalled}
|
||||
onChange={(checked) => patch(["delivery", "calendar_invitation", "enabled"], checked)}
|
||||
/>
|
||||
<FormField label="Tracking calendar">
|
||||
<select
|
||||
value={getText(calendarInvitation, "calendar_id")}
|
||||
disabled={locked || !calendarCatalog.available}
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "calendar_id"], event.target.value || null)}
|
||||
>
|
||||
<option value="">Select a calendar</option>
|
||||
{calendarCatalog.calendars.map((calendar) =>
|
||||
<option key={calendar.id} value={calendar.id} disabled={!calendar.writable}>
|
||||
{calendar.name}{calendar.writable ? "" : " (read-only)"}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="Summary template">
|
||||
<input
|
||||
value={getText(calendarInvitation, "summary_template")}
|
||||
disabled={locked}
|
||||
placeholder="Appointment with {{name}}"
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "summary_template"], event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Start (ISO 8601 or field template)">
|
||||
<input
|
||||
value={getText(calendarInvitation, "start_at_template")}
|
||||
disabled={locked}
|
||||
placeholder="2026-08-12T10:00 or {{appointment_start}}"
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "start_at_template"], event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="End (ISO 8601 or field template)">
|
||||
<input
|
||||
value={getText(calendarInvitation, "end_at_template")}
|
||||
disabled={locked}
|
||||
placeholder="2026-08-12T11:00 or {{appointment_end}}"
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "end_at_template"], event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Timezone">
|
||||
<input
|
||||
value={getText(calendarInvitation, "timezone", "UTC")}
|
||||
disabled={locked}
|
||||
placeholder="Europe/Berlin"
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "timezone"], event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Location template">
|
||||
<input
|
||||
value={getText(calendarInvitation, "location_template")}
|
||||
disabled={locked}
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "location_template"], event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Categories">
|
||||
<input
|
||||
value={(Array.isArray(calendarInvitation.categories) ? calendarInvitation.categories : []).join(", ")}
|
||||
disabled={locked}
|
||||
placeholder="Invitation, Campaign"
|
||||
onChange={(event) => patch(
|
||||
["delivery", "calendar_invitation", "categories"],
|
||||
event.target.value.split(",").map((value) => value.trim()).filter(Boolean)
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Description template">
|
||||
<textarea
|
||||
rows={3}
|
||||
value={getText(calendarInvitation, "description_template")}
|
||||
disabled={locked}
|
||||
onChange={(event) => patch(["delivery", "calendar_invitation", "description_template"], event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
{!mailModuleInstalled &&
|
||||
<DismissibleAlert tone="warning" dismissible={false}>
|
||||
Mail must be active to deliver iCalendar invitations.
|
||||
</DismissibleAlert>
|
||||
}
|
||||
{!calendarCatalog.available &&
|
||||
<DismissibleAlert tone="warning" dismissible={false}>
|
||||
{calendarCatalog.reason || "Calendar invitation tracking is unavailable."}
|
||||
</DismissibleAlert>
|
||||
}
|
||||
{calendarCatalog.available && calendarCatalog.calendars.every((item) => !item.writable) &&
|
||||
<DismissibleAlert tone="warning" dismissible={false}>
|
||||
No writable calendar is available for invitation tracking.
|
||||
</DismissibleAlert>
|
||||
}
|
||||
<p className="muted small-note">
|
||||
One METHOD:REQUEST attachment is frozen per recipient at build time. The Calendar mirror is created after delivery acceptance; attendee replies are shown in Campaign reports.
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-campaign.opt_ins_and_local_assistance.d0d23635" collapsible>
|
||||
<div className="toggle-grid">
|
||||
<ToggleSwitch label="i18n:govoplan-campaign.suggest_addresses_from_this_campaign.5ebe2aea" checked={getBool(optIns, "campaign_address_suggestions", true)} disabled={locked} onChange={(checked) => patchEditor(["opt_ins", "campaign_address_suggestions"], checked)} />
|
||||
|
||||
Reference in New Issue
Block a user