375 lines
19 KiB
PHP
375 lines
19 KiB
PHP
<?php
|
|
|
|
use Reservair\Forms\RsvFormBuilder;
|
|
use Reservair\Layout\RsvColumnLayout;
|
|
|
|
class RsvTimetablePage extends RsvAdminPage {
|
|
|
|
protected function render_content(): void {
|
|
if (isset($_GET['action'])) {
|
|
if ($_GET['action'] === 'view' && isset($_GET['id'])) {
|
|
$this->show_view(intval($_GET['id']));
|
|
return;
|
|
} elseif ($_GET['action'] === 'edit' && isset($_GET['id'])) {
|
|
$this->show_capacity(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).
|
|
}
|
|
|
|
$this->show_list();
|
|
}
|
|
|
|
private function show_list(): void {
|
|
?>
|
|
<h1>Timetables</h1>
|
|
<hr>
|
|
<?php
|
|
$timetable_service = new RsvTimetableService();
|
|
$existing_emails = $timetable_service->get_all_maintainer_emails();
|
|
|
|
RsvColumnLayout::split('1:2')
|
|
->column(function () use ($existing_emails) {
|
|
echo RsvFormBuilder::create('add_timetable_form', get_rest_url(null, 'reservations/v1/timetable'), 'POST', 'Timetable created.')
|
|
->heading('Add timetable')
|
|
->datalist('maintainer_email_suggestions', $existing_emails)
|
|
->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();
|
|
?>
|
|
<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>
|
|
<?php })
|
|
->column(function () { ?>
|
|
<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>
|
|
<?php })
|
|
->output();
|
|
?>
|
|
<?php
|
|
}
|
|
|
|
private function show_view(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
|
|
}
|
|
|
|
private function show_capacity(int $id): void {
|
|
$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();
|
|
?>
|
|
|
|
<?php
|
|
RsvFormBuilder::create('timetable_settings_form', get_rest_url(null, 'reservations/v1/timetable/' . $id), 'PATCH', 'Settings saved.')
|
|
->heading('Settings')
|
|
->datalist('maintainer_email_suggestions', $existing_emails)
|
|
->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();
|
|
?>
|
|
|
|
<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 $this->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
|
|
}
|
|
|
|
private function create_capacity_form(int $timetable_id): void {
|
|
$form = RsvFormBuilder::create('create_capacity_form', get_rest_url(null, 'reservations/v1/timetable/' . $timetable_id . '/capacity'), 'POST', 'Capacity created.');
|
|
$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->output();
|
|
}
|
|
}
|