PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.9
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.9
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
subscription / includes / Admin / views / subscription-details.php

subscription-details.php in Subscriptions for WooCommerce with Stripe Recurring Payments 1.10.9, at includes/Admin/views/subscription-details.php

1,047 lines 37.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Standalone subscription details page view.
4 *
5 * Rendered by Menu::render_subscription_details_page(). Matches the
6 * WPSubscription admin design system (wpsubs-* components + :root tokens).
7 *
8 * @package SpringDevs\Subscription\Admin
9 *
10 * @var int $subscription_id Subscription post ID.
11 * @var array $subscription_data Structured data from Helper::get_subscription_data().
12 * @var array $rows Filtered info rows from Subscriptions::get_info_rows().
13 * @var \WC_Order $order Related WooCommerce order.
14 * @var mixed $order_item Related order line item.
15 * @var array $actions Allowed action slugs for the current status.
16 * @var array $actions_data Map of action slug => label/value.
17 * @var array $order_histories Related order relation rows.
18 * @var string $list_url Subscriptions list URL.
19 * @var string $form_action URL the status form posts to.
20 */
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 use SpringDevs\Subscription\Illuminate\Helper;
27
28 $product_name = $order_item ? $order_item->get_name() : '-';
29 $product_id = $order_item ? $order_item->get_product_id() : 0;
30 $product_link = $product_id ? get_the_permalink( $product_id ) : '';
31 $product_obj = $product_id ? wc_get_product( $product_id ) : null;
32 $product_image = $product_obj ? $product_obj->get_image( 'thumbnail', array( 'class' => 'subscrpt-plan-img' ) ) : '';
33
34 $subscrpt_status = $subscription_data['status'] ?? get_post_status( $subscription_id );
35 $verbose_status = Helper::get_verbose_status( $subscrpt_status );
36
37 $badge_mod_map = array(
38 'active' => 'active',
39 'pending' => 'pending',
40 'pe_cancelled' => 'pending-cancel',
41 'cancelled' => 'cancelled',
42 'expired' => 'expired',
43 'draft' => 'draft',
44 'trash' => 'trash',
45 );
46 $badge_mod = $badge_mod_map[ $subscrpt_status ] ?? 'expired';
47
48 $is_grace_period = isset( $subscription_data['grace_period'] );
49 $grace_remaining = $subscription_data['grace_period']['remaining_days'] ?? 0;
50 $grace_end_date = $subscription_data['grace_period']['end_date'] ?? '';
51 $grace_end_date = ! empty( $grace_end_date ) ? wp_date( get_option( 'date_format' ) . ' - ' . get_option( 'time_format' ), strtotime( $grace_end_date ) ) : '';
52
53 if ( $is_grace_period && $grace_remaining > 0 ) {
54 $badge_mod = 'active';
55 $verbose_status = __( 'Active', 'subscription' );
56 }
57
58 $customer_id = $order ? $order->get_customer_id() : 0;
59 $customer_name = $order ? $order->get_formatted_billing_full_name() : '-';
60 $customer_email = $order ? $order->get_billing_email() : '';
61 $customer_phone = $order ? $order->get_billing_phone() : '';
62 $customer_url = $customer_id ? admin_url( 'user-edit.php?user_id=' . $customer_id ) : '';
63
64 // Page header: product title + meta description (badge · #id · email · next payment).
65 $header_title = ( $product_name && '-' !== $product_name )
66 ? $product_name
67 /* translators: %s: Subscription ID */
68 : sprintf( __( 'Subscription #%s', 'subscription' ), $subscription_id );
69
70 $next_payment_date = $subscription_data['next_date'] ?? '';
71 $next_payment_date = ! empty( $next_payment_date ) ? wp_date( get_option( 'date_format' ), strtotime( $next_payment_date ) ) : '';
72
73 $header_badge = '<span class="wpsubs-badge wpsubs-badge--' . esc_attr( $badge_mod ) . '">' . esc_html( $verbose_status );
74 if ( $is_grace_period && $grace_remaining > 0 ) {
75 /* translators: %d: days remaining */
76 $grace_title = sprintf( __( '%d days remaining in grace period', 'subscription' ), $grace_remaining );
77 $header_badge .= ' <span class="dashicons dashicons-warning" style="font-size:11px;width:11px;height:11px;color:#d97706;" title="' . esc_attr( $grace_title ) . '"></span>';
78 }
79 $header_badge .= '</span>';
80
81 $header_segments = array(
82 $header_badge,
83 '#' . (int) $subscription_id,
84 );
85 if ( $customer_email ) {
86 $header_segments[] = esc_html( $customer_email );
87 }
88 if ( $next_payment_date ) {
89 /* translators: %s: next payment date */
90 $header_segments[] = esc_html( sprintf( __( 'Next payment on %s', 'subscription' ), $next_payment_date ) );
91 }
92 $header_desc_html = implode( '<span class="subscrpt-detail-head__sep">·</span>', $header_segments );
93
94 $rows = is_array( $rows ) ? $rows : array();
95
96 // Top summary tiles — pulled from the info rows so Pro's filtered values stay
97 // in sync. Their keys are marked used so they are not repeated below.
98 $summary_tiles = array();
99 if ( $order && isset( $subscription_data['price'] ) && '' !== $subscription_data['price'] ) {
100 // Build the recurring price from structured data so the price and period can
101 // be styled separately (price prominent, "/ period" small and muted).
102 $timing_per = (int) ( $subscription_data['schedule']['timing_per'] ?? 1 );
103 $timing_option = $subscription_data['schedule']['timing_option'] ?? '';
104 $period_label = '';
105 if ( $timing_option ) {
106 $period_label = $timing_per > 1
107 ? $timing_per . ' ' . ucfirst( $timing_option ) . 's'
108 : ucfirst( $timing_option );
109 }
110
111 $recurring_value = wc_price( (float) $subscription_data['price'], array( 'currency' => $order->get_currency() ) );
112 if ( $period_label ) {
113 $recurring_value .= '<span class="subscrpt-summary__unit"> / ' . esc_html( $period_label ) . '</span>';
114 }
115
116 $summary_tiles[] = array(
117 'label' => __( 'Recurring', 'subscription' ),
118 'value' => $recurring_value,
119 );
120 }
121 if ( ! empty( $subscription_data['start_date'] ) ) {
122 $summary_tiles[] = array(
123 'label' => __( 'Started', 'subscription' ),
124 'value' => esc_html( wp_date( get_option( 'date_format' ), strtotime( $subscription_data['start_date'] ) ) ),
125 );
126 }
127 if ( ! empty( $subscription_data['next_date'] ) ) {
128 $summary_tiles[] = array(
129 'label' => __( 'Next Payment', 'subscription' ),
130 'value' => esc_html( wp_date( get_option( 'date_format' ), strtotime( $subscription_data['next_date'] ) ) ),
131 );
132 }
133 // Total payments is not part of $subscription_data; pull it from the info rows.
134 if ( isset( $rows['total_payments'] ) ) {
135 $summary_tiles[] = array(
136 'label' => __( 'Total Payments', 'subscription' ),
137 'value' => $rows['total_payments']['value'],
138 );
139 }
140
141 // Group the remaining info rows into cards. Unmapped keys — including rows added
142 // through the subscrpt_admin_info_rows filter (Pro) — fall into "Additional
143 // Details" so nothing is dropped. Status is shown as the header badge.
144 $used_keys = array(
145 'cost' => true,
146 'start_date' => true,
147 'next_date' => true,
148 'total_payments' => true,
149 'status' => true,
150 );
151
152 // Plan card: product + qty render side by side; trial/signup fee as extra rows.
153 $plan_product = isset( $rows['product'] ) ? $rows['product']['value'] : '';
154 $plan_qty = isset( $rows['quantity'] ) ? $rows['quantity']['value'] : '';
155 $plan_extra = array();
156 foreach ( array( 'product', 'quantity', 'signup_fee', 'trial', 'trial_period' ) as $plan_key ) {
157 $used_keys[ $plan_key ] = true;
158 if ( in_array( $plan_key, array( 'signup_fee', 'trial', 'trial_period' ), true ) && isset( $rows[ $plan_key ] ) ) {
159 $plan_extra[ $plan_key ] = $rows[ $plan_key ];
160 }
161 }
162
163 $build_sections = function ( array $map ) use ( $rows, &$used_keys ) {
164 $out = array();
165 foreach ( $map as $section ) {
166 $section_rows = array();
167 foreach ( $section['keys'] as $key ) {
168 if ( isset( $rows[ $key ] ) ) {
169 $section_rows[ $key ] = $rows[ $key ];
170 $used_keys[ $key ] = true;
171 }
172 }
173 if ( ! empty( $section_rows ) ) {
174 $out[] = array(
175 'title' => $section['title'],
176 'rows' => $section_rows,
177 );
178 }
179 }
180 return $out;
181 };
182
183 // Payment Method renders next to the Plan card (main column). Shows the gateway
184 // icon + title only — no key/value table. Extra rows (e.g. Stripe auto renewal)
185 // keep their kv layout beneath.
186 $used_keys['payment_method'] = true;
187 $used_keys['stripe_auto_renewal'] = true;
188 $payment_title = $order ? $order->get_payment_method_title() : '';
189 $payment_icon = '';
190 $payment_plugin = '';
191 if ( $order ) {
192 $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array();
193 $gateway_id = $order->get_payment_method();
194 $gateway_obj = isset( $gateways[ $gateway_id ] ) ? $gateways[ $gateway_id ] : null;
195 $payment_icon = $gateway_obj ? $gateway_obj->get_icon() : '';
196
197 // Resolve which plugin registered this gateway (e.g. several Stripe methods
198 // all come from "WooCommerce Stripe Gateway"). Map the gateway class file to
199 // its plugin folder, then read that plugin's header Name.
200 if ( $gateway_obj ) {
201 try {
202 $gateway_file = wp_normalize_path( ( new ReflectionClass( $gateway_obj ) )->getFileName() );
203 $plugins_dir = wp_normalize_path( WP_PLUGIN_DIR );
204 if ( $gateway_file && 0 === strpos( $gateway_file, $plugins_dir ) ) {
205 $gateway_slug = strtok( ltrim( substr( $gateway_file, strlen( $plugins_dir ) ), '/' ), '/' );
206 if ( ! function_exists( 'get_plugins' ) ) {
207 require_once ABSPATH . 'wp-admin/includes/plugin.php';
208 }
209 foreach ( get_plugins() as $plugin_path => $plugin_data ) {
210 if ( strtok( $plugin_path, '/' ) === $gateway_slug ) {
211 $payment_plugin = $plugin_data['Name'];
212 break;
213 }
214 }
215 }
216 } catch ( \Exception $e ) {
217 $payment_plugin = '';
218 }
219 }
220 }
221 // Payment Method card is always shown; the title falls back to '-' when no
222 // gateway is resolved.
223 $has_payment = true;
224
225 // Secondary info → sidebar.
226 $side_sections = $build_sections(
227 array(
228 array(
229 'title' => __( 'Billing & Shipping', 'subscription' ),
230 'keys' => array( 'billing_address', 'shipping_address' ),
231 ),
232 )
233 );
234
235 // Any leftover rows → Additional Details (main column).
236 $main_sections = array();
237 $leftover_rows = array_diff_key( $rows, $used_keys );
238 if ( ! empty( $leftover_rows ) ) {
239 $main_sections[] = array(
240 'title' => __( 'Additional Details', 'subscription' ),
241 'rows' => $leftover_rows,
242 );
243 }
244
245 // Resolve related orders once.
246 $related_rows = array();
247 foreach ( $order_histories as $history ) {
248 $related_order = wc_get_order( $history->order_id );
249 if ( ! $related_order ) {
250 continue;
251 }
252 $related_rows[] = array(
253 'order' => $related_order,
254 'label' => ucfirst( str_replace( '-', ' ', (string) $history->type ) ),
255 );
256 }
257
258 // Reusable per-page + date toolbar for the paginated tables. Uses the admin
259 // advanced-select component; the date options are injected client-side from the
260 // table's date column.
261 $subscrpt_render_table_tools = function () {
262 ?>
263 <div class="subscrpt-card__tools">
264 <?php
265 wpsubs_render_adv_select(
266 array(
267 'name' => 'subscrpt_date_filter',
268 'placeholder' => __( 'All Dates', 'subscription' ),
269 'value' => '',
270 'align' => 'right',
271 'class' => 'subscrpt-filter-date',
272 'options' => array(
273 array(
274 'value' => '',
275 'label' => __( 'All Dates', 'subscription' ),
276 ),
277 ),
278 )
279 );
280 wpsubs_render_adv_select(
281 array(
282 'name' => 'subscrpt_per_page',
283 'value' => '10',
284 'align' => 'right',
285 'class' => 'subscrpt-filter-perpage',
286 'options' => array(
287 array(
288 'value' => '10',
289 'label' => __( '10 / page', 'subscription' ),
290 ),
291 array(
292 'value' => '20',
293 'label' => __( '20 / page', 'subscription' ),
294 ),
295 array(
296 'value' => '50',
297 'label' => __( '50 / page', 'subscription' ),
298 ),
299 array(
300 'value' => '100',
301 'label' => __( '100 / page', 'subscription' ),
302 ),
303 ),
304 )
305 );
306 ?>
307 </div>
308 <?php
309 };
310
311 /**
312 * Shared context passed to every subscription-details extension hook so that
313 * third-party plugins can render extra sections without re-querying.
314 *
315 * @var array $subscrpt_details_ctx {
316 * @type int $subscription_id Subscription post ID.
317 * @type array $subscription_data Structured data from Helper::get_subscription_data().
318 * @type \WC_Order $order Related WooCommerce order (or null).
319 * @type mixed $order_item Related order line item (or null).
320 * @type string $status Subscription post status.
321 * }
322 */
323 $subscrpt_details_ctx = array(
324 'subscription_id' => $subscription_id,
325 'subscription_data' => $subscription_data,
326 'order' => $order,
327 'order_item' => $order_item,
328 'status' => $subscrpt_status,
329 );
330 ?>
331 <div class="wp-subscription-admin-content list-page subscrpt-subs-details">
332
333 <!-- Page header -->
334 <div class="subscrpt-detail-head">
335 <h1 class="subscrpt-detail-head__title"><?php echo esc_html( $header_title ); ?></h1>
336 <div class="subscrpt-detail-head__desc"><?php echo wp_kses_post( $header_desc_html ); ?></div>
337 <div class="subscrpt-detail-head__divider"></div>
338 </div>
339
340 <div class="subscrpt-detail-grid">
341
342 <!-- Main column -->
343 <div class="subscrpt-detail-main">
344
345 <?php
346 /**
347 * Fires at the top of the details main (left) column, before the summary strip.
348 *
349 * Echo a `.subscrpt-card` to add a full-width section here.
350 *
351 * @param array $subscrpt_details_ctx Subscription details context.
352 */
353 do_action( 'subscrpt_details_main_top', $subscrpt_details_ctx );
354 ?>
355
356 <!-- Summary strip -->
357 <?php if ( ! empty( $summary_tiles ) ) : ?>
358 <div class="subscrpt-summary">
359 <?php foreach ( $summary_tiles as $tile ) : ?>
360 <div class="subscrpt-summary__tile">
361 <span class="subscrpt-summary__label"><?php echo esc_html( $tile['label'] ); ?></span>
362 <span class="subscrpt-summary__value"><?php echo wp_kses_post( $tile['value'] ); ?></span>
363 </div>
364 <?php endforeach; ?>
365 </div>
366 <?php endif; ?>
367
368 <?php if ( $is_grace_period ) : ?>
369 <!-- Grace period card -->
370 <div class="subscrpt-card">
371 <div class="subscrpt-card__head"><?php esc_html_e( 'Grace Period', 'subscription' ); ?></div>
372 <div class="subscrpt-card__body">
373 <table class="subscrpt-kv">
374 <tbody>
375 <tr>
376 <th><?php esc_html_e( 'Remaining', 'subscription' ); ?></th>
377 <td><?php echo esc_html( sprintf( /* translators: %d: days */ _n( '%d day', '%d days', (int) $grace_remaining, 'subscription' ), (int) $grace_remaining ) ); ?></td>
378 </tr>
379 <?php if ( $grace_end_date ) : ?>
380 <tr>
381 <th><?php esc_html_e( 'End Date', 'subscription' ); ?></th>
382 <td><?php echo esc_html( $grace_end_date ); ?></td>
383 </tr>
384 <?php endif; ?>
385 </tbody>
386 </table>
387 </div>
388 </div>
389 <?php endif; ?>
390
391 <!-- Plan + Payment Method side by side -->
392 <?php
393 $has_plan = $plan_product || $plan_qty || ! empty( $plan_extra );
394 if ( $has_plan || $has_payment ) :
395 ?>
396 <div class="subscrpt-plan-pay">
397 <?php if ( $has_plan ) : ?>
398 <!-- Plan card (product + qty side by side) -->
399 <div class="subscrpt-card subscrpt-card--plan">
400 <div class="subscrpt-card__head"><?php esc_html_e( 'Product', 'subscription' ); ?></div>
401 <div class="subscrpt-card__body">
402 <div class="subscrpt-plan-row">
403 <div class="subscrpt-plan-thumb">
404 <?php if ( $product_image ) : ?>
405 <?php echo wp_kses_post( $product_image ); ?>
406 <?php else : ?>
407 <span class="subscrpt-plan-thumb__ph dashicons dashicons-format-image"></span>
408 <?php endif; ?>
409 </div>
410 <div class="subscrpt-plan-meta">
411 <span class="subscrpt-plan-name"><?php echo $plan_product ? wp_kses_post( $plan_product ) : '&mdash;'; ?></span>
412 <span class="subscrpt-plan-qty">
413 <?php
414 /* translators: %s: quantity */
415 printf( esc_html__( 'Quantity: %s', 'subscription' ), $plan_qty ? wp_kses_post( $plan_qty ) : '1' );
416 ?>
417 </span>
418 </div>
419 </div>
420 <?php if ( ! empty( $plan_extra ) ) : ?>
421 <table class="subscrpt-kv subscrpt-plan-extra">
422 <tbody>
423 <?php foreach ( $plan_extra as $row ) : ?>
424 <tr>
425 <th><?php echo esc_html( $row['label'] ); ?></th>
426 <td><?php echo wp_kses_post( $row['value'] ); ?></td>
427 </tr>
428 <?php endforeach; ?>
429 </tbody>
430 </table>
431 <?php endif; ?>
432 </div>
433 </div>
434 <?php endif; ?>
435
436 <?php if ( $has_payment ) : ?>
437 <!-- Payment method card (gateway icon + title) -->
438 <div class="subscrpt-card">
439 <div class="subscrpt-card__head"><?php esc_html_e( 'Payment Method', 'subscription' ); ?></div>
440 <div class="subscrpt-card__body">
441 <?php if ( $payment_title ) : ?>
442 <div class="subscrpt-payment">
443 <span class="subscrpt-payment__icon">
444 <?php if ( $payment_icon ) : ?>
445 <?php echo wp_kses_post( $payment_icon ); ?>
446 <?php else : ?>
447 <span class="subscrpt-payment__icon-ph dashicons dashicons-bank"></span>
448 <?php endif; ?>
449 </span>
450 <span class="subscrpt-payment__meta">
451 <span class="subscrpt-payment__title"><?php echo esc_html( $payment_title ); ?></span>
452 <?php if ( $payment_plugin ) : ?>
453 <span class="subscrpt-payment__plugin"><?php echo esc_html( $payment_plugin ); ?></span>
454 <?php endif; ?>
455 </span>
456 </div>
457 <?php else : ?>
458 <p class="subscrpt-muted"><?php esc_html_e( 'No payment method.', 'subscription' ); ?></p>
459 <?php endif; ?>
460 </div>
461 </div>
462 <?php endif; ?>
463 </div>
464 <?php endif; ?>
465
466 <!-- Detail sections -->
467 <?php foreach ( $main_sections as $section ) : ?>
468 <div class="subscrpt-card">
469 <div class="subscrpt-card__head"><?php echo esc_html( $section['title'] ); ?></div>
470 <div class="subscrpt-card__body">
471 <table class="subscrpt-kv">
472 <tbody>
473 <?php foreach ( $section['rows'] as $row ) : ?>
474 <tr>
475 <th><?php echo esc_html( $row['label'] ); ?></th>
476 <td><?php echo wp_kses_post( $row['value'] ); ?></td>
477 </tr>
478 <?php endforeach; ?>
479 </tbody>
480 </table>
481 </div>
482 </div>
483 <?php endforeach; ?>
484
485 <?php if ( empty( $summary_tiles ) && empty( $main_sections ) && empty( $side_sections ) && ! $has_payment && ! $plan_product && ! $plan_qty && empty( $plan_extra ) && ! $is_grace_period ) : ?>
486 <div class="subscrpt-card">
487 <div class="subscrpt-card__head"><?php esc_html_e( 'Subscription Details', 'subscription' ); ?></div>
488 <div class="subscrpt-card__body">
489 <p class="subscrpt-muted"><?php esc_html_e( 'No subscription details available.', 'subscription' ); ?></p>
490 </div>
491 </div>
492 <?php endif; ?>
493
494 <!-- Related orders card -->
495 <div class="subscrpt-card" data-subscrpt-paginate data-per-page="10" data-date-col="2">
496 <div class="subscrpt-card__head subscrpt-card__head--toolbar">
497 <span class="subscrpt-card__title"><?php esc_html_e( 'Related Orders', 'subscription' ); ?></span>
498 <?php if ( ! empty( $related_rows ) ) : ?>
499 <?php $subscrpt_render_table_tools(); ?>
500 <?php endif; ?>
501 </div>
502 <?php if ( ! empty( $related_rows ) ) : ?>
503 <table class="wpsubs-table subscrpt-table--compact">
504 <thead>
505 <tr>
506 <th><?php esc_html_e( 'Order', 'subscription' ); ?></th>
507 <th><?php esc_html_e( 'Type', 'subscription' ); ?></th>
508 <th><?php esc_html_e( 'Date', 'subscription' ); ?></th>
509 <th><?php esc_html_e( 'Status', 'subscription' ); ?></th>
510 <th><?php esc_html_e( 'Items', 'subscription' ); ?></th>
511 <th><?php esc_html_e( 'Payment', 'subscription' ); ?></th>
512 <th><?php esc_html_e( 'Total', 'subscription' ); ?></th>
513 </tr>
514 </thead>
515 <tbody>
516 <?php
517 // Map WC order statuses onto the shared subscription badge
518 // modifiers so they get the same bg/border/color treatment.
519 $order_badge_map = array(
520 'completed' => 'active', // green
521 'pending' => 'pending', // blue
522 'processing' => 'pending-cancel', // yellow
523 'on-hold' => 'pending-cancel', // yellow
524 'failed' => 'expired', // red
525 'refunded' => 'expired', // red
526 'trash' => 'trash', // red
527 'cancelled' => 'cancelled', // gray
528 );
529 foreach ( $related_rows as $related ) :
530 $related_order = $related['order'];
531 $related_status = $related_order->get_status();
532 $badge_class = $order_badge_map[ $related_status ] ?? 'draft';
533 ?>
534 <tr>
535 <td>
536 <a href="<?php echo esc_url( $related_order->get_edit_order_url() ); ?>" target="_blank" class="wpsubs-cell-title">
537 #<?php echo esc_html( $related_order->get_order_number() ); ?>
538 </a>
539 </td>
540 <td><?php echo esc_html( $related['label'] ); ?></td>
541 <td><?php echo esc_html( $related_order->get_date_created()->date( get_option( 'date_format' ) . ' - ' . get_option( 'time_format' ) ) ); ?></td>
542 <td><span class="wpsubs-badge wpsubs-badge--<?php echo esc_attr( $badge_class ); ?>"><?php echo esc_html( wc_get_order_status_name( $related_status ) ); ?></span></td>
543 <td><?php echo esc_html( number_format_i18n( $related_order->get_item_count() ) ); ?></td>
544 <td><?php echo $related_order->get_payment_method_title() ? esc_html( $related_order->get_payment_method_title() ) : '&mdash;'; ?></td>
545 <td><?php echo wp_kses_post( $related_order->get_formatted_order_total() ); ?></td>
546 </tr>
547 <?php endforeach; ?>
548 </tbody>
549 </table>
550 <?php
551 // Initial pager; WPSubsPager hydrates and re-derives totals on init.
552 $related_total = count( $related_rows );
553 wpsubs_render_pager(
554 array(
555 'current' => 1,
556 'total' => max( 1, (int) ceil( $related_total / 10 ) ),
557 'info' => true,
558 'per_page' => 10,
559 'item_count' => $related_total,
560 'link_mode' => 'cb',
561 'class' => 'subscrpt-pager',
562 // translators: %1$s: first item number, %2$s: last item number, %3$s: total items
563 'info_format' => __( 'Showing %1$s–%2$s of %3$s related orders', 'subscription' ),
564 'context' => 'subscription-details-related-orders',
565 )
566 );
567 ?>
568 <?php else : ?>
569 <div class="subscrpt-card__body"><p class="subscrpt-muted"><?php esc_html_e( 'No related orders found.', 'subscription' ); ?></p></div>
570 <?php endif; ?>
571 </div>
572
573 <!-- Activities card -->
574 <?php $subscrpt_pro_on = function_exists( 'subscrpt_pro_activated' ) && subscrpt_pro_activated(); ?>
575 <div class="subscrpt-card" data-subscrpt-paginate data-per-page="10" data-date-col="2">
576 <div class="subscrpt-card__head subscrpt-card__head--toolbar">
577 <span class="subscrpt-card__title"><?php esc_html_e( 'Subscription Activities', 'subscription' ); ?></span>
578 <?php if ( $subscrpt_pro_on ) : ?>
579 <?php $subscrpt_render_table_tools(); ?>
580 <?php endif; ?>
581 </div>
582 <?php if ( $subscrpt_pro_on ) : ?>
583 <div class="subscrpt-activities">
584 <?php
585 /**
586 * Fires inside the subscription details activities card.
587 *
588 * The Pro plugin renders the activity table here.
589 *
590 * @param int $subscription_id Subscription post ID.
591 */
592 do_action( 'subscrpt_order_activities', $subscription_id );
593 ?>
594 </div>
595 <?php
596 // Activities pager. Total pages are unknown at render time (Pro
597 // injects the rows); WPSubsPager recomputes on init via render().
598 wpsubs_render_pager(
599 array(
600 'current' => 1,
601 'total' => 1,
602 'info' => true,
603 'per_page' => 10,
604 'item_count' => 0,
605 'link_mode' => 'cb',
606 'class' => 'subscrpt-pager',
607 // translators: %1$s: first item number, %2$s: last item number, %3$s: total items
608 'info_format' => __( 'Showing %1$s–%2$s of %3$s activities', 'subscription' ),
609 'context' => 'subscription-details-activities',
610 )
611 );
612 ?>
613 <?php else : ?>
614 <div class="subscrpt-card__body">
615 <div class="subscrpt-upgrade-banner">
616 <div>
617 <strong><?php esc_html_e( 'Upgrade to WPSubscription Pro', 'subscription' ); ?></strong>
618 <p><?php esc_html_e( 'Track subscription activity history, automation, and more.', 'subscription' ); ?></p>
619 </div>
620 <a href="https://wpsubscription.co/" target="_blank" class="wpsubs-btn wpsubs-btn--primary wpsubs-btn--sm" rel="noreferrer noopener">
621 <?php esc_html_e( 'Upgrade to Pro', 'subscription' ); ?>
622 </a>
623 </div>
624 </div>
625 <?php endif; ?>
626 </div>
627
628 <?php
629 /**
630 * Fires at the bottom of the details main (left) column, after the activities card.
631 *
632 * Echo a `.subscrpt-card` to add a full-width section here.
633 *
634 * @param array $subscrpt_details_ctx Subscription details context.
635 */
636 do_action( 'subscrpt_details_main_bottom', $subscrpt_details_ctx );
637 ?>
638
639 </div><!-- /.subscrpt-detail-main -->
640
641 <!-- Sidebar column -->
642 <div class="subscrpt-detail-side">
643
644 <?php
645 /**
646 * Fires at the top of the details sidebar (right) column, before the action card.
647 *
648 * Echo a `.subscrpt-card` to add a section here.
649 *
650 * @param array $subscrpt_details_ctx Subscription details context.
651 */
652 do_action( 'subscrpt_details_side_top', $subscrpt_details_ctx );
653 ?>
654
655 <!-- Status action card -->
656 <div class="subscrpt-card subscrpt-card--overflow">
657 <div class="subscrpt-card__head"><?php esc_html_e( 'Subscription Action', 'subscription' ); ?></div>
658 <div class="subscrpt-card__body">
659 <?php if ( ! empty( $actions ) ) : ?>
660 <form method="post" action="<?php echo esc_url( $form_action ); ?>" class="subscrpt-action-form">
661 <?php wp_nonce_field( 'subscrpt_order_action_nonce', 'subscrpt_order_action_nonce_field' ); ?>
662 <?php
663 $action_options = array();
664 foreach ( $actions as $action_slug ) {
665 if ( isset( $actions_data[ $action_slug ] ) ) {
666 $action_options[] = array(
667 'value' => $actions_data[ $action_slug ]['value'],
668 'label' => $actions_data[ $action_slug ]['label'],
669 );
670 }
671 }
672 wpsubs_render_adv_select(
673 array(
674 'name' => 'subscrpt_order_action',
675 'placeholder' => __( 'Choose Action', 'subscription' ),
676 'value' => '',
677 'options' => $action_options,
678 'class' => 'subscrpt-action-select',
679 )
680 );
681 ?>
682 <button type="submit" class="wpsubs-btn wpsubs-btn--primary" style="width:100%;justify-content:center;margin-top:10px;">
683 <?php esc_html_e( 'Process', 'subscription' ); ?>
684 </button>
685 </form>
686 <?php else : ?>
687 <p class="subscrpt-muted"><?php esc_html_e( 'No actions available for this status.', 'subscription' ); ?></p>
688 <?php endif; ?>
689 </div>
690 </div>
691
692 <!-- Customer card -->
693 <div class="subscrpt-card">
694 <div class="subscrpt-card__head"><?php esc_html_e( 'Customer Details', 'subscription' ); ?></div>
695 <div class="subscrpt-card__body">
696 <?php if ( $order ) : ?>
697 <div class="subscrpt-customer-name">
698 <?php if ( $customer_url ) : ?>
699 <a href="<?php echo esc_url( $customer_url ); ?>" target="_blank"><?php echo esc_html( $customer_name ); ?></a>
700 <?php else : ?>
701 <?php echo esc_html( $customer_name ); ?>
702 <?php endif; ?>
703 </div>
704 <?php if ( $customer_email ) : ?>
705 <div class="subscrpt-customer-line"><a href="mailto:<?php echo esc_attr( $customer_email ); ?>"><?php echo esc_html( $customer_email ); ?></a></div>
706 <?php endif; ?>
707 <?php if ( $customer_phone ) : ?>
708 <div class="subscrpt-customer-line"><a href="tel:<?php echo esc_attr( $customer_phone ); ?>"><?php echo esc_html( $customer_phone ); ?></a></div>
709 <?php endif; ?>
710
711 <?php else : ?>
712 <p class="subscrpt-muted"><?php esc_html_e( 'Order not found.', 'subscription' ); ?></p>
713 <?php endif; ?>
714 </div>
715 </div>
716
717 <!-- Secondary info cards (payment method, addresses) -->
718 <?php foreach ( $side_sections as $section ) : ?>
719 <div class="subscrpt-card">
720 <div class="subscrpt-card__head"><?php echo esc_html( $section['title'] ); ?></div>
721 <div class="subscrpt-card__body">
722 <table class="subscrpt-kv">
723 <tbody>
724 <?php foreach ( $section['rows'] as $row ) : ?>
725 <tr>
726 <th><?php echo esc_html( $row['label'] ); ?></th>
727 <td><?php echo wp_kses_post( $row['value'] ); ?></td>
728 </tr>
729 <?php endforeach; ?>
730 </tbody>
731 </table>
732 </div>
733 </div>
734 <?php endforeach; ?>
735
736 <?php
737 /**
738 * Fires at the bottom of the details sidebar (right) column, after the secondary info cards.
739 *
740 * Echo a `.subscrpt-card` to add a section here.
741 *
742 * @param array $subscrpt_details_ctx Subscription details context.
743 */
744 do_action( 'subscrpt_details_side_bottom', $subscrpt_details_ctx );
745 ?>
746
747 </div><!-- /.subscrpt-detail-side -->
748
749 </div><!-- /.subscrpt-detail-grid -->
750
751 </div>
752
753 <style>
754 /* Page sits on WP admin gray; the cards are the white surfaces (matches list/health pages). */
755 .wp-subscription-admin-content.list-page.subscrpt-subs-details {
756 background: transparent;
757 padding: 0;
758 border-radius: 0;
759 margin-top: 24px;
760 margin-bottom: 32px;
761 }
762 .subscrpt-subs-details .subscrpt-detail-head { margin-bottom: 20px; }
763 .subscrpt-detail-head__title {
764 font-size: 1.375rem;
765 font-weight: 700;
766 color: var(--wpsubs-text);
767 line-height: 1.2;
768 margin: 0 0 8px;
769 padding: 0;
770 }
771 .subscrpt-detail-head__desc {
772 display: flex;
773 align-items: center;
774 flex-wrap: wrap;
775 gap: 8px;
776 font-size: 13px;
777 color: var(--wpsubs-text-muted);
778 margin: 0 0 12px;
779 line-height: 1.5;
780 }
781 .subscrpt-detail-head__sep { color: var(--wpsubs-text-subtle); }
782 .subscrpt-detail-head__divider { border-top: 1px dashed #d0d3d7; }
783
784 /* Summary metric strip */
785 .subscrpt-summary {
786 display: grid;
787 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
788 gap: 20px;
789 }
790 .subscrpt-summary__tile {
791 background: var(--wpsubs-surface);
792 border: 1px solid var(--wpsubs-border);
793 border-radius: var(--wpsubs-radius);
794 padding: 16px 18px;
795 display: flex;
796 flex-direction: column;
797 gap: 6px;
798 }
799 .subscrpt-summary__label {
800 font-size: 11px;
801 font-weight: 600;
802 text-transform: uppercase;
803 letter-spacing: 0.06em;
804 color: var(--wpsubs-text-muted);
805 }
806 .subscrpt-summary__value {
807 font-size: 18px;
808 font-weight: 700;
809 color: #111827;
810 line-height: 1.3;
811 }
812 .subscrpt-summary__value .subscrpt-summary__unit {
813 font-size: 13px;
814 font-weight: 500;
815 color: var(--wpsubs-text-muted);
816 }
817
818 .subscrpt-detail-grid {
819 display: grid;
820 grid-template-columns: minmax(0, 1fr) 340px;
821 gap: 20px;
822 align-items: start;
823 }
824 @media (max-width: 960px) {
825 .subscrpt-detail-grid { grid-template-columns: 1fr; }
826 }
827 .subscrpt-detail-main, .subscrpt-detail-side {
828 display: flex;
829 flex-direction: column;
830 gap: 20px;
831 min-width: 0;
832 }
833
834 .subscrpt-card {
835 background: var(--wpsubs-surface);
836 border: 1px solid var(--wpsubs-border);
837 border-radius: var(--wpsubs-radius);
838 box-shadow: none;
839 overflow: hidden;
840 }
841 .subscrpt-card__head {
842 padding: 14px 18px;
843 font-size: 14px;
844 font-weight: 600;
845 color: #111827;
846 border-bottom: 1px solid var(--wpsubs-border);
847 background: var(--wpsubs-surface);
848 }
849 .subscrpt-card__body { padding: 16px 18px; }
850 /* Cards holding an adv-select dropdown must not clip the popover. Without
851 overflow:hidden the head no longer inherits the card's rounded corners, so
852 round the head (and body) explicitly. */
853 .subscrpt-card--overflow { overflow: visible; }
854 .subscrpt-card--overflow .subscrpt-card__head {
855 border-top-left-radius: var(--wpsubs-radius);
856 border-top-right-radius: var(--wpsubs-radius);
857 }
858 .subscrpt-card--overflow .subscrpt-card__body:last-child {
859 border-bottom-left-radius: var(--wpsubs-radius);
860 border-bottom-right-radius: var(--wpsubs-radius);
861 }
862
863 /* Card head with right-aligned filter tools. */
864 .subscrpt-card__head--toolbar {
865 display: flex;
866 align-items: center;
867 justify-content: space-between;
868 gap: 12px;
869 flex-wrap: wrap;
870 font-weight: 400;
871 }
872 .subscrpt-card__title { font-size: 14px; font-weight: 600; color: #111827; }
873 /* Filter dropdown items must not inherit the card-head bold weight. */
874 .subscrpt-card__tools .wpsubs-adv-select__label,
875 .subscrpt-card__tools .wpsubs-adv-select__item { font-weight: 400; }
876 .subscrpt-card__tools {
877 display: flex;
878 align-items: center;
879 gap: 8px;
880 flex-wrap: wrap;
881 }
882 .subscrpt-card__tools .wpsubs-adv-select__trigger {
883 height: 32px;
884 padding-top: 0;
885 padding-bottom: 0;
886 }
887
888 /* Pager (uses .wpsubs-pagination layout; styles in admin-components.css). */
889 .subscrpt-pager { margin: 0; }
890 .subscrpt-empty-row td { color: var(--wpsubs-text-muted); font-size: 13px; }
891
892 /* Tables render flush to the card edges (the table thead is the column strip). */
893 .subscrpt-card .wpsubs-table { border: 0; }
894 .subscrpt-card .wpsubs-table tbody tr:last-child td { border-bottom: 0; }
895 /* Match the page's 13px body text (default wpsubs-table cells run larger). */
896 .subscrpt-card .wpsubs-table th,
897 .subscrpt-card .wpsubs-table td,
898 .subscrpt-activities .wpsubs-table th,
899 .subscrpt-activities .wpsubs-table td { font-size: 13px; }
900 /* Tighter rows for the data tables. */
901 .subscrpt-table--compact th,
902 .subscrpt-table--compact td,
903 .subscrpt-activities .wpsubs-table th,
904 .subscrpt-activities .wpsubs-table td { padding-top: 12px; padding-bottom: 12px; }
905 .subscrpt-table--compact .wpsubs-badge { font-size: 11px; }
906 .subscrpt-activities .widefat,
907 .subscrpt-activities .wpsubs-table { border: 0; margin: 0; }
908 .subscrpt-card__actions {
909 display: flex;
910 flex-wrap: wrap;
911 gap: 8px;
912 margin-top: 14px;
913 }
914
915 .subscrpt-kv { width: 100%; border-collapse: collapse; }
916 .subscrpt-kv th, .subscrpt-kv td {
917 text-align: left;
918 padding: 9px 0;
919 font-size: 13px;
920 vertical-align: top;
921 border-bottom: 1px solid var(--wpsubs-border);
922 }
923 .subscrpt-kv tr:last-child th, .subscrpt-kv tr:last-child td { border-bottom: 0; }
924 .subscrpt-kv th {
925 width: 40%;
926 font-weight: 500;
927 color: var(--wpsubs-text-muted);
928 padding-right: 12px;
929 }
930 .subscrpt-kv td { color: var(--wpsubs-text); }
931 .subscrpt-kv a { color: var(--wpsubs-brand); text-decoration: none; }
932 .subscrpt-kv a:hover { text-decoration: underline; }
933
934 /* Plan + Payment Method row: Plan wider (2-col content), Payment narrower. */
935 .subscrpt-plan-pay {
936 display: grid;
937 grid-template-columns: repeat(3, minmax(0, 1fr));
938 gap: 20px;
939 align-items: stretch;
940 }
941 .subscrpt-plan-pay > .subscrpt-card { margin: 0; }
942 /* Plan spans 2 of the 3 columns so its right edge aligns with the summary's
943 2nd/3rd column boundary; the gateway card fills the remaining column. */
944 .subscrpt-plan-pay > .subscrpt-card--plan { grid-column: span 2; }
945 @media (max-width: 782px) {
946 .subscrpt-plan-pay { grid-template-columns: 1fr; }
947 }
948
949 /* Plan card: product image (1:1) + name/qty stacked. */
950 .subscrpt-plan-row {
951 display: grid;
952 grid-template-columns: 64px 1fr;
953 gap: 16px;
954 align-items: center;
955 }
956 .subscrpt-plan-thumb {
957 width: 64px;
958 height: 64px;
959 border: 1px solid var(--wpsubs-border);
960 border-radius: var(--wpsubs-radius-sm);
961 overflow: hidden;
962 background: var(--wpsubs-surface-alt, #f6f7f7);
963 display: flex;
964 align-items: center;
965 justify-content: center;
966 }
967 .subscrpt-plan-thumb img,
968 .subscrpt-plan-thumb .subscrpt-plan-img {
969 width: 100%;
970 height: 100%;
971 object-fit: cover;
972 display: block;
973 }
974 .subscrpt-plan-thumb__ph {
975 font-size: 28px;
976 width: 28px;
977 height: 28px;
978 color: var(--wpsubs-text-subtle, #a7aaad);
979 }
980 .subscrpt-plan-meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
981 .subscrpt-plan-name { font-size: 14px; font-weight: 600; color: var(--wpsubs-text); line-height: 1.3; }
982 .subscrpt-plan-name a { color: var(--wpsubs-text); text-decoration: none; }
983 .subscrpt-plan-name a:hover { color: var(--wpsubs-brand); }
984 .subscrpt-plan-qty { font-size: 13px; color: var(--wpsubs-text-muted); }
985 .subscrpt-plan-col { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
986 .subscrpt-plan-label {
987 font-size: 11px;
988 font-weight: 600;
989 text-transform: uppercase;
990 letter-spacing: 0.05em;
991 color: var(--wpsubs-text-muted);
992 }
993 .subscrpt-plan-value { font-size: 13px; color: var(--wpsubs-text); }
994 .subscrpt-plan-value a { color: var(--wpsubs-brand); text-decoration: none; }
995 .subscrpt-plan-value a:hover { text-decoration: underline; }
996 .subscrpt-plan-extra { margin-top: 14px; }
997
998 /* Payment method: gateway icon + (gateway title / source plugin). Mirrors the
999 plan-row layout so the two cards line up. */
1000 .subscrpt-payment { display: grid; grid-template-columns: 40px 1fr; gap: 12px; align-items: center; }
1001 .subscrpt-payment__icon {
1002 width: 40px;
1003 height: 40px;
1004 display: flex;
1005 align-items: center;
1006 justify-content: center;
1007 border: 1px solid var(--wpsubs-border);
1008 border-radius: var(--wpsubs-radius-sm);
1009 background: var(--wpsubs-surface-alt, #f6f7f7);
1010 overflow: hidden;
1011 }
1012 .subscrpt-payment__icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
1013 .subscrpt-payment__icon-ph { font-size: 22px; width: 22px; height: 22px; color: var(--wpsubs-text-muted); }
1014 .subscrpt-payment__meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1015 .subscrpt-payment__title { font-size: 14px; font-weight: 600; color: var(--wpsubs-text); line-height: 1.3; }
1016 .subscrpt-payment__plugin { font-size: 13px; color: var(--wpsubs-text-muted); }
1017
1018 /* Plan + Payment cards stretch to equal height; bodies center their content. */
1019 .subscrpt-plan-pay > .subscrpt-card { display: flex; flex-direction: column; }
1020 .subscrpt-plan-pay > .subscrpt-card .subscrpt-card__body { flex: 1; display: flex; flex-direction: column; justify-content: center; }
1021
1022 .subscrpt-customer-name { font-size: 14px; font-weight: 600; color: var(--wpsubs-text); }
1023 .subscrpt-customer-name a { color: var(--wpsubs-text); text-decoration: none; }
1024 .subscrpt-customer-name a:hover { color: var(--wpsubs-brand); }
1025 .subscrpt-customer-line { font-size: 13px; margin-top: 3px; }
1026 .subscrpt-customer-line a { color: var(--wpsubs-brand); text-decoration: none; }
1027 .subscrpt-customer-line a:hover { text-decoration: underline; }
1028
1029 .subscrpt-subs-details .subscrpt-muted { color: var(--wpsubs-text-muted); font-size: 13px; margin: 0; }
1030 .subscrpt-action-form .wpsubs-adv-select { width: 100%; }
1031 .subscrpt-action-form .wpsubs-adv-select__trigger { width: 100%; }
1032
1033 .subscrpt-upgrade-banner {
1034 display: flex;
1035 align-items: center;
1036 justify-content: space-between;
1037 gap: 16px;
1038 flex-wrap: wrap;
1039 padding: 16px;
1040 border: 1px solid var(--wpsubs-brand);
1041 border-radius: var(--wpsubs-radius-sm);
1042 background: var(--wpsubs-brand-light);
1043 }
1044 .subscrpt-upgrade-banner strong { color: var(--wpsubs-text); font-size: 14px; }
1045 .subscrpt-upgrade-banner p { margin: 4px 0 0; font-size: 13px; color: var(--wpsubs-text-muted); }
1046 </style>
1047