WP Manifestindependent plugin directory
manifest / events / cpj-appointment-scheduler

CPJ Appointment Scheduler

WordPress Plugin for appointment scheduling.

by Paul Jarvis · github.com/cpj9251/cpj-appointment-scheduler · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/cpj9251/cpj-appointment-scheduler/archive/refs/heads/main.zip

A WordPress plugin for booking appointments: visitors pick a date and time on a front-end calendar, submit their contact info, and both they and the site admin get a confirmation email. Built as a Gutenberg block + React front end on top of a small, customer-centric REST API.

  • Author: Paul Jarvis (cpauljarvis.com)
  • Requires: PHP 8.2+, WordPress with the block editor
  • Status: v1 beta — see Roadmap for what's intentionally not built yet

How it works

  1. A page or post has the CPJ Appointment Scheduler block added to it.
  2. A visitor picks a date on the calendar (only dates with open slots are selectable), then a time, then enters their name/email/phone.
  3. On submit, the plugin finds-or-creates a Customer record (matched by email, so repeat visitors don't get duplicated) and creates a linked Appointment record.
  4. The visitor gets a confirmation email; the admin gets a notification email. Both are template-driven and editable from the settings screen.
  5. Admins manage everything from wp-admin: the Appointments and Customers post-list screens, each with a real Gutenberg block (not a classic meta box) for editing the underlying data.

The block editor preview for the scheduler block renders the exact same React component tree as the live front end — there's no separate "editor preview" to keep in sync.

Data model

Customer-centric, not appointment-centric: a customer can accumulate many appointments over time without their info being re-entered or duplicated.

cpj_customer (post)                cpj_appointment (post)
├─ meta: first_name                ├─ meta: customer_id ──┐
├─ meta: last_name                 ├─ meta: date          │
├─ meta: email                     ├─ meta: time          │
├─ meta: phone                     ├─ meta: duration_minutes
├─ meta: address                   ├─ meta: status (confirmed|cancelled|completed)
└─ meta: preferred_contact         └─ meta: notes
        ▲                                    │
        └────────────────────────────────────┘

Both post types register their meta via register_post_meta (validated, show_in_rest) and template-lock a dedicated Gutenberg block onto their editor screen, so wp-admin shows real form controls instead of the classic "Custom Fields" box. Because the customer-facing booking flow creates these posts anonymously (no logged-in user), post creation deliberately bypasses wp_kses for that one call — otherwise WordPress strips the block markup right back out of post_content before it's saved.

REST API

Namespace: cpj-appt-sched/v1. Availability/booking routes are public (anonymous) by design; the customer-appointments lookup is admin-only.

Method Route Auth Purpose
GET /availability?month=YYYY-MM Public Which dates in a month have at least one open slot
GET /availability/times?date=YYYY-MM-DD Public Open time slots for one date
POST /appointments Public Book an appointment (honeypot + rate-limited)
GET /customers/{id}/appointments edit_posts An admin-screen lookup for a customer's appointment history

Settings are read/written through WordPress's own /wp/v2/settings endpoint rather than a custom route — see below.

Abuse protection on POST /appointments: a hidden honeypot field (website) silently no-ops the request if filled in, and submissions are rate-limited per IP (5 requests / 10 minutes) via transients. There's no CAPTCHA in v1 — see Roadmap.

Settings

Stored as a single option, cpj_appt_scheduler_settings, registered with show_in_rest so it's readable/writable at /wp/v2/settings. Configurable from Settings → Appointment Scheduler:

Field Description
appointmentDurationMinutes Length of one appointment slot
minimumNoticeMinutes How far in advance a booking must be made
maximumAdvanceDays How far out bookings are allowed
weeklyAvailability Recurring open times per weekday
dateOverrides Per-date exceptions — close a whole day or block specific times
notifyEmail Admin address for new-booking notifications
customerEmailSubject / customerEmailBody Confirmation email sent to the customer
adminEmailSubject / adminEmailBody Notification email sent to the admin

Email bodies support {first_name}, {last_name}, {date}, and {time} placeholders, substituted at send time.

Project structure

cpj-appointment-scheduler.php   Plugin bootstrap
src/
├─ Plugin.php                   Wires everything up on load
├─ Blocks/SchedulerBlock.php    Registers + conditionally enqueues the booking block
├─ CustomPostTypes/             Appointments, Customers
├─ MetaFields/                  register_post_meta + block-template wiring per CPT
├─ Rest/                        Route registration + request handling
├─ Services/                    Business logic: Availability, Appointment, Email
└─ Settings/Settings.php        Options schema, defaults, sanitization
assets/src/
├─ block/                       Scheduler block registration + editor preview
├─ frontend/                    The booking flow itself (calendar → time → details → confirmation)
├─ metafields/                  Appointment CPT's editor block
├─ customer-metafields/         Customer CPT's editor block
└─ settings/                    Settings screen React app

assets/build/ is compiled output (gitignored). prev_version/ is the original procedural/jQuery plugin, kept for reference only — nothing in the active plugin loads it.

Development setup

composer install
npm install

npm run start     # webpack watch mode
npm run build     # production build (required before deploying)

Linting

Both linters are configured against shared rulesets and are expected to pass clean before anything ships:

composer run lint       # phpcs
composer run lint:fix   # phpcbf — re-run `lint` after, it doesn't catch everything

npm run lint             # eslint
npm run lint:fix         # eslint --fix

Note: phpcbf can occasionally mangle complex compound conditionals (e.g. a Yoda-condition fix inside an || expression) into something that no longer parses. Always run php -l on anything it touches before trusting the auto-fix.

Roadmap

Deliberately out of scope for v1 beta:

  • Payment collection at booking time.
  • A custom admin dashboard — v1 uses the default WordPress post-list screens (with a few extra sortable columns) rather than a bespoke React admin UI.
  • CAPTCHA/third-party anti-spam — the honeypot + rate limit is judged sufficient for now.
  • Plugin uninstall/cleanup routine — no uninstall.php yet; deactivating the plugin does not remove its data or options.