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

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