@@ -106,6 +106,7 @@ class RsvFormsPage extends RsvAdminPage {
|
||||
|
||||
$next_id = count($elements_with_ids) + 1;
|
||||
$timetables = (new RsvTimetableService())->get_all();
|
||||
$programs = array_map(fn($p) => $p->to_array(), (new RsvMembershipProgramRepository())->all());
|
||||
|
||||
$email_key_options = ['' => '— select field —'];
|
||||
foreach ($raw_elements as $el) {
|
||||
@@ -130,6 +131,25 @@ class RsvFormsPage extends RsvAdminPage {
|
||||
->render();
|
||||
?>
|
||||
|
||||
<hr>
|
||||
<h2>Membership Discounts</h2>
|
||||
<p>Map a membership program to a discount percentage. The chosen field's value must match a key in that program for the discount to apply.</p>
|
||||
<?php
|
||||
$membership = $definition['membership'] ?? [];
|
||||
$bindings = $membership['bindings'] ?? [];
|
||||
?>
|
||||
<div id="rsv_membership_bindings_table"></div>
|
||||
<p>
|
||||
<button type="button" class="button" id="rsv_add_binding_btn">+ Add Binding</button>
|
||||
</p>
|
||||
<label>
|
||||
Combine mode:
|
||||
<select name="definition.membership_combine">
|
||||
<option value="max" <?= ($membership['combine'] ?? 'max') === 'max' ? 'selected' : '' ?>>Max (best discount wins)</option>
|
||||
<option value="sum" <?= ($membership['combine'] ?? 'max') === 'sum' ? 'selected' : '' ?>>Sum (capped at 100%)</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
<?php
|
||||
RsvColumnLayout::split('3:2')
|
||||
@@ -149,18 +169,22 @@ class RsvFormsPage extends RsvAdminPage {
|
||||
->output();
|
||||
?>
|
||||
|
||||
<?php $this->elements_table_script($elements_with_ids, $next_id, 'edit_form_definition', $element_types, $timetables); ?>
|
||||
<?php $this->elements_table_script($elements_with_ids, $next_id, 'edit_form_definition', $element_types, $timetables, $programs, $bindings); ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function elements_table_script(array $elements_with_ids, int $next_id, string $form_id, array $element_types, array $timetables = []): void {
|
||||
private function elements_table_script(array $elements_with_ids, int $next_id, string $form_id, array $element_types, array $timetables = [], array $programs = [], array $bindings = []): void {
|
||||
$elements_json = json_encode($elements_with_ids);
|
||||
$types_json = json_encode(array_values($element_types));
|
||||
$timetables_json = json_encode(array_values($timetables));
|
||||
$programs_json = json_encode(array_values($programs));
|
||||
$bindings_json = json_encode(array_values($bindings));
|
||||
$bindings_next = count($bindings) + 1;
|
||||
?>
|
||||
<script>
|
||||
const rsv_element_types = <?= $types_json ?>;
|
||||
const rsv_timetables = <?= $timetables_json ?>;
|
||||
const rsv_membership_programs = <?= $programs_json ?>;
|
||||
|
||||
const RSV_EMAIL_DEFAULTS = {
|
||||
accepted_subject: <?= json_encode('Rezervace přijata') ?>,
|
||||
@@ -249,15 +273,63 @@ class RsvFormsPage extends RsvAdminPage {
|
||||
};
|
||||
})(<?= $elements_json ?>, <?= $next_id ?>);
|
||||
|
||||
const rsv_bindings_source = (function(initial_items, next_id_start) {
|
||||
const items = (initial_items || []).map((b, i) => ({
|
||||
id: i + 1,
|
||||
program_id: parseInt(b.program_id) || 0,
|
||||
discount: parseFloat(b.discount) || 0,
|
||||
field: b.field ?? '',
|
||||
}));
|
||||
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('Binding not found'));
|
||||
items[idx] = {
|
||||
id,
|
||||
program_id: parseInt(data.program_id) || 0,
|
||||
discount: parseFloat(data.discount) || 0,
|
||||
field: data.field ?? '',
|
||||
};
|
||||
return Promise.resolve(items[idx]);
|
||||
},
|
||||
add() {
|
||||
const item = { id: next_id++, program_id: 0, discount: 0, field: '' };
|
||||
items.push(item);
|
||||
return item;
|
||||
},
|
||||
remove(id) {
|
||||
const idx = items.findIndex(e => e.id === id);
|
||||
if (idx !== -1) items.splice(idx, 1);
|
||||
},
|
||||
get_all() {
|
||||
return items
|
||||
.filter(b => b.program_id > 0)
|
||||
.map(({ id, ...rest }) => rest);
|
||||
},
|
||||
};
|
||||
})(<?= $bindings_json ?>, <?= $bindings_next ?>);
|
||||
|
||||
function rsv_collect_definition() {
|
||||
const form = document.getElementById('<?= $form_id ?>');
|
||||
const get = (n) => form?.querySelector(`[name="${n}"]`)?.value ?? '';
|
||||
|
||||
return {
|
||||
name: get('name'),
|
||||
definition: {
|
||||
email_key: get('definition.email_key'),
|
||||
success_message: get('definition.success_message'),
|
||||
elements: rsv_elements_source.get_all(),
|
||||
email_key: get('definition.email_key'),
|
||||
success_message: get('definition.success_message'),
|
||||
membership: {
|
||||
bindings: rsv_bindings_source.get_all(),
|
||||
combine: get('definition.membership_combine') || 'max',
|
||||
},
|
||||
elements: rsv_elements_source.get_all(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -468,8 +540,87 @@ class RsvFormsPage extends RsvAdminPage {
|
||||
}
|
||||
rsv_email_key_select?.addEventListener('focus', rsv_sync_email_key_options);
|
||||
|
||||
// Membership discount bindings — edited in a data grid backed by the
|
||||
// in-memory source above; persisted with the rest of the definition.
|
||||
function rsv_binding_program_options() {
|
||||
return [
|
||||
{ value: '', label: '— select program —' },
|
||||
...rsv_membership_programs.map(p => ({ value: p.id, label: p.name })),
|
||||
];
|
||||
}
|
||||
|
||||
function rsv_binding_field_options() {
|
||||
const options = [{ value: '', label: '— select field —' }];
|
||||
for (const el of rsv_elements_source.get_all()) {
|
||||
if (!el.name) continue;
|
||||
options.push({ value: el.name, label: el.label ? `${el.label} (${el.name})` : el.name });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function rsv_render_binding_inline_form(dt, row, data) {
|
||||
return RsvInlineFormBuilder.create(rsv_bindings_source)
|
||||
.fieldset('Discount binding', '100%')
|
||||
.input_select('program_id', 'Program', rsv_binding_program_options(), data?.program_id ?? '')
|
||||
.input_number('discount', 'Discount %', data?.discount ?? 0)
|
||||
.input_select('field', 'Field', rsv_binding_field_options(), data?.field ?? '')
|
||||
.build({
|
||||
id: data?.id,
|
||||
colspan: 3,
|
||||
save_label: 'Save',
|
||||
on_success: () => { rsv_bindings_dt.refresh(); rsv_schedule_preview(); },
|
||||
on_cancel: () => { rsv_bindings_dt.refresh(); rsv_schedule_preview(); },
|
||||
});
|
||||
}
|
||||
|
||||
const rsv_bindings_table_el = document.getElementById('rsv_membership_bindings_table');
|
||||
if (rsv_bindings_table_el) {
|
||||
var rsv_bindings_dt = RsvDataGrid.create_data_grid(
|
||||
rsv_bindings_table_el,
|
||||
rsv_bindings_source,
|
||||
{
|
||||
'program_id': RsvDataGrid.action_column('Program', 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_binding_inline_form(dt, row, data));
|
||||
}),
|
||||
'Remove': RsvDataGrid.func_action(function(dt, row, data) {
|
||||
rsv_bindings_source.remove(data.id);
|
||||
dt.refresh();
|
||||
rsv_schedule_preview();
|
||||
}),
|
||||
}),
|
||||
'discount': RsvDataGrid.column('Discount %', false),
|
||||
'field': RsvDataGrid.column('Field', false),
|
||||
}
|
||||
);
|
||||
rsv_bindings_dt.map_column('program_id', (dt, row, data) => {
|
||||
const td = document.createElement('td');
|
||||
const p = rsv_membership_programs.find(p => String(p.id) === String(data.program_id));
|
||||
td.innerText = p ? p.name : (data.program_id ? `#${data.program_id}` : '—');
|
||||
return td;
|
||||
});
|
||||
rsv_bindings_dt.map_column('field', (dt, row, data) => {
|
||||
const td = document.createElement('td');
|
||||
const el = rsv_elements_source.get_all().find(e => e.name === data.field);
|
||||
td.innerText = data.field ? (el && el.label ? `${el.label} (${el.name})` : data.field) : '—';
|
||||
return td;
|
||||
});
|
||||
rsv_bindings_dt.refresh();
|
||||
|
||||
document.getElementById('rsv_add_binding_btn')?.addEventListener('click', function() {
|
||||
rsv_bindings_source.add();
|
||||
rsv_bindings_dt.refresh();
|
||||
rsv_schedule_preview();
|
||||
});
|
||||
}
|
||||
|
||||
const rsv_meta_form = document.getElementById('<?= $form_id ?>');
|
||||
['name', 'definition.email_key', 'definition.success_message'].forEach((n) => {
|
||||
['name', 'definition.email_key', 'definition.success_message', 'definition.membership_combine'].forEach((n) => {
|
||||
const el = rsv_meta_form?.querySelector(`[name="${n}"]`);
|
||||
el?.addEventListener('input', rsv_schedule_preview);
|
||||
el?.addEventListener('change', rsv_schedule_preview);
|
||||
|
||||
Reference in New Issue
Block a user