PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.4.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.4.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
← All changes | includes/Admin/views/subscription-list.php +552 -135 1.4.0 → 2.4.0 View file →
@@ -1,140 +1,557 @@
1 -<?php if (!isset($date_filter)) { $date_filter = ''; } ?>
2 1 <?php
3 -// Determine if filters are active
4 -$filters_active = !empty($status) || !empty($date_filter) || !empty($search);
5 -$months = [];
6 -for ($i = 0; $i < 12; $i++) {
7 - $month = strtotime("-$i month");
8 - $months[date('Y-m', $month)] = date('F Y', $month);
2 +/**
3 + * Subscription admin list view.
4 + *
5 + * @package SpringDevs\Subscription\Admin
6 + */
7 +
8 +if ( ! defined( 'ABSPATH' ) ) {
9 + exit;
9 10 }
11 +
12 +if ( ! isset( $date_filter ) ) {
13 + $date_filter = '';
14 +}
15 +if ( ! isset( $renewal_due ) ) {
16 + $renewal_due = 0;
17 +}
18 +
19 +$filters_active = ! empty( $status ) || ! empty( $date_filter ) || ! empty( $search ) || ! empty( $renewal_due );
20 +
21 +// The last twelve of the store's months, counted back from the 1st: stepping
22 +// back from today skips a month on the 29th–31st (31 Oct − 1 month is 1 Oct).
23 +// The store's timezone, because the list filters on the local post date.
24 +$months = array();
25 +$this_month = ( new DateTimeImmutable( 'now', wp_timezone() ) )->modify( 'first day of this month' )->setTime( 0, 0 );
26 +for ( $i = 0; $i < 12; $i++ ) {
27 + $month = strtotime( "-$i month" );
28 + $months[ gmdate( 'Y-m', $month ) ] = wp_date( 'F Y', $month, new \DateTimeZone( 'UTC' ) );
29 +}
10 30 ?>
11 31 <div class="wp-subscription-admin-content list-page">
12 - <div class="wp-subscription-list-title"><h1 class="wp-heading-inline">Subscriptions</h1></div>
13 - <div class="wp-subscription-list-header">
14 - <form method="get">
15 - <input type="hidden" name="page" value="wp-subscription" />
16 - <select name="subscrpt_status" value="<?php echo esc_attr($status); ?>">
17 - <option value=""><?php esc_html_e('All Status', 'wp_subscription'); ?></option>
18 - <option value="active" <?php selected($status, 'active'); ?>><?php esc_html_e('Active', 'wp_subscription'); ?></option>
19 - <option value="cancelled" <?php selected($status, 'cancelled'); ?>><?php esc_html_e('Cancelled', 'wp_subscription'); ?></option>
20 - <option value="draft" <?php selected($status, 'draft'); ?>><?php esc_html_e('Draft', 'wp_subscription'); ?></option>
21 - </select>
22 - <select name="date_filter" value="<?php echo esc_attr($date_filter); ?>">
23 - <option value=""><?php esc_html_e('All Dates', 'wp_subscription'); ?></option>
24 - <?php foreach ($months as $val => $label): ?>
25 - <option value="<?php echo esc_attr($val); ?>" <?php selected($date_filter, $val); ?>><?php echo esc_html($label); ?></option>
26 - <?php endforeach; ?>
27 - </select>
28 - <input type="search" name="s" value="<?php echo esc_attr($search); ?>" placeholder="<?php esc_attr_e('Search by subscription ID...', 'wp_subscription'); ?>" />
29 - <select name="per_page">
30 - <?php foreach ([10, 20, 50, 100] as $n): ?>
31 - <option value="<?php echo $n; ?>" <?php selected(isset($_GET['per_page']) ? intval($_GET['per_page']) : 20, $n); ?>><?php echo $n; ?> per page</option>
32 - <?php endforeach; ?>
33 - </select>
34 - <button type="submit" class="button">Search</button>
35 - <?php if ($filters_active): ?>
36 - <a href="<?php echo esc_url(remove_query_arg(['subscrpt_status','date_filter','s','title','paged'])); ?>" class="button">Reset</a>
37 - <?php endif; ?>
38 - <?php if ($filters_active): ?>
39 - <span>Filters applied</span>
40 - <?php endif; ?>
41 - </form>
42 - </div>
43 - <h2 class="screen-reader-text">Subscriptions list</h2>
44 - <table class="wp-list-table widefat fixed striped wp-subscription-modern-table">
45 - <thead>
46 - <tr>
47 - <th style="width:200px;">ID</th>
48 - <th style="min-width:320px;">Title</th>
49 - <th style="width:200px;">Customer</th>
50 - <th style="width:100px;">Start Date</th>
51 - <th style="width:100px;">Renewal Date</th>
52 - <th style="width:80px;">Status</th>
53 - <th style="width:80px;">Actions</th>
54 - </tr>
55 - </thead>
56 - <tbody>
57 - <?php if (!empty($subscriptions)) : ?>
58 - <?php foreach ($subscriptions as $subscription) :
59 - $order_id = get_post_meta($subscription->ID, '_subscrpt_order_id', true);
60 - $order = wc_get_order($order_id);
61 - $order_item_id = get_post_meta($subscription->ID, '_subscrpt_order_item_id', true);
62 - $order_item = $order ? $order->get_item($order_item_id) : null;
63 - $product_name = $order_item ? $order_item->get_name() : '-';
64 - $customer = $order ? $order->get_formatted_billing_full_name() : '-';
65 - $customer_id = $order ? $order->get_customer_id() : 0;
66 - $customer_url = $customer_id ? admin_url('user-edit.php?user_id=' . $customer_id) : '';
67 - $customer_email = $order ? $order->get_billing_email() : '';
68 - $start_date = get_post_meta($subscription->ID, '_subscrpt_start_date', true);
69 - $renewal_date = get_post_meta($subscription->ID, '_subscrpt_next_date', true);
70 - $status_obj = get_post_status_object(get_post_status($subscription->ID));
71 - ?>
72 - <tr>
73 - <td>
74 - <a href="<?php echo esc_url(get_edit_post_link($subscription->ID)); ?>" class="subscrpt-id-link">
75 - #<?php echo esc_html(get_the_title($subscription->ID)); ?>
76 - </a>
77 - </td>
78 - <td style="min-width:320px;">
79 - <div class="wp-subscription-title-wrap">
80 - <span><?php echo esc_html($product_name); ?></span>
81 - <div class="wp-subscription-row-actions">
82 - <a href="<?php echo esc_url(get_edit_post_link($subscription->ID)); ?>">View</a>
83 - <a href="<?php echo esc_url(admin_url('admin.php?page=wp-subscription&action=duplicate&sub_id=' . $subscription->ID)); ?>">Duplicate</a>
84 - <a href="<?php echo esc_url(get_delete_post_link($subscription->ID)); ?>">Delete</a>
85 - </div>
86 - </div>
87 - </td>
88 - <td>
89 - <?php if ($customer_url): ?>
90 - <a href="<?php echo esc_url($customer_url); ?>" target="_blank"><?php echo esc_html($customer); ?></a>
91 - <?php else: ?>
92 - <?php echo esc_html($customer); ?>
93 - <?php endif; ?>
94 - <?php if ($customer_email): ?>
95 - <div class="wp-subscription-customer-email"><?php echo esc_html($customer_email); ?></div>
96 - <?php endif; ?>
97 - </td>
98 - <td><?php echo $start_date ? esc_html(gmdate('F d, Y', $start_date)) : '-'; ?></td>
99 - <td><?php echo $renewal_date ? esc_html(gmdate('F d, Y', $renewal_date)) : '-'; ?></td>
100 - <td>
101 - <span class="subscrpt-status-badge subscrpt-status-<?php echo esc_attr($status_obj->name); ?>">
102 - <?php echo esc_html($status_obj->label); ?>
103 - </span>
104 - </td>
105 - <td>
106 - <a href="<?php echo esc_url(get_edit_post_link($subscription->ID)); ?>" class="button button-small"><?php esc_html_e('Edit', 'wp_subscription'); ?></a>
107 - </td>
108 - </tr>
109 - <?php endforeach; ?>
110 - <?php else : ?>
111 - <tr>
112 - <td colspan="8" class="wp-subscription-list-empty">
113 - <?php esc_html_e('No subscriptions found.', 'wp_subscription'); ?>
114 - </td>
115 - </tr>
116 - <?php endif; ?>
117 - </tbody>
118 - </table>
119 - <?php if ($max_num_pages > 1): ?>
120 - <div class="wp-subscription-pagination">
121 - <span class="total">Total <?php echo intval($total); ?></span>
122 - <?php
123 - $base_url = remove_query_arg('paged');
124 - $show_pages = $max_num_pages > 1 || $max_num_pages == 1;
125 - for ($i = 1; $i <= $max_num_pages; $i++):
126 - $url = add_query_arg(['paged' => $i, 'per_page' => $per_page], $base_url);
127 - $is_current = $i == $paged;
128 - ?>
129 - <a href="<?php echo esc_url($url); ?>" class="button<?php if ($is_current) echo ' button-primary'; ?>" <?php if ($is_current) echo 'disabled'; ?>><?php echo $i; ?></a>
130 - <?php endfor; ?>
131 - <span class="goto-label">Go to</span>
132 - <form method="get">
133 - <input type="hidden" name="page" value="wp-subscription" />
134 - <input type="number" name="paged" min="1" max="<?php echo $max_num_pages; ?>" value="<?php echo $paged; ?>" />
135 - <input type="hidden" name="per_page" value="<?php echo $per_page; ?>" />
136 - <button type="submit" class="button">OK</button>
137 - </form>
138 - </div>
139 - <?php endif; ?>
32 +
33 + <?php
34 + // Getting started ("Welcome to WPSubscription") card hidden for now — re-enable later.
35 + // require __DIR__ . '/subscription-gsc.php';
36 + ?>
37 +
38 + <?php
39 + wpsubs_render_page_header(
40 + array(
41 + 'title' => __( 'Subscriptions', 'subscription' ),
42 + 'description' => __( 'Manage all your WooCommerce subscriptions.', 'subscription' ),
43 + )
44 + );
45 + ?>
46 +
47 + <form method="post" id="subscriptions-form">
48 + <?php wp_nonce_field( 'subscrpt_list_action' ); ?>
49 + <input type="hidden" name="page" value="wp-subscription-list" />
50 +
51 + <!-- Toolbar -->
52 + <div class="wpsubs-toolbar">
53 +
54 + <?php
55 + wpsubs_render_adv_select(
56 + array(
57 + 'name' => 'subscrpt_status',
58 + 'placeholder' => __( 'All Status', 'subscription' ),
59 + 'value' => $status,
60 + 'options' => array(
61 + array(
62 + 'value' => 'active',
63 + 'label' => __( 'Active', 'subscription' ),
64 + ),
65 + array(
66 + 'value' => 'on_hold',
67 + 'label' => __( 'On Hold', 'subscription' ),
68 + ),
69 + array(
70 + 'value' => 'pending',
71 + 'label' => __( 'Pending', 'subscription' ),
72 + ),
73 + array(
74 + 'value' => 'pe_cancelled',
75 + 'label' => __( 'Pending Cancellation', 'subscription' ),
76 + ),
77 + array(
78 + 'value' => 'cancelled',
79 + 'label' => __( 'Cancelled', 'subscription' ),
80 + ),
81 + array(
82 + 'value' => 'expired',
83 + 'label' => __( 'Expired', 'subscription' ),
84 + ),
85 + array(
86 + 'value' => 'completed',
87 + 'label' => __( 'Completed', 'subscription' ),
88 + ),
89 + array(
90 + 'value' => 'trash',
91 + 'label' => __( 'Trash', 'subscription' ),
92 + ),
93 + ),
94 + )
95 + );
96 + ?>
97 +
98 + <?php
99 + $date_options = array();
100 + foreach ( $months as $val => $label ) {
101 + $date_options[] = array(
102 + 'value' => $val,
103 + 'label' => $label,
104 + );
105 + }
106 + wpsubs_render_adv_select(
107 + array(
108 + 'name' => 'date_filter',
109 + 'placeholder' => __( 'All Dates', 'subscription' ),
110 + 'value' => $date_filter,
111 + 'options' => $date_options,
112 + )
113 + );
114 + ?>
115 +
116 + <?php
117 + // Next-renewal window. The Overview's "Renewals due" figure opens the
118 + // list with 7 chosen; a window reached by URL is offered too, so the
119 + // dropdown always shows what is applied.
120 + $renewal_windows = array_unique( array_merge( array( 7, 14, 30 ), $renewal_due ? array( $renewal_due ) : array() ) );
121 + sort( $renewal_windows );
122 + $renewal_options = array();
123 + foreach ( $renewal_windows as $days ) {
124 + $renewal_options[] = array(
125 + 'value' => (string) $days,
126 + /* translators: %d: number of days ahead. */
127 + 'label' => sprintf( _n( 'Due in %d day', 'Due in %d days', $days, 'subscription' ), $days ),
128 + );
129 + }
130 + wpsubs_render_adv_select(
131 + array(
132 + 'name' => 'renewal_due',
133 + 'placeholder' => __( 'All Renewals', 'subscription' ),
134 + 'value' => $renewal_due ? (string) $renewal_due : '',
135 + 'options' => $renewal_options,
136 + )
137 + );
138 + ?>
139 +
140 + <div class="wpsubs-search">
141 + <div class="wpsubs-input-wrap wpsubs-input-wrap--icon-l">
142 + <svg class="wpsubs-input-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/></svg>
143 + <input type="search" name="s" value="<?php echo esc_attr( $search ); ?>" placeholder="<?php esc_attr_e( 'Search subscriptions...', 'subscription' ); ?>" class="wpsubs-input" />
144 + </div>
145 + </div>
146 +
147 + <button type="submit" name="filter_action" value="filter" class="wpsubs-btn wpsubs-btn--outline">
148 + <?php esc_html_e( 'Filter', 'subscription' ); ?>
149 + </button>
150 +
151 + <?php if ( $filters_active ) : ?>
152 + <a href="<?php echo esc_url( remove_query_arg( array( 'subscrpt_status', 'date_filter', 's', 'title', 'paged', 'renewal_due' ) ) ); ?>" class="wpsubs-btn wpsubs-btn--outline">
153 + <?php esc_html_e( 'Clear', 'subscription' ); ?>
154 + </a>
155 + <?php endif; ?>
156 +
157 + <div class="wpsubs-toolbar__spacer"></div>
158 +
159 + <?php // Page size and bulk actions wrap as one right-aligned group, never one without the other. ?>
160 + <div style="display:flex;flex-wrap:wrap;gap:8px;margin-inline-start:auto;">
161 + <?php
162 + $current_per_page = isset( $_GET['per_page'] ) ? intval( wp_unslash( $_GET['per_page'] ) ) : 20;
163 + wpsubs_render_per_page_select(
164 + array(
165 + 'name' => 'per_page',
166 + 'value' => (string) $current_per_page,
167 + 'align' => 'right',
168 + 'id' => 'wpsubs-per-page-select',
169 + )
170 + );
171 + ?>
172 +
173 + <?php
174 + // Bulk action advanced-select + hidden submit.
175 + $bulk_options = 'trash' === $status
176 + ? array(
177 + array(
178 + 'value' => 'restore',
179 + 'label' => __( 'Restore selected', 'subscription' ),
180 + ),
181 + array(
182 + 'value' => 'delete',
183 + 'label' => __( 'Delete permanently', 'subscription' ),
184 + 'danger' => true,
185 + 'confirm' => __( 'Delete selected subscriptions permanently? This cannot be undone.', 'subscription' ),
186 + ),
187 + )
188 + : array(
189 + array(
190 + 'value' => 'trash',
191 + 'label' => __( 'Move to trash', 'subscription' ),
192 + 'danger' => true,
193 + 'confirm' => __( 'Move selected subscriptions to trash?', 'subscription' ),
194 + ),
195 + );
196 +
197 + wpsubs_render_adv_select(
198 + array(
199 + 'name' => 'action',
200 + 'placeholder' => __( 'Select Action', 'subscription' ),
201 + 'value' => '-1',
202 + 'options' => $bulk_options,
203 + 'id' => 'wpsubs-bulk-action-select',
204 + 'align' => 'right',
205 + )
206 + );
207 + ?>
208 + <button type="submit" name="bulk_action" value="1" id="wpsubs-bulk-action-submit" style="display:none;" aria-hidden="true"></button>
209 +
210 + <?php
211 + if ( 'trash' === $status && ! empty( $subscriptions ) ) :
212 + $empty_trash_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=clean_trash&sub_id=all' ), 'wpsubs_action_clean_trash' );
213 + ?>
214 + <a href="<?php echo esc_url( $empty_trash_url ); ?>" class="wpsubs-btn wpsubs-btn--danger" onclick="return confirm('<?php esc_attr_e( 'Permanently delete all trash items? This cannot be undone.', 'subscription' ); ?>')">
215 + <?php esc_html_e( 'Empty Trash', 'subscription' ); ?>
216 + </a>
217 + <?php endif; ?>
218 + </div>
219 +
220 + </div><!-- /.wpsubs-toolbar -->
221 +
222 + <!-- Table card -->
223 + <h2 class="screen-reader-text"><?php esc_html_e( 'Subscriptions list', 'subscription' ); ?></h2>
224 + <div class="wpsubs-table-card">
225 + <table class="wpsubs-table">
226 + <thead>
227 + <tr>
228 + <th class="wpsubs-col--check">
229 + <input type="checkbox" id="cb-select-all" class="wpsubs-checkbox">
230 + </th>
231 + <th style="width:1%;white-space:nowrap;"><?php esc_html_e( 'Status', 'subscription' ); ?></th>
232 + <th><?php esc_html_e( 'Subscription', 'subscription' ); ?></th>
233 + <th><?php esc_html_e( 'Customer', 'subscription' ); ?></th>
234 + <th><?php esc_html_e( 'Amount', 'subscription' ); ?></th>
235 + <th><?php esc_html_e( 'Started', 'subscription' ); ?></th>
236 + <th><?php esc_html_e( 'Next Renewal', 'subscription' ); ?></th>
237 + <th class="wpsubs-col--actions"><?php esc_html_e( 'Actions', 'subscription' ); ?></th>
238 + </tr>
239 + </thead>
240 + <tbody>
241 +
242 + <?php if ( ! empty( $subscriptions ) ) : ?>
243 + <?php
244 + foreach ( $subscriptions as $subscription ) :
245 + $subscription_id = $subscription->ID;
246 + $subscription_data = SpringDevs\Subscription\Illuminate\Helper::get_subscription_data( $subscription_id );
247 +
248 + $subscrpt_status = $subscription_data['status'] ?? '';
249 + $subscrpt_status = empty( $subscrpt_status ) ? get_post_status( $subscription_id ) : $subscrpt_status;
250 +
251 + $order_id = $subscription_data['order']['order_id'] ?? 0;
252 + $order_item_id = $subscription_data['order']['order_item_id'] ?? 0;
253 + $order = $order_id ? wc_get_order( $order_id ) : null;
254 + $order_item = $order ? $order->get_item( $order_item_id ) : null;
255 + $product_name = $order_item ? $order_item->get_name() : '-';
256 +
257 + $product_id = $subscription_data['product']['variation_id'] ?: ( $subscription_data['product']['product_id'] ?? 0 );
258 + $product_url = $product_id ? get_edit_post_link( $product_id ) : '';
259 +
260 + $customer = $order ? $order->get_formatted_billing_full_name() : '-';
261 + $customer_id = $order ? $order->get_customer_id() : 0;
262 + $customer_url = $customer_id ? admin_url( 'user-edit.php?user_id=' . $customer_id ) : '';
263 + $customer_email = $order ? $order->get_billing_email() : '';
264 +
265 + $start_date = $subscription_data['start_date'] ? strtotime( $subscription_data['start_date'] ) : 0;
266 + $renewal_date = $subscription_data['next_date'] && SpringDevs\Subscription\Illuminate\Helper::has_next_payment( $subscrpt_status ) ? strtotime( $subscription_data['next_date'] ) : 0;
267 +
268 + $is_trash = 'trash' === $subscription->post_status;
269 + $is_grace_period = isset( $subscription_data['grace_period'] );
270 + $grace_remaining = $subscription_data['grace_period']['remaining_days'] ?? 0;
271 +
272 + // Amount + timing. Net of any discount that carries into renewals.
273 + $quantity = $order_item ? (int) $order_item->get_quantity() : 1;
274 + $amount_totals = SpringDevs\Subscription\Illuminate\Helper::get_subscription_display_totals( $subscription_id, $order_item );
275 + $price = '' !== ( $subscription_data['price'] ?? '' ) ? (float) $amount_totals['total'] : '';
276 + $price_full = (float) $amount_totals['full_excl'] + $amount_totals['tax'] + $amount_totals['discount_tax'];
277 + $has_discount = (bool) $amount_totals['has_discount'];
278 + $price_per_unit = '' !== $price ? $price / max( 1, $quantity ) : '';
279 + $timing_per = $subscription_data['schedule']['timing_per'] ?? '';
280 + $timing_option = $subscription_data['schedule']['timing_option'] ?? '';
281 + $timing_label = '';
282 + if ( $timing_per && $timing_option ) {
283 + $timing_unit = \SpringDevs\Subscription\Illuminate\Helper::get_typos( $timing_per, $timing_option );
284 + $timing_label = ( (int) $timing_per > 1 )
285 + ? $timing_per . ' ' . $timing_unit
286 + : $timing_unit;
287 + }
288 +
289 + // Avatar
290 + $name_parts = array_values( array_filter( explode( ' ', trim( $customer ) ) ) );
291 + $initials = '?';
292 + if ( $name_parts ) {
293 + $initials = '';
294 + foreach ( array_slice( $name_parts, 0, 2 ) as $part ) {
295 + $initials .= strtoupper( $part[0] );
296 + }
297 + }
298 + $color_slot = ord( strtolower( $initials[0] ?? 'a' ) ) % 8;
299 +
300 + // Action URLs
301 + $nonce_action = 'wpsubs_action_' . $subscription->ID;
302 + $view_url = admin_url( 'admin.php?page=wp-subscription-details&id=' . $subscription->ID );
303 + $trash_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=trash&sub_id=' . $subscription->ID ), $nonce_action );
304 + $delete_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=delete&sub_id=' . $subscription->ID ), $nonce_action );
305 + $restore_url = wp_nonce_url( admin_url( 'admin.php?page=wp-subscription-list&action=restore&sub_id=' . $subscription->ID ), $nonce_action );
306 +
307 + // Status badge
308 + $badge_mod_map = array(
309 + 'active' => 'active',
310 + 'pending' => 'pending',
311 + 'pe_cancelled' => 'pending-cancel',
312 + 'cancelled' => 'cancelled',
313 + 'expired' => 'expired',
314 + 'completed' => 'completed',
315 + 'draft' => 'draft',
316 + 'trash' => 'trash',
317 + );
318 + $badge_mod = $badge_mod_map[ $subscrpt_status ] ?? 'expired';
319 + $verbose_status = SpringDevs\Subscription\Illuminate\Helper::get_verbose_status( $subscrpt_status );
320 +
321 + if ( $is_grace_period && $grace_remaining > 0 ) {
322 + $badge_mod = 'active';
323 + $verbose_status = __( 'Active', 'subscription' );
324 + }
325 + ?>
326 + <tr>
327 + <!-- Checkbox -->
328 + <td class="wpsubs-col--check">
329 + <input type="checkbox" name="subscription_ids[]" value="<?php echo esc_attr( $subscription->ID ); ?>" class="wpsubs-checkbox wpsubs-row-check">
330 + </td>
331 +
332 + <!-- Status badge -->
333 + <td style="white-space:nowrap;">
334 + <span class="wpsubs-badge wpsubs-badge--<?php echo esc_attr( $badge_mod ); ?>">
335 + <?php echo esc_html( $verbose_status ); ?>
336 + <?php if ( $is_grace_period && $grace_remaining > 0 ) : ?>
337 + <span class="dashicons dashicons-warning" style="font-size:11px;width:11px;height:11px;color:#d97706;" title="<?php echo esc_attr( sprintf( /* translators: %d: number of days left in the grace period. */ __( '%d days remaining in grace period', 'subscription' ), $grace_remaining ) ); ?>"></span>
338 + <?php endif; ?>
339 + </span>
340 + </td>
341 +
342 + <!-- Subscription: product name + subscription ID -->
343 + <td>
344 + <a href="<?php echo esc_url( $view_url ); ?>" class="wpsubs-cell-title" style="font-weight:600;"><?php echo esc_html( $product_name ); ?></a>
345 + <span class="wpsubs-cell-id">#<?php echo (int) $subscription->ID; ?></span>
346 + </td>
347 +
348 + <!-- Customer avatar + name + email -->
349 + <td>
350 + <div class="wpsubs-customer">
351 + <div class="wpsubs-avatar" data-color="<?php echo (int) $color_slot; ?>">
352 + <?php echo esc_html( $initials ); ?>
353 + </div>
354 + <div class="wpsubs-customer__info">
355 + <?php if ( $customer_url ) : ?>
356 + <a href="<?php echo esc_url( $customer_url ); ?>" class="wpsubs-customer__name"><?php echo esc_html( $customer ); ?></a>
357 + <?php else : ?>
358 + <span class="wpsubs-customer__name"><?php echo esc_html( $customer ); ?></span>
359 + <?php endif; ?>
360 + <?php if ( $customer_email ) : ?>
361 + <span class="wpsubs-customer__sub"><?php echo esc_html( $customer_email ); ?></span>
362 + <?php endif; ?>
363 + </div>
364 + </div>
365 + </td>
366 +
367 + <!-- Subscription amount + timing -->
368 + <td style="white-space:nowrap;">
369 + <?php if ( '' !== $price ) : ?>
370 + <span class="wpsubs-cell-title" style="font-variant-numeric:tabular-nums;"><?php echo wp_kses_post( wc_price( $price ) ); ?></span>
371 + <?php if ( $has_discount ) : ?>
372 + <span class="wpsubs-cell-id"><del aria-hidden="true"><?php echo wp_kses_post( wc_price( $price_full ) ); ?></del></span>
373 + <?php endif; ?>
374 + <?php if ( $quantity > 1 ) : ?>
375 + <span class="wpsubs-cell-id"><?php echo wp_kses_post( wc_price( (float) $price_per_unit ) ); ?> &times; <?php echo (int) $quantity; ?></span>
376 + <?php endif; ?>
377 + <?php if ( $timing_label ) : ?>
378 + <span class="wpsubs-cell-id">/ <?php echo esc_html( $timing_label ); ?></span>
379 + <?php endif; ?>
380 + <?php else : ?>
381 + <span class="wpsubs-cell--muted">&#8212;</span>
382 + <?php endif; ?>
383 + </td>
384 +
385 + <!-- Start date -->
386 + <td class="wpsubs-col--nowrap">
387 + <?php if ( $start_date ) : ?>
388 + <span class="wpsubs-cell-title"><?php echo esc_html( wp_date( get_option( 'date_format' ), $start_date ) ); ?></span>
389 + <span class="wpsubs-cell-id"><?php echo esc_html( wp_date( get_option( 'time_format' ), $start_date ) ); ?></span>
390 + <?php else : ?>
391 + <span class="wpsubs-cell--muted">&#8212;</span>
392 + <?php endif; ?>
393 + </td>
394 +
395 + <!-- Renewal date -->
396 + <td class="wpsubs-col--nowrap">
397 + <?php if ( $renewal_date ) : ?>
398 + <span class="wpsubs-cell-title"><?php echo esc_html( wp_date( get_option( 'date_format' ), $renewal_date ) ); ?></span>
399 + <span class="wpsubs-cell-id"><?php echo esc_html( wp_date( get_option( 'time_format' ), $renewal_date ) ); ?></span>
400 + <?php else : ?>
401 + <span class="wpsubs-cell--muted">&#8212;</span>
402 + <?php endif; ?>
403 + </td>
404 +
405 + <!-- Actions -->
406 + <td class="wpsubs-cell--actions">
407 + <div class="wpsubs-row-actions">
408 + <button type="button" class="wpsubs-row-actions__trigger" aria-label="<?php esc_attr_e( 'Row actions', 'subscription' ); ?>">···</button>
409 + <div class="wpsubs-dropdown">
410 + <a href="<?php echo esc_url( $view_url ); ?>" class="wpsubs-dropdown__item"><?php esc_html_e( 'View / Edit', 'subscription' ); ?></a>
411 + <div class="wpsubs-dropdown__divider"></div>
412 + <?php if ( ! $is_trash ) : ?>
413 + <a href="<?php echo esc_url( $trash_url ); ?>" class="wpsubs-dropdown__item wpsubs-dropdown__item--danger" onclick="return confirm('<?php esc_attr_e( 'Move this subscription to trash?', 'subscription' ); ?>')"><?php esc_html_e( 'Trash', 'subscription' ); ?></a>
414 + <?php else : ?>
415 + <a href="<?php echo esc_url( $restore_url ); ?>" class="wpsubs-dropdown__item"><?php esc_html_e( 'Restore', 'subscription' ); ?></a>
416 + <a href="<?php echo esc_url( $delete_url ); ?>" class="wpsubs-dropdown__item wpsubs-dropdown__item--danger" onclick="return confirm('<?php esc_attr_e( 'Delete permanently? This cannot be undone.', 'subscription' ); ?>')"><?php esc_html_e( 'Delete Permanently', 'subscription' ); ?></a>
417 + <?php endif; ?>
418 + </div>
419 + </div>
420 + </td>
421 + </tr>
422 + <?php endforeach; ?>
423 +
424 + <?php else : ?>
425 + <tr>
426 + <td colspan="8">
427 + <div class="wpsubs-empty">
428 + <div class="wpsubs-empty__icon">📋</div>
429 + <div class="wpsubs-empty__title"><?php esc_html_e( 'No subscriptions found', 'subscription' ); ?></div>
430 + <div class="wpsubs-empty__desc"><?php esc_html_e( 'Subscriptions will appear here once customers complete checkout.', 'subscription' ); ?></div>
431 + </div>
432 + </td>
433 + </tr>
434 + <?php endif; ?>
435 +
436 + </tbody>
437 + </table>
438 +
439 + <!-- Pagination inside table card -->
440 + <?php
441 + wpsubs_render_pager(
442 + array(
443 + 'current' => (int) $paged,
444 + 'total' => max( 1, (int) $max_num_pages ),
445 + 'info' => true,
446 + 'per_page' => (int) $per_page,
447 + 'item_count' => (int) $total,
448 + 'base_url' => remove_query_arg( 'paged' ),
449 + 'link_mode' => 'url',
450 + // translators: %1$s = first item number, %2$s = last item number, %3$s = total items.
451 + 'info_format' => __( 'Showing %1$s–%2$s of %3$s subscriptions', 'subscription' ),
452 + 'context' => 'subscription-list',
453 + )
454 + );
455 + ?>
456 +
457 + </div><!-- /.wpsubs-table-card -->
458 +
459 + </form>
460 +
140 461 </div>
462 +
463 +<script>
464 +( function () {
465 + var selectAll = document.getElementById( 'cb-select-all' );
466 + var form = document.getElementById( 'subscriptions-form' );
467 +
468 + function rowChecks() {
469 + return form ? Array.from( form.querySelectorAll( '.wpsubs-row-check' ) ) : [];
470 + }
471 +
472 + function syncSelectAll() {
473 + if ( ! selectAll ) return;
474 + var checks = rowChecks();
475 + var checked = checks.filter( function ( cb ) { return cb.checked; } );
476 + checks.forEach( function ( cb ) {
477 + var row = cb.closest( 'tr' );
478 + if ( row ) row.classList.toggle( 'wpsubs-row--selected', cb.checked );
479 + } );
480 + selectAll.indeterminate = checked.length > 0 && checked.length < checks.length;
481 + selectAll.checked = checks.length > 0 && checked.length === checks.length;
482 + }
483 +
484 + if ( selectAll ) {
485 + selectAll.addEventListener( 'change', function () {
486 + rowChecks().forEach( function ( cb ) { cb.checked = selectAll.checked; } );
487 + syncSelectAll();
488 + } );
489 + }
490 +
491 + if ( form ) {
492 + form.addEventListener( 'change', function ( e ) {
493 + if ( e.target.classList.contains( 'wpsubs-row-check' ) ) syncSelectAll();
494 + } );
495 + }
496 +
497 + // ── Advanced-select event handlers ──────────────────────────
498 + var bulkSubmit = document.getElementById( 'wpsubs-bulk-action-submit' );
499 + document.addEventListener( 'wpsubs:select', function ( e ) {
500 + var id = e.target && e.target.id;
501 +
502 + // Bulk action: submit via hidden button
503 + if ( id === 'wpsubs-bulk-action-select' && bulkSubmit ) {
504 + bulkSubmit.click();
505 + return;
506 + }
507 +
508 + // Per-page: auto-submit as a filter action
509 + if ( id === 'wpsubs-per-page-select' && form ) {
510 + var fi = document.createElement( 'input' );
511 + fi.type = 'hidden';
512 + fi.name = 'filter_action';
513 + fi.value = 'filter';
514 + form.appendChild( fi );
515 + form.submit();
516 + }
517 + } );
518 +
519 + // ── Row action dropdowns ─────────────────────────────────────
520 + function closeAllRowMenus() {
521 + document.querySelectorAll( '.wpsubs-row-actions--open' ).forEach( function ( el ) {
522 + el.classList.remove( 'wpsubs-row-actions--open' );
523 + var d = el.querySelector( '.wpsubs-dropdown' );
524 + if ( d ) d.classList.remove( 'wpsubs-dropdown--open' );
525 + } );
526 + }
527 +
528 + document.addEventListener( 'click', function ( e ) {
529 + var trigger = e.target.closest( '.wpsubs-row-actions__trigger' );
530 +
531 + document.querySelectorAll( '.wpsubs-row-actions--open' ).forEach( function ( el ) {
532 + if ( el !== ( trigger && trigger.closest( '.wpsubs-row-actions' ) ) ) {
533 + el.classList.remove( 'wpsubs-row-actions--open' );
534 + var d = el.querySelector( '.wpsubs-dropdown' );
535 + if ( d ) d.classList.remove( 'wpsubs-dropdown--open' );
536 + }
537 + } );
538 +
539 + if ( ! trigger ) return;
540 +
541 + e.stopPropagation();
542 + var wrapper = trigger.closest( '.wpsubs-row-actions' );
543 + var dropdown = wrapper && wrapper.querySelector( '.wpsubs-dropdown' );
544 + if ( ! wrapper || ! dropdown ) return;
545 +
546 + var isOpen = wrapper.classList.contains( 'wpsubs-row-actions--open' );
547 + wrapper.classList.toggle( 'wpsubs-row-actions--open', ! isOpen );
548 + dropdown.classList.toggle( 'wpsubs-dropdown--open', ! isOpen );
549 + } );
550 +
551 + document.addEventListener( 'keydown', function ( e ) {
552 + if ( e.key === 'Escape' ) closeAllRowMenus();
553 + } );
554 +
555 + syncSelectAll();
556 +}() );
557 +</script>