last_section = $this->last_section( $page, $this->step() );
$this->page_url = cmplz_tc_settings_page();
// if a post id was passed, we copy the contents of that page to the wizard settings.
if ( isset( $_GET['post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading post_id from URL for display purposes only; no state change occurs here.
$post_id = intval( $_GET['post_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading post_id from URL for display purposes only; no state change occurs here.
// get all fields for this page.
$fields = COMPLIANZ_TC::$config->fields( $page );
foreach ( $fields as $fieldname => $field ) {
$fieldvalue = get_post_meta( $post_id, $fieldname, true );
if ( $fieldvalue ) {
// Save single or multiple-value fields appropriately.
if ( ! COMPLIANZ_TC::$field->is_multiple_field( $fieldname ) ) {
COMPLIANZ_TC::$field->save_field( $fieldname, $fieldvalue );
} else {
$field[ $fieldname ] = $fieldvalue;
COMPLIANZ_TC::$field->save_multiple( $field );
}
}
}
}
}
/**
* Renders feedback after the wizard's last step is reached.
*
* If not all required fields are completed, displays a prompt to finish
* the remaining questions. Otherwise, shows a success message and the
* last-step tip template (e.g. share/download links).
*
* @since 1.0.0
* @access public
*
* @return void
*/
public function last_step_callback() {
if ( ! $this->all_required_fields_completed( 'terms-conditions' ) ) {
echo '
';
esc_html_e( 'Not all required fields are completed yet. Please check the steps to complete all required questions', 'complianz-terms-conditions' );
echo '
';
} else {
echo '' . esc_html__( "You're done! Here are some tips & tricks to use this document to your full advantage.", 'complianz-terms-conditions' ) . '
';
echo cmplz_tc_get_template( 'wizard/last-step.php' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Template function returns trusted internal HTML.
}
}
/**
* Processes completion actions after a wizard step is submitted.
*
* Fires on the `cmplz_tc_terms-conditions_wizard` action. Clears cached
* shortcode transients so the live document reflects any new answers.
* When the user clicks Finish or navigates to the last step via Next,
* marks the wizard as having been completed at least once.
*
* @since 1.0.0
* @access public
*
* @return void
*/
public function wizard_after_step() {
if ( ! cmplz_tc_user_can_manage() ) {
return;
}
// Clear document cache so the rendered document reflects latest answers.
COMPLIANZ_TC::$document->clear_shortcode_transients();
// when clicking to the last page, or clicking finish, run the finish sequence.
if ( isset( $_POST['cmplz-finish'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified upstream in the form handler.
|| ( isset( $_POST['step'] ) && 3 === (int) $_POST['step'] // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified upstream in the form handler.
&& isset( $_POST['cmplz-next'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified upstream in the form handler.
) {
$this->set_wizard_completed_once();
}
}
/**
* Runs before a wizard field option is saved.
*
* Updates the document modification timestamp whenever the wizard is
* submitted, regardless of whether any individual field value changed.
* Returns early (skips further processing) when the new value is identical
* to the previous value.
*
* @since 1.0.0
* @access public
*
* @param string $fieldname The name of the field being saved.
* @param mixed $fieldvalue The new value submitted for the field.
* @param mixed $prev_value The previously stored value for the field.
* @param string $type The field type identifier.
* @return void
*/
public function before_save_wizard_option( $fieldname, $fieldvalue, $prev_value, $type ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Required by the complianz_tc_before_save hook signature.
// Stamp the document update time on every save attempt.
update_option( 'cmplz_tc_documents_update_date', time() );
// Only run when changes have been made.
if ( $fieldvalue === $prev_value ) {
return;
}
}
/**
* Hook callback invoked after all wizard fields have been saved in a single submission.
*
* Reserved for future post-save logic that should only execute once all
* fields in a submission have been processed (rather than per-field).
* Currently a no-op placeholder.
*
* @since 1.0.0
* @access public
*
* @param array $posted_fields Associative array of all field names and values
* that were included in the current save operation.
* @return void
*/
public function after_saved_all_fields( $posted_fields ) {
}
/**
* Reacts to individual wizard field saves to keep dependent data up to date.
*
* Currently handles language-related fields: when `language_communication`,
* `address_company`, or `multilanguage_communication` changes, the list of
* languages for which PDFs should be generated is refreshed. This ensures
* withdrawal forms and other locale-specific PDFs are regenerated in the
* correct languages after a language change.
*
* @since 1.0.0
* @access public
*
* @param string $fieldname The name of the field that was just saved.
* @param mixed $fieldvalue The new saved value.
* @param mixed $prev_value The value before the save.
* @param string $type The field type identifier.
* @return void
*/
public function after_save_wizard_option( $fieldname, $fieldvalue, $prev_value, $type ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Required by the complianz_tc_after_save hook signature.
// Only run when changes have been made.
if ( $fieldvalue === $prev_value ) {
return;
}
// if languages have been changed, we update the withdrawal form, if those should be generated.
if ( 'language_communication' === $fieldname || 'address_company' === $fieldname || 'multilanguage_communication' === $fieldname ) {
$languages = cmplz_tc_get_value( 'multilanguage_communication' );
if ( ! empty( $languages ) ) {
// Filter out empty values before storing the active language list.
$languages = array_filter( $languages );
update_option( 'cmplz_generate_pdf_languages', $languages );
}
}
// When only the primary language changes, reset the PDF language list to that single locale.
if ( 'language_communication' === $fieldname ) {
$languages = array( cmplz_tc_sanitize_language( get_locale() ) );
$languages = array_filter( $languages );
update_option( 'cmplz_generate_pdf_languages', $languages );
}
}
/**
* Returns the next step index that contains at least one visible field.
*
* Recursively increments the step counter until a step with fields is found
* or the last step is reached, whichever comes first. Used to skip over
* steps that are entirely empty (e.g. due to unmet conditions).
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index to check first.
* @return int The next non-empty step index, or $step if already at the last step.
*/
public function get_next_not_empty_step( $page, $step ) {
if ( ! COMPLIANZ_TC::$field->step_has_fields( $page, $step ) ) {
if ( $step >= $this->total_steps( $page ) ) {
return $step;
}
++$step;
// Recurse to check the incremented step.
$step = $this->get_next_not_empty_step( $page, $step );
}
return $step;
}
/**
* Returns the next section index (within a step) that contains visible fields.
*
* Sections are keyed with non-sequential integers, so this method resolves the
* actual position of the current section within the array before incrementing.
* Returns false when all remaining sections in the step are empty.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step containing the sections to search.
* @param int $section The section key to start from.
* @return int|bool The next non-empty section key, or false if none remain.
*/
public function get_next_not_empty_section( $page, $step, $section ) {
if ( ! COMPLIANZ_TC::$field->step_has_fields( $page, $step, $section ) ) {
// some keys are missing, so we need to count the actual number of keys.
if ( isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'] ) ) {
$n = array_keys( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'] ); // <---- Grab all the keys of your actual array and put in another array
$count = array_search( $section, $n, true ); // <--- Returns the position of the offset from this array using search.
// this is the actual list up to section key.
$new_arr = array_slice( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'], 0, $count + 1, true );// <--- Slice it with the 0 index as start and position+1 as the length parameter.
$section_count = count( $new_arr ) + 1;
} else {
$section_count = $section + 1;
}
++$section;
if ( $section_count > $this->total_sections( $page, $step ) ) {
return false;
}
$section = $this->get_next_not_empty_section( $page, $step, $section );
}
return $section;
}
/**
* Returns the previous step index that contains at least one visible field.
*
* Recursively decrements the step counter until a non-empty step is found
* or step 1 is reached. Used when the user clicks the Previous button to
* avoid landing on a step that would show no fields.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index to check first.
* @return int The previous non-empty step index, or 1 if already at the first step.
*/
public function get_previous_not_empty_step( $page, $step ) {
if ( ! COMPLIANZ_TC::$field->step_has_fields( $page, $step ) ) {
if ( $step <= 1 ) {
return $step;
}
--$step;
$step = $this->get_previous_not_empty_step( $page, $step );
}
return $step;
}
/**
* Returns the previous section index (within a step) that contains visible fields.
*
* Recursively decrements the section key until a non-empty section is found.
* Returns false when there are no earlier non-empty sections in the current step,
* signalling the caller to move to the previous step instead.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step containing the sections to search.
* @param int $section The section key to start searching backwards from.
* @return false|int The previous non-empty section key, or false if none exist.
*/
public function get_previous_not_empty_section( $page, $step, $section ) {
if ( ! COMPLIANZ_TC::$field->step_has_fields(
$page,
$step,
$section
)
) {
--$section;
if ( $section < 1 ) {
return false;
}
$section = $this->get_previous_not_empty_section(
$page,
$step,
$section
);
}
return $section;
}
/**
* Locks the wizard to prevent concurrent edits by other users.
*
* Stores the current user's ID in a transient whose expiry is controlled by
* the `cmplz_wizard_lock_time` filter (default: 2 minutes). The lock is
* automatically refreshed each time the wizard page is loaded by the same user.
*
* @since 1.0.0
* @access public
*
* @see cmplz_tc_wizard::wizard_is_locked()
* @see cmplz_tc_wizard::get_lock_user()
*
* @return void
*/
public function lock_wizard() {
$user_id = get_current_user_id();
/**
* Filters the wizard lock duration in seconds.
*
* @since 1.0.0
*
* @param int $duration Lock duration in seconds. Default is 2 minutes (120).
*/
set_transient( 'cmplz_wizard_locked_by_user', $user_id, apply_filters( 'cmplz_wizard_lock_time', 2 * MINUTE_IN_SECONDS ) );
}
/**
* Checks whether the wizard is currently locked by a different user.
*
* Compares the ID stored in the lock transient against the currently
* logged-in user. Returns true only when there is an active lock held by
* someone else; returns false if there is no lock or the lock belongs to
* the current user.
*
* @since 1.0.0
* @access public
*
* @see cmplz_tc_wizard::lock_wizard()
* @see cmplz_tc_wizard::get_lock_user()
*
* @return bool True if locked by another user, false otherwise.
*/
public function wizard_is_locked() {
$user_id = get_current_user_id();
$lock_user_id = (int) $this->get_lock_user();
if ( $lock_user_id && $user_id !== $lock_user_id ) {
return true;
}
return false;
}
/**
* Retrieves the ID of the user who holds the current wizard lock.
*
* Returns the value stored in the `cmplz_wizard_locked_by_user` transient.
* Returns false when no lock exists or the transient has expired.
*
* @since 1.0.0
* @access public
*
* @see cmplz_tc_wizard::lock_wizard()
*
* @return false|int User ID of the locking user, or false if unlocked.
*/
public function get_lock_user() {
return get_transient( 'cmplz_wizard_locked_by_user' );
}
/**
* Renders the complete wizard UI for a given document page.
*
* Performs a capability check and, if the wizard is locked by another user,
* displays a warning notice and returns early. Otherwise it locks the wizard
* for the current user, determines the correct step and section to display
* (advancing forward on Next or backward on Previous), then renders the
* navigation menu and question content into the admin wrapper template.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param string $wizard_title Optional heading shown above the wizard menu.
* Default empty string (no heading rendered).
* @return void
*/
public function wizard( $page, $wizard_title = '' ) {
if ( ! cmplz_tc_user_can_manage() ) {
return;
}
if ( $this->wizard_is_locked() ) {
// Retrieve lock owner details to include in the warning message.
$user_id = $this->get_lock_user();
$user = get_user_by( 'id', $user_id );
/**
* Filters the wizard lock duration in seconds.
*
* @since 1.0.0
*
* @param int $duration Lock duration in seconds. Default is 2 minutes (120).
*/
$lock_time = apply_filters(
'cmplz_wizard_lock_time',
2 * MINUTE_IN_SECONDS
) / 60;
cmplz_tc_notice(
sprintf(
// translators: %s is the display name of the user currently editing the wizard.
__(
'The wizard is currently being edited by %s',
'complianz-terms-conditions'
),
$user->user_nicename
) . '` block.
* Section-level intros take precedence over step-level intros when sections
* are present.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index.
* @param int $section The section key within the step.
* @return string HTML intro block, or an empty string if no intro is configured.
*/
public function get_intro( $page, $step, $section ) {
// Only show when in action.
$intro = '';
if ( COMPLIANZ_TC::$config->has_sections( $page, $step ) ) {
if ( isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'][ $section ]['intro'] ) ) {
$intro .= COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'][ $section ]['intro'];
}
} elseif ( isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['intro'] ) ) {
$intro .= COMPLIANZ_TC::$config->steps[ $page ][ $step ]['intro'];
}
if ( strlen( $intro ) > 0 ) {
$intro = '
'
. $intro
. '
';
}
return $intro;
}
/**
* Retrieves the regional identifiers that apply to a wizard step or section.
*
* Reads the `region` key from the configuration for the given step or section,
* normalises it to an array, removes any regions that are not enabled in the
* current installation (via `cmplz_has_region()`), and uppercases the remaining
* region codes. Returns false when no applicable regions remain.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index.
* @param int $section The section key within the step.
* @return array|false Array of uppercase region codes (e.g. ['EU', 'US']),
* or false if no enabled regions apply.
*/
public function get_section_regions( $page, $step, $section ) {
// Only show when in action.
$regions = false;
if ( COMPLIANZ_TC::$config->has_sections( $page, $step ) ) {
if ( isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'][ $section ]['region'] ) ) {
$regions
= COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'][ $section ]['region'];
}
} elseif ( isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['region'] ) ) {
$regions
= COMPLIANZ_TC::$config->steps[ $page ][ $step ]['region'];
}
if ( $regions ) {
// Normalise scalar region to a single-element array.
if ( ! is_array( $regions ) ) {
$regions = array( $regions );
}
// Remove regions that are not active in this installation.
foreach ( $regions as $index => $region ) {
if ( ! cmplz_has_region( $region ) ) {
unset( $regions[ $index ] );
}
}
if ( 0 === count( $regions ) ) {
$regions = false;
}
}
if ( $regions ) {
// Uppercase region codes for consistent display (e.g. 'EU', 'US').
$regions = array_map( 'strtoupper', $regions );
}
return $regions;
}
/**
* Resolves the document page type identifier from a post ID or the current request URL.
*
* When a post ID is supplied, the region and post type are combined to form
* the page slug (e.g. 'terms-conditions-eu'). Otherwise the `page` query
* parameter is read and the 'cmplz-' prefix is stripped. Returns false when
* neither source provides a valid value.
*
* @since 1.0.0
* @access public
*
* @param int|false $post_id Optional post ID to derive the page type from.
* Default false (falls back to $_GET['page']).
* @return string|false The page type slug, or false if it cannot be determined.
*/
public function get_type( $post_id = false ) {
$page = false;
if ( $post_id ) {
$region = COMPLIANZ_TC::$document->get_region( $post_id );
$post_type = get_post_type( $post_id );
// Combine post type (without 'cmplz-' prefix) and region into the page slug.
$page = str_replace( 'cmplz-', '', $post_type ) . '-'
. $region;
}
if ( isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug from URL for routing; no state change performed here.
$page = str_replace(
'cmplz-',
'',
sanitize_title( wp_unslash( $_GET['page'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading page slug from URL for routing; no state change performed here.
);
}
return $page;
}
/**
* Checks whether the wizard has been completed at least once.
*
* Reads the `cmplz_wizard_completed_once` option. The option is set to true
* by set_wizard_completed_once() when the user finishes the wizard for the
* first time. Other parts of the plugin use this to determine whether to show
* first-run prompts or assume the document is already configured.
*
* @since 1.0.0
* @access public
*
* @see cmplz_tc_wizard::set_wizard_completed_once()
*
* @return mixed The stored option value (true when complete, empty string or false otherwise).
*/
public function wizard_completed_once() {
return get_option( 'cmplz_wizard_completed_once' );
}
/**
* Marks the wizard as having been completed at least once.
*
* Persists true to the `cmplz_wizard_completed_once` option. Called
* automatically when the user clicks Finish or navigates past the last step.
*
* @since 1.0.0
* @access public
*
* @see cmplz_tc_wizard::wizard_completed_once()
*
* @return void
*/
public function set_wizard_completed_once() {
update_option( 'cmplz_wizard_completed_once', true );
}
/**
* Returns the validated current step index from the request.
*
* Reads the step from `$_GET` or `$_POST` (POST takes precedence), clamps the
* value to the range [1, total_steps]. Defaults to 1 when no step parameter
* is present. When `$page` is not supplied, defaults to 'terms-conditions'.
*
* @since 1.0.0
* @access public
*
* @param string|false $page The document page identifier to count steps for.
* Default false (uses 'terms-conditions').
* @return int The current step index, always within [1, total_steps].
*/
public function step( $page = false ) {
$step = 1;
if ( ! $page ) {
$page = 'terms-conditions';
}
$total_steps = $this->total_steps( $page );
if ( isset( $_GET['step'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading step from URL for navigation; no state change performed here.
$step = intval( $_GET['step'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading step from URL for navigation; no state change performed here.
}
// POST takes precedence over GET (form submission overrides URL parameter).
if ( isset( $_POST['step'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reading step for navigation only; nonce verified by the form handler upstream.
$step = intval( $_POST['step'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reading step for navigation only; nonce verified by the form handler upstream.
}
// Clamp to valid range.
if ( $step > $total_steps ) {
$step = $total_steps;
}
if ( $step <= 1 ) {
$step = 1;
}
return $step;
}
/**
* Returns the validated current section index from the request.
*
* Reads the section from `$_GET` or `$_POST` (POST takes precedence), clamps
* the value to the range [1, last_section]. Defaults to 1 when no section
* parameter is present.
*
* @since 1.0.0
* @access public
*
* @return int The current section index, always within [1, last_section].
*/
public function section() {
$section = 1;
if ( isset( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading section from URL for navigation; no state change performed here.
$section = intval( $_GET['section'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading section from URL for navigation; no state change performed here.
}
// POST takes precedence over GET (form submission overrides URL parameter).
if ( isset( $_POST['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reading section for navigation only; nonce verified by the form handler upstream.
$section = intval( $_POST['section'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reading section for navigation only; nonce verified by the form handler upstream.
}
if ( $section > $this->last_section ) {
$section = $this->last_section;
}
if ( $section <= 1 ) {
$section = 1;
}
return $section;
}
/**
* Returns the total number of steps configured for a document page.
*
* Counts the entries in the `steps` configuration array for the given page.
* Steps are expected to be keyed sequentially starting at 1.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @return int Total number of steps.
*/
public function total_steps( $page ) {
return count( COMPLIANZ_TC::$config->steps[ $page ] );
}
/**
* Returns the total number of sections within a wizard step.
*
* Returns 0 when the step has no sections key in the configuration.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index to count sections for.
* @return int Total number of sections, or 0 if the step has no sections.
*/
public function total_sections( $page, $step ) {
if ( ! isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'] ) ) {
return 0;
}
return count( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'] );
}
/**
* Returns the highest section key for a given step.
*
* When a step has no sections, returns 1 as a safe default so that navigation
* logic can always treat a step as having at least one section. The maximum
* key is used rather than the count because section keys may not be sequential.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index to inspect.
* @return int The highest section key, or 1 if the step has no sections.
*/
public function last_section( $page, $step ) {
if ( ! isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'] ) ) {
return 1;
}
$array = COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'];
return max( array_keys( $array ) );
}
/**
* Returns the lowest (first) section key for a given step.
*
* When a step has no sections, returns 1 as a safe default. Uses key() on
* the sections array to get the first key rather than assuming it is 1,
* because section keys can be arbitrary integers.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step index to inspect.
* @return int The first section key, or 1 if the step has no sections.
*/
public function first_section( $page, $step ) {
if ( ! isset( COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'] ) ) {
return 1;
}
$arr = COMPLIANZ_TC::$config->steps[ $page ][ $step ]['sections'];
$first_key = key( $arr );
return $first_key;
}
/**
* Estimates the remaining time (in minutes) to complete the wizard from a given position.
*
* Sums the `time` values defined in each field's configuration for all steps
* and sections from the current position to the end of the wizard. Sections
* are iterated only for the current step (to account for partial completion);
* remaining steps are summed in full. The result is rounded up by 0.45 to
* avoid showing 0 minutes when a small amount of time remains.
*
* @since 1.0.0
* @access public
*
* @param string $page The document page identifier (e.g. 'terms-conditions').
* @param int $step The step the user is currently on.
* @param int|false $section The section the user is currently on, or false
* if the step has no sections.
* @return int Estimated minutes remaining, rounded up.
*/
public function remaining_time( $page, $step, $section = false ) {
// get remaining steps including this one.
$time = 0;
$total_steps = $this->total_steps( $page );
for ( $i = $total_steps; $i >= $step; $i-- ) {
$sub = 0;
// if we're on a step with sections, we should add the sections that still need to be done.
if ( ( $i === $step )
&& COMPLIANZ_TC::$config->has_sections( $page, $step )
) {
for (
$s = $this->last_section( $page, $i ); $s >= $section;
$s--
) {
$subsub = 0;
$section_fields = COMPLIANZ_TC::$config->fields(
$page,
$step,
$s
);
foreach (
$section_fields as $section_fieldname =>
$section_field
) {
if ( isset( $section_field['time'] ) ) {
$sub += $section_field['time'];
$subsub += $section_field['time'];
$time += $section_field['time'];
}
}
}
} else {
// For steps other than the current one, sum all fields.
$fields = COMPLIANZ_TC::$config->fields( $page, $i, false );
foreach ( $fields as $fieldname => $field ) {
if ( isset( $field['time'] ) ) {
$sub += $field['time'];
$time += $field['time'];
}
}
}
}
return (int) round( $time + 0.45 );
}
/**
* Calculates the overall wizard completion percentage.
*
* Counts all required fields across every step and section (excluding fields
* whose conditions are not met) and determines how many have a non-empty value.
* Also factors in whether the required shortcode pages (e.g. Terms & Conditions
* page) have been created. The result is rounded up and cached in
* $this->percentage_complete to avoid redundant recalculation on the same request.
*
* @since 1.0.0
* @access public
*
* @param bool $count_warnings Whether to include warning-state fields in
* the total. Default true (currently unused internally).
* @return int Completion percentage between 0 and 100.
*/
public function wizard_percentage_complete( $count_warnings = true ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Parameter reserved for future use; public API must remain stable.
// Return cached value to avoid recalculation within the same request.
if ( false !== $this->percentage_complete ) {
return $this->percentage_complete;
}
$total_fields = 0;
$completed_fields = 0;
$total_steps = $this->total_steps( 'terms-conditions' );
for ( $i = 1; $i <= $total_steps; $i++ ) {
$fields = COMPLIANZ_TC::$config->fields( 'terms-conditions', $i, false );
foreach ( $fields as $fieldname => $field ) {
// Determine whether this field is required under current conditions.
$required = isset( $field['required'] ) ? $field['required'] : false;
if ( ( isset( $field['condition'] ) || isset( $field['callback_condition'] ) ) && ! COMPLIANZ_TC::$field->condition_applies( $field )
) {
$required = false;
}
if ( $required ) {
$value = cmplz_tc_get_value( $fieldname, false, false );
++$total_fields;
if ( ! empty( $value ) ) {
++$completed_fields;
}
}
}
}
// Factor in required document pages (e.g. Terms & Conditions shortcode page).
$pages = COMPLIANZ_TC::$document->get_required_pages();
foreach ( $pages as $region => $region_pages ) {
foreach ( $region_pages as $type => $page ) {
if ( COMPLIANZ_TC::$document->page_exists( $type ) ) {
++$completed_fields;
}
++$total_fields;
}
}
// Round up slightly (+ 0.45) so a nearly-complete wizard shows 100% rather than 99%.
$percentage = (int) round( 100 * ( $completed_fields / $total_fields ) + 0.45 );
$this->percentage_complete = $percentage;
return $percentage;
}
}
} //class closure