PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / apps / my-wordpress / parts / woocommerce.php

woocommerce.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.10, at apps/my-wordpress/parts/woocommerce.php

665 lines 21.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * My WordPress — the WooCommerce surface.
4 *
5 * Part of the `my-wordpress` app: required by `my-wordpress.os.php`,
6 * same namespace, plain `.php` on purpose — only `*.os.php` files are
7 * app entries to the framework loader. This part is the app's half of
8 * the WooCommerce integration: the Orders and Customers sections, the
9 * band-ordered Products / Coupons queries, and the per-row facts the
10 * shared `os.my-wordpress.*` JS seams read. Everything here is inert
11 * unless WooCommerce is active.
12 *
13 * It is deliberately thin: every rule — which band a product is in,
14 * how customers are ranked, what an order row says — lives in the
15 * existing `openstation_my_wordpress_woo_*` helpers WP Explorer
16 * already runs (`includes/my-wordpress/integrations/`), called here
17 * behind `function_exists` guards. One set of rules, two windows; the
18 * client half is the same `os-my-wordpress-woocommerce` bundle both
19 * windows load, subscribed to the same hook bus.
20 *
21 * @package OpenStation
22 */
23
24 namespace OpenStation\Apps\MyWordPress;
25
26 use OpenStation\App\Os;
27 use OpenStation\App\State;
28
29 // Direct access, unless a standalone host is booting on bare PHP.
30 if ( ! defined( 'ABSPATH' ) ) {
31 defined( 'OPENSTATION_STANDALONE' ) || exit;
32 }
33
34 /**
35 * Whether the WooCommerce integration helpers are loaded and active.
36 *
37 * @return bool
38 */
39 function woo_ready() {
40 return function_exists( 'openstation_my_wordpress_woo_active' )
41 && openstation_my_wordpress_woo_active();
42 }
43
44 /**
45 * Whether a section descriptor is one of ours.
46 *
47 * @param array<string,mixed> $section Section descriptor.
48 * @param string $id Section id to test for.
49 * @return bool
50 */
51 function woo_section_is( array $section, $id ) {
52 return isset( $section['id'] ) && $id === $section['id'];
53 }
54
55 /**
56 * The Woo folder's group fields, through the same
57 * `openstation_my_wordpress_woo_group()` relabelling WP Explorer uses.
58 *
59 * @return array{id:string,label:string,icon:string,order:int}
60 */
61 function woo_group_fields() {
62 $group = array(
63 'id' => 'plugin:woocommerce',
64 'label' => 'WooCommerce',
65 'icon' => 'dashicons-admin-plugins',
66 'order' => 20,
67 );
68 if ( function_exists( 'openstation_my_wordpress_woo_group' ) ) {
69 $group = (array) openstation_my_wordpress_woo_group( $group, 'shop_order' );
70 }
71 return $group;
72 }
73
74 /**
75 * The sections this part adds to the root: Orders and Customers —
76 * the two shop surfaces a plain post-type folder cannot serve.
77 * Orders live outside `wp_posts` under High-Performance Order
78 * Storage, and Customers are a ranking over users, not a post type.
79 *
80 * Same labels, icons, groups and permission gates as WP Explorer's
81 * `wc-orders` / `wc-customers` entities. `flat => true` marks a
82 * section with no detail folder behind its tiles — the client keeps
83 * Navigate into and the quick-edit modal off it, the actions refuse
84 * post mutations on it.
85 *
86 * @param Os $os Host handle.
87 * @return array<int,array<string,mixed>>
88 */
89 function woo_sections( Os $os ) {
90 unset( $os );
91 if ( ! woo_ready() ) {
92 return array();
93 }
94
95 $group = woo_group_fields();
96 $sections = array();
97
98 $orders = get_post_type_object( 'shop_order' );
99 if ( $orders instanceof \WP_Post_Type && ! empty( $orders->cap->edit_posts ) ) {
100 $sections[] = array(
101 'id' => 'wc-orders',
102 'label' => __( 'Orders', 'desktop-mode' ),
103 'icon' => 'dashicons-cart',
104 'kind' => 'post',
105 // Claims `shop_order`, so the generic post-type pass skips
106 // it — the WP_Query path returns an empty folder on any
107 // HPOS store.
108 'post_type' => 'shop_order',
109 'capability' => (string) $orders->cap->edit_posts,
110 'thumbnails' => false,
111 'flat' => true,
112 'group' => $group['id'],
113 'groupLabel' => $group['label'],
114 'groupIcon' => $group['icon'],
115 'groupOrder' => (int) $group['order'],
116 );
117 }
118
119 if ( function_exists( 'openstation_my_wordpress_woo_customers_permission' )
120 && true === openstation_my_wordpress_woo_customers_permission() ) {
121 $sections[] = array(
122 'id' => 'wc-customers',
123 'label' => __( 'Customers', 'desktop-mode' ),
124 'icon' => 'dashicons-groups',
125 // Renders through the built-in user kind: avatar tiles, the
126 // dossier pane, the drag-out seam. A customer is a person
127 // before they are a row of money.
128 'kind' => 'user',
129 'post_type' => '',
130 // The two-gate permission was checked above; no single
131 // capability string expresses it.
132 'capability' => '',
133 'thumbnails' => true,
134 'group' => $group['id'],
135 'groupLabel' => $group['label'],
136 'groupIcon' => $group['icon'],
137 'groupOrder' => (int) $group['order'],
138 );
139 }
140
141 return $sections;
142 }
143
144 /**
145 * Give a discovered CPT section WooCommerce's icons and thumbnail
146 * rules, through the same `openstation_my_wordpress_woo_entity_icon()`
147 * (and its `openstation_my_wordpress_woo_section_icons` filter) that
148 * decorates WP Explorer's entities. Non-Woo types pass through
149 * untouched.
150 *
151 * @param array<string,mixed> $section Section descriptor.
152 * @param \WP_Post_Type $post_type Post type object.
153 * @return array<string,mixed>
154 */
155 function woo_decorate_section( array $section, \WP_Post_Type $post_type ) {
156 if ( ! function_exists( 'openstation_my_wordpress_woo_entity_icon' ) ) {
157 return $section;
158 }
159 $section = (array) openstation_my_wordpress_woo_entity_icon( $section, $post_type );
160 // Entity-descriptor keys the app has no reader for — `listFields`
161 // is the REST `_fields` allowlist, `listQuery` extra request
162 // params, `tileSize` the old grid's hint. The app's rows carry
163 // their facts directly and its queries are ordered server-side.
164 unset( $section['listFields'], $section['listQuery'], $section['tileSize'] );
165 return $section;
166 }
167
168 /**
169 * Sort options for the Woo-managed sections — a single honest entry,
170 * because their order is decided server-side: orders and coupons walk
171 * their bands, customers rank by band then spend, products shelve
172 * empty stock first. Null leaves a section to the generic options.
173 *
174 * @param array<string,mixed> $section Section descriptor.
175 * @return array<string,array{0:string,1:string,2:string}>|null
176 */
177 function woo_sort_options( array $section ) {
178 if ( ! woo_ready() ) {
179 return null;
180 }
181 if ( woo_section_is( $section, 'wc-orders' ) ) {
182 return array( 'default' => array( __( 'Needs attention first', 'desktop-mode' ), 'date', 'DESC' ) );
183 }
184 if ( woo_section_is( $section, 'wc-customers' ) ) {
185 return array( 'default' => array( __( 'Top spenders first', 'desktop-mode' ), '', '' ) );
186 }
187 if ( woo_section_is( $section, 'cpt-product' ) || woo_section_is( $section, 'cpt-shop_coupon' ) ) {
188 return array( 'default' => array( __( 'Store order', 'desktop-mode' ), 'date', 'DESC' ) );
189 }
190 return null;
191 }
192
193 /**
194 * Root-tile counts for the Woo sections, or null for anyone else's.
195 *
196 * @param array<string,mixed> $section Section descriptor.
197 * @return int|null
198 */
199 function woo_count( array $section ) {
200 if ( ! woo_ready() ) {
201 return null;
202 }
203 if ( woo_section_is( $section, 'wc-orders' ) ) {
204 $counted = wc_get_orders(
205 array(
206 'limit' => 1,
207 'paginate' => true,
208 'return' => 'ids',
209 )
210 );
211 return is_object( $counted ) && isset( $counted->total ) ? (int) $counted->total : 0;
212 }
213 if ( woo_section_is( $section, 'wc-customers' )
214 && function_exists( 'openstation_my_wordpress_woo_customer_plan' ) ) {
215 $plan = openstation_my_wordpress_woo_customer_plan();
216 return (int) ( $plan['customers'] ?? 0 );
217 }
218 return null;
219 }
220
221 /**
222 * One order as an app list row. The title is what a merchant scans
223 * for — number and total — and the REAL status rides `wcStatus` for
224 * the band assigner, never `status`: the tile ribbon only speaks
225 * draft/pending/private/future, and `wc-processing` would paint a
226 * meaningless ribbon on every order.
227 *
228 * @param \WC_Abstract_Order $order Order.
229 * @return array<string,mixed>
230 */
231 function woo_order_item( $order ) {
232 $total = openstation_my_wordpress_woo_price( $order->get_total(), $order->get_currency() );
233 $name = method_exists( $order, 'get_formatted_billing_full_name' )
234 ? trim( (string) $order->get_formatted_billing_full_name() )
235 : '';
236 $date = $order->get_date_created()
237 ? (string) date_i18n( (string) get_option( 'date_format' ), $order->get_date_created()->getTimestamp() )
238 : '';
239
240 return array(
241 'id' => (int) $order->get_id(),
242 'title' => sprintf(
243 /* translators: 1: order number, 2: formatted order total. */
244 __( '#%1$s · %2$s', 'desktop-mode' ),
245 $order->get_order_number(),
246 $total
247 ),
248 'subtitle' => sprintf(
249 /* translators: 1: customer name, 2: date. */
250 __( '%1$s — %2$s', 'desktop-mode' ),
251 '' !== $name ? $name : __( 'Guest', 'desktop-mode' ),
252 $date
253 ),
254 'status' => 'publish',
255 'excerpt' => '',
256 'thumb' => '',
257 // Refunds and custom order types don't carry an edit URL.
258 'link' => method_exists( $order, 'get_edit_order_url' )
259 ? esc_url_raw( (string) $order->get_edit_order_url() )
260 : '',
261 'mime' => '',
262 'lockedBy' => '',
263 'canEdit' => method_exists( $order, 'get_edit_order_url' ),
264 'canDelete' => false,
265 'wcStatus' => (string) $order->get_status(),
266 );
267 }
268
269 /**
270 * One page of the Orders section: the same band walker WP Explorer's
271 * `/woocommerce/orders` route runs — statuses sliced in display order
272 * so a band's rows arrive together and the grouping never reshuffles
273 * under the reader — reshaped as app rows.
274 *
275 * @param State $state State (`query`, `page`).
276 * @return array{items:array[],total:int,pages:int,page:int,perPage:int}
277 */
278 function woo_orders_page( State $state ) {
279 $per_page = PER_PAGE;
280 $page = max( 1, (int) $state->get( 'page' ) );
281 $query = (string) $state->get( 'query' );
282
283 $base = array(
284 'orderby' => 'date',
285 'order' => 'DESC',
286 );
287 if ( '' !== $query ) {
288 $base['s'] = $query;
289 }
290
291 $slices = openstation_my_wordpress_woo_order_band_slices( $base );
292
293 $total = 0;
294 foreach ( $slices as $slice ) {
295 $total += (int) $slice['count'];
296 }
297 $offset = ( $page - 1 ) * $per_page;
298
299 $orders = array();
300 $remaining = $per_page;
301 $cursor = 0;
302 foreach ( $slices as $slice ) {
303 if ( $remaining <= 0 ) {
304 break;
305 }
306 $count = (int) $slice['count'];
307 if ( 0 === $count ) {
308 continue;
309 }
310 if ( $offset >= $cursor + $count ) {
311 $cursor += $count;
312 continue;
313 }
314 $within = max( 0, $offset - $cursor );
315 $take = min( $remaining, $count - $within );
316
317 $batch = wc_get_orders(
318 array_merge(
319 $base,
320 array(
321 'status' => $slice['statuses'],
322 'limit' => $take,
323 'offset' => $within,
324 'paginate' => false,
325 )
326 )
327 );
328 $batch = is_object( $batch ) && isset( $batch->orders ) ? (array) $batch->orders : (array) $batch;
329
330 $orders = array_merge( $orders, $batch );
331 $remaining -= count( $batch );
332 $cursor += $count;
333 $offset = $cursor;
334 }
335
336 $items = array();
337 foreach ( $orders as $maybe_order ) {
338 // A data store may hand back ids rather than objects; and
339 // `WC_Abstract_Order` — not `WC_Order` — is what every order
340 // class actually extends, HPOS overrides and custom order
341 // types included.
342 $order = is_scalar( $maybe_order ) ? wc_get_order( (int) $maybe_order ) : $maybe_order;
343 if ( ! $order instanceof \WC_Abstract_Order ) {
344 continue;
345 }
346 try {
347 $items[] = woo_order_item( $order );
348 } catch ( \Throwable $e ) {
349 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
350 error_log(
351 sprintf(
352 '[openstation] Skipped order %d in the My WordPress app: %s',
353 is_object( $order ) ? (int) $order->get_id() : 0,
354 $e->getMessage()
355 )
356 );
357 }
358 }
359
360 return Os::page( $items, $total, $page, $per_page );
361 }
362
363 /**
364 * One page of the Customers section: the same plan-ordered roster WP
365 * Explorer's `/woocommerce/customers` route serves — band first,
366 * spend inside the band — as app user rows carrying the
367 * `openstation_woo_customer` facts the shared seams read.
368 *
369 * @param Os $os Host handle.
370 * @param State $state State (`query`, `page`).
371 * @return array{items:array[],total:int,pages:int,page:int,perPage:int}
372 */
373 function woo_customers_page( Os $os, State $state ) {
374 $per_page = PER_PAGE;
375 $page = max( 1, (int) $state->get( 'page' ) );
376 $query = trim( (string) $state->get( 'query' ) );
377
378 $plan = openstation_my_wordpress_woo_customer_plan();
379 $capped = ! empty( $plan['capped'] );
380
381 $args = array(
382 'number' => $per_page,
383 'paged' => $page,
384 'fields' => 'all',
385 );
386
387 if ( $capped ) {
388 // Past the cap the plan holds no ids, so hand the ordering
389 // back to the database: newest accounts first, the only
390 // useful order left once bands are off.
391 $args['role'] = 'customer';
392 $args['orderby'] = 'registered';
393 $args['order'] = 'DESC';
394 } else {
395 $ids = array_map( 'intval', (array) ( $plan['ids'] ?? array() ) );
396 if ( array() === $ids ) {
397 return Os::page( array(), 0, $page, $per_page );
398 }
399 $args['include'] = $ids;
400 // `include` + `orderby => include` replays the plan's order
401 // verbatim — the user-query spelling of `post__in`.
402 $args['orderby'] = 'include';
403 }
404
405 if ( '' !== $query ) {
406 $args['search'] = '*' . $query . '*';
407 $args['search_columns'] = array( 'user_login', 'user_email', 'user_nicename', 'display_name' );
408 // A search is a different question from "show me the roster",
409 // and the plan's order would hide matches below the fold.
410 if ( ! $capped ) {
411 $args['orderby'] = 'display_name';
412 $args['order'] = 'ASC';
413 }
414 }
415
416 $users = new \WP_User_Query( $args );
417 $total = (int) $users->get_total();
418
419 $items = array();
420 foreach ( (array) $users->get_results() as $user ) {
421 if ( ! $user instanceof \WP_User ) {
422 continue;
423 }
424 $row = user_row(
425 $user,
426 static function ( $user_id ) use ( $os ) {
427 return $os->can( 'edit_user', $user_id );
428 }
429 );
430 $row += woo_user_extras( (int) $user->ID );
431 $items[] = $row;
432 }
433
434 return Os::page( $items, $total, $page, $per_page );
435 }
436
437 /**
438 * The whole list page for a Woo section, or null for anyone else's.
439 *
440 * @param Os $os Host handle.
441 * @param array<string,mixed> $section Section descriptor.
442 * @param State $state State.
443 * @return array<string,mixed>|null
444 */
445 function woo_list( Os $os, array $section, State $state ) {
446 if ( ! woo_ready() ) {
447 return null;
448 }
449 if ( woo_section_is( $section, 'wc-orders' ) ) {
450 return woo_orders_page( $state );
451 }
452 if ( woo_section_is( $section, 'wc-customers' ) ) {
453 return woo_customers_page( $os, $state );
454 }
455 return null;
456 }
457
458 /**
459 * Band-order the Products and Coupons queries, exactly as the REST
460 * collections are ordered for WP Explorer: the cached plan replayed
461 * as `post__in`, so each band's rows arrive together and the client's
462 * grouping can never disagree with the order they arrive in. A capped
463 * catalogue falls back to stock status, which at least floats empty
464 * shelves to the top; a search is the user asking for relevance and
465 * is left alone.
466 *
467 * @param array<string,mixed> $args `WP_Query` args.
468 * @param array<string,mixed> $section Section descriptor.
469 * @param State $state State.
470 * @return array<string,mixed>
471 */
472 function woo_query_args( array $args, array $section, State $state ) {
473 if ( ! woo_ready() || '' !== (string) $state->get( 'query' ) ) {
474 return $args;
475 }
476
477 if ( woo_section_is( $section, 'cpt-product' )
478 && function_exists( 'openstation_my_wordpress_woo_product_plan' ) ) {
479 $plan = openstation_my_wordpress_woo_product_plan();
480 if ( ! empty( $plan['ids'] ) ) {
481 $args['post__in'] = array_map( 'intval', (array) $plan['ids'] );
482 $args['orderby'] = 'post__in';
483 unset( $args['order'] );
484 } else {
485 $args['meta_key'] = '_stock_status'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
486 $args['orderby'] = array(
487 'meta_value' => 'DESC',
488 'date' => 'DESC',
489 );
490 }
491 return $args;
492 }
493
494 if ( woo_section_is( $section, 'cpt-shop_coupon' )
495 && function_exists( 'openstation_my_wordpress_woo_coupon_plan' ) ) {
496 $plan = openstation_my_wordpress_woo_coupon_plan();
497 if ( ! empty( $plan['ids'] ) ) {
498 $args['post__in'] = array_map( 'intval', (array) $plan['ids'] );
499 $args['orderby'] = 'post__in';
500 unset( $args['order'] );
501 }
502 }
503
504 return $args;
505 }
506
507 /**
508 * The `openstation_woo` facts for a product or coupon row — the same
509 * payload the REST fields ship on `wp/v2` rows, which is what the
510 * shared band assigner and the stock-ribbon decorator read. Empty for
511 * every other row.
512 *
513 * @param \WP_Post $post Post.
514 * @return array<string,mixed>
515 */
516 function woo_extras( \WP_Post $post ) {
517 if ( ! woo_ready() ) {
518 return array();
519 }
520
521 if ( 'product' === $post->post_type
522 && function_exists( 'openstation_my_wordpress_woo_product_band_id' ) ) {
523 $product = wc_get_product( $post->ID );
524 if ( ! $product ) {
525 return array();
526 }
527 $slugs = wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'slugs' ) );
528 return array(
529 'openstation_woo' => array(
530 // The band this row belongs to, decided by the same
531 // rules that ordered the collection, so the two can't
532 // disagree.
533 'band' => openstation_my_wordpress_woo_product_band_id( $product ),
534 'stockStatus' => $product->get_stock_status(),
535 'stockLevel' => $product->managing_stock() ? (int) $product->get_stock_quantity() : null,
536 'onSale' => $product->is_on_sale(),
537 'categories' => is_wp_error( $slugs ) ? array() : array_values( $slugs ),
538 ),
539 );
540 }
541
542 if ( 'shop_coupon' === $post->post_type
543 && function_exists( 'openstation_my_wordpress_woo_coupon_band_id' ) ) {
544 $coupon = new \WC_Coupon( $post->ID );
545 if ( ! $coupon->get_id() ) {
546 return array();
547 }
548 return array(
549 'openstation_woo' => array(
550 'band' => openstation_my_wordpress_woo_coupon_band_id( $coupon ),
551 ),
552 );
553 }
554
555 return array();
556 }
557
558 /**
559 * The `openstation_woo_customer` facts for a user row — carried on
560 * the Customers section's rows ONLY. Deliberately narrower than WP
561 * Explorer (whose `/wp/v2/users` field puts spend on its Users list
562 * too): in this app the built-in Users folder is about people who
563 * write, and stays money-free — the shared bundle keys its badges
564 * and panel off these facts being present, so leaving them off a row
565 * is how a surface opts out. Gated exactly as the REST field is: a
566 * viewer who can't see orders sees no money.
567 *
568 * @param int $user_id User id.
569 * @return array<string,mixed>
570 */
571 function woo_user_extras( $user_id ) {
572 if ( ! woo_ready()
573 || ! function_exists( 'openstation_my_wordpress_woo_customer_facts' )
574 || ! function_exists( 'openstation_my_wordpress_woo_customers_permission' )
575 || true !== openstation_my_wordpress_woo_customers_permission() ) {
576 return array();
577 }
578 return array(
579 'openstation_woo_customer' => openstation_my_wordpress_woo_customer_facts( (int) $user_id ),
580 );
581 }
582
583 /**
584 * Whether the acting user may act on a Woo row — null passes the
585 * question back to the generic capability checks. Orders need it
586 * because `current_user_can( 'edit_post', $order_id )` means nothing
587 * once HPOS moves orders out of `wp_posts`.
588 *
589 * @param array<string,mixed> $section Section descriptor.
590 * @param int $id Item id.
591 * @param string $verb `edit` | `delete`.
592 * @return bool|null
593 */
594 function woo_allowed( array $section, $id, $verb ) {
595 unset( $id );
596 if ( ! woo_ready() || ! woo_section_is( $section, 'wc-orders' ) ) {
597 return null;
598 }
599 if ( 'edit' !== $verb ) {
600 // Orders are never mutated from here — WP Explorer's Orders
601 // section is read-and-open too.
602 return false;
603 }
604 $orders = get_post_type_object( 'shop_order' );
605 return $orders instanceof \WP_Post_Type
606 && ! empty( $orders->cap->edit_posts )
607 && current_user_can( $orders->cap->edit_posts );
608 }
609
610 /**
611 * The admin URL that edits a Woo row, '' for anyone else's. HPOS and
612 * legacy storage put the order screen in different places, and only
613 * WooCommerce knows which.
614 *
615 * @param array<string,mixed> $section Section descriptor.
616 * @param int $id Item id.
617 * @return string
618 */
619 function woo_edit_url( array $section, $id ) {
620 if ( ! woo_ready() || ! woo_section_is( $section, 'wc-orders' ) ) {
621 return '';
622 }
623 $order = wc_get_order( (int) $id );
624 if ( $order instanceof \WC_Abstract_Order && method_exists( $order, 'get_edit_order_url' ) ) {
625 return (string) $order->get_edit_order_url();
626 }
627 return '';
628 }
629
630 /**
631 * The dossier payload for one order. Deliberately spare: the pane's
632 * substance is the shared bundle's merchant panel, painted into the
633 * `header` slot with the full status, totals, line items and
634 * customer — repeating them as facts would say everything twice.
635 *
636 * @param array<string,mixed> $section Section descriptor.
637 * @param int $id Order id.
638 * @return array<string,mixed>|null Null when the order vanished.
639 */
640 function woo_detail( array $section, $id ) {
641 $order = wc_get_order( (int) $id );
642 if ( ! $order instanceof \WC_Abstract_Order ) {
643 return null;
644 }
645 $item = woo_order_item( $order );
646
647 return array(
648 'kind' => 'post',
649 'id' => (int) $order->get_id(),
650 'title' => (string) $item['title'],
651 'facts' => Os::facts(
652 array(
653 array(
654 __( 'Placed', 'desktop-mode' ),
655 $order->get_date_created()
656 ? (string) date_i18n( (string) get_option( 'date_format' ), $order->get_date_created()->getTimestamp() )
657 : '',
658 ),
659 )
660 ),
661 'canEdit' => ! empty( $item['canEdit'] ) && true === woo_allowed( $section, (int) $id, 'edit' ),
662 'canDelete' => false,
663 );
664 }
665