PluginProbe
Tiered Pricing Table for WooCommerce / 8.0.2
Tiered Pricing Table for WooCommerce v8.0.2
8.0.2 7.1.7 7.1.5 7.1.4 7.1.1 6.5.0 6.4.0 6.1.0 trunk 1.0 1.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2 2.2.3 2.3.0 2.3.1 2.3.2 2.3.3 All 91 releases
tier-pricing-table / src / Addons / NonLoggedInUsers / Wholesale / CPT / ApplicationAdmin.php

ApplicationAdmin.php in Tiered Pricing Table for WooCommerce 8.0.2, at src/Addons/NonLoggedInUsers/Wholesale/CPT/ApplicationAdmin.php

397 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php namespace TierPricingTable\Addons\NonLoggedInUsers\Wholesale\CPT;
2
3 use TierPricingTable\Addons\NonLoggedInUsers\Wholesale\Models\WholesaleApplication;
4 use TierPricingTable\Addons\NonLoggedInUsers\Wholesale\Services\ApplicationService;
5 use TierPricingTable\Addons\NonLoggedInUsers\Wholesale\Settings\Settings;
6 use WP_Post;
7
8 /**
9 * The applications list and the application screen: columns, approve/reject actions, bulk actions.
10 */
11 class ApplicationAdmin {
12
13 const ACTION = 'tpt_wholesale_application';
14 const NONCE = 'tpt_wholesale_application_nonce';
15
16 public function __construct() {
17 add_filter( 'manage_' . ApplicationCPT::POST_TYPE . '_posts_columns', array( $this, 'columns' ) );
18 add_action( 'manage_' . ApplicationCPT::POST_TYPE . '_posts_custom_column', array( $this, 'columnData' ), 10, 2 );
19 add_filter( 'post_row_actions', array( $this, 'rowActions' ), 10, 2 );
20 add_filter( 'bulk_actions-edit-' . ApplicationCPT::POST_TYPE, array( $this, 'bulkActions' ) );
21 add_filter( 'handle_bulk_actions-edit-' . ApplicationCPT::POST_TYPE, array( $this, 'handleBulkActions' ), 10, 3 );
22 add_filter( 'views_edit-' . ApplicationCPT::POST_TYPE, array( $this, 'removeUnusedViews' ) );
23 add_filter( 'display_post_states', array( $this, 'postStates' ), 10, 2 );
24
25 add_action( 'admin_post_' . self::ACTION, array( $this, 'handleAction' ) );
26 add_action( 'admin_notices', array( $this, 'notices' ) );
27
28 add_action( 'add_meta_boxes_' . ApplicationCPT::POST_TYPE, array( $this, 'metaBoxes' ) );
29 add_action( 'save_post_' . ApplicationCPT::POST_TYPE, array( $this, 'saveDecision' ), 10, 2 );
30 add_filter( 'enter_title_here', array( $this, 'titlePlaceholder' ), 10, 2 );
31 add_action( 'admin_head', array( $this, 'styles' ) );
32 }
33
34 /* --- list table ------------------------------------------------------------------------ */
35
36 public function columns( array $columns ): array {
37 return array(
38 'cb' => $columns['cb'] ?? '<input type="checkbox" />',
39 'applicant' => __( 'Applicant', 'tier-pricing-table' ),
40 'company' => __( 'Company', 'tier-pricing-table' ),
41 'contact' => __( 'Contact', 'tier-pricing-table' ),
42 'status' => __( 'Status', 'tier-pricing-table' ),
43 'submitted' => __( 'Submitted', 'tier-pricing-table' ),
44 'decision' => __( 'Decision', 'tier-pricing-table' ),
45 );
46 }
47
48 public function columnData( string $column, int $postId ) {
49 $application = WholesaleApplication::get( $postId );
50
51 if ( ! $application ) {
52 return;
53 }
54
55 switch ( $column ) {
56 case 'applicant':
57 $user = $application->getUser();
58 ?>
59 <strong><a href="<?php echo esc_url( $application->getAdminUrl() ); ?>"><?php echo esc_html( $application->getName() ?: $application->getEmail() ); ?></a></strong>
60 <div class="tpt-wholesale-muted">
61 <?php if ( $user ) : ?>
62 <a href="<?php echo esc_url( get_edit_user_link( $user->ID ) ); ?>"><?php echo esc_html( $application->getEmail() ); ?></a>
63 <?php else : ?>
64 <?php echo esc_html( $application->getEmail() ); ?>
65 <em><?php esc_html_e( '(account deleted)', 'tier-pricing-table' ); ?></em>
66 <?php endif; ?>
67 </div>
68 <?php
69 break;
70 case 'company':
71 echo esc_html( $application->getCompany() ?: '—' );
72
73 if ( $application->getField( 'tax_id' ) ) {
74 ?><div class="tpt-wholesale-muted"><?php echo esc_html( $application->getField( 'tax_id' ) ); ?></div><?php
75 }
76 break;
77 case 'contact':
78 echo esc_html( $application->getField( 'phone' ) ?: '—' );
79
80 if ( $application->getField( 'website' ) ) {
81 ?><div class="tpt-wholesale-muted"><a href="<?php echo esc_url( $application->getField( 'website' ) ); ?>" target="_blank" rel="noopener"><?php echo esc_html( preg_replace( '#^https?://#', '', $application->getField( 'website' ) ) ); ?></a></div><?php
82 }
83 break;
84 case 'status':
85 $this->statusBadge( $application );
86 break;
87 case 'submitted':
88 echo esc_html( $application->getDate() ? date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $application->getDate() ) ) : '—' );
89 break;
90 case 'decision':
91 if ( $application->isPending() ) {
92 ?>
93 <a class="button button-primary button-small" href="<?php echo esc_url( $this->actionUrl( 'approve', $postId ) ); ?>"><?php esc_html_e( 'Approve', 'tier-pricing-table' ); ?></a>
94 <a class="button button-small" href="<?php echo esc_url( $this->actionUrl( 'reject', $postId ) ); ?>"><?php esc_html_e( 'Reject', 'tier-pricing-table' ); ?></a>
95 <?php
96 } else {
97 $decidedAt = $application->getField( 'decided_at' );
98 $decidedBy = (int) $application->getField( 'decided_by' );
99 $by = $decidedBy ? get_user_by( 'id', $decidedBy ) : null;
100
101 echo esc_html( $decidedAt ? date_i18n( get_option( 'date_format' ), strtotime( $decidedAt ) ) : '—' );
102
103 if ( $by ) {
104 ?><div class="tpt-wholesale-muted"><?php echo esc_html( $by->display_name ); ?></div><?php
105 } elseif ( ! $decidedBy && $application->isApproved() ) {
106 ?><div class="tpt-wholesale-muted"><?php esc_html_e( 'automatic', 'tier-pricing-table' ); ?></div><?php
107 }
108 }
109 break;
110 }
111 }
112
113 protected function statusBadge( WholesaleApplication $application ) {
114 $class = str_replace( 'tpt-', '', $application->getStatus() );
115 ?>
116 <span class="tpt-wholesale-status tpt-wholesale-status--<?php echo esc_attr( $class ); ?>"><?php echo esc_html( $application->getStatusLabel() ); ?></span>
117 <?php
118 }
119
120 public function rowActions( array $actions, WP_Post $post ): array {
121 if ( ApplicationCPT::POST_TYPE !== $post->post_type ) {
122 return $actions;
123 }
124
125 $application = WholesaleApplication::get( $post->ID );
126 $new = array();
127
128 if ( isset( $actions['edit'] ) ) {
129 $new['edit'] = '<a href="' . esc_url( get_edit_post_link( $post->ID ) ) . '">' . esc_html__( 'View', 'tier-pricing-table' ) . '</a>';
130 }
131
132 if ( $application && ! $application->isApproved() ) {
133 $new['approve'] = '<a href="' . esc_url( $this->actionUrl( 'approve', $post->ID ) ) . '">' . esc_html__( 'Approve', 'tier-pricing-table' ) . '</a>';
134 }
135
136 if ( $application && ! $application->isRejected() ) {
137 $new['reject'] = '<a href="' . esc_url( $this->actionUrl( 'reject', $post->ID ) ) . '">' . esc_html__( 'Reject', 'tier-pricing-table' ) . '</a>';
138 }
139
140 if ( isset( $actions['trash'] ) ) {
141 $new['trash'] = $actions['trash'];
142 }
143
144 return $new;
145 }
146
147 public function bulkActions( array $actions ): array {
148 unset( $actions['edit'] );
149
150 return array_merge( array(
151 'tpt_approve' => __( 'Approve', 'tier-pricing-table' ),
152 'tpt_reject' => __( 'Reject', 'tier-pricing-table' ),
153 ), $actions );
154 }
155
156 public function handleBulkActions( string $redirect, string $action, array $postIds ): string {
157 if ( ! in_array( $action, array( 'tpt_approve', 'tpt_reject' ), true ) || ! current_user_can( 'manage_woocommerce' ) ) {
158 return $redirect;
159 }
160
161 $service = new ApplicationService();
162 $done = 0;
163
164 foreach ( $postIds as $postId ) {
165 $application = WholesaleApplication::get( (int) $postId );
166
167 if ( ! $application ) {
168 continue;
169 }
170
171 $ok = 'tpt_approve' === $action
172 ? $service->approve( $application, get_current_user_id() )
173 : $service->reject( $application, get_current_user_id() );
174
175 $done += $ok ? 1 : 0;
176 }
177
178 return add_query_arg( array(
179 'tpt-wholesale-notice' => 'tpt_approve' === $action ? 'approved' : 'rejected',
180 'tpt-wholesale-count' => $done,
181 ), $redirect );
182 }
183
184 public function removeUnusedViews( array $views ): array {
185 unset( $views['publish'], $views['draft'], $views['future'], $views['private'] );
186
187 return $views;
188 }
189
190 public function postStates( array $states, WP_Post $post ): array {
191 if ( ApplicationCPT::POST_TYPE === $post->post_type ) {
192 return array(); // the status column is enough
193 }
194
195 return $states;
196 }
197
198 /* --- approve / reject links -------------------------------------------------------------- */
199
200 protected function actionUrl( string $do, int $postId ): string {
201 return wp_nonce_url( add_query_arg( array(
202 'action' => self::ACTION,
203 'do' => $do,
204 'id' => $postId,
205 ), admin_url( 'admin-post.php' ) ), self::ACTION . '_' . $postId, self::NONCE );
206 }
207
208 public function handleAction() {
209 $postId = isset( $_GET['id'] ) ? (int) $_GET['id'] : 0;
210 $do = isset( $_GET['do'] ) ? sanitize_key( $_GET['do'] ) : '';
211
212 if ( ! $postId || ! current_user_can( 'manage_woocommerce' ) || ! isset( $_GET[ self::NONCE ] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ self::NONCE ] ) ), self::ACTION . '_' . $postId ) ) {
213 wp_die( esc_html__( 'You are not allowed to do this.', 'tier-pricing-table' ) );
214 }
215
216 $application = WholesaleApplication::get( $postId );
217 $service = new ApplicationService();
218 $notice = 'error';
219
220 if ( $application && 'approve' === $do && $service->approve( $application, get_current_user_id() ) ) {
221 $notice = 'approved';
222 } elseif ( $application && 'reject' === $do && $service->reject( $application, get_current_user_id() ) ) {
223 $notice = 'rejected';
224 }
225
226 $back = wp_get_referer() ?: admin_url( 'edit.php?post_type=' . ApplicationCPT::POST_TYPE );
227
228 wp_safe_redirect( add_query_arg( array( 'tpt-wholesale-notice' => $notice, 'tpt-wholesale-count' => 1 ), remove_query_arg( array( 'tpt-wholesale-notice', 'tpt-wholesale-count' ), $back ) ) );
229 exit;
230 }
231
232 public function notices() {
233 if ( empty( $_GET['tpt-wholesale-notice'] ) ) {
234 return;
235 }
236
237 $notice = sanitize_key( $_GET['tpt-wholesale-notice'] );
238 $count = isset( $_GET['tpt-wholesale-count'] ) ? (int) $_GET['tpt-wholesale-count'] : 1;
239
240 if ( 'approved' === $notice ) {
241 /* translators: %s: number of applications */
242 $message = sprintf( _n( '%s application approved. The customer now has the wholesale role.', '%s applications approved. The customers now have the wholesale role.', $count, 'tier-pricing-table' ), number_format_i18n( $count ) );
243 $class = 'notice-success';
244 } elseif ( 'rejected' === $notice ) {
245 /* translators: %s: number of applications */
246 $message = sprintf( _n( '%s application rejected.', '%s applications rejected.', $count, 'tier-pricing-table' ), number_format_i18n( $count ) );
247 $class = 'notice-info';
248 } else {
249 $message = __( 'The application could not be updated. Check that the wholesale role exists and that the customer account still exists.', 'tier-pricing-table' );
250 $class = 'notice-error';
251 }
252 ?>
253 <div class="notice <?php echo esc_attr( $class ); ?> is-dismissible"><p><?php echo esc_html( $message ); ?></p></div>
254 <?php
255 }
256
257 /* --- application screen --------------------------------------------------------------- */
258
259 public function metaBoxes( WP_Post $post ) {
260 remove_meta_box( 'submitdiv', ApplicationCPT::POST_TYPE, 'side' );
261 remove_meta_box( 'slugdiv', ApplicationCPT::POST_TYPE, 'normal' );
262
263 add_meta_box( 'tpt-wholesale-details', __( 'Application', 'tier-pricing-table' ), array( $this, 'renderDetails' ), ApplicationCPT::POST_TYPE, 'normal', 'high' );
264 add_meta_box( 'tpt-wholesale-decision', __( 'Decision', 'tier-pricing-table' ), array( $this, 'renderDecision' ), ApplicationCPT::POST_TYPE, 'side', 'high' );
265 }
266
267 public function renderDetails( WP_Post $post ) {
268 $application = new WholesaleApplication( $post );
269 $user = $application->getUser();
270 ?>
271 <table class="widefat striped tpt-wholesale-details">
272 <tbody>
273 <?php foreach ( $application->getDetails() as $label => $value ) : ?>
274 <tr>
275 <th scope="row"><?php echo esc_html( $label ); ?></th>
276 <td>
277 <?php if ( $value === $application->getField( 'website' ) && '' !== $value ) : ?>
278 <a href="<?php echo esc_url( $value ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $value ); ?></a>
279 <?php else : ?>
280 <?php echo nl2br( esc_html( $value ) ); ?>
281 <?php endif; ?>
282 </td>
283 </tr>
284 <?php endforeach; ?>
285 <tr>
286 <th scope="row"><?php esc_html_e( 'Customer account', 'tier-pricing-table' ); ?></th>
287 <td>
288 <?php if ( $user ) : ?>
289 <a href="<?php echo esc_url( get_edit_user_link( $user->ID ) ); ?>"><?php echo esc_html( $user->display_name ); ?></a>
290 <span class="tpt-wholesale-muted">(<?php echo esc_html( implode( ', ', array_map( 'translate_user_role', array_map( function ( $r ) { return wp_roles()->roles[ $r ]['name'] ?? $r; }, (array) $user->roles ) ) ) ); ?>)</span>
291 <?php else : ?>
292 <em><?php esc_html_e( 'The account no longer exists.', 'tier-pricing-table' ); ?></em>
293 <?php endif; ?>
294 </td>
295 </tr>
296 <tr>
297 <th scope="row"><?php esc_html_e( 'Submitted', 'tier-pricing-table' ); ?></th>
298 <td><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $application->getDate() ) ) ); ?></td>
299 </tr>
300 </tbody>
301 </table>
302 <?php
303 }
304
305 public function renderDecision( WP_Post $post ) {
306 $application = new WholesaleApplication( $post );
307 $role = $application->getRequestedRole() ?: Settings::getRole();
308 $roleName = $role && isset( wp_roles()->roles[ $role ] ) ? translate_user_role( wp_roles()->roles[ $role ]['name'] ) : $role;
309
310 wp_nonce_field( self::ACTION . '_decision', self::NONCE );
311 ?>
312 <p><?php $this->statusBadge( $application ); ?></p>
313
314 <?php if ( $application->isRejected() && $application->getRejectionReason() ) : ?>
315 <p class="tpt-wholesale-muted"><?php echo esc_html( $application->getRejectionReason() ); ?></p>
316 <?php endif; ?>
317
318 <?php if ( ! $application->isApproved() ) : ?>
319 <p><?php
320 /* translators: %s: role name */
321 echo esc_html( sprintf( __( 'Approving gives the customer the "%s" role.', 'tier-pricing-table' ), $roleName ) ); ?></p>
322 <p><button type="submit" name="tpt_wholesale_decision" value="approve" class="button button-primary button-large tpt-wholesale-decision__button"><?php esc_html_e( 'Approve', 'tier-pricing-table' ); ?></button></p>
323 <?php endif; ?>
324
325 <?php if ( ! $application->isRejected() ) : ?>
326 <p>
327 <label for="tpt_wholesale_reason" class="tpt-wholesale-muted"><?php esc_html_e( 'Reason (sent to the applicant, optional)', 'tier-pricing-table' ); ?></label>
328 <textarea name="tpt_wholesale_reason" id="tpt_wholesale_reason" rows="3" class="widefat"></textarea>
329 </p>
330 <p><button type="submit" name="tpt_wholesale_decision" value="reject" class="button button-large tpt-wholesale-decision__button"><?php esc_html_e( 'Reject', 'tier-pricing-table' ); ?></button></p>
331 <?php endif; ?>
332
333 <p class="tpt-wholesale-muted"><a class="submitdelete" href="<?php echo esc_url( get_delete_post_link( $post->ID ) ); ?>"><?php esc_html_e( 'Move to Trash', 'tier-pricing-table' ); ?></a></p>
334 <?php
335 }
336
337 public function saveDecision( int $postId, WP_Post $post ) {
338 if ( empty( $_POST['tpt_wholesale_decision'] ) || ! isset( $_POST[ self::NONCE ] ) ) {
339 return;
340 }
341
342 if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ self::NONCE ] ) ), self::ACTION . '_decision' ) || ! current_user_can( 'manage_woocommerce' ) ) {
343 return;
344 }
345
346 $decision = sanitize_key( $_POST['tpt_wholesale_decision'] );
347 $reason = isset( $_POST['tpt_wholesale_reason'] ) ? sanitize_textarea_field( wp_unslash( $_POST['tpt_wholesale_reason'] ) ) : '';
348 $application = WholesaleApplication::get( $postId );
349 $service = new ApplicationService();
350
351 if ( ! $application ) {
352 return;
353 }
354
355 // the post form has already saved the post; the status change below is the decision
356 remove_action( 'save_post_' . ApplicationCPT::POST_TYPE, array( $this, 'saveDecision' ), 10 );
357
358 if ( 'approve' === $decision ) {
359 $ok = $service->approve( $application, get_current_user_id() );
360 } else {
361 $ok = $service->reject( $application, get_current_user_id(), $reason );
362 }
363
364 add_filter( 'redirect_post_location', function ( $location ) use ( $decision, $ok ) {
365 return add_query_arg( array(
366 'tpt-wholesale-notice' => $ok ? ( 'approve' === $decision ? 'approved' : 'rejected' ) : 'error',
367 'tpt-wholesale-count' => 1,
368 ), remove_query_arg( 'message', $location ) );
369 } );
370 }
371
372 public function titlePlaceholder( string $placeholder, WP_Post $post ): string {
373 return ApplicationCPT::POST_TYPE === $post->post_type ? __( 'Application', 'tier-pricing-table' ) : $placeholder;
374 }
375
376 public function styles() {
377 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
378
379 if ( ! $screen || ApplicationCPT::POST_TYPE !== $screen->post_type ) {
380 return;
381 }
382 ?>
383 <style>
384 .tpt-wholesale-muted { color: #646970; font-size: 12px; }
385 .tpt-wholesale-status { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; line-height: 1.6; }
386 .tpt-wholesale-status--pending { background: #fcf9e8; color: #8a6d0b; border: 1px solid #f0e2a6; }
387 .tpt-wholesale-status--approved { background: #edfaef; color: #1e6b32; border: 1px solid #b7e2c1; }
388 .tpt-wholesale-status--rejected { background: #fcf0f1; color: #8a1f28; border: 1px solid #f1b8bd; }
389 .column-decision .button { margin-right: 4px; }
390 .tpt-wholesale-details th { width: 180px; }
391 .tpt-wholesale-decision__button { width: 100%; text-align: center; }
392 #titlediv { display: none; }
393 </style>
394 <?php
395 }
396 }
397