initial
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
<?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();
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
|
||||
function rsv_google_calendar_settings_page(): void {
|
||||
if (!current_user_can(RsvCapabilities::MANAGE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = new RsvGoogleCalendarService();
|
||||
$notice = null;
|
||||
|
||||
if (isset($_GET['connected'])) {
|
||||
$notice = ['type' => 'success', 'message' => 'Google Calendar connected successfully.'];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['rsv_google_settings_nonce'])) {
|
||||
if (!wp_verify_nonce($_POST['rsv_google_settings_nonce'], 'rsv_google_settings')) {
|
||||
wp_die('Security check failed.');
|
||||
}
|
||||
|
||||
if (isset($_POST['rsv_disconnect'])) {
|
||||
$service->disconnect();
|
||||
$notice = ['type' => 'success', 'message' => 'Disconnected from Google Calendar.'];
|
||||
} elseif (isset($_POST['rsv_register_webhook'])) {
|
||||
$result = $service->register_webhook();
|
||||
$notice = isset($result['id'])
|
||||
? ['type' => 'success', 'message' => 'Webhook registered.']
|
||||
: ['type' => 'error', 'message' => 'Webhook registration failed: ' . ($result['error'] ?? json_encode($result))];
|
||||
} elseif (isset($_POST['rsv_stop_webhook'])) {
|
||||
$service->stop_webhook();
|
||||
$notice = ['type' => 'success', 'message' => 'Webhook stopped.'];
|
||||
} else {
|
||||
update_option('rsv_google_client_id', sanitize_text_field($_POST['rsv_google_client_id'] ?? ''));
|
||||
update_option('rsv_google_calendar_id', sanitize_text_field($_POST['rsv_google_calendar_id'] ?? 'primary'));
|
||||
// Only overwrite the (encrypted) client secret when a new value is
|
||||
// supplied — the field renders blank, so otherwise saving any other
|
||||
// setting would wipe the stored secret.
|
||||
$client_secret = sanitize_text_field($_POST['rsv_google_client_secret'] ?? '');
|
||||
if ($client_secret !== '') {
|
||||
$service->set_client_secret($client_secret);
|
||||
}
|
||||
$notice = ['type' => 'success', 'message' => 'Settings saved.'];
|
||||
}
|
||||
}
|
||||
|
||||
$connected = $service->is_google_connected();
|
||||
$webhook_registered = $service->is_webhook_registered();
|
||||
$webhook_expiry = (int) get_option('rsv_google_webhook_expiration', 0);
|
||||
$client_id = esc_attr(get_option('rsv_google_client_id', ''));
|
||||
$cal_id = esc_attr(get_option('rsv_google_calendar_id', 'primary'));
|
||||
$oauth_url = esc_url($service->get_oauth_url());
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Google Calendar</h1>
|
||||
|
||||
<?php if ($notice): ?>
|
||||
<div class="notice notice-<?= esc_attr($notice['type']) ?> is-dismissible">
|
||||
<p><?= esc_html($notice['message']) ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('rsv_google_settings', 'rsv_google_settings_nonce'); ?>
|
||||
|
||||
<h2>OAuth Credentials</h2>
|
||||
<p>Create a project in <a href="https://console.cloud.google.com/" target="_blank">Google Cloud Console</a>, enable the <strong>Google Calendar API</strong>, and create OAuth 2.0 credentials. Set the authorised redirect URI to <code><?= esc_html(site_url('/wp-json/reservations/v1/google-callback')) ?></code>.</p>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="rsv_google_client_id">Client ID</label></th>
|
||||
<td><input class="regular-text" type="text" id="rsv_google_client_id" name="rsv_google_client_id" value="<?= $client_id ?>"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="rsv_google_client_secret">Client Secret</label></th>
|
||||
<td><input class="regular-text" type="password" id="rsv_google_client_secret" name="rsv_google_client_secret" placeholder="<?= $connected ? '(saved)' : '' ?>"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="rsv_google_calendar_id">Calendar ID</label></th>
|
||||
<td>
|
||||
<input class="regular-text" type="text" id="rsv_google_calendar_id" name="rsv_google_calendar_id" value="<?= $cal_id ?>">
|
||||
<p class="description">Use <code>primary</code> for the account's main calendar, or paste a specific calendar ID from Google Calendar settings.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button('Save Settings'); ?>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Connection</h2>
|
||||
|
||||
<?php if ($connected): ?>
|
||||
<p><span style="color:#46b450; font-weight:600;">✔ Connected to Google Calendar.</span></p>
|
||||
<button type="submit" name="rsv_disconnect" value="1" class="button button-secondary" style="color:#b32d2e;">
|
||||
Disconnect
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<p><span style="color:#dc3232; font-weight:600;">✘ Not connected.</span></p>
|
||||
<a href="<?= $oauth_url ?>" class="button button-primary">Connect with Google</a>
|
||||
<p class="description" style="margin-top:.5rem;">You must save the Client ID and Secret first.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($connected): ?>
|
||||
<hr>
|
||||
<h2>Webhook</h2>
|
||||
<p>The webhook lets Google Calendar notify this site when you confirm or cancel a reservation event, so the reservation state is updated automatically.</p>
|
||||
<?php $webhook_url = site_url('/wp-json/reservations/v1/google-calendar-hook'); ?>
|
||||
<p>Webhook URL: <code><?= esc_html($webhook_url) ?></code></p>
|
||||
|
||||
<?php if (!str_starts_with($webhook_url, 'https://')): ?>
|
||||
<div class="notice notice-warning inline">
|
||||
<p><strong>HTTPS required.</strong> Google Calendar only accepts webhook URLs served over HTTPS. This site is currently on HTTP, so webhook registration will be rejected by Google. Use a publicly accessible HTTPS address in production.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($webhook_registered): ?>
|
||||
<p><span style="color:#46b450; font-weight:600;">✔ Webhook active.</span>
|
||||
<?php if ($webhook_expiry): ?>
|
||||
Expires <?= esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $webhook_expiry)) ?>.
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<button type="submit" name="rsv_register_webhook" value="1" class="button">Re-register</button>
|
||||
<button type="submit" name="rsv_stop_webhook" value="1" class="button button-secondary" style="color:#b32d2e;">Stop</button>
|
||||
<?php else: ?>
|
||||
<p><span style="color:#dc3232; font-weight:600;">✘ Webhook not registered.</span></p>
|
||||
<button type="submit" name="rsv_register_webhook" value="1" class="button button-primary">Register Webhook</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
function rsv_reservations_page(): void {
|
||||
?>
|
||||
<h1>Form Submissions</h1>
|
||||
|
||||
<hr>
|
||||
<div id="reservations_table"></div>
|
||||
|
||||
<script>
|
||||
function rsv_fmt_utc(utc_str) {
|
||||
if (!utc_str) return '';
|
||||
return new Date(utc_str.replace(' ', 'T') + 'Z').toLocaleString();
|
||||
}
|
||||
|
||||
function rsv_cell_value(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function rsv_make_table(head_labels, rows_data, cell_fn) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('wp-list-table', 'widefat', 'fixed', 'striped', 'rsv-detail-table');
|
||||
|
||||
const thead = document.createElement('thead');
|
||||
const header_row = document.createElement('tr');
|
||||
for (const label of head_labels) {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = label;
|
||||
header_row.appendChild(th);
|
||||
}
|
||||
thead.appendChild(header_row);
|
||||
table.appendChild(thead);
|
||||
|
||||
const tbody = document.createElement('tbody');
|
||||
for (const row_data of rows_data) {
|
||||
const tr = document.createElement('tr');
|
||||
for (const cell of cell_fn(row_data)) {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = cell;
|
||||
tr.appendChild(td);
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
table.appendChild(tbody);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
function rsv_flatten_form_entries(obj, depth) {
|
||||
const rows = [];
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
||||
rows.push({ key, value: null, depth });
|
||||
for (const child of rsv_flatten_form_entries(val, depth + 1)) rows.push(child);
|
||||
} else if (Array.isArray(val)) {
|
||||
rows.push({ key, value: null, depth });
|
||||
val.forEach((item, i) => {
|
||||
if (item !== null && typeof item === 'object') {
|
||||
rows.push({ key: `[${i}]`, value: null, depth: depth + 1 });
|
||||
for (const child of rsv_flatten_form_entries(item, depth + 2)) rows.push(child);
|
||||
} else {
|
||||
rows.push({ key: `[${i}]`, value: rsv_cell_value(item), depth: depth + 1 });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
rows.push({ key, value: rsv_cell_value(val), depth });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function rsv_make_form_table(form_values) {
|
||||
const table = document.createElement('table');
|
||||
table.classList.add('wp-list-table', 'widefat', 'fixed', 'striped', 'rsv-detail-table');
|
||||
|
||||
const thead = document.createElement('thead');
|
||||
const header_row = document.createElement('tr');
|
||||
for (const label of ['Field', 'Value']) {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = label;
|
||||
header_row.appendChild(th);
|
||||
}
|
||||
thead.appendChild(header_row);
|
||||
table.appendChild(thead);
|
||||
|
||||
const tbody = document.createElement('tbody');
|
||||
for (const { key, value, depth } of rsv_flatten_form_entries(form_values, 0)) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const td_key = document.createElement('td');
|
||||
td_key.textContent = key;
|
||||
td_key.classList.add('rsv-form-key');
|
||||
td_key.style.setProperty('--rsv-depth', depth);
|
||||
if (value === null) td_key.classList.add('rsv-form-key--group');
|
||||
|
||||
const td_val = document.createElement('td');
|
||||
td_val.textContent = value ?? '';
|
||||
if (value === null) td_val.classList.add('rsv-form-val--null');
|
||||
|
||||
tr.appendChild(td_key);
|
||||
tr.appendChild(td_val);
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
table.appendChild(tbody);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
function rsv_render_reservation_detail(dt, row, data, detail) {
|
||||
const td = document.createElement('td');
|
||||
td.setAttribute('colspan', 3);
|
||||
td.classList.add('rsv-detail-expand');
|
||||
|
||||
const form_heading = document.createElement('h4');
|
||||
form_heading.textContent = 'Form Submission';
|
||||
form_heading.classList.add('rsv-detail-heading');
|
||||
|
||||
const form_values = detail.form_values ?? {};
|
||||
let form_content;
|
||||
if (Object.keys(form_values).length === 0) {
|
||||
form_content = document.createElement('p');
|
||||
form_content.textContent = 'No form values recorded.';
|
||||
form_content.classList.add('rsv-detail-empty');
|
||||
} else {
|
||||
form_content = rsv_make_form_table(form_values);
|
||||
}
|
||||
|
||||
const timetable_heading = document.createElement('h4');
|
||||
timetable_heading.textContent = 'Timetable Reservations';
|
||||
timetable_heading.classList.add('rsv-detail-heading');
|
||||
|
||||
const timetable_rows = detail.timetable_reservations ?? [];
|
||||
let timetable_content;
|
||||
if (timetable_rows.length === 0) {
|
||||
timetable_content = document.createElement('p');
|
||||
timetable_content.textContent = 'No timetable reservations.';
|
||||
timetable_content.classList.add('rsv-detail-empty');
|
||||
} else {
|
||||
timetable_content = rsv_make_table(
|
||||
['ID', 'Timetable', 'Start', 'End'],
|
||||
timetable_rows,
|
||||
r => [r.id, r.timetable_id, rsv_fmt_utc(r.start), rsv_fmt_utc(r.end)]
|
||||
);
|
||||
}
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.classList.add('rsv-detail-actions');
|
||||
|
||||
const close_btn = document.createElement('button');
|
||||
close_btn.classList.add('button');
|
||||
close_btn.textContent = 'Close';
|
||||
close_btn.onclick = () => dt.refresh_row(row, data);
|
||||
actions.appendChild(close_btn);
|
||||
|
||||
if (detail.pending_confirmation) {
|
||||
const base_url = `<?= get_rest_url(null, 'reservations/v1/reservation'); ?>/${data.id}`;
|
||||
|
||||
const accept_btn = document.createElement('button');
|
||||
accept_btn.classList.add('button', 'button-primary');
|
||||
accept_btn.textContent = 'Accept';
|
||||
accept_btn.onclick = () => {
|
||||
accept_btn.disabled = true;
|
||||
refuse_btn.disabled = true;
|
||||
fetch(base_url + '/accept', { method: 'POST', credentials: 'same-origin' })
|
||||
.then(() => dt.refresh())
|
||||
.catch(() => { accept_btn.disabled = false; refuse_btn.disabled = false; });
|
||||
};
|
||||
|
||||
const refuse_btn = document.createElement('button');
|
||||
refuse_btn.classList.add('button', 'button-secondary', 'rsv-btn-refuse');
|
||||
refuse_btn.textContent = 'Refuse';
|
||||
refuse_btn.onclick = () => {
|
||||
accept_btn.disabled = true;
|
||||
refuse_btn.disabled = true;
|
||||
fetch(base_url + '/refuse', { method: 'POST', credentials: 'same-origin' })
|
||||
.then(() => dt.refresh())
|
||||
.catch(() => { accept_btn.disabled = false; refuse_btn.disabled = false; });
|
||||
};
|
||||
|
||||
actions.appendChild(accept_btn);
|
||||
actions.appendChild(refuse_btn);
|
||||
}
|
||||
|
||||
td.replaceChildren(form_heading, form_content, timetable_heading, timetable_content, actions);
|
||||
return td;
|
||||
}
|
||||
|
||||
var reservations_dt = RsvDataGrid.create_data_grid(
|
||||
document.getElementById('reservations_table'),
|
||||
RsvReservationResource(),
|
||||
{
|
||||
'id': RsvDataGrid.action_column('ID', false, {
|
||||
'View': RsvDataGrid.func_action(function(dt, row, data) {
|
||||
const url = `<?= get_rest_url(null, 'reservations/v1/reservation'); ?>/${data.id}`;
|
||||
fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(detail => {
|
||||
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_reservation_detail(dt, row, data, detail));
|
||||
});
|
||||
}),
|
||||
}),
|
||||
'form_submit_id': RsvDataGrid.column('Form Submit', false),
|
||||
'is_confirmed': RsvDataGrid.column('Confirmed', false),
|
||||
}
|
||||
);
|
||||
reservations_dt.refresh();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
|
||||
use Reservair\Forms\RsvFormBuilder;
|
||||
|
||||
|
||||
function rsv_timetable_list_page() {
|
||||
?>
|
||||
<style>
|
||||
/*#col-left {
|
||||
width: 30%;
|
||||
}*/
|
||||
|
||||
/*#col-right {
|
||||
width: 70%;
|
||||
}*/
|
||||
</style>
|
||||
<h1>Timetables</h1>
|
||||
<hr>
|
||||
<div id="col-container" class="wp-clearfix">
|
||||
<div id="col-left">
|
||||
<div class="col-wrap">
|
||||
<div class="form-wrap">
|
||||
<h2>Add timetable</h2>
|
||||
|
||||
<?php
|
||||
$timetable_service = new RsvTimetableService();
|
||||
$existing_emails = $timetable_service->get_all_maintainer_emails();
|
||||
$existing_emails_json = json_encode($existing_emails);
|
||||
?>
|
||||
<datalist id="maintainer_email_suggestions">
|
||||
<?php foreach ($existing_emails as $email): ?>
|
||||
<option value="<?= esc_attr($email) ?>">
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
|
||||
<form id="add_timetable_form"
|
||||
method="post"
|
||||
data-method="POST"
|
||||
data-success-msg="Timetable created."
|
||||
action="<?= get_rest_url(null, 'reservations/v1/timetable'); ?>">
|
||||
|
||||
<?php
|
||||
echo RsvFormBuilder::create('add_timetable_form')
|
||||
->text('name', 'Name', 'Name of the timetable that can be reserved.', true)
|
||||
->number('block_size', 'Block length (minutes)', 'Duration of one reservable time block in minutes.', true, '', 1)
|
||||
->email('maintainer_email', 'Maintainer Email', 'Email address to notify when a reservation requires confirmation.', false, '', 'maintainer_email_suggestions')
|
||||
->submit('Add Timetable')
|
||||
->render();
|
||||
?>
|
||||
</form>
|
||||
<script>
|
||||
RsvAdminForm.bind(add_timetable_form, {
|
||||
transform: (body) => ({
|
||||
name: body.name,
|
||||
block_size: parseInt(body.block_size),
|
||||
maintainer_email: body.maintainer_email || null,
|
||||
}),
|
||||
refresh: () => availability_dt.refresh(),
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="col-right">
|
||||
<div class="col-wrap">
|
||||
<div id="availability_table">
|
||||
|
||||
</div>
|
||||
<script>
|
||||
var availability_dt = RsvDataGrid.create_data_grid(availability_table,
|
||||
RsvTimetableResource(), {
|
||||
'id': RsvDataGrid.column('ID', false, 20),
|
||||
'name': RsvDataGrid.action_column('Název', false, {
|
||||
'View': RsvDataGrid.link_action((data) => `<?= menu_page_url('timetable-settings', false) ?>&id=${data.id}&action=view`),
|
||||
'Edit': RsvDataGrid.link_action((data) => `<?= menu_page_url('timetable-settings', false) ?>&id=${data.id}&action=edit`),
|
||||
'Trash': RsvDataGrid.func_action((dt, row, data) => {
|
||||
if (confirm('Delete this timetable? This cannot be undone.')) {
|
||||
dt.resource.delete(data.id).then(() => dt.refresh());
|
||||
}
|
||||
}),
|
||||
}),
|
||||
'block_size': RsvDataGrid.column('Velikost bloku', false),
|
||||
});
|
||||
availability_dt.refresh();
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
function rsv_create_capacity_form($timetable_id) {
|
||||
$form = RsvFormBuilder::create("create_capacity_form", get_rest_url(null, 'reservations/v1/timetable/' . $timetable_id . '/capacity'));
|
||||
$form->date('date', 'First Date', 'Od kterého datumu platí tato kapacita.', true, new DateTime()->format('Y-m-d'));
|
||||
$form->group('Availability Range', fn($g) => $g
|
||||
->time('start_time', 'Start')
|
||||
->time('end_time', 'End')
|
||||
);
|
||||
$form->number('capacity', 'Capacity', 'How many reservations can overlap on the same time.', true, 1, 1);
|
||||
$form->number('min_lead_time_minutes', 'Minimum lead time (minutes)', 'How many minutes in advance must be the reservation created. This is useful if it takes some time to prepare the reservation.', true);
|
||||
$form->checkbox('requires_confirmation', 'Requires Confirmation?', 'If checked, all the reservations that overlap this capacity will require confirmation from maintainer. The maintainer will receive an email asking for the confirmation.', true);
|
||||
$form->custom('Is Repeating Event', function() {
|
||||
return '
|
||||
<input id="is_repeating" class="regular-text" type="checkbox" name="is_repeating" checked="true">
|
||||
<p>If the capacity is available repeatingly. For example: repeat each monday every week.</p>
|
||||
';
|
||||
});
|
||||
$form->number('repeat_period_in_days', 'Repeat Period (days)', 'How many days between each repetition.', true);
|
||||
$form->custom('Apply to Days', function() {
|
||||
return '
|
||||
<table class="option-table">
|
||||
<tbody>
|
||||
<tr class="form-day-names">
|
||||
<td>Monday</td>
|
||||
<td>Tuesday</td>
|
||||
<td>Wednesday</td>
|
||||
<td>Thursday</td>
|
||||
<td>Friday</td>
|
||||
<td>Saturday</td>
|
||||
<td>Sunday</td>
|
||||
</tr>
|
||||
<tr class="form-days">
|
||||
<td><input class="is-repeating-input" type="checkbox" name="monday"></td>
|
||||
<td><input class="is-repeating-input" type="checkbox" name="tuesday"></td>
|
||||
<td><input class="is-repeating-input" type="checkbox" name="wednesday"></td>
|
||||
<td><input class="is-repeating-input" type="checkbox" name="thursday"></td>
|
||||
<td><input class="is-repeating-input" type="checkbox" name="friday"></td>
|
||||
<td><input class="is-repeating-input" type="checkbox" name="saturday"></td>
|
||||
<td><input class="is-repeating-input" type="checkbox" name="sunday"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>';
|
||||
});
|
||||
$form->submit('Create Capacity', 'button-primary', 'submit');
|
||||
|
||||
?>
|
||||
<form
|
||||
id="create_capacity_form"
|
||||
data-method="POST"
|
||||
data-success-msg="Capacity created."
|
||||
action="<?= get_rest_url(null, 'reservations/v1/timetable/' . $timetable_id . '/capacity'); ?>" >
|
||||
<?php
|
||||
$form->output();
|
||||
|
||||
?>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
|
||||
function rsv_timetable_capacity_view($id) {
|
||||
$timetable_service = new RsvTimetableService();
|
||||
$timetable = $timetable_service->get($id);
|
||||
$gcal_service = new RsvGoogleCalendarService();
|
||||
$gcal_connected = $gcal_service->is_google_connected();
|
||||
$current_calendar_id = $timetable->google_calendar_id ?? null;
|
||||
$existing_emails = $timetable_service->get_all_maintainer_emails();
|
||||
?>
|
||||
|
||||
<h2>Settings</h2>
|
||||
|
||||
<datalist id="maintainer_email_list">
|
||||
<?php foreach ($existing_emails as $email): ?>
|
||||
<option value="<?= esc_attr($email) ?>">
|
||||
<?php endforeach; ?>
|
||||
</datalist>
|
||||
|
||||
<form id="timetable_settings_form"
|
||||
action="<?= esc_url(get_rest_url(null, 'reservations/v1/timetable/' . $id)) ?>"
|
||||
data-method="PATCH"
|
||||
data-success-msg="Settings saved.">
|
||||
|
||||
<?php
|
||||
RsvFormBuilder::create("timetable_settings_form")
|
||||
->text('name', 'Name', 'Name of the timetable that can be reserved.', true, $timetable->name)
|
||||
->number('block_size', 'Block length (minutes)', 'Duration of one reservable time block in minutes.', true, $timetable->block_size, 1)
|
||||
->email('maintainer_email', 'Maintainer Email', 'Email address to notify when a reservation requires confirmation.', false, $timetable->maintainer_email ?? '', 'maintainer_email_suggestions')
|
||||
->custom('Google Calendar', function() use ($gcal_connected) {
|
||||
if (!$gcal_connected) {
|
||||
return'<p>Not connected to Google Calendar.
|
||||
<a href="' . esc_url(admin_url('admin.php?page=rsv-google-calendar')) . '">Connect in settings →</a>
|
||||
</p>';
|
||||
} else {
|
||||
return '<select id="gcal_select" name="google_calendar_id" style="min-width:260px;">
|
||||
<option value="">— None —</option>
|
||||
</select>
|
||||
<p class="description">Sync reservations to this calendar.</p>';
|
||||
}
|
||||
})
|
||||
->submit('Save Settings', 'button-primary', 'submit')
|
||||
->output();
|
||||
?>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const gcalSelect = document.getElementById('gcal_select');
|
||||
const currentCalId = <?= json_encode($current_calendar_id) ?>;
|
||||
|
||||
if (gcalSelect) {
|
||||
fetch('<?= esc_js(get_rest_url(null, 'reservations/v1/google-calendars')) ?>', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-WP-Nonce': ReservairServiceAPI.nonce },
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(calendars => {
|
||||
for (const cal of calendars) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = cal.id;
|
||||
opt.textContent = cal.summary;
|
||||
if (cal.id === currentCalId) opt.selected = true;
|
||||
gcalSelect.appendChild(opt);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
gcalSelect.disabled = true;
|
||||
gcalSelect.options[0].textContent = 'Failed to load calendars';
|
||||
});
|
||||
}
|
||||
|
||||
RsvAdminForm.bind(timetable_settings_form);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<h2>Capacity</h2>
|
||||
|
||||
<p>Define capacities for timetable.</p>
|
||||
|
||||
<?php
|
||||
|
||||
rsv_create_capacity_form($id);
|
||||
|
||||
?>
|
||||
<script>
|
||||
(function() {
|
||||
const DAY_DOW = { monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 };
|
||||
|
||||
function nearest_weekday(base_str, dow) {
|
||||
const d = new Date(base_str + 'T00:00:00');
|
||||
const diff = (dow - d.getDay() + 7) % 7;
|
||||
d.setDate(d.getDate() + diff);
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
RsvAdminForm.bind(document.getElementById('create_capacity_form'), {
|
||||
// Expand one form submission into one capacity row per selected weekday.
|
||||
transform: (body, form) => {
|
||||
const fd = new FormData(form);
|
||||
|
||||
const is_repeating = fd.get('is_repeating') !== null;
|
||||
const repeat_period_in_days = is_repeating
|
||||
? parseInt(fd.get('repeat_period_in_days')) * parseInt(fd.get('repeat_period_multiplier'))
|
||||
: 1;
|
||||
const repeat_times = is_repeating ? parseInt(fd.get('repeat_times')) : 0;
|
||||
|
||||
const common = {
|
||||
start_time: time_to_minutes(fd.get('start_time')),
|
||||
end_time: time_to_minutes(fd.get('end_time')),
|
||||
capacity: parseInt(fd.get('capacity')),
|
||||
min_lead_time_minutes: parseInt(fd.get('min_lead_time_minutes')),
|
||||
requires_confirmation: fd.get('requires_confirmation') !== null,
|
||||
repeat_period_in_days,
|
||||
repeat_times,
|
||||
};
|
||||
|
||||
const base_date = fd.get('date');
|
||||
const selected_days = Object.keys(DAY_DOW).filter(day => fd.get(day) !== null);
|
||||
return selected_days.length > 0
|
||||
? selected_days.map(day => ({ ...common, date: nearest_weekday(base_date, DAY_DOW[day]) }))
|
||||
: [{ ...common, date: base_date }];
|
||||
},
|
||||
refresh: () => capacity_dt.refresh(),
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div id="capacity_table"></div>
|
||||
<script>
|
||||
function minutes_to_time(minutes) {
|
||||
const h = Math.floor(minutes / 60).toString().padStart(2, '0');
|
||||
const m = (minutes % 60).toString().padStart(2, '0');
|
||||
return `${h}:${m}`;
|
||||
}
|
||||
|
||||
function time_to_minutes(time_str) {
|
||||
const [h, m] = time_str.split(':').map(Number);
|
||||
return h * 60 + m;
|
||||
}
|
||||
|
||||
function rsv_render_capacity_inline_form(dt, row, data) {
|
||||
const resource_with_time_conversion = {
|
||||
...dt.resource,
|
||||
put(id, form_data) {
|
||||
return dt.resource.put(id, {
|
||||
...form_data,
|
||||
start_time: time_to_minutes(form_data.start_time),
|
||||
end_time: time_to_minutes(form_data.end_time),
|
||||
});
|
||||
},
|
||||
};
|
||||
return RsvInlineFormBuilder.create(resource_with_time_conversion)
|
||||
.input_hidden('min_lead_time_minutes', 0)
|
||||
.fieldset('Datum a čas', '50%')
|
||||
.input_date('date', 'Datum', data?.date ?? '')
|
||||
.input_time('start_time', 'Začátek', minutes_to_time(data?.start_time ?? 0))
|
||||
.input_time('end_time', 'Konec', minutes_to_time(data?.end_time ?? 0))
|
||||
.fieldset('Kapacita a opakování', '50%')
|
||||
.input_number('capacity', 'Kapacita', data?.capacity ?? '')
|
||||
.input_number('repeat_period_in_days', 'Dnů mezi opakováním', data?.repeat_period_in_days ?? '')
|
||||
.input_number('repeat_times', 'Počet opakování', data?.repeat_times ?? '')
|
||||
.input_checkbox('requires_confirmation', 'Vyžaduje potvrzení', data?.requires_confirmation == 1)
|
||||
.build({
|
||||
id: data?.id,
|
||||
colspan: 8,
|
||||
save_label: 'Aktualizovat',
|
||||
on_success: () => capacity_dt.refresh(),
|
||||
on_cancel: () => capacity_dt.refresh(),
|
||||
});
|
||||
}
|
||||
|
||||
var capacity_dt = RsvDataGrid.create_data_grid(capacity_table,
|
||||
RsvTimetableCapacityResource(<?= $id ?>),
|
||||
{
|
||||
'id': RsvDataGrid.column('ID', false),
|
||||
'date': RsvDataGrid.action_column('Datum', false, {
|
||||
'Edit': RsvDataGrid.edit_action((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_capacity_inline_form(dt, row, data));
|
||||
}),
|
||||
'Trash': RsvDataGrid.func_action((dt, row, data) => {
|
||||
dt.resource.delete(data.id).then(() => dt.refresh());
|
||||
}),
|
||||
}),
|
||||
'start_time': RsvDataGrid.column('Začátek', false),
|
||||
'end_time': RsvDataGrid.column('Konec', false),
|
||||
'capacity': RsvDataGrid.column('Kapacita', false),
|
||||
'repeat_period_in_days': RsvDataGrid.column('Dnů mezi opakováním', false),
|
||||
'repeat_times': RsvDataGrid.column('Počet opakování', false),
|
||||
'requires_confirmation': RsvDataGrid.column('Vyžaduje potvrzení', false),
|
||||
}, );
|
||||
capacity_dt.map_column('start_time', (dt, row, data) => {
|
||||
const td = document.createElement('td');
|
||||
td.innerText = minutes_to_time(data.start_time);
|
||||
return td;
|
||||
});
|
||||
capacity_dt.map_column('end_time', (dt, row, data) => {
|
||||
const td = document.createElement('td');
|
||||
td.innerText = minutes_to_time(data.end_time);
|
||||
return td;
|
||||
});
|
||||
|
||||
capacity_dt.refresh();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
function rsv_timetable_view_page(int $id): void {
|
||||
$timetable = (new RsvTimetableService())->get($id);
|
||||
if ($timetable === null) {
|
||||
echo '<div class="notice notice-error"><p>Timetable not found.</p></div>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<h1><?= esc_html($timetable->name) ?></h1>
|
||||
<a href="<?= esc_url(menu_page_url('timetable-settings', false)) ?>">← Back to Timetables</a>
|
||||
<hr>
|
||||
|
||||
<table class="form-table">
|
||||
<tr><th>Name</th><td><?= esc_html($timetable->name) ?></td></tr>
|
||||
<tr><th>Block size</th><td><?= esc_html($timetable->block_size) ?> minutes</td></tr>
|
||||
<?php if ($timetable->maintainer_email): ?>
|
||||
<tr><th>Maintainer email</th><td><?= esc_html($timetable->maintainer_email) ?></td></tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
|
||||
<h2>Reservations</h2>
|
||||
<div id="timetable_reservations_table"></div>
|
||||
<script>
|
||||
RsvDataGrid.create_data_grid(
|
||||
timetable_reservations_table,
|
||||
RsvTimetableReservationResource(<?= $id ?>),
|
||||
{
|
||||
'id': RsvDataGrid.column('ID', false),
|
||||
'reservation_id': RsvDataGrid.action_column('Reservation', false, {
|
||||
'Accept': RsvDataGrid.func_action(
|
||||
(dt, row, data) => RsvReservationClient.accept(data.reservation_id).then(() => dt.refresh()),
|
||||
(item) => item.pending_confirmation_id !== null
|
||||
),
|
||||
'Refuse': RsvDataGrid.func_action(
|
||||
(dt, row, data) => RsvReservationClient.refuse(data.reservation_id).then(() => dt.refresh()),
|
||||
(item) => item.pending_confirmation_id !== null
|
||||
),
|
||||
}),
|
||||
'start_utc': RsvDataGrid.column('Start', false),
|
||||
'end_utc': RsvDataGrid.column('End', false),
|
||||
'is_confirmed': RsvDataGrid.column('Status', false),
|
||||
}
|
||||
)
|
||||
.map_column('is_confirmed', (dt, row, data) => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = data.pending_confirmation_id !== null ? 'Pending'
|
||||
: data.is_confirmed == 1 ? 'Confirmed'
|
||||
: 'Refused';
|
||||
return td;
|
||||
})
|
||||
.refresh();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
function rsv_timetable_edit_page($id) {
|
||||
rsv_timetable_capacity_view($id);
|
||||
}
|
||||
|
||||
|
||||
function rsv_timetable_page() {
|
||||
if (isset($_GET['action'])) {
|
||||
if ($_GET['action'] === 'view' && isset($_GET['id'])) {
|
||||
rsv_timetable_view_page(intval($_GET['id']));
|
||||
return;
|
||||
} else if($_GET['action'] === 'edit' && isset($_GET['id'])) {
|
||||
rsv_timetable_edit_page(intval($_GET['id']));
|
||||
return;
|
||||
}
|
||||
// Deletion is intentionally not handled here: a state-changing GET is
|
||||
// CSRF-prone. Timetables are deleted via the nonce-authenticated REST
|
||||
// DELETE /timetable/{id} (see the "Trash" action in the list grid).
|
||||
}
|
||||
|
||||
rsv_timetable_list_page();
|
||||
}
|
||||
Reference in New Issue
Block a user