plugin_name = $plugin_name; $this->version = $version; } /** * Register the stylesheets for the admin area. * * @since 1.0.0 */ public function enqueue_styles() { wp_enqueue_style( $this->plugin_name, ABFW_URL . 'src/Admin/css/admin.css', array(), $this->asset_version( 'src/Admin/css/admin.css' ) ); } /** * Register the scripts for the admin area. * * @since 1.0.0 */ public function enqueue_scripts() { wp_enqueue_script( $this->plugin_name, ABFW_URL . 'src/Admin/js/admin.js', array( 'jquery' ), $this->asset_version( 'src/Admin/js/admin.js' ), true ); wp_localize_script( $this->plugin_name, 'abfwAdmin', $this->preview_strings() ); } /** * Add the plugin entries to the admin menu. * * @since 1.0.0 */ public function add_admin_menu() { add_menu_page( __( 'Bookings', 'abogo-booking' ), __( 'Bookings', 'abogo-booking' ), 'manage_woocommerce', self::PAGE_SLUG, array( $this, 'render_bookings_page' ), 'dashicons-calendar-alt', 56 ); add_submenu_page( self::PAGE_SLUG, __( 'Bookings', 'abogo-booking' ), __( 'Bookings', 'abogo-booking' ), 'manage_woocommerce', self::PAGE_SLUG, array( $this, 'render_bookings_page' ) ); add_submenu_page( self::PAGE_SLUG, __( 'Settings', 'abogo-booking' ), __( 'Settings', 'abogo-booking' ), 'manage_woocommerce', self::SETTINGS_SLUG, array( $this, 'render_settings_page' ) ); add_submenu_page( self::PAGE_SLUG, __( 'Tools', 'abogo-booking' ), __( 'Tools', 'abogo-booking' ), 'manage_woocommerce', self::TOOLS_SLUG, array( $this, 'render_tools_page' ) ); } /** * Render the list of bookings. * * @since 1.0.0 */ public function render_bookings_page() { $moved = $this->maybe_move_booking(); $views = array( 'list' => __( 'List', 'abogo-booking' ), 'calendar' => __( 'Calendar', 'abogo-booking' ), ); // phpcs:disable WordPress.Security.NonceVerification.Recommended $status = isset( $_GET['status'] ) ? sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; $page = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1; $view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'list'; $month =isset( $_GET['month'] ) ? sanitize_text_field( wp_unslash( $_GET['month'] ) ) : ''; $arrival = isset( $_GET['arrival'] ) ? sanitize_text_field( wp_unslash( $_GET['arrival'] ) ) : ''; $departure = isset( $_GET['departure'] ) ? sanitize_text_field( wp_unslash( $_GET['departure'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended $view = isset( $views[ $view ] ) ? $view : 'list'; if ( 'calendar' === $view ) { $this->render_bookings_calendar( $views, $view, $month, $moved ); return; } $per_page = 20; /** * What the list of bookings is narrowed to. * * @since 1.1.0 * @param array $filters Filters: status, start, end, and product_id when one is added. */ $filters = apply_filters( 'abfw_bookings_filters', array( 'status' => $status, 'start' => $arrival, 'end' => $departure, ) ); $bookings = Repository::query( $filters + array( 'page' => $page, 'per_page' => $per_page, ) ); $total = Repository::count( $filters ); /** This action is documented in src/Admin/Admin.php */ do_action( 'abfw_bookings_shown', $bookings ); include ABFW_PATH . 'src/Admin/views/bookings.php'; } /** * Render the tools page, running the repair or saving the uninstall choice first. * * @since 1.0.0 */ public function render_tools_page() { $rebuilt = $this->maybe_rebuild(); $saved = $this->maybe_save_settings(); $remove_data = Settings::get( 'remove_data' ); include ABFW_PATH . 'src/Admin/views/tools.php'; } /** * Render the settings screen, saving it first when it was just submitted. * * @since 1.0.0 */ public function render_settings_page() { $saved = $this->maybe_save_settings(); $rules = Settings::all(); /** * The tabs of the settings screen. * * @since 1.1.0 * @param array $tabs Title of each tab, by key. */ $tabs = apply_filters( 'abfw_settings_tabs', array( 'general' => __( 'General', 'abogo-booking' ), 'calendar' => __( 'Calendar', 'abogo-booking' ), 'life' => __( 'Booking life', 'abogo-booking' ), ) ); /** * Tabs shown locked, with what each would offer, while the paid version is not active. * * @since 1.1.0 * @param array $locked Title and features of each locked tab, by key. */ $locked = apply_filters( 'abfw_locked_tabs', array() ); $tabs += wp_list_pluck( $locked, 'title' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; $tab = isset( $tabs[ $tab ] ) ? $tab : 'general'; include ABFW_PATH . 'src/Admin/views/settings.php'; } /** * Show a month of bookings, day by day. * * @since 1.0.0 * @param array $views Views of the screen. * @param string $view View being shown. * @param string $month Month asked for, as a year and a month. * @param bool|\WP_Error $moved Whether a booking was just moved by hand, or why it could not be. */ private function render_bookings_calendar( $views, $view, $month, $moved ) { $first = $this->first_of_month( $month ); $last = $first->modify( 'last day of this month' ); $days = array(); $bookings = Repository::between( $first->format( 'Y-m-d' ), $last->format( 'Y-m-d' ) ); /** * The bookings a screen is about to show, so what goes with them can be read at once. * * @since 1.1.0 * @param array $bookings Booking rows. */ do_action( 'abfw_bookings_shown', $bookings ); foreach ( $bookings as $booking ) { $day = new \DateTimeImmutable( $booking->start_date, wp_timezone() ); $end = new \DateTimeImmutable( $booking->end_date, wp_timezone() ); while ( $day <= $end ) { $key = $day->format( 'Y-m-d' ); if ( $day >= $first && $day <= $last ) { $days[ $key ][] = $booking; } $day = $day->modify( '+1 day' ); } } include ABFW_PATH . 'src/Admin/views/bookings-calendar.php'; } /** * The first day of the month being shown, today's month by default. * * @since 1.0.0 * @param string $month Month asked for, as a year and a month. * @return \DateTimeImmutable */ private function first_of_month( $month ) { $asked = preg_match( '/^\d{4}-\d{2}$/', $month ) ? \DateTimeImmutable::createFromFormat( '!Y-m-d', $month . '-01', wp_timezone() ) : false; return $asked ? $asked : new \DateTimeImmutable( 'midnight first day of this month', wp_timezone() ); } /** * Rebuild the missing bookings when the tools page was submitted. * * @since 1.0.0 * @return int|null How many bookings were written, null when nothing ran. */ private function maybe_rebuild() { if ( ! isset( $_POST['abfw_rebuild_nonce'] ) || ! current_user_can( 'manage_woocommerce' ) ) { return null; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['abfw_rebuild_nonce'] ) ), 'abfw_rebuild' ) ) { return null; } return ( new Bookings() )->rebuild(); } /** * Version stamp of an asset, so a changed file is never served from a cache. * * @since 1.0.0 * @param string $relative Path inside the plugin. * @return string */ private function asset_version( $relative ) { $file = ABFW_PATH . $relative; return file_exists( $file ) ? (string) filemtime( $file ) : $this->version; } /** * Words the preview beside the settings needs. * * @since 1.0.0 * @return array */ private function preview_strings() { return array( // The preview keeps showing the whole policy, even on a tab that carries // only part of it. 'rules' => Settings::all(), 'currency' => sprintf( get_woocommerce_price_format(), get_woocommerce_currency_symbol(), '%s' ), 'decimals' => wc_get_price_decimals(), 'night' => __( 'night', 'abogo-booking' ), 'nights' => __( 'nights', 'abogo-booking' ), 'day' => __( 'day', 'abogo-booking' ), 'days' => __( 'days', 'abogo-booking' ), 'per' => __( 'per', 'abogo-booking' ), 'arrival' => __( 'Arrival', 'abogo-booking' ), 'date' => __( 'Date', 'abogo-booking' ), 'to' => __( 'to', 'abogo-booking' ), 'atLeast' => __( 'at least', 'abogo-booking' ), 'today' => __( 'today', 'abogo-booking' ), 'tomorrow' => __( 'tomorrow', 'abogo-booking' ), /* translators: %s: number of days. */ 'inDays' => __( 'in %s days', 'abogo-booking' ), 'twoDates' => __( 'two dates', 'abogo-booking' ), 'oneDate' => __( 'one date', 'abogo-booking' ), 'brokenLength' => __( 'a length that needs fixing', 'abogo-booking' ), /* translators: 1: dates to choose, 2: night or day, 3: length allowed, 4: first date, 5: number of days. */ 'recap' => __( 'Guests pick %1$s and pay per %2$s. They stay %3$s, arriving from %4$s up to %5$s ahead.', 'abogo-booking' ), 'stepBooked' => __( 'Booked', 'abogo-booking' ), 'stepArriving' => __( 'Arriving', 'abogo-booking' ), 'stepOut' => __( 'Out now', 'abogo-booking' ), 'stepBack' => __( 'Just back', 'abogo-booking' ), 'stepCompleted' => __( 'Completed', 'abogo-booking' ), 'whenPaid' => __( 'as soon as the order is paid', 'abogo-booking' ), /* translators: %s: a length such as "2 days". */ 'beforeArrival' => __( 'moves on its own, %s before', 'abogo-booking' ), 'onArrival' => __( 'moves on its own, on the arrival day', 'abogo-booking' ), /* translators: %s: a length such as "2 days". */ 'afterDeparture' => __( 'moves on its own, for %s', 'abogo-booking' ), 'nothingLeft' => __( 'moves on its own', 'abogo-booking' ), 'waitsForYou' => __( 'waits for you', 'abogo-booking' ), ); } /** * Move one booking when the list was submitted by someone allowed to. * * @since 1.0.0 * @return bool|\WP_Error Whether a booking moved, or why it could not. */ private function maybe_move_booking() { if ( ! isset( $_POST['abfw_move_nonce'] ) || ! current_user_can( 'manage_woocommerce' ) ) { return false; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['abfw_move_nonce'] ) ), 'abfw_move_booking' ) ) { return false; } $booking_id = isset( $_POST['booking_id'] ) ? (int) $_POST['booking_id'] : 0; $status = isset( $_POST['booking_status'] ) ? sanitize_text_field( wp_unslash( $_POST['booking_status'] ) ) : ''; if ( ! $booking_id || ! Statuses::exists( $status ) ) { return false; } /** * Whether a booking may be moved to a status by hand: true, or the reason it cannot. * * @since 1.1.0 * @param true|\WP_Error $allowed True so far. * @param object|null $booking Booking being moved. * @param string $status Status asked for. */ $allowed = apply_filters( 'abfw_move_booking', true, Repository::find( $booking_id ), $status ); if ( is_wp_error( $allowed ) ) { return $allowed; } return Repository::update_status( $booking_id, $status ); } /** * Store the settings when the screen was submitted by someone allowed to. * * @since 1.0.0 * @return bool Whether anything was saved. */ private function maybe_save_settings() { if ( ! isset( $_POST['abfw_settings_nonce'] ) || ! current_user_can( 'manage_woocommerce' ) ) { return false; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['abfw_settings_nonce'] ) ), 'abfw_save_settings' ) ) { return false; } $group = isset( $_POST['abfw_tab'] ) ? sanitize_key( wp_unslash( $_POST['abfw_tab'] ) ) : 'general'; $posted = array(); foreach ( array_keys( Settings::defaults() ) as $key ) { if ( isset( $_POST[ $key ] ) ) { // A rule may hold a list, such as the closed dates. $posted[ $key ] = map_deep( wp_unslash( $_POST[ $key ] ), 'sanitize_text_field' ); } } Settings::save_group( $group, $posted ); return true; } }