PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Modules / Integrations / FluentPlugins / FluentCRMDeepIntegration.php

FluentCRMDeepIntegration.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Modules/Integrations/FluentPlugins/FluentCRMDeepIntegration.php

693 lines 29.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\Integrations\FluentPlugins;
4
5 use FluentAffiliate\App\Models\Affiliate;
6 use FluentCart\Api\ModuleSettings;
7 use FluentCart\App\Helpers\Status;
8 use FluentCart\App\Models\Customer;
9 use FluentCart\App\Models\Product;
10 use FluentCart\App\Services\URL;
11 use FluentCrm\App\Models\Subscriber;
12 use FluentCrm\App\Services\Helper;
13 use FluentCrm\App\Services\Libs\ConditionAssessor;
14 use FluentCrm\Framework\Support\Arr;
15
16 class FluentCRMDeepIntegration
17 {
18 private $importKey = 'fluent_cart';
19
20 public function init()
21 {
22 // Advanced Filters
23 add_filter('fluentcrm_contacts_filter_fluent_cart', array($this, 'applyAdvancedFilters'), 10, 2);
24 add_filter('fluentcrm_advanced_filter_options', array($this, 'addAdvancedFilterOptions'), 10, 1);
25
26 add_filter('fluentcrm_ajax_options_product_selector_fluent_cart', [$this, 'handleProductSelectorAjax'], 10, 3);
27 add_filter('fluent_crm/cascade_selection_options_fct_variations', [$this, 'handleProductVariationsAjax'], 1, 2);
28
29 add_filter('fluent_crm/subscriber_info_widgets', [$this, 'pushInfoWidgetToContact'], 1, 2);
30
31
32 // temp
33 // @todo: Remove after FluentCRM version 3 release
34 add_filter('fluent_crm/purchase_history_fluent_cart', function ($data) {
35
36 if (!$data || empty($data['data'])) {
37 return $data;
38 }
39
40 $dataItems = $data['data'];
41
42 foreach ($dataItems as $index => $item) {
43 $action = $item['action'];
44 $action = str_replace('fluent-crm#/orders/', 'fluent-cart#/orders/', $action);
45 $dataItems[$index]['action'] = $action;
46 }
47
48 $data['data'] = $dataItems;
49
50 return $data;
51 }, 99, 1);
52
53 }
54
55 /**
56 * @param \FluentCrm\Framework\Database\Orm\Builder|\FluentCrm\Framework\Database\Query\Builder $query
57 * @param array $filters
58 * @return \FluentCrm\Framework\Database\Orm\Builder|\FluentCrm\Framework\Database\Query\Builder
59 */
60 public function applyAdvancedFilters($query, $filters)
61 {
62 foreach ($filters as $filter) {
63 $query = $this->applyFilter($query, $filter);
64 }
65
66 return $query;
67 }
68
69 public function addAdvancedFilterOptions($groups)
70 {
71 $conditionItems = [
72 [
73 'value' => 'commerce_exist',
74 'label' => __('Is a customer?', 'fluent-cart'),
75 'type' => 'selections',
76 'is_multiple' => false,
77 'disable_values' => true,
78 'value_description' => __('This filter will check if a contact has at least one shop order or not', 'fluent-cart'),
79 'custom_operators' => [
80 'exist' => __('Yes', 'fluent-cart'),
81 'not_exist' => __('No', 'fluent-cart'),
82 ]
83 ],
84 [
85 'value' => 'ltv',
86 'label' => __('Lifetime Value', 'fluent-cart'),
87 'type' => 'numeric'
88 ],
89 [
90 'value' => 'aov',
91 'label' => __('Average Order Value', 'fluent-cart'),
92 'type' => 'numeric',
93 ],
94 [
95 'value' => 'first_purchase_date',
96 'label' => __('First Order Date', 'fluent-cart'),
97 'type' => 'dates'
98 ],
99 [
100 'value' => 'last_purchase_date',
101 'label' => __('Last Order Date', 'fluent-cart'),
102 'type' => 'dates'
103 ],
104 [
105 'value' => 'purchased_items',
106 'label' => __('Products', 'fluent-cart'),
107 'type' => 'selections',
108 'component' => 'product_selector',
109 'is_multiple' => true,
110 'custom_operators' => [
111 'exist' => __('purchased', 'fluent-cart'),
112 'not_exist' => __('not purchased', 'fluent-cart'),
113 ],
114 'help' => __('Will filter the contacts who have at least one order', 'fluent-cart')
115 ],
116 [
117 'value' => 'variation_purchased',
118 'label' => __('Product Variations', 'fluent-cart'),
119 'type' => 'cascade_selections',
120 'provider' => 'fct_variations',
121 'is_multiple' => true,
122 'value_description' => __('This filter will check if a contact has purchased at least one specific product variation or not', 'fluent-cart'),
123 'custom_operators' => [
124 'exist' => __('purchased', 'fluent-cart'),
125 'not_exist' => __('not purchased', 'fluent-cart'),
126 ]
127 ],
128 [
129 'value' => 'purchased_categories',
130 'label' => __('Product Categories', 'fluent-cart'),
131 'type' => 'selections',
132 'component' => 'tax_selector',
133 'taxonomy' => 'product-categories',
134 'is_multiple' => true,
135 'disabled' => true,
136 'help' => __('Will filter the contacts who have at least one order', 'fluent-cart'),
137 'custom_operators' => [
138 'exist' => __('purchased', 'fluent-cart'),
139 'not_exist' => __('not purchased', 'fluent-cart'),
140 ]
141 ],
142 [
143 'value' => 'commerce_coupons',
144 'label' => __('Used Coupons', 'fluent-cart'),
145 'type' => 'selections',
146 'component' => 'ajax_selector',
147 'option_key' => 'fct_coupons',
148 'is_multiple' => true,
149 'disabled' => true,
150 'custom_operators' => [
151 'exist' => __('in', 'fluent-cart'),
152 'not_exist' => __('not in', 'fluent-cart'),
153 ],
154 'help' => __('Will filter the contacts who have at least one order', 'fluent-cart')
155 ]
156 ];
157
158 if (ModuleSettings::isActive('license')) {
159 $conditionItems[] = [
160 'value' => 'active_licenses',
161 'label' => __('Active Licenses', 'fluent-cart'),
162 'type' => 'selections',
163 'component' => 'product_selector',
164 'is_multiple' => true,
165 'custom_operators' => [
166 'exist' => __('have', 'fluent-cart'),
167 'not_exist' => __('do not have', 'fluent-cart'),
168 ],
169 'help' => __('Will filter the contacts who have at least one active licenses or not', 'fluent-cart')
170 ];
171 $conditionItems[] = [
172 'value' => 'active_variation_licenses',
173 'label' => __('Active Variation Licenses', 'fluent-cart'),
174 'type' => 'cascade_selections',
175 'provider' => 'fct_variations',
176 'is_multiple' => true,
177 'value_description' => __('This filter will check if a contact has at least one specific variation license or not', 'fluent-cart'),
178 'custom_operators' => [
179 'exist' => __('have', 'fluent-cart'),
180 'not_exist' => __('do not have', 'fluent-cart'),
181 ]
182 ];
183 $conditionItems[] = [
184 'value' => 'expired_licenses',
185 'label' => __('Expired Licenses', 'fluent-cart'),
186 'type' => 'selections',
187 'component' => 'product_selector',
188 'is_multiple' => true,
189 'custom_operators' => [
190 'exist' => __('have', 'fluent-cart'),
191 'not_exist' => __('do not have', 'fluent-cart'),
192 ],
193 'help' => __('Will filter the contacts who have at least one expired licenses or not', 'fluent-cart')
194 ];
195 $conditionItems[] = [
196 'value' => 'expired_variation_licenses',
197 'label' => __('Expired Variation Licenses', 'fluent-cart'),
198 'type' => 'cascade_selections',
199 'provider' => 'fct_variations',
200 'is_multiple' => true,
201 'value_description' => __('This filter will check if a contact has at least one specific variation expired license or not', 'fluent-cart'),
202 'custom_operators' => [
203 'exist' => __('have', 'fluent-cart'),
204 'not_exist' => __('do not have', 'fluent-cart'),
205 ]
206 ];
207 $conditionItems[] = [
208 'value' => 'license_exist',
209 'label' => __('Has any active license?', 'fluent-cart'),
210 'type' => 'selections',
211 'is_multiple' => false,
212 'disable_values' => true,
213 'value_description' => __('Check if contacts has any active license from any products', 'fluent-cart'),
214 'custom_operators' => [
215 'exist' => __('Yes', 'fluent-cart'),
216 'not_exist' => __('No', 'fluent-cart'),
217 ]
218 ];
219 }
220
221 $groups['fluent_cart'] = [
222 'label' => __('FluentCart', 'fluent-cart'),
223 'value' => 'fluent_cart',
224 'children' => $conditionItems
225 ];
226
227 return $groups;
228 }
229
230 private function applyFilter($query, $filter)
231 {
232 $key = Arr::get($filter, 'property', '');
233 $value = Arr::get($filter, 'value', '');
234 $operator = Arr::get($filter, 'operator', '');
235
236 if (!$key || !$operator) {
237 return $query;
238 }
239
240 if ($key == 'commerce_exist') {
241 if ($operator === 'exist') {
242 return $query->whereExists(function ($q) {
243 $q->select(fluentCrmDb()->raw(1))
244 ->from('fct_customers')
245 ->whereColumn('fct_customers.email', 'fc_subscribers.email');
246 });
247 }
248
249 return $query->whereNotExists(function ($q) {
250 $q->select(fluentCrmDb()->raw(1))
251 ->from('fct_customers')
252 ->whereColumn('fct_customers.email', 'fc_subscribers.email');
253 });
254 }
255
256 if ($key === 'license_exist') {
257 if ($operator === 'exist') {
258 return $query->whereExists(function ($q) {
259 $q->select(fluentCrmDb()->raw(1))
260 ->from('fct_customers')
261 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
262 ->join('fct_licenses', function ($join) {
263 $join->on('fct_licenses.customer_id', '=', 'fct_customers.id')
264 ->whereIn('fct_licenses.status', ['active', 'inactive']);
265 });
266 });
267 }
268
269 return $query->whereNotExists(function ($q) {
270 $q->select(fluentCrmDb()->raw(1))
271 ->from('fct_customers')
272 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
273 ->join('fct_licenses', function ($join) {
274 $join->on('fct_licenses.customer_id', '=', 'fct_customers.id')
275 ->whereIn('fct_licenses.status', ['active', 'inactive']);
276 });
277 });
278 }
279
280 if ($value === '') {
281 return $query;
282 }
283
284 $customerProperties = ['ltv', 'aov'];
285
286 if (in_array($key, $customerProperties)) {
287 $value = \FluentCart\App\Helpers\Helper::toCent($value);
288 return $query->whereExists(function ($q) use ($key, $value, $operator) {
289 $q->select(fluentCrmDb()->raw(1))
290 ->from('fct_customers')
291 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
292 ->where('fct_customers.' . $key, $operator, $value);
293 });
294 }
295
296 if ($key == 'first_purchase_date' || $key == 'last_purchase_date') {
297 $filter = Subscriber::filterParser($filter);
298 return $query->whereExists(function ($q) use ($filter, $key) {
299 $q->select(fluentCrmDb()->raw(1))
300 ->from('fct_customers')
301 ->whereColumn('fct_customers.email', 'fc_subscribers.email');
302 if ($filter['operator'] == 'BETWEEN') {
303 return $q->whereBetween('fct_customers.' . $key, $filter['value']);
304 } else {
305 return $q->where('fct_customers.' . $key, $filter['operator'], $filter['value']);
306 }
307 });
308 }
309
310 if ($key == 'last_payout_date') {
311 $filter = Subscriber::filterParser($filter);
312 return $query->whereExists(function ($q) use ($filter) {
313 $q->select(fluentCrmDb()->raw(1))
314 ->from('fa_affiliates')
315 ->whereColumn('fa_affiliates.user_id', 'fc_subscribers.user_id')
316 ->join('fa_payout_transactions', 'fa_payout_transactions.affiliate_id', '=', 'fa_affiliates.id');
317 if ($filter['operator'] == 'BETWEEN') {
318 return $q->whereBetween('fa_payout_transactions.created_at', $filter['value']);
319 }
320
321 return $q->where('fa_payout_transactions.created_at', $filter['operator'], $filter['value']);
322 });
323 }
324
325 if ($key === 'purchased_items' || $key === 'variation_purchased') {
326 if ($key === 'variation_purchased') {
327 $value = array_map(function ($item) {
328 $parts = explode('||', $item);
329 return isset($parts[1]) ? (int)$parts[1] : 0;
330 }, is_array($value) ? $value : [$value]);
331
332 $value = array_values(array_filter($value, 'is_numeric'));
333 }
334
335 $itemColumn = $key === 'purchased_items' ? 'post_id' : 'object_id';
336 $value = is_array($value) ? $value : [$value];
337
338 if ($operator === 'exist') {
339 return $query->whereExists(function ($q) use ($itemColumn, $value) {
340 $q->select(fluentCrmDb()->raw(1))
341 ->from('fct_customers')
342 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
343 ->join('fct_orders', function ($join) {
344 $join->on('fct_orders.customer_id', '=', 'fct_customers.id')
345 ->whereIn('fct_orders.payment_status', Status::getOrderPaymentSuccessStatuses());
346 })
347 ->join('fct_order_items', 'fct_order_items.order_id', '=', 'fct_orders.id')
348 ->whereIn('fct_order_items.' . $itemColumn, $value);
349 });
350 }
351
352 return $query->whereNotExists(function ($q) use ($itemColumn, $value) {
353 $q->select(fluentCrmDb()->raw(1))
354 ->from('fct_customers')
355 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
356 ->join('fct_orders', function ($join) {
357 $join->on('fct_orders.customer_id', '=', 'fct_customers.id')
358 ->whereIn('fct_orders.payment_status', Status::getOrderPaymentSuccessStatuses());
359 })
360 ->join('fct_order_items', 'fct_order_items.order_id', '=', 'fct_orders.id')
361 ->whereIn('fct_order_items.' . $itemColumn, $value);
362 });
363 }
364
365 if ($key === 'active_licenses' || $key === 'active_variation_licenses') {
366 if ($key === 'active_variation_licenses') {
367 $value = array_map(function ($item) {
368 $parts = explode('||', $item);
369 return isset($parts[1]) ? (int)$parts[1] : 0;
370 }, is_array($value) ? $value : [$value]);
371 $value = array_values(array_filter($value, 'is_numeric'));
372 }
373
374 $itemColumn = $key === 'active_licenses' ? 'product_id' : 'variation_id';
375 $value = is_array($value) ? $value : [$value];
376
377 if ($operator === 'exist') {
378 return $query->whereExists(function ($q) use ($itemColumn, $value) {
379 $q->select(fluentCrmDb()->raw(1))
380 ->from('fct_customers')
381 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
382 ->join('fct_licenses', function ($join) {
383 $join->on('fct_licenses.customer_id', '=', 'fct_customers.id')
384 ->whereIn('fct_licenses.status', ['active', 'inactive']);
385 })
386 ->whereIn('fct_licenses.' . $itemColumn, $value);
387 });
388 }
389
390 return $query->whereNotExists(function ($q) use ($itemColumn, $value) {
391 $q->select(fluentCrmDb()->raw(1))
392 ->from('fct_customers')
393 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
394 ->join('fct_licenses', function ($join) {
395 $join->on('fct_licenses.customer_id', '=', 'fct_customers.id')
396 ->whereIn('fct_licenses.status', ['active', 'inactive']);
397 })
398 ->whereIn('fct_licenses.' . $itemColumn, $value);
399 });
400 }
401
402 if ($key === 'expired_licenses' || $key === 'expired_variation_licenses') {
403 if ($key === 'expired_variation_licenses') {
404 $value = array_map(function ($item) {
405 $parts = explode('||', $item);
406 return isset($parts[1]) ? (int)$parts[1] : 0;
407 }, is_array($value) ? $value : [$value]);
408 $value = array_values(array_filter($value, 'is_numeric'));
409 }
410
411 $itemColumn = $key === 'expired_licenses' ? 'product_id' : 'variation_id';
412 $value = is_array($value) ? $value : [$value];
413
414 if ($operator === 'exist') {
415 return $query->whereExists(function ($q) use ($itemColumn, $value) {
416 $q->select(fluentCrmDb()->raw(1))
417 ->from('fct_customers')
418 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
419 ->join('fct_licenses', function ($join) {
420 $join->on('fct_licenses.customer_id', '=', 'fct_customers.id');
421 })
422 ->where('fct_licenses.status', 'expired')
423 ->whereNotIn('fct_licenses.status', ['active', 'inactive'])
424 ->whereIn('fct_licenses.' . $itemColumn, $value);
425 });
426 }
427
428 return $query->whereNotExists(function ($q) use ($itemColumn, $value) {
429 $q->select(fluentCrmDb()->raw(1))
430 ->from('fct_customers')
431 ->whereColumn('fct_customers.email', 'fc_subscribers.email')
432 ->join('fct_licenses', function ($join) {
433 $join->on('fct_licenses.customer_id', '=', 'fct_customers.id');
434 })
435 ->where('fct_licenses.status', 'expired')
436 ->whereNotIn('fct_licenses.status', ['active', 'inactive'])
437 ->whereIn('fct_licenses.' . $itemColumn, $value);
438 });
439 }
440
441 return $query;
442 }
443
444 public function handleProductSelectorAjax($options, $searchTerm, $includedIds = [])
445 {
446 if (!$includedIds || !is_array($includedIds)) {
447 $includedIds = [];
448 }
449
450 $products = Product::query()->whereLike('post_title', $searchTerm)->limit(50)->get();
451
452 $formattedProducts = [];
453
454 $pushedIds = [];
455
456 foreach ($products as $product) {
457 $pushedIds[] = (string)$product->ID;
458 $formattedProducts[] = [
459 'id' => (string)$product->ID,
460 'title' => $product->post_title
461 ];
462 }
463
464 $leftoverIds = array_diff($includedIds, $pushedIds);
465
466 if ($leftoverIds) {
467 $leftoverProducts = Product::query()->whereIn('ID', $leftoverIds)->get();
468 foreach ($leftoverProducts as $product) {
469 $formattedProducts[] = [
470 'id' => (string)$product->ID,
471 'title' => $product->post_title
472 ];
473 }
474 }
475
476 return $formattedProducts;
477 }
478
479 public function handleProductVariationsAjax($response, $reqestData = [])
480 {
481 $prevValues = Arr::get($reqestData, 'values', []);
482 $search = Arr::get($reqestData, 'search', '');
483 $prevItemIds = [];
484
485 if ($prevValues) {
486 $prevValues = (array)$prevValues;
487 foreach ($prevValues as $prevValue) {
488 $prevItemIds[] = explode('||', $prevValue)[0];
489 }
490 $prevItemIds = array_values(array_unique($prevItemIds));
491 }
492
493 // get wc variable products
494 $variableProducts = Product::query()->whereLike('post_title', $search)
495 ->with(['variants'])
496 ->limit(50)
497 ->get();
498
499 $formattedProducts = [];
500
501 $includedProductIds = [];
502
503 foreach ($variableProducts as $product) {
504 $item = [
505 'value' => (string)$product->ID,
506 'label' => $product->post_title
507 ];
508
509 $formattedVariations = [];
510 foreach ($product->variants as $variant) {
511 $formattedVariations[] = [
512 'value' => $item['value'] . '||' . $variant->id,
513 'label' => $variant->variation_title
514 ];
515 }
516 $item['children'] = $formattedVariations;
517 $formattedProducts[] = $item;
518
519 $includedProductIds[] = $item['value'];
520 }
521
522 if ($prevItemIds) {
523 $includedProductIds = array_diff($prevItemIds, $includedProductIds);
524 if ($includedProductIds) {
525 $variableProducts = Product::query()->whereIn('ID', $includedProductIds)
526 ->with(['variants'])
527 ->get();
528
529 foreach ($variableProducts as $product) {
530 $item = [
531 'value' => (string)$product->id,
532 'label' => $product->post_title
533 ];
534
535 $variations = $product->get_children();
536 $formattedVariations = [];
537 foreach ($product->variants as $variant) {
538 $formattedVariations[] = [
539 'value' => $item['value'] . '||' . $variant->id,
540 'label' => $variant->variation_title
541 ];
542 }
543 $item['children'] = $formattedVariations;
544 $formattedProducts[] = $item;
545 }
546 }
547 }
548
549 $formattedProducts = array_filter($formattedProducts, function ($item) {
550 return !empty($item['children']) && count($item['children']) > 1;
551 });
552
553
554 return [
555 'options' => $formattedProducts,
556 'has_more' => true
557 ];
558 }
559
560 public function pushInfoWidgetToContact($widgets, $subscriber)
561 {
562 $userId = $subscriber->user_id;
563 $customer = Customer::query();
564 if ($userId) {
565 $customer = $customer->where('user_id', $userId)
566 ->orWhere('email', $subscriber->email);
567 } else {
568 $customer = $customer->where('email', $subscriber->email);
569 }
570
571 $customer = $customer->first();
572
573 if (!$customer) {
574 return $widgets;
575 }
576
577
578 $widgets['fluent_cart'] = [
579 'title' => __('Commerce Info', 'fluent-cart'),
580 'content' => $this->getStatsHtml($customer)
581 ];
582
583 return $widgets;
584 }
585
586 public function getStatsHtml($customer)
587 {
588 $viewUrl = URL::getDashboardUrl('customers/' . $customer->id . '/view');
589 $naLabel = __('N/A', 'fluent-cart');
590
591 // Compact, consistent labels (no trailing colons) so they read well as
592 // stat-card captions on the FluentCRM contact profile.
593 $stats = [
594 [
595 'label' => __('Lifetime Value', 'fluent-cart'),
596 // toDecimal() returns the amount prefixed with an HTML-entity
597 // currency sign (e.g. &#36;), so it is rendered raw here; the
598 // value is a controlled currency string, never user input.
599 'value' => '<a href="' . esc_url($viewUrl) . '" target="_blank" rel="noopener" class="fc_view_more">' . \FluentCart\App\Helpers\Helper::toDecimal($customer->ltv) . '</a>'
600 ],
601 [
602 'label' => __('Purchases', 'fluent-cart'),
603 'value' => esc_html($customer->purchase_count)
604 ],
605 [
606 'label' => __('First Order', 'fluent-cart'),
607 'value' => $customer->first_purchase_date ? esc_html(gmdate('M j, Y', strtotime($customer->first_purchase_date))) : esc_html($naLabel)
608 ],
609 [
610 'label' => __('Last Order', 'fluent-cart'),
611 'value' => $customer->last_purchase_date ? esc_html(gmdate('M j, Y', strtotime($customer->last_purchase_date))) : esc_html($naLabel)
612 ],
613 ];
614
615 // FluentCRM styles this markup via .fcrm_fluentcart_customer_commerce_info
616 $html = '<div class="fcrm_fluentcart_customer_commerce_info">';
617
618 $html .= '<ul class="fcrm_fc_stat_grid">';
619 foreach ($stats as $stat) {
620 $html .= '<li class="fcrm_fc_stat">'
621 . '<span class="fcrm_fc_stat_label">' . $stat['label'] . '</span>'
622 . '<span class="fcrm_fc_stat_value">' . $stat['value'] . '</span>'
623 . '</li>';
624 }
625 $html .= '</ul>';
626
627 $orderedItems = $customer->success_order_items()->orderBy('id', 'DESC')->get();
628
629 // Group identical products and count repeat purchases
630 $formattedItems = [];
631 foreach ($orderedItems as $orderedItem) {
632 $count = isset($formattedItems[$orderedItem->object_id]) ? $formattedItems[$orderedItem->object_id]['count'] + 1 : 1;
633 $formattedItems[$orderedItem->object_id] = [
634 'title' => $orderedItem->title,
635 'post_title' => $orderedItem->post_title,
636 'count' => $count,
637 // Items are iterated newest-first, so the last write for a
638 // product keeps its oldest row: the customer's first order for
639 // it. Both the shown date and the link point at that order.
640 'created_at' => $orderedItem->created_at,
641 'order_id' => $orderedItem->order_id
642 ];
643 }
644
645 if ($formattedItems) {
646 $html .= '<div class="fcrm_fc_products">';
647 $html .= '<div class="fcrm_fc_products_title">' . esc_html__('Recent purchases', 'fluent-cart') . '</div>';
648 $html .= '<ul class="fcrm_fc_product_list">';
649
650 foreach ($formattedItems as $formattedItem) {
651 $qtyHtml = '';
652 if ($formattedItem['count'] > 1) {
653 $qtyHtml = '<span class="fcrm_fc_product_qty">' . esc_html($formattedItem['count']) . '</span>';
654 }
655
656 // Only show the variant when it differs from the product name,
657 // so single-variant products don't read as "Name - Name"
658 $variantHtml = '';
659 if ($formattedItem['title'] && $formattedItem['title'] !== $formattedItem['post_title']) {
660 $variantHtml = '<span class="fcrm_fc_product_variant">' . esc_html($formattedItem['title']) . '</span>';
661 }
662
663 // Link the date straight to the (first) order for this product
664 $dateText = esc_html(gmdate('M j, Y', strtotime($formattedItem['created_at'])));
665 if (!empty($formattedItem['order_id'])) {
666 $orderUrl = URL::getDashboardUrl('orders/' . $formattedItem['order_id'] . '/view');
667 $dateHtml = '<a class="fcrm_fc_product_date" href="' . esc_url($orderUrl) . '" target="_blank" rel="noopener">' . $dateText . '</a>';
668 } else {
669 $dateHtml = '<span class="fcrm_fc_product_date">' . $dateText . '</span>';
670 }
671
672 $html .= '<li class="fcrm_fc_product">'
673 . '<span class="fcrm_fc_product_main">'
674 . '<span class="fcrm_fc_product_name">' . esc_html($formattedItem['post_title']) . '</span>'
675 . $variantHtml
676 . '</span>'
677 . '<span class="fcrm_fc_product_meta">'
678 . $qtyHtml
679 . $dateHtml
680 . '</span>'
681 . '</li>';
682 }
683
684 $html .= '</ul></div>';
685 }
686
687 $html .= '</div>';
688
689 return $html;
690 }
691
692 }
693