initial
This commit is contained in:
@@ -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