| 1 |
<?php namespace TierPricingTable\Addons\GlobalTieredPricing\CPT; |
| 2 |
|
| 3 |
use TierPricingTable\Addons\GlobalTieredPricing\GlobalPricingRule; |
| 4 |
use TierPricingTable\Addons\GlobalTieredPricing\RuleOverlaps; |
| 5 |
use TierPricingTable\Addons\Tools\PricingTest\PricingTester; |
| 6 |
use WP_Post; |
| 7 |
|
| 8 |
/** |
| 9 |
* The plugin's own sidebar box in place of WordPress's Publish box: the rule's status (active, suspended, |
| 10 |
* scheduled, expired, not published), the suspend switch, the priority, the date window, the rules it |
| 11 |
* overlaps with, the Pricing test link, and the Update / Move to Trash actions. |
| 12 |
* |
| 13 |
* The classic editor's script and the save pipeline look for the hidden status inputs, the "#submitpost" |
| 14 |
* wrapper, the "#publish" button and the "#major-publishing-actions" bar; they keep their ids and names. |
| 15 |
*/ |
| 16 |
class StatusBox { |
| 17 |
|
| 18 |
/** |
| 19 |
* Posted with the box, so other save paths (WP-CLI, bulk edits) keep the stored values. |
| 20 |
*/ |
| 21 |
const MARKER = 'tpt_status_box'; |
| 22 |
|
| 23 |
public function __construct() { |
| 24 |
add_action( 'add_meta_boxes_' . GlobalTieredPricingCPT::SLUG, array( $this, 'register' ) ); |
| 25 |
} |
| 26 |
|
| 27 |
public function register() { |
| 28 |
remove_meta_box( 'submitdiv', GlobalTieredPricingCPT::SLUG, 'side' ); |
| 29 |
|
| 30 |
add_meta_box( 'tpt-rule-status-box', __( 'Rule status', 'tier-pricing-table' ), array( $this, 'render' ), GlobalTieredPricingCPT::SLUG, 'side', 'high' ); |
| 31 |
} |
| 32 |
|
| 33 |
public function render( WP_Post $post ) { |
| 34 |
$rule = GlobalPricingRule::build( $post->ID ); |
| 35 |
$isNew = 'auto-draft' === $post->post_status; |
| 36 |
$published = 'publish' === $post->post_status; |
| 37 |
$status = $this->status( $post, $rule ); |
| 38 |
// only a rule in force can be overridden, and only by rules in force (skipped ones never apply) |
| 39 |
$overriding = ( $isNew || ! $published || ! $rule->canBeOverridden() ) |
| 40 |
? array() |
| 41 |
: ( new RuleOverlaps() )->findOverriding( $rule, GlobalTieredPricingCPT::getGlobalRules( true, false ) ); |
| 42 |
$format = get_option( 'date_format' ); |
| 43 |
?> |
| 44 |
<div class="submitbox" id="submitpost"> |
| 45 |
<div style="display: none;"><?php submit_button( __( 'Save', 'tier-pricing-table' ), '', 'save' ); ?></div> |
| 46 |
<input type="hidden" name="hidden_post_status" id="hidden_post_status" value="<?php echo esc_attr( $isNew ? 'draft' : $post->post_status ); ?>"> |
| 47 |
<input type="hidden" name="post_status" id="post_status" value="<?php echo esc_attr( $isNew ? 'draft' : $post->post_status ); ?>"> |
| 48 |
<input type="hidden" name="<?php echo esc_attr( self::MARKER ); ?>" value="1"> |
| 49 |
|
| 50 |
<div class="tpt-rule-box"> |
| 51 |
<div class="tpt-rule-box__state tpt-rule-box__state--<?php echo esc_attr( $status['key'] ); ?>"> |
| 52 |
<span class="dashicons <?php echo esc_attr( $status['icon'] ); ?>"></span> |
| 53 |
<div class="tpt-rule-box__text"> |
| 54 |
<strong><?php echo esc_html( $status['label'] ); ?></strong> |
| 55 |
<div class="tpt-rule-box__detail"><?php echo esc_html( $status['detail'] ); ?></div> |
| 56 |
</div> |
| 57 |
<?php if ( $published ) : ?> |
| 58 |
<?php // a link-styled submit: saves the form and flips the state in one click ?> |
| 59 |
<?php if ( $rule->isSuspended() ) : ?> |
| 60 |
<button type="submit" name="tpt_suspend_action" value="reactivate" class="button-link tpt-rule-box__toggle"><?php esc_html_e( 'Reactivate', 'tier-pricing-table' ); ?></button> |
| 61 |
<?php else : ?> |
| 62 |
<button type="submit" name="tpt_suspend_action" value="suspend" class="button-link tpt-rule-box__toggle"><?php esc_html_e( 'Suspend', 'tier-pricing-table' ); ?></button> |
| 63 |
<?php endif; ?> |
| 64 |
<?php endif; ?> |
| 65 |
</div> |
| 66 |
|
| 67 |
<details class="tpt-rule-box__section" <?php echo GlobalPricingRule::DEFAULT_PRIORITY !== $rule->getPriority() ? 'open' : ''; ?>> |
| 68 |
<summary> |
| 69 |
<span><?php esc_html_e( 'Priority', 'tier-pricing-table' ); ?></span> |
| 70 |
<span class="tpt-rule-box__meta"><?php echo esc_html( number_format_i18n( $rule->getPriority() ) ); ?></span> |
| 71 |
</summary> |
| 72 |
<div class="tpt-rule-box__body"> |
| 73 |
<input type="number" id="tpt_priority" name="tpt_priority" min="0" step="1" value="<?php echo esc_attr( $rule->getPriority() ); ?>" aria-label="<?php esc_attr_e( 'Priority', 'tier-pricing-table' ); ?>"> |
| 74 |
<p class="description"><?php esc_html_e( 'The rule with the lowest number is applied first. If two rules have the same number, the newer rule is applied first.', 'tier-pricing-table' ); ?></p> |
| 75 |
</div> |
| 76 |
</details> |
| 77 |
|
| 78 |
<details class="tpt-rule-box__section" <?php echo $rule->hasSchedule() ? 'open' : ''; ?>> |
| 79 |
<summary> |
| 80 |
<span><?php esc_html_e( 'Schedule', 'tier-pricing-table' ); ?></span> |
| 81 |
<span class="tpt-rule-box__meta"><?php echo esc_html( self::scheduleSummary( $rule ) ); ?></span> |
| 82 |
</summary> |
| 83 |
<div class="tpt-rule-box__body"> |
| 84 |
<label for="tpt_start_date"><?php esc_html_e( 'Start date', 'tier-pricing-table' ); ?></label> |
| 85 |
<input type="date" id="tpt_start_date" name="tpt_start_date" value="<?php echo esc_attr( (string) $rule->getStartDate() ); ?>"> |
| 86 |
<label for="tpt_end_date"><?php esc_html_e( 'End date', 'tier-pricing-table' ); ?></label> |
| 87 |
<input type="date" id="tpt_end_date" name="tpt_end_date" value="<?php echo esc_attr( (string) $rule->getEndDate() ); ?>"> |
| 88 |
<p class="description"> |
| 89 |
<?php |
| 90 |
printf( |
| 91 |
/* translators: %s: timezone, e.g. "Europe/Kyiv" */ |
| 92 |
esc_html__( 'Whole days in the site timezone (%s). Leave a date empty for no limit.', 'tier-pricing-table' ), |
| 93 |
esc_html( wp_timezone_string() ) |
| 94 |
); |
| 95 |
?> |
| 96 |
</p> |
| 97 |
</div> |
| 98 |
</details> |
| 99 |
|
| 100 |
<?php if ( $overriding ) : // only when another rule takes precedence somewhere ?> |
| 101 |
<details class="tpt-rule-box__section" open> |
| 102 |
<summary> |
| 103 |
<span><?php esc_html_e( 'Overridden by', 'tier-pricing-table' ); ?></span> |
| 104 |
<span class="tpt-rule-box__meta tpt-rule-box__meta--warn"><?php echo esc_html( number_format_i18n( count( $overriding ) ) ); ?></span> |
| 105 |
</summary> |
| 106 |
<div class="tpt-rule-box__body"> |
| 107 |
<ul class="tpt-rule-box__overlaps"> |
| 108 |
<?php foreach ( $overriding as $other ) : ?> |
| 109 |
<li> |
| 110 |
<a href="<?php echo esc_url( get_edit_post_link( $other->getId(), 'raw' ) ); ?>" title="<?php echo esc_attr( $other->getTitle() ); ?>"><?php echo esc_html( $other->getTitle() ); ?></a> |
| 111 |
<span class="tpt-rule-box__meta"> |
| 112 |
<?php |
| 113 |
/* translators: %d: priority number */ |
| 114 |
echo esc_html( sprintf( __( 'priority %d', 'tier-pricing-table' ), $other->getPriority() ) ); |
| 115 |
if ( $other->hasSchedule() ) { |
| 116 |
echo ', ' . esc_html( self::scheduleSummary( $other ) ); |
| 117 |
} |
| 118 |
?> |
| 119 |
</span> |
| 120 |
</li> |
| 121 |
<?php endforeach; ?> |
| 122 |
</ul> |
| 123 |
<p class="description"><?php esc_html_e( 'These rules have a higher priority. When one of them matches the same product and customer, that rule is applied and the rule you are editing is skipped.', 'tier-pricing-table' ); ?></p> |
| 124 |
</div> |
| 125 |
</details> |
| 126 |
<?php endif; ?> |
| 127 |
|
| 128 |
<?php // no test link for a skipped rule: the test could only say it changes nothing ?> |
| 129 |
<?php if ( ! $isNew && PricingTester::isAvailable() && ! ( $published && 'skipped' === $rule->getEffectiveStatus() ) ) : ?> |
| 130 |
<p class="tpt-rule-box__test"> |
| 131 |
<span class="dashicons dashicons-search"></span> |
| 132 |
<a href="<?php echo esc_url( PricingTester::getUrl( $post->ID ) ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Test this rule', 'tier-pricing-table' ); ?></a> |
| 133 |
<span class="description"><?php esc_html_e( 'What a customer pays for a product, and which rule wins.', 'tier-pricing-table' ); ?></span> |
| 134 |
</p> |
| 135 |
<?php endif; ?> |
| 136 |
</div> |
| 137 |
|
| 138 |
<div id="major-publishing-actions"> |
| 139 |
<div id="delete-action"> |
| 140 |
<?php if ( ! $isNew && current_user_can( 'delete_post', $post->ID ) ) : ?> |
| 141 |
<a class="submitdelete deletion" href="<?php echo esc_url( get_delete_post_link( $post->ID ) ); ?>"> |
| 142 |
<?php echo EMPTY_TRASH_DAYS ? esc_html__( 'Move to Trash', 'tier-pricing-table' ) : esc_html__( 'Delete permanently', 'tier-pricing-table' ); ?> |
| 143 |
</a> |
| 144 |
<?php endif; ?> |
| 145 |
</div> |
| 146 |
<div id="publishing-action"> |
| 147 |
<span class="spinner"></span> |
| 148 |
<?php if ( $published ) : ?> |
| 149 |
<input name="original_publish" type="hidden" id="original_publish" value="<?php esc_attr_e( 'Update', 'tier-pricing-table' ); ?>"> |
| 150 |
<?php submit_button( __( 'Update', 'tier-pricing-table' ), 'primary large', 'save', false, array( 'id' => 'publish' ) ); ?> |
| 151 |
<?php else : ?> |
| 152 |
<input name="original_publish" type="hidden" id="original_publish" value="<?php esc_attr_e( 'Publish', 'tier-pricing-table' ); ?>"> |
| 153 |
<?php submit_button( __( 'Publish', 'tier-pricing-table' ), 'primary large', 'publish', false ); ?> |
| 154 |
<?php endif; ?> |
| 155 |
</div> |
| 156 |
<div class="clear"></div> |
| 157 |
</div> |
| 158 |
</div> |
| 159 |
<style> |
| 160 |
#tpt-rule-status-box .inside { |
| 161 |
margin: 0; |
| 162 |
padding: 0; |
| 163 |
} |
| 164 |
|
| 165 |
.tpt-rule-box { |
| 166 |
padding: 12px; |
| 167 |
} |
| 168 |
|
| 169 |
.tpt-rule-box__state { |
| 170 |
display: flex; |
| 171 |
gap: 8px; |
| 172 |
align-items: flex-start; |
| 173 |
margin-bottom: 12px; |
| 174 |
padding: 10px 12px; |
| 175 |
border-radius: 4px; |
| 176 |
line-height: 1.4; |
| 177 |
} |
| 178 |
|
| 179 |
.tpt-rule-box__state .dashicons { |
| 180 |
flex: 0 0 auto; |
| 181 |
font-size: 20px; |
| 182 |
width: 20px; |
| 183 |
height: 20px; |
| 184 |
} |
| 185 |
|
| 186 |
.tpt-rule-box__state--active { |
| 187 |
background: #edfaef; |
| 188 |
color: #007017; |
| 189 |
} |
| 190 |
|
| 191 |
.tpt-rule-box__state--scheduled { |
| 192 |
background: #f0f6fc; |
| 193 |
color: #2271b1; |
| 194 |
} |
| 195 |
|
| 196 |
.tpt-rule-box__state--suspended, |
| 197 |
.tpt-rule-box__state--skipped { |
| 198 |
background: #fcf9e8; |
| 199 |
color: #996800; |
| 200 |
} |
| 201 |
|
| 202 |
.tpt-rule-box__state--expired, |
| 203 |
.tpt-rule-box__state--draft { |
| 204 |
background: #f0f0f1; |
| 205 |
color: #50575e; |
| 206 |
} |
| 207 |
|
| 208 |
.tpt-rule-box__detail { |
| 209 |
margin-top: 2px; |
| 210 |
font-size: 12px; |
| 211 |
} |
| 212 |
|
| 213 |
.tpt-rule-box__text { |
| 214 |
flex: 1 1 auto; |
| 215 |
min-width: 0; |
| 216 |
} |
| 217 |
|
| 218 |
.tpt-rule-box__toggle { |
| 219 |
flex: 0 0 auto; |
| 220 |
align-self: flex-start; |
| 221 |
margin-top: 1px; |
| 222 |
font-size: 12px; |
| 223 |
line-height: 1.4; |
| 224 |
text-decoration: underline; |
| 225 |
} |
| 226 |
|
| 227 |
.tpt-rule-box__body label { |
| 228 |
display: block; |
| 229 |
margin: 0 0 4px; |
| 230 |
font-weight: 600; |
| 231 |
} |
| 232 |
|
| 233 |
.tpt-rule-box__body label + label, |
| 234 |
.tpt-rule-box__body input + label { |
| 235 |
margin-top: 8px; |
| 236 |
} |
| 237 |
|
| 238 |
.tpt-rule-box__body input[type="date"], |
| 239 |
.tpt-rule-box__body input[type="number"] { |
| 240 |
width: 100%; |
| 241 |
} |
| 242 |
|
| 243 |
.tpt-rule-box .description { |
| 244 |
margin: 6px 0 0; |
| 245 |
font-size: 12px; |
| 246 |
color: #646970; |
| 247 |
} |
| 248 |
|
| 249 |
.tpt-rule-box__section { |
| 250 |
margin-top: 12px; |
| 251 |
padding-top: 10px; |
| 252 |
border-top: 1px solid #dcdcde; |
| 253 |
} |
| 254 |
|
| 255 |
.tpt-rule-box__section summary { |
| 256 |
display: flex; |
| 257 |
align-items: center; |
| 258 |
gap: 8px; |
| 259 |
cursor: pointer; |
| 260 |
font-weight: 600; |
| 261 |
list-style: none; |
| 262 |
} |
| 263 |
|
| 264 |
.tpt-rule-box__section summary::-webkit-details-marker { |
| 265 |
display: none; |
| 266 |
} |
| 267 |
|
| 268 |
.tpt-rule-box__section summary > span:first-child { |
| 269 |
flex: 1 1 auto; |
| 270 |
} |
| 271 |
|
| 272 |
.tpt-rule-box__section summary::after { |
| 273 |
content: "\f140"; |
| 274 |
font-family: dashicons; |
| 275 |
font-size: 18px; |
| 276 |
color: #787c82; |
| 277 |
transition: transform .15s; |
| 278 |
} |
| 279 |
|
| 280 |
.tpt-rule-box__section[open] summary::after { |
| 281 |
transform: rotate(180deg); |
| 282 |
} |
| 283 |
|
| 284 |
.tpt-rule-box__meta { |
| 285 |
font-weight: 400; |
| 286 |
font-size: 12px; |
| 287 |
color: #646970; |
| 288 |
} |
| 289 |
|
| 290 |
.tpt-rule-box__body { |
| 291 |
padding: 10px 0 2px; |
| 292 |
} |
| 293 |
|
| 294 |
.tpt-rule-box__overlaps { |
| 295 |
margin: 0; |
| 296 |
} |
| 297 |
|
| 298 |
.tpt-rule-box__overlaps li { |
| 299 |
margin: 0 0 8px; |
| 300 |
padding-left: 10px; |
| 301 |
border-left: 3px solid #dba617; |
| 302 |
line-height: 1.5; |
| 303 |
} |
| 304 |
|
| 305 |
.tpt-rule-box__overlaps a { |
| 306 |
display: block; |
| 307 |
overflow: hidden; |
| 308 |
text-overflow: ellipsis; |
| 309 |
white-space: nowrap; |
| 310 |
font-weight: 600; |
| 311 |
} |
| 312 |
|
| 313 |
.tpt-rule-box__meta--warn { |
| 314 |
color: #996800; |
| 315 |
font-weight: 600; |
| 316 |
} |
| 317 |
|
| 318 |
.tpt-rule-box__test { |
| 319 |
display: flex; |
| 320 |
flex-wrap: wrap; |
| 321 |
gap: 2px 6px; |
| 322 |
align-items: center; |
| 323 |
margin: 12px 0 0; |
| 324 |
padding-top: 12px; |
| 325 |
border-top: 1px solid #dcdcde; |
| 326 |
} |
| 327 |
|
| 328 |
.tpt-rule-box__test .dashicons { |
| 329 |
color: #8c8f94; |
| 330 |
} |
| 331 |
|
| 332 |
.tpt-rule-box__test .description { |
| 333 |
flex: 1 1 100%; |
| 334 |
margin: 0; |
| 335 |
} |
| 336 |
</style> |
| 337 |
<?php |
| 338 |
} |
| 339 |
|
| 340 |
/** |
| 341 |
* @return array{key: string, label: string, detail: string, icon: string} |
| 342 |
*/ |
| 343 |
protected function status( WP_Post $post, GlobalPricingRule $rule ): array { |
| 344 |
$format = get_option( 'date_format' ); |
| 345 |
|
| 346 |
if ( 'publish' !== $post->post_status ) { |
| 347 |
return array( |
| 348 |
'key' => 'draft', |
| 349 |
'label' => __( 'Not published', 'tier-pricing-table' ), |
| 350 |
'detail' => __( 'Publish the rule to put it in force.', 'tier-pricing-table' ), |
| 351 |
'icon' => 'dashicons-edit', |
| 352 |
); |
| 353 |
} |
| 354 |
|
| 355 |
switch ( $rule->getEffectiveStatus() ) { |
| 356 |
case 'suspended': |
| 357 |
return array( |
| 358 |
'key' => 'suspended', |
| 359 |
'label' => __( 'Suspended', 'tier-pricing-table' ), |
| 360 |
'detail' => __( 'The rule is saved but does not apply until you reactivate it.', 'tier-pricing-table' ), |
| 361 |
'icon' => 'dashicons-controls-pause', |
| 362 |
); |
| 363 |
case 'expired': |
| 364 |
return array( |
| 365 |
'key' => 'expired', |
| 366 |
'label' => __( 'Expired', 'tier-pricing-table' ), |
| 367 |
/* translators: %s: date */ |
| 368 |
'detail' => sprintf( __( 'Ended %s. Clear or move the end date to use the rule again.', 'tier-pricing-table' ), wp_date( $format, $rule->getEndTimestamp() ) ), |
| 369 |
'icon' => 'dashicons-dismiss', |
| 370 |
); |
| 371 |
case 'skipped': |
| 372 |
return array( |
| 373 |
'key' => 'skipped', |
| 374 |
'label' => __( 'Skipped', 'tier-pricing-table' ), |
| 375 |
'detail' => __( 'The rule changes neither prices nor quantity limits, so the storefront skips it. Set a price, tiers or a limit on the tabs on the left.', 'tier-pricing-table' ), |
| 376 |
'icon' => 'dashicons-warning', |
| 377 |
); |
| 378 |
case 'scheduled': |
| 379 |
return array( |
| 380 |
'key' => 'scheduled', |
| 381 |
'label' => __( 'Scheduled', 'tier-pricing-table' ), |
| 382 |
/* translators: %s: date */ |
| 383 |
'detail' => sprintf( __( 'Starts %s.', 'tier-pricing-table' ), wp_date( $format, $rule->getStartTimestamp() ) ), |
| 384 |
'icon' => 'dashicons-clock', |
| 385 |
); |
| 386 |
} |
| 387 |
|
| 388 |
return array( |
| 389 |
'key' => 'active', |
| 390 |
'label' => __( 'Active', 'tier-pricing-table' ), |
| 391 |
'detail' => $rule->getEndDate() |
| 392 |
/* translators: %s: date */ |
| 393 |
? sprintf( __( 'Applies now, until %s.', 'tier-pricing-table' ), wp_date( $format, $rule->getEndTimestamp() ) ) |
| 394 |
: __( 'Applies now.', 'tier-pricing-table' ), |
| 395 |
'icon' => 'dashicons-yes-alt', |
| 396 |
); |
| 397 |
} |
| 398 |
|
| 399 |
/** |
| 400 |
* "Always", "From …", "Until …" or "… to …". |
| 401 |
*/ |
| 402 |
public static function scheduleSummary( GlobalPricingRule $rule ): string { |
| 403 |
if ( ! $rule->hasSchedule() ) { |
| 404 |
return __( 'Always', 'tier-pricing-table' ); |
| 405 |
} |
| 406 |
|
| 407 |
$format = get_option( 'date_format' ); |
| 408 |
$from = $rule->getStartDate() ? wp_date( $format, $rule->getStartTimestamp() ) : null; |
| 409 |
$to = $rule->getEndDate() ? wp_date( $format, $rule->getEndTimestamp() ) : null; |
| 410 |
|
| 411 |
if ( $from && $to ) { |
| 412 |
/* translators: 1: start date, 2: end date */ |
| 413 |
return sprintf( __( '%1$s to %2$s', 'tier-pricing-table' ), $from, $to ); |
| 414 |
} |
| 415 |
|
| 416 |
if ( $from ) { |
| 417 |
/* translators: %s: start date */ |
| 418 |
return sprintf( __( 'From %s', 'tier-pricing-table' ), $from ); |
| 419 |
} |
| 420 |
|
| 421 |
/* translators: %s: end date */ |
| 422 |
return sprintf( __( 'Until %s', 'tier-pricing-table' ), $to ); |
| 423 |
} |
| 424 |
} |
| 425 |
|