PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.11.2
Subscriptions for WooCommerce with Stripe Recurring Payments v1.11.2
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 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / includes / Admin / views / subscription-details.php

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

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