From cac4551262c87b71cb0d1b0767fd5c35731af9a3 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms Date: Tue, 11 Jun 2024 20:32:43 +0000 Subject: convert consultation creator to vue and fix several problems, fixes #3349 Closes #3349, #1872, #1871, and #3350 Merge request studip/studip!3039 --- app/controllers/consultation/admin.php | 11 +- .../consultation/consultation_controller.php | 11 +- app/controllers/consultation/overview.php | 2 + app/views/consultation/admin/create.php | 264 +---------- app/views/consultation/admin/edit.php | 10 +- app/views/consultation/admin/index.php | 2 +- app/views/consultation/admin/ungrouped.php | 2 +- app/views/consultation/overview/ungrouped.php | 11 + ....0.7_add_consecutive_flag_for_consultations.php | 45 ++ lib/classes/JsonApi/RouteMap.php | 4 + .../Routes/Consultations/SlotCreationCount.php | 105 +++++ lib/models/ConsultationBlock.php | 11 +- lib/models/ConsultationSlot.php | 45 +- resources/assets/javascripts/bootstrap/vue.js | 12 +- .../javascripts/lib/RestrictedDatesHelper.ts | 89 ++++ resources/assets/javascripts/studip-ui.js | 84 +--- resources/assets/stylesheets/scss/forms.scss | 22 + resources/vue/components/ConsultationCreator.vue | 496 +++++++++++++++++++++ resources/vue/components/Datepicker.vue | 146 ++++-- resources/vue/components/StudipTooltipIcon.vue | 3 + resources/vue/components/Timepicker.vue | 37 ++ 21 files changed, 1023 insertions(+), 389 deletions(-) create mode 100644 db/migrations/6.0.7_add_consecutive_flag_for_consultations.php create mode 100644 lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php create mode 100644 resources/assets/javascripts/lib/RestrictedDatesHelper.ts create mode 100644 resources/vue/components/ConsultationCreator.vue create mode 100644 resources/vue/components/Timepicker.vue diff --git a/app/controllers/consultation/admin.php b/app/controllers/consultation/admin.php index bc63a1a..8ee7575 100644 --- a/app/controllers/consultation/admin.php +++ b/app/controllers/consultation/admin.php @@ -138,6 +138,7 @@ class Consultation_AdminController extends ConsultationController $this->room = ''; $this->responsible = false; + $this->slot_count_threshold = self::SLOT_COUNT_THRESHOLD; // TODO: inst_default? if ($this->range instanceof User) { @@ -155,6 +156,8 @@ class Consultation_AdminController extends ConsultationController $block->range = $this->range; $this->responsible = $block->getPossibleResponsibilites(); } + + $this->response->add_header('X-No-Buttons', ''); } public function store_action() @@ -186,7 +189,7 @@ class Consultation_AdminController extends ConsultationController $end, Request::int('day-of-week'), Request::int('interval'), - Request::int('duration'), + $duration, $pause_time, $pause_duration ); @@ -214,6 +217,7 @@ class Consultation_AdminController extends ConsultationController $block->note = Request::get('note'); $block->size = Request::int('size', 1); $block->lock_time = Request::int('lock_time'); + $block->consecutive = Request::bool('consecutive', false); $slots = $block->createSlots(Request::int('duration'), $pause_time, $pause_duration); if (count($slots) === 0) { @@ -403,6 +407,7 @@ class Consultation_AdminController extends ConsultationController $this->block->mail_to_tutors = Request::bool('mail-to-tutors', false); $this->block->confirmation_text = trim(Request::get('confirmation-text')); $this->block->lock_time = Request::int('lock_time'); + $this->block->consecutive = Request::bool('consecutive', false); foreach ($this->block->slots as $slot) { $slot->note = ''; @@ -535,7 +540,7 @@ class Consultation_AdminController extends ConsultationController public function toggle_action($what, $state, $expired = false) { if ($what === 'messages') { - // TODO: Applicable everywhere? + // TODO: Applicable everywhere? $this->getUserConfig()->store( 'CONSULTATION_SEND_MESSAGES', (bool) $state @@ -808,7 +813,7 @@ class Consultation_AdminController extends ConsultationController _('Terminblöcke anlegen'), $this->createURL(), Icon::create('add') - )->asDialog('size=auto'); + )->asDialog('size=big'); $actions->addLink( _('Namen des Reiters ändern'), $this->tabURL($action === 'expired'), diff --git a/app/controllers/consultation/consultation_controller.php b/app/controllers/consultation/consultation_controller.php index d6927af..00e10ad 100644 --- a/app/controllers/consultation/consultation_controller.php +++ b/app/controllers/consultation/consultation_controller.php @@ -19,7 +19,7 @@ abstract class ConsultationController extends AuthenticatedController $this->range = Context::get(); $type = 'object'; } else { - $this->range = $GLOBALS['user']->getAuthenticatedUser(); + $this->range = User::findCurrent(); } if (!$this->range) { @@ -60,7 +60,7 @@ abstract class ConsultationController extends AuthenticatedController $this->render_template('consultation/not_found', $this->layout); } - protected function activateNavigation($path) + protected function activateNavigation($path): void { $path = ltrim($path, '/'); @@ -73,7 +73,7 @@ abstract class ConsultationController extends AuthenticatedController } } - protected function getConsultationTitle() + protected function getConsultationTitle(): string { return $this->range->getConfiguration()->CONSULTATION_TAB_TITLE; } @@ -103,7 +103,8 @@ abstract class ConsultationController extends AuthenticatedController return $block; } - protected function loadSlot($block_id, $slot_id) + + protected function loadSlot($block_id, $slot_id): ConsultationSlot { $block = $this->loadBlock($block_id); $slot = $block->slots->find($slot_id); @@ -115,7 +116,7 @@ abstract class ConsultationController extends AuthenticatedController return $slot; } - protected function loadBooking($block_id, $slot_id, $booking_id) + protected function loadBooking($block_id, $slot_id, $booking_id): ConsultationBooking { $slot = $this->loadSlot($block_id, $slot_id); $booking = $slot->bookings->find($booking_id); diff --git a/app/controllers/consultation/overview.php b/app/controllers/consultation/overview.php index ce6cd31..2afb710 100644 --- a/app/controllers/consultation/overview.php +++ b/app/controllers/consultation/overview.php @@ -71,6 +71,8 @@ class Consultation_OverviewController extends ConsultationController if ($this->slot->isOccupied()) { PageLayout::postError(_('Dieser Termin ist bereits belegt.')); + } elseif (!$this->slot->isBookable()) { + PageLayout::postError(_('Dieser Termin ist für Buchungen gesperrt.')); } else { $booking = new ConsultationBooking(); $booking->slot_id = $this->slot->id; diff --git a/app/views/consultation/admin/create.php b/app/views/consultation/admin/create.php index 3da70e0..34385b5 100644 --- a/app/views/consultation/admin/create.php +++ b/app/views/consultation/admin/create.php @@ -5,248 +5,32 @@ * @var string|null $room * @var array $responsible * @var Range $range + * @var int $slot_count_threshold */ -$days_of_the_week = [ - _('Montag') => 1, - _('Dienstag') => 2, - _('Mittwoch') => 3, - _('Donnerstag') => 4, - _('Freitag') => 5, - _('Samstag') => 6, - _('Sonntag') => 0 -]; -$intervals = [ - _('wöchentlich') => 1, - _('zweiwöchentlich') => 2, - _('dreiwöchentlich') => 3, - _('monatlich') => 4, -]; -?> - -
- - - - ' . - sprintf( - _('Ja, ich möchte wirklich %s Termine erstellen.'), - number_format($flash['confirm-many'], 0, ',', '.') - ) . - '' - ] - )->hideClose() ?> - - -
- - - - - - - - - - - - - - - - - - - +$convertResponsibilities = function ($input) { + if ($input === false) { + return json_encode(false); + } - + foreach ($input as $key => $values) { + $input[$key] = array_map( + fn($item) => ['id' => $item->id, 'label' => $item instanceof Statusgruppen ? $item->getName() : $item->getFullName()], + $values + ); + } - + return json_encode($input); +} - - - - - - - -
- - -
- - - -

- -

-

- - -

- - - render_partial('consultation/admin/block-responsibilities.php', compact('responsible')) ?> -
- - -
- - - - - - - - - - - - - -
- - - - - -
- - -
- -
- - indexURL() - ) ?> -
-
+?> +
diff --git a/app/views/consultation/admin/edit.php b/app/views/consultation/admin/edit.php index 347a05a..f9b416a 100644 --- a/app/views/consultation/admin/edit.php +++ b/app/views/consultation/admin/edit.php @@ -41,8 +41,8 @@ - render_partial('consultation/admin/block-responsibilities.php', compact('responsible', 'block')) ?> - + render_partial('consultation/admin/block-responsibilities.php', compact('responsible', 'block')) ?> + + +
diff --git a/app/views/consultation/admin/index.php b/app/views/consultation/admin/index.php index 52a352c..43cd5bf 100644 --- a/app/views/consultation/admin/index.php +++ b/app/views/consultation/admin/index.php @@ -13,7 +13,7 @@ ', [ _('Derzeit sind keine Termine eingetragen.'), - '%s', + '%s', ]), $controller->create(), _('Terminblöcke anlegen') diff --git a/app/views/consultation/admin/ungrouped.php b/app/views/consultation/admin/ungrouped.php index f21dc6a..1796e31 100644 --- a/app/views/consultation/admin/ungrouped.php +++ b/app/views/consultation/admin/ungrouped.php @@ -14,7 +14,7 @@ ', [ _('Derzeit sind keine Termine eingetragen.'), - '%s', + '%s', ]), $controller->create(), _('Terminblöcke anlegen') diff --git a/app/views/consultation/overview/ungrouped.php b/app/views/consultation/overview/ungrouped.php index b4d5c62..147f588 100644 --- a/app/views/consultation/overview/ungrouped.php +++ b/app/views/consultation/overview/ungrouped.php @@ -1,3 +1,14 @@ + hideClose() ?> diff --git a/db/migrations/6.0.7_add_consecutive_flag_for_consultations.php b/db/migrations/6.0.7_add_consecutive_flag_for_consultations.php new file mode 100644 index 0000000..52b623d --- /dev/null +++ b/db/migrations/6.0.7_add_consecutive_flag_for_consultations.php @@ -0,0 +1,45 @@ +exec($query); + + $query = "ALTER TABLE `consultation_slots` + ADD COLUMN `previous_slot_id` INT(11) UNSIGNED DEFAULT NULL AFTER `block_id`"; + DBManager::get()->exec($query); + + // THis will set the previous slot relation for all slots + $query = "UPDATE consultation_slots AS s0 + JOIN consultation_slots AS s1 + ON s1.slot_id = ( + SELECT slot_id + FROM consultation_slots AS s2 + WHERE s2.block_id = s0.block_id + AND s2.start_time < s0.start_time + AND s2.slot_id != s0.slot_id + ORDER BY s2.start_time DESC + LIMIT 1 + ) + SET s0.previous_slot_id = s1.slot_id"; + DBManager::get()->exec($query); + } + + protected function down() + { + $query = "ALTER TABLE `consultation_slots` + DROP COLUMN `previous_slot_id`"; + DBManager::get()->exec($query); + + $query = "ALTER TABLE `consultation_blocks` + DROP COLUMN `consecutive`"; + DBManager::get()->exec($query); + } +}; diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php index 149f681..63c69e6 100644 --- a/lib/classes/JsonApi/RouteMap.php +++ b/lib/classes/JsonApi/RouteMap.php @@ -5,6 +5,7 @@ namespace JsonApi; use JsonApi\Contracts\JsonApiPlugin; use JsonApi\Middlewares\Authentication; use JsonApi\Middlewares\DangerousRouteHandler; +use JsonApi\Routes\Consultations\SlotCreationCount; use JsonApi\Routes\Holidays\HolidaysShow; use Slim\Routing\RouteCollectorProxy; @@ -220,6 +221,9 @@ class RouteMap private function addAuthenticatedConsultationRoutes(RouteCollectorProxy $group): void { + // TODO: I know, not very JSONAPI-like but it's a NonJsonApiController ¯\_(ツ)_/¯ + $group->get('/consultation-slots/count', SlotCreationCount::class); + $group->get('/{type:courses|institutes|users}/{id}/consultations', Routes\Consultations\BlocksByRangeIndex::class); $group->get('/consultation-blocks/{id}', Routes\Consultations\BlockShow::class); diff --git a/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php b/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php new file mode 100644 index 0000000..c378771 --- /dev/null +++ b/lib/classes/JsonApi/Routes/Consultations/SlotCreationCount.php @@ -0,0 +1,105 @@ +getQueryParams(); + + $this->validateParameters($parameters); + + // Determine duration of a slot and pause times + $slot_count = ConsultationBlock::countSlots( + strtotime($parameters['start']), + strtotime($parameters['end']), + $parameters['dow'], + $parameters['interval'], + $parameters['duration'], + $parameters['pause_time'] ?? null, + $parameters['pause_duration'] ?? null + ); + + $response->getBody()->write((string) $slot_count); + return $response->withAddedHeader('Content-Type', 'application/json'); + } + + private function validateParameters(array $parameters): void + { + $collection = new ErrorCollection(); + + foreach (['start', 'end', 'dow', 'interval', 'duration'] as $key) { + if (!isset($parameters[$key])) { + $collection->addQueryParameterError($key, 'Parameter is missing'); + } + } + + if (isset($parameters['start'], $parameters['end'])) { + $start = strtotime($parameters['start']); + $end = strtotime($parameters['end']); + + if (!$start) { + $collection->addQueryParameterError('start', 'Parameter has invalid datetime format'); + } + + if (!$end) { + $collection->addQueryParameterError('end', 'Parameter has invalid datetime format'); + } + + if ($start && $end && $start > $end) { + $collection->addQueryParameterError('start', 'Datetime value of start must be before end'); + } + } + + if ( + isset($parameters['dow']) + && ( + !ctype_digit($parameters['dow']) + || $parameters['dow'] < 0 + || $parameters['dow'] > 6 + ) + ) { + $collection->addQueryParameterError('dow', 'Parameter must be a number between 0 and 6'); + } + + if ( + isset($parameters['interval']) + && ( + !ctype_digit($parameters['interval']) + || $parameters['interval'] < 0 + || $parameters['interval'] > 4 + ) + ) { + $collection->addQueryParameterError('interval', 'Parameter must be a number between 0 and 4'); + } + + if ( + isset($parameters['duration']) + && ( + !ctype_digit($parameters['duration']) + || $parameters['duration'] <= 0 + ) + ) { + $collection->addQueryParameterError('duration', 'Parameter must be a positive number'); + } + + if ( + isset($parameters['pause_time'], $parameters['duration']) + && $parameters['pause_time'] < $parameters['duration'] + ) { + $collection->addQueryParameterError('pause_time', 'The defined time to a pause is shorter than the duration of a slot.'); + } + + if (count($collection) > 0) { + throw new JsonApiException($collection); + } + } +} diff --git a/lib/models/ConsultationBlock.php b/lib/models/ConsultationBlock.php index 9113547..cde8f77 100644 --- a/lib/models/ConsultationBlock.php +++ b/lib/models/ConsultationBlock.php @@ -25,6 +25,7 @@ * @property string $note database column * @property int $size database column * @property int|null $lock_time database column + * @property bool $consecutive database column * @property int $mkdate database column * @property int $chdate database column * @property SimpleORMapCollection|ConsultationSlot[] $slots has_many ConsultationSlot @@ -211,6 +212,10 @@ class ConsultationBlock extends SimpleORMap implements PrivacyObject ); } + if (!$interval) { + break; + } + $current = strtotime("+{$interval} weeks", $current); } @@ -274,9 +279,9 @@ class ConsultationBlock extends SimpleORMap implements PrivacyObject } $slots[] = ConsultationSlot::build([ - 'block_id' => $this->id, - 'start_time' => $now, - 'end_time' => strtotime("+{$duration} minutes", $now), + 'block_id' => $this->id, + 'start_time' => $now, + 'end_time' => strtotime("+{$duration} minutes", $now), ]); $now = strtotime("+{$duration} minutes", $now); diff --git a/lib/models/ConsultationSlot.php b/lib/models/ConsultationSlot.php index 0da02df..c46e47c 100644 --- a/lib/models/ConsultationSlot.php +++ b/lib/models/ConsultationSlot.php @@ -9,6 +9,7 @@ * @property int $id alias column for slot_id * @property int $slot_id database column * @property int $block_id database column + * @property int $previous_slot_id database column * @property int $start_time database column * @property int $end_time database column * @property string $note database column @@ -17,6 +18,7 @@ * @property SimpleORMapCollection|ConsultationBooking[] $bookings has_many ConsultationBooking * @property SimpleORMapCollection|ConsultationEvent[] $events has_many ConsultationEvent * @property ConsultationBlock $block belongs_to ConsultationBlock + * @property ConsultationSlot|null $previous_slot has_one ConsultationSlot * @property-read mixed $has_bookings additional field * @property-read mixed $is_expired additional field */ @@ -45,15 +47,36 @@ class ConsultationSlot extends SimpleORMap 'assoc_foreign_key' => 'slot_id', 'on_delete' => 'delete', ]; + $config['has_one']['previous_slot'] = [ + 'class_name' => ConsultationSlot::class, + 'foreign_key' => 'previous_slot_id', + ]; $config['registered_callbacks']['before_create'][] = function (ConsultationSlot $slot) { $slot->updateEvents(); }; + $config['registered_callbacks']['before_store'][] = function (ConsultationSlot $slot) { + $previous = static::findOneBySQL( + "block_id = ? AND start_time < ? ORDER BY start_time DESC", + [$slot->block_id, $slot->start_time] + ); + $slot->previous_slot_id = $previous?->id; + }; $config['registered_callbacks']['after_delete'][] = function ($slot) { $block = $slot->block; if ($block && count($block->slots) === 0) { $block->delete(); } + + // Close gap + self::findEachBySQL( + function (ConsultationSlot $s) use ($slot) { + $s->previous_slot_id = $slot->previous_slot_id; + $s->store(); + }, + 'previous_slot_id = ?', + [$slot->id] + ); }; $config['additional_fields']['has_bookings']['get'] = function ($slot): bool { @@ -139,10 +162,6 @@ class ConsultationSlot extends SimpleORMap /** * Returns whether this slot is occupied (by a given user). - * - * @param mixed $user_id Id of the user (optional) - * @return boolean indicating whether the slot is occupied (by the given - * user) */ public function isOccupied($user_id = null) { @@ -154,7 +173,6 @@ class ConsultationSlot extends SimpleORMap /** * Returns whether the slot is locked for bookings. * - * @return bool */ public function isLocked(): bool { @@ -163,6 +181,20 @@ class ConsultationSlot extends SimpleORMap } /** + * Returns whether the slot is bookable for the given user_id + */ + public function isBookable(?string $user_id = null): bool + { + return !$this->isOccupied($user_id) + && !$this->isLocked() + && !( + $this->block->consecutive + && $this->previous_slot + && !$this->previous_slot->isOccupied() + ); + } + + /** * Creates a Stud.IP calendar event relating to the slot. * * @param User $user User object to create the event for @@ -306,8 +338,7 @@ class ConsultationSlot extends SimpleORMap $user = $user ?? User::findCurrent(); return ConsultationBooking::userMayCreateBookingForRange($this->block->range, $user) - && !$this->isOccupied() - && !$this->isLocked(); + && $this->isBookable($user->id); } diff --git a/resources/assets/javascripts/bootstrap/vue.js b/resources/assets/javascripts/bootstrap/vue.js index b8c938d..c6816a2 100644 --- a/resources/assets/javascripts/bootstrap/vue.js +++ b/resources/assets/javascripts/bootstrap/vue.js @@ -28,27 +28,17 @@ STUDIP.ready(() => { }); STUDIP.Vue.load().then(async ({createApp, store}) => { - let vm; if (config.store) { const storeConfig = await import(`../../../vue/store/${config.store}.js`); - console.log('store', storeConfig.default); store.registerModule(config.id, storeConfig.default, {root: true}); Object.keys(data).forEach(command => { store.commit(`${config.id}/${command}`, data[command]); }); - vm = createApp({components}); - } else { - vm = createApp({data, components}); } - // import myCoursesStore from '../stores/MyCoursesStore.js'; - // - // myCoursesStore.namespaced = true; - // - // store.registerModule('my-courses', myCoursesStore); - vm.$mount(this); + createApp({components, data}).$mount(this); }); $(this).attr('data-vue-app-created', ''); diff --git a/resources/assets/javascripts/lib/RestrictedDatesHelper.ts b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts new file mode 100644 index 0000000..bcc0af2 --- /dev/null +++ b/resources/assets/javascripts/lib/RestrictedDatesHelper.ts @@ -0,0 +1,89 @@ +import { jsonapi } from "./jsonapi"; + +type RestrictedDate = { + year: Number, + month: Number, + day: Number, + + reason: string | null, + lock: boolean +} + +class RestrictedDatesHelper +{ + static #loadedYears : Number[] = []; + static #restrictedDates: RestrictedDate[] = []; + + static isDateRestricted(date: Date, returnBoolean: Boolean = false): RestrictedDate | Boolean { + const restrictedDate : RestrictedDate | undefined = this.#restrictedDates.find(item => { + return item.year === date.getFullYear() + && item.month === date.getMonth() + 1 + && item.day === date.getDate(); + }); + + if (returnBoolean) { + return !!restrictedDate; + } + + return restrictedDate ?? this.#convertDate(date, null, false); + } + + static async loadRestrictedDatesByYear(year: Number): Promise { + if (this.#loadedYears.includes(year)) { + return Promise.reject(); + } + + this.#loadedYears.push(year); + + jsonapi.withPromises().request('holidays', {data: { + 'filter[year]': year + }}).then((response: [] | Object) => { + // Since PHP will return an empty object as an array, + // we need to check + if (Array.isArray(response)) { + return; + } + + for (const [date, data] of Object.entries(response)) { + this.#addRestrictedDate( + new Date(date), + data.holiday, + data.mandatory + ); + } + }); + } + + static #addRestrictedDate(date: Date, reason: string, lock: boolean = true): void { + const restricted = this.#convertDate(date, reason, lock); + + this.#restrictedDates = this.#restrictedDates.filter(item => { + return item.year !== restricted.year + || item.month !== restricted.month + || item.day !== restricted.day; + }); + + this.#restrictedDates.push(restricted); + } + + static removeRestrictedDate(date: Date): void { + this.#restrictedDates = this.#restrictedDates.filter(item => { + return item.year !== date.getFullYear() + || item.month !== date.getMonth() + 1 + || item.day !== date.getDate(); + }); + } + + static #convertDate(date: Date, reason: string | null, lock: boolean): RestrictedDate { + return { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + + reason, + lock + }; + } +} + +export default RestrictedDatesHelper; diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js index e611150..f98ba94 100644 --- a/resources/assets/javascripts/studip-ui.js +++ b/resources/assets/javascripts/studip-ui.js @@ -1,5 +1,6 @@ import { $gettext } from './lib/gettext'; import eventBus from "./lib/event-bus.ts"; +import RestrictedDatesHelper from './lib/RestrictedDatesHelper'; /** * This file contains extensions/adjustments for jQuery UI. @@ -28,33 +29,11 @@ import eventBus from "./lib/event-bus.ts"; } function disableHolidaysBeforeShow(date) { - const year = date.getFullYear(); - - if (STUDIP.UI.restrictedDates[year] === undefined) { - STUDIP.UI.restrictedDates[year] = {}; - - STUDIP.jsonapi.withPromises().get('holidays', {data: { - 'filter[year]': year - }}).then(response => { - // Since PHP will return an empty object as an array, - // we need to check - if (Array.isArray(response)) { - return; - } - - for (const [date, data] of Object.entries(response)) { - STUDIP.UI.addRestrictedDate( - new Date(date), - data.holiday, - data.mandatory - ); - } - - $(this).datepicker('refresh'); - }); - } - - const {reason, lock} = STUDIP.UI.isDateRestricted(date, false); + RestrictedDatesHelper.loadRestrictedDatesByYear(date.getFullYear()).then( + () => $(this).datepicker('refresh'), + () => null + ); + const {reason, lock} = RestrictedDatesHelper.isDateRestricted(date); return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason]; } @@ -83,57 +62,8 @@ import eventBus from "./lib/event-bus.ts"; return; } + STUDIP.UI = {}; // Setup Stud.IP's own datepicker extensions - STUDIP.UI = Object.assign(STUDIP.UI || {}, { - restrictedDates: {}, - addRestrictedDate(date, reason = '', lock = true) { - if (this.isDateRestricted(date)) { - return; - } - - const [year, month, day] = this.convertDateForRestriction(date); - if (this.restrictedDates[year] === undefined) { - this.restrictedDates[year] = {}; - } - if (this.restrictedDates[year][month] === undefined) { - this.restrictedDates[year][month] = {}; - } - - this.restrictedDates[year][month][day] = {reason, lock}; - }, - removeRestrictedDate(date) { - if (!this.isDateRestricted(date)) { - return false; - } - const [year, month, day] = this.convertDateForRestriction(date); - - delete this.restrictedDates[year][month][day]; - - if (Object.keys(this.restrictedDates[year][month]).length === 0) { - delete this.restrictedDates[year][month]; - } - - return true; - }, - isDateRestricted(date, return_bool = true) { - const [year, month, day] = this.convertDateForRestriction(date); - if ( - this.restrictedDates[year] === undefined - || this.restrictedDates[year][month] === undefined - || this.restrictedDates[year][month][day] === undefined - ) { - return return_bool ? false : { - reason: null, - lock: false, - }; - } - - return return_bool ? true : this.restrictedDates[year][month][day]; - }, - convertDateForRestriction(date) { - return [date.getFullYear(), date.getMonth() + 1, date.getDate()]; - } - }); STUDIP.UI.Datepicker = { selector: '.has-date-picker,[data-date-picker]', // Initialize all datepickers that not yet been initialized (e.g. in dialogs) diff --git a/resources/assets/stylesheets/scss/forms.scss b/resources/assets/stylesheets/scss/forms.scss index c647ee8..888cf56 100644 --- a/resources/assets/stylesheets/scss/forms.scss +++ b/resources/assets/stylesheets/scss/forms.scss @@ -621,6 +621,28 @@ form.inline { } } +.studip-dialog { + form[data-vue-app] { + display: flex; + flex-direction: column; + min-height: 100%; + + fieldset { + flex: 0; + } + + footer[data-dialog-button] { + background: var(--white); + border-top-color: var(--base-color-20); + bottom: -0.5em; + margin-top: auto; + padding: 1.3em 0; + position: sticky; + text-align: center; + } + } +} + @media (min-width: 800px) { form.default .form-columns { display: flex; diff --git a/resources/vue/components/ConsultationCreator.vue b/resources/vue/components/ConsultationCreator.vue new file mode 100644 index 0000000..375e98a --- /dev/null +++ b/resources/vue/components/ConsultationCreator.vue @@ -0,0 +1,496 @@ + + + diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/Datepicker.vue index 3db44ce..5c2c0f7 100644 --- a/resources/vue/components/Datepicker.vue +++ b/resources/vue/components/Datepicker.vue @@ -1,75 +1,143 @@