Skip to main content

Campaign Planning Calendar

Overview

The Campaign Planner is a monthly calendar view that gathers every campaign and lays it out by day. It is built for administrators and marketing teams who need to plan when campaigns go out, spot days that are getting crowded, and catch drafts that were left unfinished.

The feature is entirely read-only: campaigns cannot be created, edited or deleted here, and it has no service layer of its own — it reads the same data as Campaign Management. Every entry on the calendar links back to that campaign's detail view.

In other words, the Planner is an alternative way into the same data, organised by time rather than by table rows.

At the top of the page, summary cards describe the month currently in view: the total number of campaigns plus a breakdown across the four statuses — Sent, Scheduled, Cancelled and Draft.

Business Flow

  1. The user opens /campaign-planner. The app checks campaign view permission, then sets the breadcrumb and highlights the matching sidebar entry.
  2. The view starts on the current month and computes the date range to fetch as seven days before the first of the month through seven days after the last, so the leading and trailing calendar cells that spill into neighbouring months are populated too.
  3. Four separate requests are issued, one per status, so each gets its own cache entry:
    • Sent and scheduled campaigns, ordered by start date, oldest first.
    • Draft and cancelled campaigns, ordered by last-updated date, newest first.
  4. The four result sets are merged and grouped by date, choosing the date field according to status: sent and scheduled campaigns use their start date, while drafts and cancelled campaigns use their last-updated date. Campaigns with no usable date are skipped.
  5. Within each day, entries are ordered by status priority first — scheduled, sent, draft, cancelled — then by date and time.
  6. The summary cards count only campaigns that actually fall within the month in view, excluding the seven-day padding on either side.
  7. While data is still loading, the summary cards show a dash instead of a number and the calendar area shows a loading indicator.
  8. Once loaded, a full-screen calendar is rendered with a colour legend for the four statuses above it.
  9. Each day cell shows up to three campaigns. Each entry is a coloured dot next to the campaign name and links through to the detail view.
  10. Hovering an entry reveals a tooltip with the status badge, the campaign name, the recipient audience (or "all friends on the OA" when no audience is specified), the recipient count, and a date line whose label changes with the status — sent on, scheduled for, cancelled on, or updated on.
  11. When a day holds more than three campaigns, a "+N more" control opens a popover listing everything for that day, with a header showing the date and total count. Each row links to the detail view as well.
  12. Changing the month on the calendar recomputes the date range and fetches the new month's data automatically.

Key Screens & Components

Page and main controller

/campaign-planner is a thin route that only wires up the breadcrumb, the sidebar and the permission check. All logic lives in the container (src/components/campaign-planner/campaign-planner.container.tsx), which fetches all four statuses, merges and groups the results by day, computes the monthly statistics, and renders the summary cards.

The container also tolerates two response shapes — a plain array and an array wrapped in an object — because the API returns different shapes to this page and to the list page.

Calendar

The calendar component draws the day cells and colour legend, renders the hover tooltip and the "+N more" control, and reports month changes back to the container.

Tooltip and day list

The hover tooltip and the full day list in the popover are separate components, but they share the same status badge, keeping colours and icons consistent across the page.

Status badge

A single status badge component defines the colour and icon for all four statuses: green for sent, blue for scheduled, red for cancelled and grey for draft. The label text is deliberately not hard-coded in that configuration — it is looked up from the translation layer at render time so that switching languages produces the right wording.

API endpoint used

The feature calls only Campaign Management's GET /campaign, four times per month change. Each call specifies the status, the date range, the sort order, and a result limit of 500.

Dependencies

  • Campaign Management — the single, central dependency: it supplies the data fetch, the status constants, the data model, and the destination of every link on the page.
  • Access control — the Planner reuses Campaign Management's view permission rather than defining its own. The server-side campaign module unlocks both the management page and the calendar together, while the sidebar uses its own key to highlight the active menu item.
  • Shared components — the standard page section header and the shared link component that works with the static export build.
  • Formatting note — date and number formatting on this page is pinned to an English locale, so it does not follow the language the user has selected.

Backend Details (CMS API)

There is no dedicated backend module

cms-api has no module or endpoint specific to the campaign planner. The page uses the very same GET /api/campaign as Campaign Management, simply calling it several times with different criteria. Everything the calendar does — grouping and ordering — happens entirely on the frontend.

That has several practical implications:

  • The backend does not compute the date window. The frontend sends the date range and sort order itself, so changing how the calendar displays data requires no backend change.
  • Firing four requests per month change is a frontend design choice, not an API constraint — the same endpoint accepts a status parameter normally.
  • The 500-record cap per request is set by the frontend. If a month holds more campaigns than that in a single status, the surplus silently never appears on the calendar.

Enforced permissions

Because it is the same endpoint, the enforced permissions are identical to Campaign Management's:

  • ModuleGate for the campaign module applies, and it is genuinely enforced. If campaigns are disabled for an organization, the calendar cannot load data any more than the list page can.
  • An OA-scoped token is required, so the calendar's contents are automatically limited to the selected channel.
  • There is no separate permission for the calendar, and no situation where a user can see a campaign on the calendar but cannot open its detail page.

Why the response shape appears in two forms

The note that the container handles both a bare array and an object-wrapped result traces back to the backend: GET /api/campaign returns a structure containing the rows, the filtered total, and the overall total — never a bare array. Handling both shapes is defensive coding, not a sign that the backend is inconsistent.

Some fields are assembled on the backend

The backend repository merges the audience data into each campaign row, which is why the calendar can show the recipient group's name and size in its hover card without issuing any additional requests.