398 lines
18 KiB
PHP
398 lines
18 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use Reservair\Forms\RsvFormBuilder;
|
||
|
|
|
||
|
|
// Shared inline script for the elements data grid.
|
||
|
|
// $elements_with_ids: array of element objects already carrying an 'id' key.
|
||
|
|
// $next_id: the first integer not yet used as an id.
|
||
|
|
function rsv_elements_table_script(array $elements_with_ids, int $next_id, string $form_id, array $element_types, array $timetables = []): void {
|
||
|
|
$elements_json = json_encode($elements_with_ids);
|
||
|
|
$types_json = json_encode(array_values($element_types));
|
||
|
|
$timetables_json = json_encode(array_values($timetables));
|
||
|
|
?>
|
||
|
|
<script>
|
||
|
|
const rsv_element_types = <?= $types_json ?>;
|
||
|
|
const rsv_timetables = <?= $timetables_json ?>;
|
||
|
|
|
||
|
|
const RSV_EMAIL_DEFAULTS = {
|
||
|
|
accepted_subject: <?= json_encode('Rezervace přijata') ?>,
|
||
|
|
accepted_body: <?= json_encode("<h1>Vaše rezervace byla přijata</h1>\n<p>Vaše rezervace byla schválena. Těšíme se na vás!</p>") ?>,
|
||
|
|
refused_subject: <?= json_encode('Rezervace zamítnuta') ?>,
|
||
|
|
refused_body: <?= json_encode("<h1>Vaše rezervace byla zamítnuta</h1>\n<p>Vaše rezervace bohužel nebyla schválena. Zkuste prosím jiný termín.</p>") ?>,
|
||
|
|
};
|
||
|
|
|
||
|
|
const rsv_elements_source = (function(initial_items, next_id_start) {
|
||
|
|
const items = initial_items;
|
||
|
|
let next_id = next_id_start;
|
||
|
|
|
||
|
|
return {
|
||
|
|
get_page(skip, limit) {
|
||
|
|
skip = skip ?? 0;
|
||
|
|
limit = limit ?? 20;
|
||
|
|
return Promise.resolve({ total: items.length, data: items.slice(skip, skip + limit) });
|
||
|
|
},
|
||
|
|
put(id, data) {
|
||
|
|
const idx = items.findIndex(e => e.id === id);
|
||
|
|
if (idx === -1) return Promise.reject(new Error('Element not found'));
|
||
|
|
// Destructure reservation-specific fields so they don't bleed into extra_attrs.
|
||
|
|
const { id: _id, name: _n, label: _l, type: _t, desc: _d, required: _r,
|
||
|
|
price_per_block: _p, email_templates: _et, timetable_id: _ti, ...extra_attrs } = items[idx];
|
||
|
|
items[idx] = {
|
||
|
|
...extra_attrs,
|
||
|
|
id,
|
||
|
|
name: data.name ?? '',
|
||
|
|
label: data.label ?? '',
|
||
|
|
type: data.type ?? 'text',
|
||
|
|
desc: data.desc ?? '',
|
||
|
|
required: data.required === 'on',
|
||
|
|
...(data.type === 'input-text' ? {
|
||
|
|
validation: data.validation ?? '',
|
||
|
|
pattern: data.pattern ?? '',
|
||
|
|
pattern_message: data.pattern_message ?? '',
|
||
|
|
} : {}),
|
||
|
|
...(data.type === 'reservation' ? {
|
||
|
|
timetable_id: data.timetable_id ? parseInt(data.timetable_id) : null,
|
||
|
|
price_per_block: parseFloat(data.price_per_block ?? '0') || 0,
|
||
|
|
email_templates: {
|
||
|
|
on_accepted: {
|
||
|
|
enabled: !!data.email_accepted_enabled,
|
||
|
|
subject: data.email_accepted_subject ?? RSV_EMAIL_DEFAULTS.accepted_subject,
|
||
|
|
body: data.email_accepted_body ?? RSV_EMAIL_DEFAULTS.accepted_body,
|
||
|
|
},
|
||
|
|
on_refused: {
|
||
|
|
enabled: !!data.email_refused_enabled,
|
||
|
|
subject: data.email_refused_subject ?? RSV_EMAIL_DEFAULTS.refused_subject,
|
||
|
|
body: data.email_refused_body ?? RSV_EMAIL_DEFAULTS.refused_body,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
} : {}),
|
||
|
|
};
|
||
|
|
return Promise.resolve(items[idx]);
|
||
|
|
},
|
||
|
|
add() {
|
||
|
|
const item = { id: next_id++, name: '', label: '', type: 'text', desc: '', required: false };
|
||
|
|
items.push(item);
|
||
|
|
return item;
|
||
|
|
},
|
||
|
|
move_up(id) {
|
||
|
|
const idx = items.findIndex(e => e.id === id);
|
||
|
|
if (idx > 0) [items[idx - 1], items[idx]] = [items[idx], items[idx - 1]];
|
||
|
|
},
|
||
|
|
move_down(id) {
|
||
|
|
const idx = items.findIndex(e => e.id === id);
|
||
|
|
if (idx !== -1 && idx < items.length - 1) [items[idx], items[idx + 1]] = [items[idx + 1], items[idx]];
|
||
|
|
},
|
||
|
|
remove(id) {
|
||
|
|
const idx = items.findIndex(e => e.id === id);
|
||
|
|
if (idx !== -1) items.splice(idx, 1);
|
||
|
|
},
|
||
|
|
get_all() {
|
||
|
|
return items.map(({ id, ...rest }) => rest);
|
||
|
|
},
|
||
|
|
};
|
||
|
|
})(<?= $elements_json ?>, <?= $next_id ?>);
|
||
|
|
|
||
|
|
function rsv_render_element_inline_form(dt, row, data) {
|
||
|
|
const builder = RsvInlineFormBuilder.create(rsv_elements_source)
|
||
|
|
.fieldset('Element', '50%')
|
||
|
|
.input_text('name', 'Slug', data?.name ?? '')
|
||
|
|
.input_text('label', 'Label', data?.label ?? '')
|
||
|
|
.input_select('type', 'Type', rsv_element_types, data?.type ?? rsv_element_types[0])
|
||
|
|
.fieldset('Options', '50%')
|
||
|
|
.input_text('desc', 'Description', data?.desc ?? '')
|
||
|
|
.input_checkbox('required', 'Required', data?.required ?? false);
|
||
|
|
|
||
|
|
if ((data?.type ?? rsv_element_types[0]) === 'reservation') {
|
||
|
|
const accepted = data?.email_templates?.on_accepted ?? {};
|
||
|
|
const refused = data?.email_templates?.on_refused ?? {};
|
||
|
|
const timetable_options = [
|
||
|
|
{ value: '', label: '— none —' },
|
||
|
|
...rsv_timetables.map(t => ({ value: t.id, label: t.name })),
|
||
|
|
];
|
||
|
|
builder
|
||
|
|
.input_select('timetable_id', 'Timetable', timetable_options, data?.timetable_id ?? '')
|
||
|
|
.input_number('price_per_block', 'Price per block', data?.price_per_block ?? 0)
|
||
|
|
.fieldset('Email — accepted', '100%')
|
||
|
|
.input_checkbox('email_accepted_enabled', 'Send email when accepted', accepted.enabled ?? true)
|
||
|
|
.input_text('email_accepted_subject', 'Subject', accepted.subject ?? RSV_EMAIL_DEFAULTS.accepted_subject)
|
||
|
|
.input_textarea('email_accepted_body', 'Body', accepted.body ?? RSV_EMAIL_DEFAULTS.accepted_body)
|
||
|
|
.fieldset('Email — refused', '100%')
|
||
|
|
.input_checkbox('email_refused_enabled', 'Send email when refused', refused.enabled ?? true)
|
||
|
|
.input_text('email_refused_subject', 'Subject', refused.subject ?? RSV_EMAIL_DEFAULTS.refused_subject)
|
||
|
|
.input_textarea('email_refused_body', 'Body', refused.body ?? RSV_EMAIL_DEFAULTS.refused_body);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((data?.type ?? rsv_element_types[0]) === 'input-text') {
|
||
|
|
builder
|
||
|
|
.input_select('validation', 'Validation', [
|
||
|
|
{ value: '', label: '— none —' },
|
||
|
|
{ value: 'email', label: 'Email' },
|
||
|
|
{ value: 'phone', label: 'Phone' },
|
||
|
|
{ value: 'digits', label: 'Digits only' },
|
||
|
|
{ value: 'pattern', label: 'Custom pattern' },
|
||
|
|
], data?.validation ?? '')
|
||
|
|
.input_text('pattern', 'Custom pattern (regex)', data?.pattern ?? '')
|
||
|
|
.show_if(RsvInlineFormBuilder.match_p('validation', 'pattern'))
|
||
|
|
.input_text('pattern_message', 'Pattern error message', data?.pattern_message ?? '')
|
||
|
|
.show_if(RsvInlineFormBuilder.match_p('validation', 'pattern'));
|
||
|
|
}
|
||
|
|
|
||
|
|
const node = builder.build({
|
||
|
|
id: data?.id,
|
||
|
|
colspan: 5,
|
||
|
|
save_label: 'Save',
|
||
|
|
on_success: () => elements_dt.refresh(),
|
||
|
|
on_cancel: () => elements_dt.refresh(),
|
||
|
|
});
|
||
|
|
|
||
|
|
// Type swaps whole fieldsets, so re-render the inline form on change.
|
||
|
|
// Common fields are carried over; type-specific fields reset to stored data.
|
||
|
|
const type_select = node.querySelector('select[name="type"]');
|
||
|
|
if (type_select) {
|
||
|
|
type_select.addEventListener('change', () => {
|
||
|
|
const current = Object.fromEntries(new FormData(node.querySelector('form')));
|
||
|
|
const merged = {
|
||
|
|
...data,
|
||
|
|
name: current.name ?? data?.name,
|
||
|
|
label: current.label ?? data?.label,
|
||
|
|
desc: current.desc ?? data?.desc,
|
||
|
|
required: 'required' in current,
|
||
|
|
type: type_select.value,
|
||
|
|
};
|
||
|
|
row.replaceChildren(rsv_render_element_inline_form(dt, row, merged));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return node;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The elements data grid only exists on the edit page. Guard it so a missing
|
||
|
|
// container can't abort the script and strip the form's submit handler.
|
||
|
|
const elements_table_el = document.getElementById('form_elements_table');
|
||
|
|
if (elements_table_el) {
|
||
|
|
var elements_dt = RsvDataGrid.create_data_grid(
|
||
|
|
elements_table_el,
|
||
|
|
rsv_elements_source,
|
||
|
|
{
|
||
|
|
'name': RsvDataGrid.action_column('Name', false, {
|
||
|
|
'Edit': RsvDataGrid.edit_action(function(dt, row, data) {
|
||
|
|
row.classList.add(
|
||
|
|
'inline-edit-row', 'inline-edit-row-post',
|
||
|
|
'quick-edit-row', 'quick-edit-row-post',
|
||
|
|
'inline-edit-post', 'inline-editor'
|
||
|
|
);
|
||
|
|
row.replaceChildren(rsv_render_element_inline_form(dt, row, data));
|
||
|
|
}),
|
||
|
|
'Move Up': RsvDataGrid.func_action(function(dt, row, data) {
|
||
|
|
rsv_elements_source.move_up(data.id);
|
||
|
|
dt.refresh();
|
||
|
|
}),
|
||
|
|
'Move Down': RsvDataGrid.func_action(function(dt, row, data) {
|
||
|
|
rsv_elements_source.move_down(data.id);
|
||
|
|
dt.refresh();
|
||
|
|
}),
|
||
|
|
'Remove': RsvDataGrid.func_action(function(dt, row, data) {
|
||
|
|
rsv_elements_source.remove(data.id);
|
||
|
|
dt.refresh();
|
||
|
|
}),
|
||
|
|
}),
|
||
|
|
'label': RsvDataGrid.column('Label', false),
|
||
|
|
'type': RsvDataGrid.column('Type', false),
|
||
|
|
'desc': RsvDataGrid.column('Description', false),
|
||
|
|
'required': RsvDataGrid.column('Required', false),
|
||
|
|
'details': RsvDataGrid.column('Details', false),
|
||
|
|
}
|
||
|
|
);
|
||
|
|
elements_dt.map_column('details', (dt, row, data) => {
|
||
|
|
const td = document.createElement('td');
|
||
|
|
if (data.type === 'reservation') {
|
||
|
|
const parts = [];
|
||
|
|
if (data.timetable_id) {
|
||
|
|
const t = rsv_timetables.find(t => t.id === data.timetable_id);
|
||
|
|
parts.push(`Timetable: ${t ? t.name : data.timetable_id}`);
|
||
|
|
}
|
||
|
|
if (data.price_per_block != null) parts.push(`Price/block: ${data.price_per_block}`);
|
||
|
|
const et = data.email_templates ?? {};
|
||
|
|
const emails = [];
|
||
|
|
if (et.on_accepted?.enabled) emails.push('accepted');
|
||
|
|
if (et.on_refused?.enabled) emails.push('refused');
|
||
|
|
if (emails.length) parts.push(`Emails: ${emails.join(', ')}`);
|
||
|
|
td.innerText = parts.join(' · ');
|
||
|
|
}
|
||
|
|
return td;
|
||
|
|
});
|
||
|
|
elements_dt.refresh();
|
||
|
|
|
||
|
|
document.getElementById('rsv_add_element_btn').onclick = function() {
|
||
|
|
rsv_elements_source.add();
|
||
|
|
elements_dt.refresh();
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
RsvAdminForm.bind(document.getElementById('<?= $form_id ?>'), {
|
||
|
|
transform: (body) => ({
|
||
|
|
name: body.name,
|
||
|
|
definition: {
|
||
|
|
email_key: body.definition?.email_key ?? '',
|
||
|
|
elements: rsv_elements_source.get_all(),
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
refresh: () => { if (typeof forms_dt !== 'undefined') forms_dt.refresh(); },
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
function rsv_form_info_page(): void {
|
||
|
|
global $rsv_form_registry;
|
||
|
|
$element_types = array_keys($rsv_form_registry->handlers);
|
||
|
|
$elements_with_ids = [];
|
||
|
|
$next_id = 1;
|
||
|
|
$timetables = (new RsvTimetableService())->get_all();
|
||
|
|
?>
|
||
|
|
|
||
|
|
<h1>Formuláře</h1>
|
||
|
|
<hr>
|
||
|
|
<div id="col-container" class="wp-clearfix">
|
||
|
|
<div id="col-left">
|
||
|
|
<div class="col-wrap">
|
||
|
|
<div class="form-wrap">
|
||
|
|
<h2>Přidat formulář</h2>
|
||
|
|
|
||
|
|
<form id="add_form_definition"
|
||
|
|
method="post"
|
||
|
|
data-method="POST"
|
||
|
|
data-success-msg="Form definition created."
|
||
|
|
action="<?= get_rest_url(null, 'reservations/v1/form-definition'); ?>">
|
||
|
|
<?php wp_nonce_field('my_action', 'my_nonce'); ?>
|
||
|
|
|
||
|
|
<?php echo RsvFormBuilder::create('add_form_definition')->text('name', 'Název')->render(); ?>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<hr>
|
||
|
|
<p class="submit">
|
||
|
|
<button type="submit" form="add_form_definition" class="button button-primary">Add Form Definition</button>
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="col-right">
|
||
|
|
<div class="col-wrap">
|
||
|
|
<div id="forms_table"></div>
|
||
|
|
<script>
|
||
|
|
var forms_dt = RsvDataGrid.create_data_grid(forms_table,
|
||
|
|
RsvFormDefinitionResource(), {
|
||
|
|
'form_id': RsvDataGrid.column('ID', false, 30),
|
||
|
|
'name': RsvDataGrid.action_column('Název', false, {
|
||
|
|
'Edit': RsvDataGrid.link_action((data) =>
|
||
|
|
`<?= menu_page_url('forms-settings', false) ?>&id=${data.form_id}&action=edit`
|
||
|
|
),
|
||
|
|
'Trash': RsvDataGrid.func_action(function(dt, row, data) {
|
||
|
|
dt.resource.delete(data.form_id).then(() => forms_dt.refresh()).catch(err => alert(err.message));
|
||
|
|
}),
|
||
|
|
'Clone': RsvDataGrid.func_action(function(dt, row, data) {
|
||
|
|
const base_url = `<?= get_rest_url(null, 'reservations/v1/form-definition') ?>`;
|
||
|
|
fetch(`${base_url}/${data.form_id}`, {
|
||
|
|
headers: { 'Accept': 'application/json' },
|
||
|
|
})
|
||
|
|
.then(r => {
|
||
|
|
if (!r.ok) return r.json().then(err => { throw new Error(err.error || 'Fetch failed'); });
|
||
|
|
return r.json();
|
||
|
|
})
|
||
|
|
.then(form_def => fetch(base_url, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
|
|
body: JSON.stringify({
|
||
|
|
name: 'Copy of ' + form_def.name,
|
||
|
|
definition: form_def.definition,
|
||
|
|
}),
|
||
|
|
}))
|
||
|
|
.then(r => {
|
||
|
|
if (!r.ok) return r.json().then(err => { throw new Error(err.error || 'Clone failed'); });
|
||
|
|
forms_dt.refresh();
|
||
|
|
})
|
||
|
|
.catch(err => alert(err.message));
|
||
|
|
}),
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
forms_dt.refresh();
|
||
|
|
</script>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
|
||
|
|
<?php rsv_elements_table_script($elements_with_ids, $next_id, 'add_form_definition', $element_types, $timetables); ?>
|
||
|
|
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
function rsv_form_definition_edit_page(int $id): void {
|
||
|
|
global $rsv_form_registry;
|
||
|
|
$element_types = array_keys($rsv_form_registry->handlers);
|
||
|
|
|
||
|
|
$repo = new RsvFormDefinitionRepository();
|
||
|
|
$form_def = $repo->get($id);
|
||
|
|
|
||
|
|
if ($form_def === null) {
|
||
|
|
echo '<div class="notice notice-error"><p>Form definition not found.</p></div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$definition = $form_def['definition'] ?? [];
|
||
|
|
$raw_elements = array_values($definition['elements'] ?? []);
|
||
|
|
|
||
|
|
$elements_with_ids = array_map(function (array $el, int $idx): array {
|
||
|
|
return array_merge($el, ['id' => $idx + 1]);
|
||
|
|
}, $raw_elements, array_keys($raw_elements));
|
||
|
|
|
||
|
|
$next_id = count($elements_with_ids) + 1;
|
||
|
|
$timetables = (new RsvTimetableService())->get_all();
|
||
|
|
|
||
|
|
?>
|
||
|
|
<h1>Edit Form: <?= esc_html($form_def['name']) ?></h1>
|
||
|
|
<a href="<?= menu_page_url('forms-settings', false) ?>">← Back to Forms</a>
|
||
|
|
<hr>
|
||
|
|
|
||
|
|
<form id="edit_form_definition"
|
||
|
|
method="post"
|
||
|
|
data-method="PUT"
|
||
|
|
data-success-msg="Form definition updated."
|
||
|
|
action="<?= get_rest_url(null, 'reservations/v1/form-definition/' . $id); ?>">
|
||
|
|
|
||
|
|
<?php
|
||
|
|
echo RsvFormBuilder::create('edit_form_definition')
|
||
|
|
->text('name', 'Name', '', true, $form_def['name'])
|
||
|
|
->text('definition.email_key', 'Email Key', "Name of the form field that holds the submitter's email address.", true, $definition['email_key'] ?? '')
|
||
|
|
->render();
|
||
|
|
?>
|
||
|
|
</form>
|
||
|
|
|
||
|
|
<hr>
|
||
|
|
<h2>Form Elements</h2>
|
||
|
|
<p>Define the fields that will appear in this form.</p>
|
||
|
|
<div id="form_elements_table"></div>
|
||
|
|
<p>
|
||
|
|
<button type="button" class="button" id="rsv_add_element_btn">+ Add Element</button>
|
||
|
|
</p>
|
||
|
|
<p class="submit">
|
||
|
|
<button type="submit" form="edit_form_definition" class="button button-primary">Update Form Definition</button>
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<?php rsv_elements_table_script($elements_with_ids, $next_id, 'edit_form_definition', $element_types, $timetables); ?>
|
||
|
|
|
||
|
|
<?php
|
||
|
|
}
|
||
|
|
|
||
|
|
function rsv_forms_page(): void {
|
||
|
|
if (isset($_GET['action'])) {
|
||
|
|
if ($_GET['action'] === 'edit' && isset($_GET['id'])) {
|
||
|
|
rsv_form_definition_edit_page(intval($_GET['id']));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
rsv_form_info_page();
|
||
|
|
}
|