Skip to content

Commit 81624db

Browse files
rpgmemclaude
andauthored
feat(templates): appointment-receipt config as Scheduling Settings tab + inline editor (#945) (#947)
Relocates the receipt-template selection from a standalone submenu into a new 'Receipt' tab on Scheduling → Settings, contributed via new ffc_scheduling_settings_tabs / _render_tab_* extension hooks (no cross-module reference). The tab adds an inline CodeMirror editor per mode: change the dropdown to live-load a template's HTML (AJAX), 'Duplicate to edit' a shipped/default into an editable copy, and Save persists the selection plus the edited HTML (wp_kses). Removes the old ffc-appointment-receipt submenu. Claude-Session: https://claude.ai/code/session_01R1xM9b1XBiRACXKcfa9Vwk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1502a93 commit 81624db

7 files changed

Lines changed: 662 additions & 104 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The format follows [Keep a Changelog] (https://keepachangelog.com/en/1.1.0/).
88
## [Unreleased]
99

1010
### Added
11-
- **Editable, pool-backed appointment-receipt templates (per mode)** (#945): the appointment receipt (comprovante) PDF is no longer a single hardcoded layout — it now draws from the certificate **template pool** (#865), reused via a `_ffc_template_kind` discriminator (absent = certificate, fully retrocompatible). Two receipt defaults ship (Regular + Custom), editable/duplicable like any pool template, with a Category column + filter and a kind-aware editor. A new **Scheduling → Appointment Receipt** settings page selects, globally per scheduling mode, which template each calendar type uses (Regular vs Custom), falling back to the shipped default when unset. Adds `{{appointment_end}}` / `{{appointment_time_range}}` receipt tokens. No data migration; unconfigured installs render exactly as before.
11+
- **Editable, pool-backed appointment-receipt templates (per mode)** (#945): the appointment receipt (comprovante) PDF is no longer a single hardcoded layout — it now draws from the certificate **template pool** (#865), reused via a `_ffc_template_kind` discriminator (absent = certificate, fully retrocompatible). Two receipt defaults ship (Regular + Custom), editable/duplicable like any pool template, with a Category column + filter in the Templates screen. A new **Receipt** tab under **Scheduling → Settings** selects, globally per scheduling mode, which template each calendar type uses (Regular vs Custom) **and edits its HTML inline** (CodeMirror; duplicate a default to get an editable copy), falling back to the shipped default when unset. Adds `{{appointment_end}}` / `{{appointment_time_range}}` receipt tokens. No data migration; unconfigured installs render exactly as before.
1212
- **Self-scheduling per-user block cap + occupancy report** (#941, phase 3): custom calendars gain `max_blocks_per_user` — a cap on how many distinct blocks a single user may hold (booking validation rejects a user over the limit, counting their active **and** waitlisted bookings; the admin overbook bypass skips it; `0` = no limit). Custom calendars also get a read-only **Occupancy Report** meta box pairing each block with its live booked/capacity, occupancy % and waitlist length. Off/empty by default; existing calendars unchanged.
1313
- **Self-scheduling waitlist with automatic FIFO promotion** (#941, phase 2): a per-calendar waitlist (both scheduling modes) — when a slot/block is full, users can join a queue (`waitlist_enabled`, optional `waitlist_capacity`) instead of being turned away. Queued bookings carry a new `waitlist` status excluded from every capacity count. When a spot frees up (a cancellation or an admin rejection), the oldest queued booking is promoted automatically and transactionally (`FOR UPDATE`) to confirmed or pending per the calendar's approval setting, with a promotion email; the front-end offers a "Join waitlist" action on full slots and reflects the queued status. Off by default, so existing calendars are unchanged.
1414
- **Custom (irregular) self-scheduling calendars** (#941, phase 1): a calendar can now use a `custom` schedule type — explicit date/time blocks with per-block vacancies (e.g. 03/09 08h–13h, 40 vagas) — instead of the `regular` weekday working-hours pattern. Blocks are configured in the calendar editor (date + start/end + capacity + optional label), stored as JSON alongside the existing config; the front-end calendar exposes only the configured dates and lists each block with its real time range and remaining vacancies. Booking enforces each block's own capacity via the existing `FOR UPDATE` slot check; a new `ffc_bypass_appointment_capacity` capability lets an authorised operator overbook a full block. The mode locks to `regular`/`custom` once a calendar has bookings.

‎assets/js/ffc-receipt-templates.js‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* FFC — Appointment-receipt templates tab (Scheduling Settings → Receipt, #945).
3+
*
4+
* Per scheduling mode (Regular / Custom): a dropdown selects which pool template
5+
* the comprovante PDF uses, and an inline CodeMirror editor edits that template's
6+
* HTML. Shipped/default templates are read-only; "Duplicate to edit" creates an
7+
* editable copy via AJAX and selects it. Changing the dropdown live-loads the
8+
* chosen template's HTML.
9+
*
10+
* @since 6.20.0
11+
*/
12+
( function ( $ ) {
13+
'use strict';
14+
15+
var cfg = window.ffcReceiptTemplates || {};
16+
17+
$( function () {
18+
var modes = cfg.modes || [ 'regular', 'custom' ];
19+
var editors = {};
20+
21+
modes.forEach( function ( mode ) {
22+
var editorId = 'ffc_receipt_' + mode + '_editor';
23+
var $ta = $( '#' + editorId );
24+
if ( ! $ta.length ) {
25+
return;
26+
}
27+
28+
var cm = initEditor( editorId );
29+
if ( cm ) {
30+
editors[ mode ] = cm;
31+
// The server rendered `readonly` for non-editable selections.
32+
setReadOnly( cm, $ta.prop( 'readonly' ) );
33+
}
34+
35+
// Dropdown change → live-load the chosen template's HTML.
36+
$( '#ffc_receipt_' + mode ).on( 'change', function () {
37+
loadTemplate( mode, parseInt( $( this ).val(), 10 ) || 0, editors );
38+
} );
39+
40+
// Duplicate the currently-selected template into an editable copy.
41+
$( '.ffc-receipt-duplicate[data-mode="' + mode + '"]' ).on( 'click', function () {
42+
duplicateTemplate( mode, editors );
43+
} );
44+
} );
45+
46+
// Sync every editor back to its textarea on submit.
47+
$( '.ffc-receipt-form' ).on( 'submit', function () {
48+
Object.keys( editors ).forEach( function ( mode ) {
49+
editors[ mode ].save();
50+
} );
51+
} );
52+
} );
53+
54+
/**
55+
* Initialize CodeMirror on a textarea; returns the CodeMirror instance or null.
56+
*
57+
* @param {string} textareaId Textarea element id.
58+
* @return {Object|null} CodeMirror instance.
59+
*/
60+
function initEditor( textareaId ) {
61+
if ( ! cfg.editorSettings || 'undefined' === typeof window.wp || ! window.wp.codeEditor ) {
62+
return null;
63+
}
64+
var editor;
65+
try {
66+
editor = window.wp.codeEditor.initialize( textareaId, cfg.editorSettings );
67+
} catch ( err ) {
68+
return null;
69+
}
70+
if ( ! editor || ! editor.codemirror ) {
71+
return null;
72+
}
73+
var cm = editor.codemirror;
74+
cm.on( 'change', function () {
75+
cm.save();
76+
} );
77+
return cm;
78+
}
79+
80+
/**
81+
* Toggle a CodeMirror instance's read-only state.
82+
*
83+
* @param {Object} cm CodeMirror instance.
84+
* @param {boolean} readonly Whether the editor is read-only.
85+
* @return {void}
86+
*/
87+
function setReadOnly( cm, readonly ) {
88+
cm.setOption( 'readOnly', readonly ? 'nocursor' : false );
89+
$( cm.getWrapperElement() ).toggleClass( 'ffc-editor-readonly', !! readonly );
90+
}
91+
92+
/**
93+
* Reflect an editable/read-only state across the editor, note and duplicate
94+
* button for a mode.
95+
*
96+
* @param {string} mode Scheduling mode.
97+
* @param {Object} cm CodeMirror instance.
98+
* @param {boolean} editable Whether the selected template is editable.
99+
* @return {void}
100+
*/
101+
function applyEditable( mode, cm, editable ) {
102+
setReadOnly( cm, ! editable );
103+
var strings = cfg.strings || {};
104+
$( '.ffc-receipt-note[data-mode="' + mode + '"]' ).text(
105+
editable ? ( strings.editableNote || '' ) : ( strings.readonlyNote || '' )
106+
);
107+
// The duplicate button only makes sense for a read-only selection.
108+
$( '.ffc-receipt-duplicate[data-mode="' + mode + '"]' ).toggle( ! editable );
109+
}
110+
111+
/**
112+
* Load a template's HTML into the mode's editor via AJAX.
113+
*
114+
* @param {string} mode Scheduling mode.
115+
* @param {number} id Template id (0 = shipped default).
116+
* @param {Object} editors Map of mode → CodeMirror.
117+
* @return {void}
118+
*/
119+
function loadTemplate( mode, id, editors ) {
120+
var cm = editors[ mode ];
121+
if ( ! cm ) {
122+
return;
123+
}
124+
$.post( cfg.ajaxUrl, {
125+
action: cfg.loadAction,
126+
nonce: cfg.nonce,
127+
id: id,
128+
} )
129+
.done( function ( res ) {
130+
if ( res && res.success && res.data ) {
131+
cm.setValue( res.data.html || '' );
132+
cm.save();
133+
applyEditable( mode, cm, !! res.data.editable );
134+
}
135+
} );
136+
}
137+
138+
/**
139+
* Duplicate the mode's currently-selected template into an editable copy.
140+
*
141+
* @param {string} mode Scheduling mode.
142+
* @param {Object} editors Map of mode → CodeMirror.
143+
* @return {void}
144+
*/
145+
function duplicateTemplate( mode, editors ) {
146+
var $select = $( '#ffc_receipt_' + mode );
147+
var id = parseInt( $select.val(), 10 ) || 0;
148+
149+
$.post( cfg.ajaxUrl, {
150+
action: cfg.dupAction,
151+
nonce: cfg.nonce,
152+
id: id,
153+
} )
154+
.done( function ( res ) {
155+
if ( ! res || ! res.success || ! res.data ) {
156+
window.alert( ( cfg.strings && cfg.strings.dupFailed ) || 'Error' );
157+
return;
158+
}
159+
// Add + select the new editable copy.
160+
$( '<option>' )
161+
.attr( 'value', res.data.id )
162+
.attr( 'data-default', '0' )
163+
.text( res.data.label )
164+
.appendTo( $select );
165+
$select.val( String( res.data.id ) );
166+
167+
var cm = editors[ mode ];
168+
if ( cm ) {
169+
cm.setValue( res.data.html || '' );
170+
cm.save();
171+
applyEditable( mode, cm, true );
172+
}
173+
} )
174+
.fail( function () {
175+
window.alert( ( cfg.strings && cfg.strings.dupFailed ) || 'Error' );
176+
} );
177+
}
178+
} )( jQuery );

‎assets/js/ffc-receipt-templates.min.js‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)