assign_labels( $event_id, [ $cat_id ], null, $owner ); } /** * Replace an event's tags, leaving its category alone. * * @param int $event_id Event ID. * @param int[] $tag_ids Tag category IDs; an empty list clears them. * @param string|null $owner Assignment owner; defaults to the current user. */ public function save_tags( int $event_id, array $tag_ids, ?string $owner = null ): void { $this->assign_labels( $event_id, null, $tag_ids, $owner ); } /** * Write an event's label set — categories and tags together. * * Core stores both in `webcal_entry_categories` and `assignToEvent()` * replaces every row for `(event, owner)`, so writing one kind alone * deletes the other. Passing null for either kind means "keep what is * already there"; passing an array replaces that kind wholesale. * * Categories are written first so they keep the lower `cat_order` and * stay the event's primary category. * * @param int $event_id Event ID. * @param int[]|null $category_ids Categories to set, or null to keep. * @param int[]|null $tag_ids Tags to set, or null to keep. * @param string|null $owner Assignment owner; defaults to current user. */ private function assign_labels( int $event_id, ?array $category_ids, ?array $tag_ids, ?string $owner ): void { $container = Container::get_instance(); /** @var \WebCalendar\Core\Domain\Repository\CategoryRepositoryInterface */ $category_repo = $container->get( 'category_repository' ); $user_login = $owner ?? \wp_get_current_user()->user_login; $event = new EventId( $event_id ); if ( null === $tag_ids ) { $tag_ids = array_map( static fn( $tag ): int => $tag->id(), $category_repo->getTagsForEvent( $event, $user_login ) ); } if ( null === $category_ids ) { // getForEvent() returns both kinds by design, so filter here — // there is no category-only read on the repository. $category_ids = []; foreach ( $category_repo->getForEvent( $event, $user_login ) as $label ) { if ( ! $label->isTag() ) { $category_ids[] = $label->id(); } } } $category_repo->assignToEvent( $event, $user_login, array_values( array_unique( array_merge( $category_ids, $tag_ids ) ) ) ); } /** * Delete category associations for an event. * * @param int $event_id Event ID. */ public function delete_category( int $event_id ): void { // Clears the category and keeps the tags — they are separate user // choices that happen to share a table. $this->assign_labels( $event_id, [], null, null ); } /** * Notify participants about an event change. * * @param Event $event Event entity. * @param array $participant_logins User logins. */ public function notify_participants( Event $event, array $participant_logins ): void { // email_notifications is a free-tier feature; no gating. $container = Container::get_instance(); /** @var \WebCalendar\Core\Domain\Repository\UserRepositoryInterface */ $user_repo = $container->get( 'user_repository' ); /** @var \WebCalendar\WP\NotificationManager */ $notification_mgr = $container->get( 'notification_manager' ); $participants = []; foreach ( $participant_logins as $login ) { $user = $user_repo->findByLogin( $login ); if ( $user ) { $participants[] = $user; } } if ( ! empty( $participants ) ) { $notification_mgr->notify_participants( $event, $participants ); } } /** * Parse an access level string to an AccessLevel enum. * * @param string $value Access level value (P, C, or R). * @return AccessLevel */ public function parse_access_level( string $value ): AccessLevel { return match ( \sanitize_text_field( $value ) ) { 'R' => AccessLevel::PRIVATE, 'C' => AccessLevel::CONFIDENTIAL, default => AccessLevel::PUBLIC, }; } }