option_changed( $option, array(), $value ); }, 10, 2 ); add_filter( 'abientot_notification_query_since', array( $this, 'query_since' ), 10, 2 ); add_filter( 'abientot_notification_since', array( $this, 'row_since' ), 10, 3 ); } public static function booking_changes( int $id, array $before, array $after ): bool { $repo = new ActivityLogRepository(); $labels = array( 'start_utc' => __( 'Start time', 'abientot' ), 'end_utc' => __( 'End time', 'abientot' ), 'status_id' => __( 'Status', 'abientot' ), 'payment_status' => __( 'Payment status', 'abientot' ), 'staff_id' => __( 'Staff', 'abientot' ), 'location_id' => __( 'Location', 'abientot' ), 'price_cents' => __( 'Price', 'abientot' ) ); foreach ( $labels as$key => $label ) { if ( ! array_key_exists( $key, $after ) || (string) ( $before[ $key ] ?? '' ) === (string) $after[ $key ] ) { continue; } $event = in_array( $key, array( 'start_utc', 'end_utc' ), true ) ? 'booking_rescheduled' : ( $key === 'payment_status' ? 'payment_status_changed' : ( $key === 'status_id' ? 'status_changed' : 'booking_updated' ) ); $old = (string) ( $before[ $key ] ?? '' ); $new = (string) $after[ $key ]; if ( $key === 'status_id' ) { $statuses = new \Abientot\Repositories\StatusesRepository(); $oldrow = $statuses->find( (int) $old ); $newrow = $statuses->find( (int) $new ); $old = $oldrow['label'] ?? $old; $new = $newrow['label'] ?? $new; if ( ( $newrow['slug'] ?? '' ) === 'cancelled' ) { $event = 'booking_cancelled'; } } elseif ( in_array( $key, array( 'start_utc', 'end_utc' ), true ) ) { $old = DateTimeHelper::format_utc_datetime( $old ); $new = DateTimeHelper::format_utc_datetime( $new ); } if ( ! $repo->record_event( 'appointment', $id, $event, sprintf( /* translators: 1: booking ID, 2: field label, 3: previous value, 4: new value. */ __( 'Booking #%1$d: %2$s changed from %3$s to %4$s.', 'abientot' ), $id, $label, $old, $new ), array( 'location_id' => (int) ( $after['location_id'] ?? $before['location_id'] ?? 0 ), 'field' => $key ) ) ) { return false; } } return true; } public function option_changed( string $option, $old, $new ): void { if ( ! current_user_can( 'abientot_manage_settings' ) || $old === $new ) { return; } if ( $option !== Settings::OPTION_NAME && $option !== 'abientot_notification_preferences' && ! preg_match( '/^abientot_(?:location_weekly_schedule|pro_location_schedule_\d+)$/D', $option ) ) { return; } if ( $option === 'abientot_location_weekly_schedule' ) { $location = ( new \Abientot\Repositories\LocationsRepository() )->default_location(); self::schedule_changed( (int) ( $location['id'] ?? 0 ), is_array( $old ) ? $old : null, is_array( $new ) ? $new : array() ); return; } $repo = new ActivityLogRepository(); $old = is_array( $old ) ? $old : array(); $new = is_array( $new ) ? $new : array(); $safe = array( 'business_name', 'business_timezone', 'plugin_language', 'currency', 'date_display_format', 'time_display_format', 'booking_min_notice_hours', 'booking_window_days', 'booking_slot_interval_minutes', 'default_booking_status', 'default_service_duration_minutes', 'hours', 'extend_closures', 'categories', 'booking_form_source', 'booking_form_resource_category_id', 'match_customer_by_email', 'match_customer_by_phone', 'cancellation_cutoff_hours', 'rescheduling_cutoff_hours', 'form_primary_color', 'form_background_color', 'form_background_opacity', 'form_text_color', 'form_font_family', 'form_density', 'form_font_size', 'form_auto_text_color', 'form_max_width', 'form_padding', 'form_container_border_radius', 'form_border_radius', 'form_hours_border_radius', 'form_border_width', 'form_border_color', 'form_service_border_width', 'form_service_border_color_mode', 'form_service_border_color', 'form_service_background_color_mode', 'form_service_background_color', 'form_category_background_color', 'form_field_text_color', 'form_field_background_color', 'form_field_border_color', 'form_field_border_radius', 'form_field_border_width', 'form_service_layout', 'form_group_services', 'form_category_behavior', 'form_show_service_images', 'form_show_category_images', 'form_image_size', 'form_image_radius', 'form_image_border_width', 'form_image_fit', 'form_show_map', 'form_map_height', 'email_customer_confirmation_enabled', 'email_customer_confirmed_enabled', 'email_admin_new_booking_enabled', 'email_customer_cancellation_enabled', 'email_customer_rescheduling_enabled', 'email_customer_completed_enabled' ); $fields = array_unique( array_merge( array_keys( $old ), array_keys( $new ) ) ); $labels = array( 'business_name' => __( 'Business name', 'abientot' ), 'business_timezone' => __( 'Business timezone', 'abientot' ), 'plugin_language' => __( 'Language', 'abientot' ), 'currency' => __( 'Currency', 'abientot' ), 'date_display_format' => __( 'Date format', 'abientot' ), 'time_display_format' => __( 'Time format', 'abientot' ), 'booking_min_notice_hours' => __( 'Minimum booking notice (hours)', 'abientot' ), 'booking_window_days' => __( 'Booking window (days)', 'abientot' ), 'booking_slot_interval_minutes' => __( 'Time slot interval (minutes)', 'abientot' ), 'default_booking_status' => __( 'Default booking status', 'abientot' ), 'hours' => __( 'Notification window (hours)', 'abientot' ), 'extend_closures' => __( 'Include closing periods', 'abientot' ), 'categories' => __( 'Notification categories', 'abientot' ), 'booking_form_source' => __( 'Booking type', 'abientot' ), 'booking_form_resource_category_id' => __( 'Resource category', 'abientot' ), 'match_customer_by_email' => __( 'Match by email', 'abientot' ), 'match_customer_by_phone' => __( 'Match by phone', 'abientot' ), 'cancellation_cutoff_hours' => __( 'Cancellation cutoff (hours)', 'abientot' ), 'rescheduling_cutoff_hours' => __( 'Rescheduling cutoff (hours)', 'abientot' ), 'form_primary_color' => __( 'Primary color', 'abientot' ), 'form_background_color' => __( 'Background color', 'abientot' ), 'form_background_opacity' => __( 'Background visibility', 'abientot' ), 'form_text_color' => __( 'Text color', 'abientot' ), 'form_font_family' => __( 'Font', 'abientot' ), 'form_density' => __( 'Density', 'abientot' ), 'form_font_size' => __( 'Text size', 'abientot' ), 'form_auto_text_color' => __( 'Automatic text color', 'abientot' ), 'form_max_width' => __( 'Form width', 'abientot' ), 'form_padding' => __( 'Form padding', 'abientot' ), 'form_container_border_radius' => __( 'Border radius', 'abientot' ) . ' (' . __( 'Form surface', 'abientot' ) . ')', 'form_border_radius' => __( 'Card radius', 'abientot' ), 'form_hours_border_radius' => __( 'Availability radius', 'abientot' ), 'form_border_width' => __( 'Border width', 'abientot' ) . ' (' . __( 'Form surface', 'abientot' ) . ')', 'form_border_color' => __( 'Border color', 'abientot' ) . ' (' . __( 'Form surface', 'abientot' ) . ')', 'form_service_border_width' => __( 'Card border width', 'abientot' ), 'form_service_border_color_mode' => __( 'Card border color mode', 'abientot' ), 'form_service_border_color' => __( 'Card border color', 'abientot' ), 'form_service_background_color_mode' => __( 'Card background color mode', 'abientot' ), 'form_service_background_color' => __( 'Card background color', 'abientot' ), 'form_category_background_color' => __( 'Background', 'abientot' ) . ' (' . __( 'Categories', 'abientot' ) . ')', 'form_field_text_color' => __( 'Text color', 'abientot' ) . ' (' . __( 'Fields', 'abientot' ) . ')', 'form_field_background_color' => __( 'Background color', 'abientot' ) . ' (' . __( 'Fields', 'abientot' ) . ')', 'form_field_border_color' => __( 'Border color', 'abientot' ) . ' (' . __( 'Fields', 'abientot' ) . ')', 'form_field_border_radius' => __( 'Border radius', 'abientot' ) . ' (' . __( 'Fields', 'abientot' ) . ')', 'form_field_border_width' => __( 'Border width', 'abientot' ) . ' (' . __( 'Fields', 'abientot' ) . ')', 'form_service_layout' => __( 'Layout', 'abientot' ), 'form_group_services' => __( 'Grouping', 'abientot' ), 'form_category_behavior' => __( 'Behavior', 'abientot' ) . ' (' . __( 'Categories', 'abientot' ) . ')', 'form_show_service_images' => __( 'Show service images', 'abientot' ), 'form_show_category_images' => __( 'Show category images', 'abientot' ), 'form_image_size' => __( 'Image size', 'abientot' ), 'form_image_radius' => __( 'Image radius', 'abientot' ), 'form_image_border_width' => __( 'Image border width', 'abientot' ), 'form_image_fit' => __( 'Image fit', 'abientot' ), 'form_show_map' => __( 'Show location map when the default location has a map URL', 'abientot' ), 'form_map_height' => __( 'Map height', 'abientot' ), 'privacy_retention_statement' => __( 'Retention period or criteria', 'abientot' ), 'admin_notification_email' => __( 'Admin notification email', 'abientot' ), 'email_from_name' => __( 'Email from name', 'abientot' ), 'email_from_address' => __( 'Email from address', 'abientot' ), 'email_reply_to_address' => __( 'Reply-to address', 'abientot' ), 'privacy_controller_name' => __( 'Controller legal name', 'abientot' ), 'privacy_contact' => __( 'Privacy and rights contact', 'abientot' ), 'privacy_policy_url' => __( 'Published privacy policy URL', 'abientot' ), 'privacy_recipients' => __( 'Recipients and external providers', 'abientot' ), 'booking_terms' => __( 'Terms & conditions', 'abientot' ) ); foreach ( \Abientot\Notifications\EmailService::template_labels() as$template => $template_label ) { $labels[ 'email_' . $template . '_enabled' ] = $template_label; $labels[ 'email_' . $template . '_subject' ] = $template_label . ': ' . __( 'Subject', 'abientot' ); $labels[ 'email_' . $template . '_body' ] = $template_label . ': ' . __( 'Body', 'abientot' ); } $labels['form_title'] = __( 'Form title', 'abientot' ); $labels['form_intro'] = __( 'Introductory text', 'abientot' ); $labels['form_button_label'] = __( 'Button label', 'abientot' ); $labels['confirmation_message'] = __( 'Confirmation message', 'abientot' ); $labels['policy_checkbox_text'] = __( 'Consent label', 'abientot' ); $labels['payment_instructions'] = __( 'Customer payment instructions', 'abientot' ); foreach ( $fields as$key ) { if ( ( $old[ $key ] ?? null ) === ( $new[ $key ] ?? null ) ) { continue; } $label = $labels[ $key ] ?? __( 'Other configuration', 'abientot' ); if ( in_array( $key, $safe, true ) ) { $format = static function ( $v ) use ( $key ): string { if ( 'categories' === $key && is_array( $v ) ) { $available = \Abientot\Admin\ActivityCenter::categories(); $selected = array(); foreach ( $v as$category ) { if ( is_scalar( $category ) && isset( $available[ (string) $category ] ) ) { $selected[] = $available[ (string) $category ]; } } return implode( ', ', $selected ); } return is_bool( $v ) ? ( $v ? __( 'Enabled', 'abientot' ) : __( 'Disabled', 'abientot' ) ) : ( is_array( $v ) ? implode( ', ', array_filter( $v, 'is_scalar' ) ) : ( is_scalar( $v ) ? (string) $v : __( 'Not set', 'abientot' ) ) ); }; $message = sprintf( /* translators: 1: setting label, 2: previous value, 3: new value. */ __( 'Setting %1$s changed from %2$s to %3$s.', 'abientot' ), $label, $format( $old[ $key ] ?? null ), $format( $new[ $key ] ?? null ) ); } else { $message = sprintf( /* translators: %s: setting label. */ __( 'Setting %s changed.', 'abientot' ), $label ); } $repo->record_event( 'settings', 0, 'settings_changed', $message, array( 'field' => (string) $key ) ); } } public static function schedule_changed( int $location, ?array $old, array $new ): bool { if ( $old === $new ) { return true; } $days = array( __( 'Sunday', 'abientot' ), __( 'Monday', 'abientot' ), __( 'Tuesday', 'abientot' ), __( 'Wednesday', 'abientot' ), __( 'Thursday', 'abientot' ), __( 'Friday', 'abientot' ), __( 'Saturday', 'abientot' ) ); $format = static function ( ?array $week ) use ( $days ): string { if ( $week === null ) { return __( 'Not set', 'abientot' ); } $parts = array(); foreach ( $days as$id => $day ) { $periods = ScheduleIntervals::normalize_entry( $week[ $id ] ?? array() )['periods']; $ranges = array_map( static fn( array $p ): string=>substr( $p['start_time'], 0, 5 ) . '–' . substr( $p['end_time'], 0, 5 ) . ( ( $p['end_time'] < $p['start_time'] || ! empty( $p['day_offset'] ) ) ? ' (+1)' : '' ), $periods ); $parts[] = $day . ': ' . ( $ranges ? implode( ', ', $ranges ) : __( 'Closed', 'abientot' ) ); } return implode( '; ', $parts ); }; return( new ActivityLogRepository() )->record_event( 'settings', $location, 'settings_changed', sprintf( /* translators: 1: location ID, 2: previous weekly schedule, 3: new weekly schedule. */ __( 'Location #%1$d schedule changed from %2$s to %3$s.', 'abientot' ), $location, $format( $old ), $format( $new ) ), array( 'location_id' => $location ) ); } public static function customer_changes( int $id, array $before, array $after, int $location = 0 ): bool { $labels = array( 'first_name' => __( 'First name', 'abientot' ), 'last_name' => __( 'Last name', 'abientot' ), 'email' => __( 'Email', 'abientot' ), 'phone' => __( 'Phone', 'abientot' ), 'notes' => __( 'Notes', 'abientot' ), 'marketing_opt_in' => __( 'Marketing consent', 'abientot' ) ); $changed = array(); foreach ( $labels as$key => $label ) { if ( array_key_exists( $key, $after ) && (string) ( $before[ $key ] ?? '' ) !== (string) $after[ $key ] ) { $changed[ $key ] = $label; } } return ! $changed || ( new ActivityLogRepository() )->record_event( 'customer', $id, 'customer_updated', sprintf( /* translators: 1: customer ID, 2: comma-separated list of changed field labels. */ __( 'Customer #%1$d information updated: %2$s.', 'abientot' ), $id, implode( ', ', $changed ) ), array( 'fields' => array_keys( $changed ), 'location_id' => $location ) ); } private function windows( array $prefs ): array { static $cache = array(); $key = (int) $prefs['hours'] . ':' . (int) $prefs['extend_closures']; if ( ! isset( $cache[ $key ] ) ) { $cache[ $key ] = ( new \Abientot\Repositories\ActivityLocationsRepository() )->notification_windows( time(), (int) $prefs['hours'], (bool) $prefs['extend_closures'] ); } return $cache[ $key ]; } public function query_since( int $since, array $prefs ): int { $windows = $this->windows( $prefs ); return $windows ? min( $since, min( $windows ) ) : $since; } public function row_since( int $since, array $row, array $prefs ): int { $windows = $this->windows( $prefs ); $location = (int) ( $row['location_id'] ?? 0 ); return $location ? ( $windows[ $location ] ?? $since ) : ( $windows ? min( $since, min( $windows ) ) : $since ); } }