mirror of
https://github.com/nextcloud/calendar.git
synced 2024-10-07 16:40:09 +02:00
Add an appointment booking page
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
parent
e1e97a7dcf
commit
a902e1aa46
33 changed files with 1621 additions and 688 deletions
|
@ -36,6 +36,9 @@ return [
|
|||
['name' => 'view#index', 'url' => '/{view}/{timeRange}/edit/{mode}/{objectId}/{recurrenceId}', 'verb' => 'GET', 'requirements' => ['view' => 'timeGridDay|timeGridWeek|dayGridMonth|listMonth'], 'postfix' => 'view.timerange.edit'],
|
||||
// Appointments
|
||||
['name' => 'appointment#index', 'url' => '/appointments/{userId}', 'verb' => 'GET'],
|
||||
['name' => 'appointment#show', 'url' => '/appointment/{token}', 'verb' => 'GET'],
|
||||
['name' => 'booking#getBookableSlots', 'url' => '/appointment/{appointmentConfigId}/slots', 'verb' => 'GET'],
|
||||
['name' => 'booking#bookSlot', 'url' => '/appointment/{appointmentConfigId}/book', 'verb' => 'POST'],
|
||||
// Public views
|
||||
['name' => 'publicView#public_index_with_branding', 'url' => '/p/{token}', 'verb' => 'GET'],
|
||||
['name' => 'publicView#public_index_with_branding', 'url' => '/p/{token}/{view}/{timeRange}', 'verb' => 'GET', 'postfix' => 'publicview.timerange'],
|
||||
|
|
|
@ -27,13 +27,17 @@ namespace OCA\Calendar\Controller;
|
|||
|
||||
use OCA\Calendar\AppInfo\Application;
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use OCA\Calendar\Exception\ClientException;
|
||||
use OCA\Calendar\Exception\ServiceException;
|
||||
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserManager;
|
||||
use RuntimeException;
|
||||
use function array_filter;
|
||||
|
||||
class AppointmentController extends Controller {
|
||||
|
@ -97,4 +101,49 @@ class AppointmentController extends Controller {
|
|||
TemplateResponse::RENDER_AS_PUBLIC
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @PublicPage
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function show(string $token): Response {
|
||||
try {
|
||||
$config = $this->configService->findByToken($token);
|
||||
} catch (ClientException $e) {
|
||||
if ($e->getHttpCode() === Http::STATUS_NOT_FOUND) {
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'appointments/404-booking',
|
||||
[],
|
||||
TemplateResponse::RENDER_AS_GUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$user = $this->userManager->get($config->getUserId());
|
||||
if ($user === null) {
|
||||
throw new ServiceException("Appointment config $token does not belong to a valid user");
|
||||
}
|
||||
$this->initialState->provideInitialState(
|
||||
'userInfo',
|
||||
[
|
||||
'uid' => $user->getUID(),
|
||||
'displayName' => $user->getDisplayName(),
|
||||
],
|
||||
);
|
||||
$this->initialState->provideInitialState(
|
||||
'config',
|
||||
$config
|
||||
);
|
||||
|
||||
return new TemplateResponse(
|
||||
Application::APP_ID,
|
||||
'appointments/booking',
|
||||
[],
|
||||
TemplateResponse::RENDER_AS_PUBLIC
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,24 +24,17 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\Calendar\Controller;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use OCA\Calendar\Exception\ServiceException;
|
||||
use OCA\Calendar\Http\JsonResponse;
|
||||
use OCA\Calendar\Service\Appointments\AppointmentConfigService;
|
||||
use OCA\Calendar\Service\Appointments\Booking;
|
||||
use OCA\Calendar\Service\Appointments\BookingService;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Calendar\IManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\IInitialStateService;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
/**
|
||||
* Class PublicViewController
|
||||
*
|
||||
* @package OCA\Calendar\Controller
|
||||
*/
|
||||
class BookingController extends Controller {
|
||||
|
||||
/** @var BookingService */
|
||||
|
@ -53,22 +46,11 @@ class BookingController extends Controller {
|
|||
/** @var AppointmentConfigService */
|
||||
private $appointmentConfigService;
|
||||
|
||||
/** @var IManager */
|
||||
private $manager;
|
||||
|
||||
/**
|
||||
* @param string $appName
|
||||
* @param IRequest $request an instance of the request
|
||||
* @param IConfig $config
|
||||
* @param IInitialStateService $initialStateService
|
||||
* @param IURLGenerator $urlGenerator
|
||||
*/
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
ITimeFactory $timeFactory,
|
||||
BookingService $bookingService,
|
||||
AppointmentConfigService $appointmentConfigService,
|
||||
IManager $manager) {
|
||||
AppointmentConfigService $appointmentConfigService) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->bookingService = $bookingService;
|
||||
|
@ -77,30 +59,63 @@ class BookingController extends Controller {
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws ServiceException
|
||||
* @throws \JsonException
|
||||
* @NoAdminRequired
|
||||
* @PublicPage
|
||||
*
|
||||
* @param int $appointmentConfigId
|
||||
* @param int $startTime UNIX time stamp for the start time in UTC
|
||||
* @param int $endTime UNIX time stamp for the start time in UTC
|
||||
* @param string $timeZone
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function getBookableSlots(int $appointmentConfigId, int $unixStartTime, int $unixEndTime) {
|
||||
public function getBookableSlots(int $appointmentConfigId,
|
||||
int $startTime,
|
||||
int $endTime,
|
||||
string $timeZone): JsonResponse {
|
||||
// Convert the timestamps to the beginning and end of the respective day in the specified timezone
|
||||
$tz = new DateTimeZone($timeZone);
|
||||
$startTimeInTz = (new DateTimeImmutable())
|
||||
->setTimestamp($startTime)
|
||||
->setTimezone($tz)
|
||||
->setTime(0, 0)
|
||||
->getTimestamp();
|
||||
$endTimeInTz = (new DateTimeImmutable())
|
||||
->setTimestamp($endTime)
|
||||
->setTimezone($tz)
|
||||
->setTime(23, 59, 59)
|
||||
->getTimestamp();
|
||||
|
||||
if ($startTimeInTz > $endTimeInTz) {
|
||||
return JsonResponse::fail('Invalid time range', Http::STATUS_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
// rate limit this to only allow ranges between 0 to 7 days
|
||||
if (ceil(($unixEndTime - $unixStartTime) / 86400) > 7) {
|
||||
return JsonResponse::error('Date Range too large.', 403);
|
||||
if (ceil(($endTimeInTz - $startTimeInTz) / 86400) > 7) {
|
||||
return JsonResponse::fail('Date Range too large.', Http::STATUS_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
$now = $this->timeFactory->getTime();
|
||||
if ($now > $endTimeInTz) {
|
||||
return JsonResponse::fail('Slot time range must be in the future', Http::STATUS_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
if ($this->timeFactory->getTime() > $unixStartTime || $this->timeFactory->getTime() > $unixEndTime) {
|
||||
throw new ServiceException('Booking time must be in the future', 403);
|
||||
try {
|
||||
$config = $this->appointmentConfigService->findById($appointmentConfigId);
|
||||
} catch (ServiceException $e) {
|
||||
return JsonResponse::fail(null, Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
|
||||
$appointmentConfig = $this->appointmentConfigService->findById($appointmentConfigId);
|
||||
$booking = new Booking($appointmentConfig, $unixStartTime, $unixEndTime);
|
||||
$data = $this->bookingService->getSlots($booking);
|
||||
return JsonResponse::success($data);
|
||||
return JsonResponse::success(
|
||||
$this->bookingService->getAvailableSlots($config, $startTimeInTz, $endTimeInTz)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $calendarData
|
||||
*/
|
||||
public function bookSlot(string $calendarData) {
|
||||
$this->bookingService->book($calendarData);
|
||||
public function bookSlot(int $appointmentConfigId,
|
||||
int $start,
|
||||
string $name,
|
||||
string $email,
|
||||
string $description): JsonResponse {
|
||||
return JsonResponse::success();
|
||||
|
||||
//$this->bookingService->book($calendarData);
|
||||
return JsonResponse::success();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace OCA\Calendar\Db;
|
|||
|
||||
use JsonSerializable;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use function json_decode;
|
||||
|
||||
/**
|
||||
* @method int getId()
|
||||
|
@ -50,7 +51,7 @@ use OCP\AppFramework\Db\Entity;
|
|||
* @method string|null getCalendarFreebusyUris()
|
||||
* @method void setCalendarFreebusyUris(?string $freebusyUris)
|
||||
* @method string getAvailability()
|
||||
* @method void setAvailability(string $availability)
|
||||
* @method void setAvailability(?string $availability)
|
||||
* @method int getLength()
|
||||
* @method void setLength(int $length)
|
||||
* @method int getIncrement()
|
||||
|
@ -90,7 +91,7 @@ class AppointmentConfig extends Entity implements JsonSerializable {
|
|||
/** @var string|null */
|
||||
protected $calendarFreebusyUris;
|
||||
|
||||
/** @var string */
|
||||
/** @var string|null */
|
||||
protected $availability;
|
||||
|
||||
/** @var int */
|
||||
|
@ -132,10 +133,14 @@ class AppointmentConfig extends Entity implements JsonSerializable {
|
|||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPrincipalUri() : string {
|
||||
public function getPrincipalUri(): string {
|
||||
return 'principals/users/' . $this->userId;
|
||||
}
|
||||
|
||||
public function getCalendarFreebusyUrisAsArray(): array {
|
||||
return json_decode($this->getCalendarFreebusyUris(), true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function jsonSerialize() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
|
|
|
@ -75,9 +75,7 @@ class AppointmentConfigMapper extends QBMapper {
|
|||
/**
|
||||
* @param string $token
|
||||
* @return AppointmentConfig
|
||||
* @throws DbException
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
*/
|
||||
public function findByToken(string $token) : AppointmentConfig {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
|
47
lib/Exception/ClientException.php
Normal file
47
lib/Exception/ClientException.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Exception;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class ClientException extends Exception {
|
||||
|
||||
/** @var int|null */
|
||||
private $httpCode;
|
||||
|
||||
public function __construct($message = "",
|
||||
$code = 0,
|
||||
Throwable $previous = null,
|
||||
int $httpCode = null) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->httpCode = $httpCode;
|
||||
}
|
||||
|
||||
public function getHttpCode(): ?int {
|
||||
return $this->httpCode;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Calendar App
|
||||
*
|
||||
|
@ -22,13 +23,16 @@ declare(strict_types=1);
|
|||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use OCA\Calendar\Db\AppointmentConfigMapper;
|
||||
use OCA\Calendar\Exception\ClientException;
|
||||
use OCA\Calendar\Exception\ServiceException;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\DB\Exception as DbException;
|
||||
|
||||
class AppointmentConfigService {
|
||||
|
@ -60,6 +64,7 @@ class AppointmentConfigService {
|
|||
/**
|
||||
* @param int $id
|
||||
* @param string $userId
|
||||
*
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function delete(int $id, string $userId): void {
|
||||
|
@ -72,6 +77,7 @@ class AppointmentConfigService {
|
|||
|
||||
/**
|
||||
* @param AppointmentConfig $appointmentConfig
|
||||
*
|
||||
* @return AppointmentConfig
|
||||
* @throws ServiceException
|
||||
*/
|
||||
|
@ -85,32 +91,53 @@ class AppointmentConfigService {
|
|||
|
||||
/**
|
||||
* @param int $id
|
||||
*
|
||||
* @return AppointmentConfig
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function findById(int $id): AppointmentConfig {
|
||||
try {
|
||||
return $this->mapper->findById($id);
|
||||
} catch (DbException |DoesNotExistException|MultipleObjectsReturnedException $e) {
|
||||
throw new ServiceException('Could not find a record for id', $e->getCode(), $e);
|
||||
} catch (DbException | DoesNotExistException | MultipleObjectsReturnedException $e) {
|
||||
throw new ClientException(
|
||||
'Could not find a record for id',
|
||||
$e->getCode(),
|
||||
$e,
|
||||
Http::STATUS_NOT_FOUND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function findByToken(string $token): AppointmentConfig {
|
||||
try {
|
||||
return $this->mapper->findByToken($token);
|
||||
} catch (DoesNotExistException $e) {
|
||||
throw new ClientException(
|
||||
"Appointment config $token does not exist",
|
||||
0,
|
||||
$e,
|
||||
Http::STATUS_NOT_FOUND
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
*
|
||||
* @return AppointmentConfig
|
||||
* @throws ServiceException
|
||||
*/
|
||||
public function findByIdAndUser(int $id, string $userId): AppointmentConfig {
|
||||
try {
|
||||
return $this->mapper->findByIdForUser($id, $userId);
|
||||
} catch (DbException |DoesNotExistException|MultipleObjectsReturnedException $e) {
|
||||
} catch (DbException | DoesNotExistException | MultipleObjectsReturnedException $e) {
|
||||
throw new ServiceException('Could not find a record for id', $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AppointmentConfig $appointmentConfig
|
||||
*
|
||||
* @return AppointmentConfig
|
||||
* @throws ServiceException
|
||||
*/
|
||||
|
@ -126,6 +153,7 @@ class AppointmentConfigService {
|
|||
* @param int $id ?? maybe pass the appointment
|
||||
* @param int $unixStartTime
|
||||
* @param int $unixEndTime
|
||||
*
|
||||
* @return array
|
||||
* @throws ServiceException
|
||||
*/
|
||||
|
@ -141,7 +169,7 @@ class AppointmentConfigService {
|
|||
// move this to controller
|
||||
try {
|
||||
$appointment = $this->mapper->findByIdForUser($id);
|
||||
} catch (DbException |DoesNotExistException|MultipleObjectsReturnedException $e) {
|
||||
} catch (DbException | DoesNotExistException | MultipleObjectsReturnedException $e) {
|
||||
throw new ServiceException('Appointment not found', 404, $e);
|
||||
}
|
||||
|
||||
|
@ -194,6 +222,7 @@ class AppointmentConfigService {
|
|||
* @param int $time
|
||||
* @param string $outboxUri
|
||||
* @param array $freeBusyUris
|
||||
*
|
||||
* @return [][]
|
||||
*
|
||||
* Check if slot is conflicting with existing appointments
|
||||
|
|
57
lib/Service/Appointments/AvailabilityGenerator.php
Normal file
57
lib/Service/Appointments/AvailabilityGenerator.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
|
||||
class AvailabilityGenerator {
|
||||
|
||||
/**
|
||||
* Generate intervals at which the user is generally available
|
||||
*
|
||||
* @param AppointmentConfig $config
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
*
|
||||
* @return Interval[]
|
||||
*/
|
||||
public function generate(AppointmentConfig $config,
|
||||
int $start,
|
||||
int $end): array {
|
||||
if ($config->getAvailability() === null) {
|
||||
// No availability -> full time range is available
|
||||
return [
|
||||
new Interval($start, $end),
|
||||
];
|
||||
}
|
||||
|
||||
// TODO: derive intervals from RRULE
|
||||
return [
|
||||
new Interval($start, $end),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Calendar App
|
||||
*
|
||||
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
|
||||
*
|
||||
* @author Anna Larch <anna.larch@gmx.net>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3 of the License, or any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public
|
||||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use Recurr\Exception\InvalidRRule;
|
||||
use Recurr\Exception\InvalidWeekday;
|
||||
use Recurr\Recurrence;
|
||||
use Recurr\Rule;
|
||||
use Recurr\Transformer\ArrayTransformer;
|
||||
use Recurr\Transformer\ArrayTransformerConfig;
|
||||
|
||||
class Booking {
|
||||
|
||||
/** @var AppointmentConfig */
|
||||
private $appointmentConfig;
|
||||
|
||||
/** @var int */
|
||||
private $startTime;
|
||||
|
||||
/** @var int */
|
||||
private $endTime;
|
||||
|
||||
/** @var Slot[] */
|
||||
private $slots;
|
||||
|
||||
public function __construct(AppointmentConfig $appointmentConfig, int $startTime, int $endTime, array $slots = []) {
|
||||
$this->appointmentConfig = $appointmentConfig;
|
||||
$this->startTime = $startTime;
|
||||
$this->endTime = $endTime;
|
||||
$this->slots = $slots;
|
||||
}
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getStartTime(): int {
|
||||
return $this->startTime;
|
||||
}
|
||||
|
||||
// Trait maybe?
|
||||
public function getStartTimeDTObj() : DateTimeImmutable {
|
||||
return (new DateTimeImmutable())->setTimestamp($this->startTime);
|
||||
}
|
||||
|
||||
public function getEndTimeDTObj() : DateTimeImmutable {
|
||||
return (new DateTimeImmutable())->setTimestamp($this->endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $startTime
|
||||
*/
|
||||
public function setStartTime(int $startTime): void {
|
||||
$this->startTime = $startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getEndTime(): int {
|
||||
return $this->endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $endTime
|
||||
*/
|
||||
public function setEndTime(int $endTime): void {
|
||||
$this->endTime = $endTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Slot[]
|
||||
*/
|
||||
public function getSlots(): array {
|
||||
return $this->slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Slot[] $slots
|
||||
*/
|
||||
public function setSlots(array $slots): void {
|
||||
$this->slots = $slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AppointmentConfig
|
||||
*/
|
||||
public function getAppointmentConfig(): AppointmentConfig {
|
||||
return $this->appointmentConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AppointmentConfig $appointmentConfig
|
||||
*/
|
||||
public function setAppointmentConfig(AppointmentConfig $appointmentConfig): void {
|
||||
$this->appointmentConfig = $appointmentConfig;
|
||||
}
|
||||
|
||||
public function generateSlots(): array {
|
||||
$slots = [];
|
||||
$unixStartTime = $this->getStartTime();
|
||||
$length = $this->getAppointmentConfig()->getTotalLength()*60;
|
||||
while(($unixStartTime + $length) <= $this->getEndTime() ) {
|
||||
$slots[] = new Slot($unixStartTime, $unixStartTime+$this->getAppointmentConfig()->getTotalLength()*60);
|
||||
$unixStartTime += $this->getAppointmentConfig()->getIncrement()*60;
|
||||
}
|
||||
$this->slots = $slots;
|
||||
return $slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $booked
|
||||
* @return int
|
||||
*/
|
||||
public function getAvailableSlotsAmount(int $booked): int {
|
||||
return ($this->appointmentConfig->getDailyMax() !== null) ? $this->appointmentConfig->getDailyMax() - $booked : 99999;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self;
|
||||
*/
|
||||
public function parseRRule(): self {
|
||||
try {
|
||||
// RRule Array Transformer does not work with constraints atm
|
||||
// so this is (kind of) superfluous
|
||||
$startDT = $this->getStartTimeDTObj();
|
||||
$endDT = $this->getEndTimeDTObj();
|
||||
// force UTC
|
||||
$startDT->setTimezone(new \DateTimeZone('UTC'));
|
||||
$endDT->setTimezone(new \DateTimeZone('UTC'));
|
||||
$rule = new Rule($this->appointmentConfig->getAvailability());
|
||||
} catch (InvalidRRule $e) {
|
||||
$this->slots = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
$config = new ArrayTransformerConfig();
|
||||
$config->enableLastDayOfMonthFix();
|
||||
|
||||
$transformer = new ArrayTransformer();
|
||||
$transformer->setConfig($config);
|
||||
|
||||
try {
|
||||
$collection = $transformer->transform($rule);
|
||||
} catch (InvalidWeekday $e) {
|
||||
// throw an error here?
|
||||
$this->slots = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->slots = $collection->map(function (Recurrence $slot) {
|
||||
$start = $slot->getStart()->getTimestamp();
|
||||
$end = $start + ($this->appointmentConfig->getTotalLength() * 60);
|
||||
return new Slot($start, $end);
|
||||
})->toArray();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -22,24 +22,36 @@ declare(strict_types=1);
|
|||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use OC\Calendar\CalendarQuery;
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use OCA\Calendar\Db\AppointmentConfigMapper;
|
||||
use OCP\Calendar\IManager;
|
||||
|
||||
class BookingService {
|
||||
|
||||
/** @var IManager */
|
||||
private $manager;
|
||||
/** @var AvailabilityGenerator */
|
||||
private $availabilityGenerator;
|
||||
|
||||
/** @var AppointmentConfigMapper */
|
||||
private $mapper;
|
||||
/** @var SlotExtrapolator */
|
||||
private $extrapolator;
|
||||
|
||||
public function __construct(IManager $manager,
|
||||
AppointmentConfigMapper $mapper) {
|
||||
$this->manager = $manager;
|
||||
$this->mapper = $mapper;
|
||||
/** @var DailyLimitFilter */
|
||||
private $dailyLimitFilter;
|
||||
|
||||
/** @var EventConflictFilter */
|
||||
private $eventConflictFilter;
|
||||
|
||||
public function __construct(AvailabilityGenerator $availabilityGenerator,
|
||||
SlotExtrapolator $extrapolator,
|
||||
DailyLimitFilter $dailyLimitFilter,
|
||||
EventConflictFilter $eventConflictFilter) {
|
||||
$this->availabilityGenerator = $availabilityGenerator;
|
||||
$this->extrapolator = $extrapolator;
|
||||
$this->dailyLimitFilter = $dailyLimitFilter;
|
||||
$this->eventConflictFilter = $eventConflictFilter;
|
||||
}
|
||||
|
||||
// CREATE
|
||||
|
@ -47,74 +59,20 @@ class BookingService {
|
|||
// use new ICreateFromString::create method
|
||||
}
|
||||
|
||||
public function getBookingInformation(string $token) {
|
||||
// unmarshal token for ID would be an option too
|
||||
// this also returns all bookings for this token
|
||||
// needs a unique identifier - X-NC-USERID or something?
|
||||
$config = $this->mapper->findByToken($token);
|
||||
$query = $this->manager->newQuery($config->getPrincipalUri());
|
||||
$query->addSearchCalendar($config->getTargetCalendarUri());
|
||||
return $this->manager->searchForPrincipal($query);
|
||||
}
|
||||
|
||||
public function getSlots(Booking $booking): array {
|
||||
$bookedSlots = $this->findBookedSlotsAmount($booking);
|
||||
|
||||
// negotiate available slots
|
||||
if ($booking->getAvailableSlotsAmount($bookedSlots) <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// decide if we want to use the complete 24 hour period to intersect via:
|
||||
// $booking->generateSlots();
|
||||
// Remove unavailable slots via comparing with RRule
|
||||
$booking->parseRRule();
|
||||
|
||||
// remove conflicting slots via calendar free busy
|
||||
$booking = $this->getCalendarFreeTimeblocks($booking);
|
||||
return $booking->getSlots();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Booking $booking
|
||||
* @return Booking
|
||||
*
|
||||
* Check if slot is conflicting with existing appointments
|
||||
* @return Interval[]
|
||||
*/
|
||||
public function getCalendarFreeTimeblocks(Booking $booking): Booking {
|
||||
$query = $this->manager->newQuery($booking->getAppointmentConfig()->getPrincipalUri());
|
||||
$query->addSearchCalendar($booking->getAppointmentConfig()->getTargetCalendarUri());
|
||||
public function getAvailableSlots(AppointmentConfig $config, int $startTime, int $endTime): array {
|
||||
// 1. Build intervals at which slots may be booked
|
||||
$availabilityIntervals = $this->availabilityGenerator->generate($config, $startTime, $endTime);
|
||||
// 2. Generate all possible slots
|
||||
$allPossibleSlots = $this->extrapolator->extrapolate($config, $availabilityIntervals);
|
||||
// 3. Filter out the daily limits
|
||||
$filteredByDailyLimit = $this->dailyLimitFilter->filter($config, $allPossibleSlots);
|
||||
// 4. Filter out booking conflicts
|
||||
$filteredByConflict = $this->eventConflictFilter->filter($config, $filteredByDailyLimit);
|
||||
|
||||
if (!empty($booking->getAppointmentConfig()->getCalendarFreebusyUris())) {
|
||||
foreach ($booking->getAppointmentConfig()->getCalendarFreebusyUris() as $uri) {
|
||||
$query->addSearchCalendar($uri);
|
||||
}
|
||||
}
|
||||
|
||||
$slots = $booking->getSlots();
|
||||
foreach ($slots as $k => $slot) {
|
||||
$query->setTimerangeStart($slot->getStartTimeDTObj());
|
||||
$query->setTimerangeEnd($slot->getEndTimeDTObj());
|
||||
// cache the query maybe? or maybe run everything at once?
|
||||
$events = $this->manager->searchForPrincipal($query);
|
||||
if (!empty($events)) {
|
||||
unset($slots[$k]);
|
||||
}
|
||||
}
|
||||
$booking->setSlots($slots);
|
||||
return $booking;
|
||||
}
|
||||
|
||||
public function findBookedSlotsAmount(Booking $booking): int {
|
||||
/** @var CalendarQuery $query */
|
||||
$query = $this->manager->newQuery($booking->getAppointmentConfig()->getPrincipalUri());
|
||||
$query->addSearchCalendar($booking->getAppointmentConfig()->getTargetCalendarUri());
|
||||
$query->addSearchProperty('X-NC-APPOINTMENT');
|
||||
$query->setSearchPattern($booking->getAppointmentConfig()->getToken());
|
||||
$query->setTimerangeStart($booking->getStartTimeDTObj());
|
||||
$query->setTimerangeEnd($booking->getEndTimeDTObj());
|
||||
$events = $this->manager->searchForPrincipal($query);
|
||||
return count($events);
|
||||
return $filteredByConflict;
|
||||
}
|
||||
|
||||
// Update
|
||||
|
|
89
lib/Service/Appointments/DailyLimitFilter.php
Normal file
89
lib/Service/Appointments/DailyLimitFilter.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use OC\Calendar\CalendarQuery;
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use OCP\Calendar\IManager;
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
class DailyLimitFilter {
|
||||
|
||||
/** @var IManager */
|
||||
private $calendarManger;
|
||||
|
||||
public function __construct(IManager $calendarManger) {
|
||||
$this->calendarManger = $calendarManger;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AppointmentConfig $config
|
||||
* @param Interval[] $slots
|
||||
*
|
||||
* @return Interval[]
|
||||
*/
|
||||
public function filter(AppointmentConfig $config, array $slots): array {
|
||||
// 0. If there is no limit then we don't have to filter anything
|
||||
if ($config->getDailyMax() === 0) {
|
||||
return $slots;
|
||||
}
|
||||
|
||||
// 1. Find all days
|
||||
$days = [];
|
||||
foreach ($slots as $slot) {
|
||||
$startOfDay = $slot->getStartAsObject()->setTime(0, 0, 0, 0);
|
||||
$ts = $startOfDay->getTimestamp();
|
||||
$days[$ts] = $startOfDay;
|
||||
}
|
||||
|
||||
// 2. Check what days are bookable
|
||||
/** @var CalendarQuery $query */
|
||||
$query = $this->calendarManger->newQuery($config->getPrincipalUri());
|
||||
$query->addSearchCalendar($config->getTargetCalendarUri());
|
||||
$query->addSearchProperty('X-NC-APPOINTMENT');
|
||||
$query->setSearchPattern($config->getToken());
|
||||
$available = [];
|
||||
foreach ($days as $ts => $day) {
|
||||
$nextDay = $day->modify('+1 day');
|
||||
$query->setTimerangeStart($day);
|
||||
$query->setTimerangeEnd($nextDay);
|
||||
|
||||
$events = $this->calendarManger->searchForPrincipal($query);
|
||||
|
||||
// Only days with less than the max number are still available
|
||||
$available[$ts] = count($events) < $config->getDailyMax();
|
||||
}
|
||||
|
||||
// 3. Filter out the slots that are on an unavailable day
|
||||
return array_values(array_filter($slots, function(Interval $slot) use ($available): bool {
|
||||
$startOfDay = $slot->getStartAsObject()->setTime(0, 0, 0, 0);
|
||||
$ts = $startOfDay->getTimestamp();
|
||||
return $available[$ts];
|
||||
}));
|
||||
}
|
||||
}
|
72
lib/Service/Appointments/EventConflictFilter.php
Normal file
72
lib/Service/Appointments/EventConflictFilter.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use DateInterval;
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
use OCP\Calendar\IManager;
|
||||
use function array_filter;
|
||||
|
||||
class EventConflictFilter {
|
||||
|
||||
/** @var IManager */
|
||||
private $calendarManager;
|
||||
|
||||
public function __construct(IManager $calendarManager) {
|
||||
$this->calendarManager = $calendarManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter appointment slots to those that do not conflict with existing calendar events
|
||||
*
|
||||
* @todo try to combine slots a bit to lower the number of calendar queries
|
||||
*
|
||||
* @param AppointmentConfig $config
|
||||
* @param Interval[] $slots
|
||||
*
|
||||
* @return Interval[]
|
||||
*/
|
||||
public function filter(AppointmentConfig $config, array $slots): array {
|
||||
$query = $this->calendarManager->newQuery($config->getPrincipalUri());
|
||||
foreach ($config->getCalendarFreebusyUrisAsArray() as $uri) {
|
||||
$query->addSearchCalendar($uri);
|
||||
}
|
||||
// Always check the target calendar for conflicts
|
||||
$query->addSearchCalendar($config->getTargetCalendarUri());
|
||||
$preparationDuration = DateInterval::createFromDateString($config->getPreparationDuration() . ' minutes');
|
||||
$followUpDuration = DateInterval::createFromDateString($config->getFollowupDuration() . ' minutes');
|
||||
|
||||
return array_filter($slots, function(Interval $slot) use ($followUpDuration, $preparationDuration, $query): bool {
|
||||
$query->setTimerangeStart($slot->getStartAsObject()->sub($preparationDuration));
|
||||
$query->setTimerangeEnd($slot->getEndAsObject()->add($followUpDuration));
|
||||
|
||||
$objects = $this->calendarManager->searchForPrincipal($query);
|
||||
|
||||
// If there is at least one event at this time then the slot is taken
|
||||
return empty($objects);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Calendar App
|
||||
*
|
||||
|
@ -22,12 +23,21 @@ declare(strict_types=1);
|
|||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use JsonSerializable;
|
||||
|
||||
class Slot {
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class Interval implements JsonSerializable {
|
||||
|
||||
/** @var int */
|
||||
private $start;
|
||||
|
||||
/** @var int */
|
||||
private $end;
|
||||
|
||||
public function __construct(int $start, int $end) {
|
||||
|
@ -35,31 +45,26 @@ class Slot {
|
|||
$this->end = $end;
|
||||
}
|
||||
|
||||
public function getStartTime(): int {
|
||||
public function getStart(): int {
|
||||
return $this->start;
|
||||
}
|
||||
|
||||
public function setStartTime(int $start): void {
|
||||
$this->start = $start;
|
||||
}
|
||||
|
||||
public function getEndTime(): int {
|
||||
public function getEnd(): int {
|
||||
return $this->end;
|
||||
}
|
||||
|
||||
public function setEndTime(int $end): void {
|
||||
$this->end = $end;
|
||||
}
|
||||
|
||||
public function getStartTimeDTObj() : DateTimeImmutable {
|
||||
public function getStartAsObject(): DateTimeImmutable {
|
||||
return (new DateTimeImmutable())->setTimestamp($this->start);
|
||||
}
|
||||
|
||||
public function getEndTimeDTObj() : DateTimeImmutable {
|
||||
public function getEndAsObject(): DateTimeImmutable {
|
||||
return (new DateTimeImmutable())->setTimestamp($this->end);
|
||||
}
|
||||
|
||||
public function isViable(int $start, int $end): bool {
|
||||
return !($this->start > $start || $this->end < $end);
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'start' => $this->start,
|
||||
'end' => $this->end,
|
||||
];
|
||||
}
|
||||
}
|
56
lib/Service/Appointments/SlotExtrapolator.php
Normal file
56
lib/Service/Appointments/SlotExtrapolator.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Service\Appointments;
|
||||
|
||||
use OCA\Calendar\Db\AppointmentConfig;
|
||||
|
||||
class SlotExtrapolator {
|
||||
|
||||
/**
|
||||
* @param AppointmentConfig $config
|
||||
* @param Interval[] $availabilityIntervals
|
||||
* @param int $to
|
||||
*
|
||||
* @return Interval[]
|
||||
*/
|
||||
public function extrapolate(AppointmentConfig $config,
|
||||
array $availabilityIntervals): array {
|
||||
$increment = $config->getIncrement() * 60;
|
||||
$length = $config->getLength() * 60;
|
||||
$slots = [];
|
||||
|
||||
foreach ($availabilityIntervals as $available) {
|
||||
$from = $available->getStart();
|
||||
$to = $available->getEnd();
|
||||
|
||||
for ($t = $from; ($t + $length) <= $to; $t += $increment) {
|
||||
$slots[] = new Interval($t, $t + $length);
|
||||
}
|
||||
}
|
||||
|
||||
return $slots;
|
||||
}
|
||||
}
|
53
src/appointments/main-booking.js
Normal file
53
src/appointments/main-booking.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { linkTo } from '@nextcloud/router'
|
||||
import { translate, translatePlural } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
|
||||
import Booking from '../views/Appointments/Booking'
|
||||
|
||||
// CSP config for webpack dynamic chunk loading
|
||||
// eslint-disable-next-line
|
||||
__webpack_nonce__ = btoa(getRequestToken())
|
||||
|
||||
// Correct the root of the app for chunk loading
|
||||
// OC.linkTo matches the apps folders
|
||||
// OC.generateUrl ensure the index.php (or not)
|
||||
// We do not want the index.php since we're loading files
|
||||
// eslint-disable-next-line
|
||||
__webpack_public_path__ = linkTo('calendar', 'js/')
|
||||
|
||||
Vue.prototype.$t = translate
|
||||
Vue.prototype.$n = translatePlural
|
||||
|
||||
const config = loadState('calendar', 'config')
|
||||
const userInfo = loadState('calendar', 'userInfo')
|
||||
|
||||
export default new Vue({
|
||||
el: '#appointment-booking',
|
||||
render: h => h(Booking, {
|
||||
props: {
|
||||
config,
|
||||
userInfo,
|
||||
},
|
||||
}),
|
||||
})
|
|
@ -52,7 +52,7 @@
|
|||
v-if="index2 !== (shortcut.keys.length - 1)"
|
||||
:key="`${category.categoryId}-${index}-${index2}`"
|
||||
class="shortcut-section-item__spacer">
|
||||
{{ t('calendar', 'or') }}
|
||||
{{ $t('calendar', 'or') }}
|
||||
</span>
|
||||
</template>
|
||||
</span>
|
||||
|
|
166
src/components/Appointments/AppointmentDetails.vue
Normal file
166
src/components/Appointments/AppointmentDetails.vue
Normal file
|
@ -0,0 +1,166 @@
|
|||
<template>
|
||||
<Modal
|
||||
size="large"
|
||||
@close="$emit('close')">
|
||||
<div class="booking-appointment-details">
|
||||
<div class="booking-details">
|
||||
<Avatar
|
||||
:user="userInfo.uid"
|
||||
:display-name="userInfo.displayName"
|
||||
:disable-tooltip="true"
|
||||
:disable-menu="true"
|
||||
:size="64" />
|
||||
<div class="booking__display-name">
|
||||
<strong>{{ userInfo.displayName }}</strong>
|
||||
</div>
|
||||
<h2 class="booking__name">
|
||||
{{ config.name }}
|
||||
</h2>
|
||||
<span class="booking__description">{{ config.description }}</span>
|
||||
<span class="booking__time">{{ startTime }} - {{ endTime }}</span>
|
||||
</div>
|
||||
<div class="appointment-details">
|
||||
<h2>{{ $t('calendar', 'Enter details') }}</h2>
|
||||
<div>
|
||||
{{ $t('calendar', 'Name') }}
|
||||
</div>
|
||||
<input id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
class="no-close"
|
||||
required>
|
||||
<div>
|
||||
{{ $t('calendar', 'Email') }}
|
||||
</div>
|
||||
<input ref="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
autocapitalize="none"
|
||||
autocomplete="on"
|
||||
autocorrect="off"
|
||||
required>
|
||||
<div class="meeting-info">
|
||||
{{ $t('calendar', 'Please share anything that will help prepare for our meeting') }}
|
||||
<div class="meeting-text">
|
||||
<textarea
|
||||
id="biography"
|
||||
v-model="description"
|
||||
rows="8"
|
||||
autocapitalize="none"
|
||||
autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="button primary"
|
||||
@click="save">
|
||||
{{ $t('calendar', 'Book the appointment') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
|
||||
import { timeStampToLocaleTime } from '../../utils/localeTime'
|
||||
|
||||
export default {
|
||||
name: 'AppointmentDetails',
|
||||
components: {
|
||||
Avatar,
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
timeSlot: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
userInfo: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
description: '',
|
||||
email: '',
|
||||
name: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
startTime() {
|
||||
return timeStampToLocaleTime(this.timeSlot.start, this.timeZoneId)
|
||||
},
|
||||
endTime() {
|
||||
return timeStampToLocaleTime(this.timeSlot.end, this.timeZoneId)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
this.$emit('save', {
|
||||
slot: this.timeSlot,
|
||||
description: this.description,
|
||||
email: this.email,
|
||||
name: this.name,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .modal-container {
|
||||
width: calc(100vw - 120px) !important;
|
||||
height: calc(100vh - 120px) !important;
|
||||
max-width: 600px !important;
|
||||
max-height: 500px !important;
|
||||
}
|
||||
.booking-appointment-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.booking-details {
|
||||
padding-left: 30px;
|
||||
padding-top: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.appointment-details {
|
||||
padding-left: 120px;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.add-guest {
|
||||
display: block;
|
||||
color: var(--color-primary);
|
||||
background-color: transparent;
|
||||
}
|
||||
.meeting-info {
|
||||
padding-right: 10px;
|
||||
}
|
||||
.meeting-text {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
grid-area: 1 / 1;
|
||||
width: 100%;
|
||||
margin: 3px 3px 3px 0;
|
||||
padding: 7px 6px;
|
||||
color: var(--color-main-text);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-main-background);
|
||||
cursor: text;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary-element) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
63
src/components/Appointments/AppointmentSlot.vue
Normal file
63
src/components/Appointments/AppointmentSlot.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<!--
|
||||
- @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
-
|
||||
- @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<button class="appointment-slot" @click="$emit('click', $event)">
|
||||
{{ startTime }} - {{ endTime }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { timeStampToLocaleTime } from '../../utils/localeTime'
|
||||
|
||||
export default {
|
||||
name: 'AppointmentSlot',
|
||||
props: {
|
||||
start: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
end: {
|
||||
required: true,
|
||||
type: Number,
|
||||
},
|
||||
timeZoneId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dateTimeFormatter() {
|
||||
return Intl.DateTimeFormat(undefined, {
|
||||
timeZone: this.timeZoneId,
|
||||
timeStyle: 'full',
|
||||
dateStyle: 'short',
|
||||
})
|
||||
},
|
||||
startTime() {
|
||||
return timeStampToLocaleTime(this.start, this.timeZoneId)
|
||||
},
|
||||
endTime() {
|
||||
return timeStampToLocaleTime(this.end, this.timeZoneId)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|