PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.11.0
Subscriptions for WooCommerce with Stripe Recurring Payments v1.11.0
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.11.0, at includes/Admin/views/subscription-details.php

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