Group class term dates, single-class embed mode, and offering editing
CI / Tests (PHP 8.2) (pull_request) Successful in 45s
CI / Tests (PHP 8.1) (pull_request) Successful in 48s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 1m14s
CI / PHPStan (pull_request) Successful in 1m16s
CI / Tests (PHP 8.3) (pull_request) Successful in 37s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / Tests (PHP 8.2) (pull_request) Successful in 45s
CI / Tests (PHP 8.1) (pull_request) Successful in 48s
CI / No Debug Code (pull_request) Successful in 2s
CI / Coding Standards (pull_request) Successful in 1m14s
CI / PHPStan (pull_request) Successful in 1m16s
CI / Tests (PHP 8.3) (pull_request) Successful in 37s
CI / Build Plugin Zip (pull_request) Has been skipped
Group class offerings now carry real dates: the add/edit form takes a start date plus a sessions control (one-off, or weekly for N sessions; the end date is computed as start + (N-1) weeks via Offering::weeklyTermEnd). Dates are validated strictly (Y-m-d) and shown in the offerings list and on the student-facing class card, including the weekly session count. [us_group_classes offering="<id>"] (block attribute offeringId, chosen from a dropdown of active classes fetched from the public offerings endpoint) restricts the page to a single class so the enrolment flow can be embedded on a page dedicated to that class; a pinned class that is no longer offered reports itself closed instead of falling back to the catalog. Offerings are now editable from the admin screen: an Edit button prefills the shared add/edit form and saving posts usc_action=update. Updates always preserve the original owner and currency, and non-admin instructors can only load and update their own offerings. The form also gains the previously missing description field and an Active toggle (the admin-UI counterpart of the REST is_active flag) so an edit cannot wipe data the form never collected. Closes #59 Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
+55
-1
@@ -3,10 +3,11 @@
|
||||
'use strict';
|
||||
|
||||
const { registerBlockType } = wp.blocks;
|
||||
const { createElement: el } = wp.element;
|
||||
const { createElement: el, useState, useEffect } = wp.element;
|
||||
const { useBlockProps, InspectorControls } = wp.blockEditor;
|
||||
const { PanelBody, SelectControl, ToggleControl } = wp.components;
|
||||
const { useSelect } = wp.data;
|
||||
const apiFetch = wp.apiFetch;
|
||||
const ServerSideRender = wp.serverSideRender;
|
||||
const { __ } = wp.i18n;
|
||||
|
||||
@@ -42,6 +43,46 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dropdown of active group classes fetched from the plugin's public
|
||||
* offerings endpoint. Values are offering IDs; 0 means all classes.
|
||||
*/
|
||||
function GroupClassSelect(props) {
|
||||
const [offerings, setOfferings] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
apiFetch({ path: '/us-scheduler/v1/offerings?kind=group_class' })
|
||||
.then(setOfferings)
|
||||
.catch(() => setOfferings([]));
|
||||
}, []);
|
||||
|
||||
const options = [{ label: __('All classes', 'unsupervised-schedular'), value: '0' }].concat(
|
||||
(offerings || []).map((o) => ({
|
||||
label: o.title || __('(no title)', 'unsupervised-schedular'),
|
||||
value: String(o.id),
|
||||
}))
|
||||
);
|
||||
|
||||
// A previously chosen class that is no longer offered (deleted or
|
||||
// deactivated) keeps its stored id visible instead of silently
|
||||
// pretending "All classes" is selected.
|
||||
const value = String(props.value || 0);
|
||||
if (offerings !== null && !options.some((opt) => opt.value === value)) {
|
||||
options.push({
|
||||
label: __('Unavailable class #', 'unsupervised-schedular') + value,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
return el(SelectControl, {
|
||||
label: props.label,
|
||||
help: props.help,
|
||||
value: value,
|
||||
options: options,
|
||||
onChange: (newValue) => props.onChange(parseInt(newValue, 10) || 0),
|
||||
});
|
||||
}
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
name: 'us-scheduler/booking',
|
||||
@@ -116,6 +157,19 @@
|
||||
icon: 'groups',
|
||||
keywords: ['group', 'class', 'enrol'],
|
||||
shortcode: 'us_group_classes',
|
||||
attributes: {
|
||||
offeringId: { type: 'number', default: 0 },
|
||||
},
|
||||
inspector: (attributes, setAttributes) => el(
|
||||
PanelBody,
|
||||
{ title: __('Classes shown', 'unsupervised-schedular') },
|
||||
el(GroupClassSelect, {
|
||||
label: __('Class', 'unsupervised-schedular'),
|
||||
help: __('Show only one group class, for embedding on a page dedicated to it.', 'unsupervised-schedular'),
|
||||
value: attributes.offeringId,
|
||||
onChange: (offeringId) => setAttributes({ offeringId }),
|
||||
})
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
const errorBox = document.getElementById('us-group-error');
|
||||
const { restUrl, nonce } = usScheduler;
|
||||
|
||||
// When the shortcode/block pins a single offering, only that class is
|
||||
// shown, so the page can be embedded alongside a full class description.
|
||||
const singleOfferingId = Number(app.dataset.offering || 0);
|
||||
|
||||
function apiFetch(path, options = {}) {
|
||||
return fetch(restUrl + path, {
|
||||
...options,
|
||||
@@ -68,16 +72,39 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Parse a Y-m-d date into local time; new Date('Y-m-d') would parse as
|
||||
// UTC midnight and can display as the previous day in western timezones.
|
||||
function formatDate(ymd) {
|
||||
const [y, m, d] = ymd.split('-').map(Number);
|
||||
return new Date(y, m - 1, d).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function termLabel(o) {
|
||||
if (!o.term_start) return '';
|
||||
if (!o.term_end || o.term_end === o.term_start) {
|
||||
return formatDate(o.term_start);
|
||||
}
|
||||
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
||||
const sessions = Math.round((new Date(o.term_end) - new Date(o.term_start)) / weekMs) + 1;
|
||||
return `${formatDate(o.term_start)} – ${formatDate(o.term_end)} (${sessions} weekly sessions)`;
|
||||
}
|
||||
|
||||
function renderClasses(offerings) {
|
||||
const groups = offerings.filter((o) => o.kind === 'group_class');
|
||||
let groups = offerings.filter((o) => o.kind === 'group_class');
|
||||
if (singleOfferingId) {
|
||||
groups = groups.filter((o) => Number(o.id) === singleOfferingId);
|
||||
}
|
||||
if (!groups.length) {
|
||||
list.innerHTML = '<p>No group classes are open for enrolment right now.</p>';
|
||||
list.innerHTML = singleOfferingId
|
||||
? '<p>This class is not open for enrolment right now.</p>'
|
||||
: '<p>No group classes are open for enrolment right now.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = groups.map((o) => `
|
||||
<div class="us-class">
|
||||
<h3>${escHtml(o.title)}</h3>
|
||||
${termLabel(o) ? `<p>${escHtml(termLabel(o))}</p>` : ''}
|
||||
${o.schedule_note ? `<p>${escHtml(o.schedule_note)}</p>` : ''}
|
||||
${o.description ? `<p>${escHtml(o.description)}</p>` : ''}
|
||||
<p>${escHtml(Number(o.price).toFixed(2))} ${escHtml(o.currency)}</p>
|
||||
|
||||
Reference in New Issue
Block a user