label, value).
* @type array $summary_cards List of cards: title, value, value_type, icon, change, is_positive,
* previous, note, no_previous_label.
* @type array $charts List of charts: id, title, date_label, full_width, type,
* data, x_key, series ( key, label, color, style, aggregate:
* 'average' for a series that must not add up by week or
* month ), label_key, value_key, value_format, empty.
* @type array $widgets List of bottom widgets: title, render (callable).
* }
*/
public function __construct( array $config ) {
$this->config = $config;
$this->columns = ! empty( $config['columns'] ) ? absint( $config['columns'] ) : 5;
$header = isset( $config['header'] ) && is_array( $config['header'] ) ? $config['header'] : [];
// Date range: use the override when supplied, otherwise resolve from the request.
$this->date_range = ! empty( $header['date_range'] ) && is_array( $header['date_range'] )
? $header['date_range']
: DDFW_Dashboard_Data::get_date_range();
$this->render();
}
/**
* Render the full dashboard.
*
* @return void
*/
protected function render() {
$this->enqueue_assets();
?>
render_header();
$this->render_summary_cards();
$this->render_charts();
$this->render_widgets();
?>
localize();
}
/**
* Enqueue the shared dashboard assets registered by DDFW_Assets.
*
* @return void
*/
protected function enqueue_assets() {
wp_enqueue_style( 'ddfw-dashboard-analytics-style' );
wp_enqueue_script( 'ddfw-dashboard-analytics-script' );
}
/**
* Render the dashboard header ( avatar + welcome + date filter ).
*
* @return void
*/
protected function render_header() {
$header = isset( $this->config['header'] ) && is_array( $this->config['header'] ) ? $this->config['header'] : [];
$show_avatar = ! isset( $header['show_avatar'] ) || $header['show_avatar'];
$show_date_filter = ! isset( $header['show_date_filter'] ) || $header['show_date_filter'];
$current_user = wp_get_current_user();
$welcome = isset( $header['welcome'] ) ? $header['welcome'] : esc_html__( 'Welcome back!', 'affiliates-for-woocommerce' );
if ( false !== strpos( $welcome, '%s' ) ) {
$welcome = sprintf( $welcome, $current_user->display_name );
}
$subtitle = isset( $header['subtitle'] ) ? $header['subtitle'] : '';
// Detail screens ( e.g. a single user ) can point the header at a specific entity instead
// of the current admin: override the avatar and add a back link.
$avatar_url = ! empty( $header['avatar_url'] ) ? $header['avatar_url'] : get_avatar_url( $current_user->ID, [ 'size' => 48 ] );
$back_link = isset( $header['back_link'] ) && is_array( $header['back_link'] ) ? $header['back_link'] : [];
?>
config['header'] ) && is_array( $this->config['header'] ) ? $this->config['header'] : [];
$extra = isset( $header['date_filter_extra'] ) && is_array( $header['date_filter_extra'] ) ? $header['date_filter_extra'] : [];
// Extra selects (an outlet, a store, a vendor) live in the same form as the dates, so
// changing one keeps the other. A filter's own name is never also sent as a hidden field.
$filters = isset( $header['filters'] ) && is_array( $header['filters'] ) ? $header['filters'] : [];
$extra = array_diff_key( $extra, array_flip( wp_list_pluck( $filters, 'name' ) ) );
$presets = DDFW_Dashboard_Data::get_presets();
$groups = DDFW_Dashboard_Data::get_preset_groups();
$active = $this->date_range['key'] ?? '30_days';
$check = ' ';
?>
config['summary_cards'] ) && is_array( $this->config['summary_cards'] ) ? $this->config['summary_cards'] : [];
if ( empty( $cards ) ) {
return;
}
// Optional panel rendered beside the cards ( e.g. a user level / rank widget ) that
// spans the full height of the card grid instead of dropping to the bottom section.
$aside = isset( $this->config['summary_aside'] ) && is_array( $this->config['summary_aside'] ) ? $this->config['summary_aside'] : [];
?>
render_summary_card( $card );
}
?>
0 ? ' ' : ' '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static markup. ?>
= 1000 ? '999+' : number_format_i18n( abs( $change ), fmod( $change, 1 ) ? 1 : 0 ) ) . '%' ); ?>
config['charts'] ) && is_array( $this->config['charts'] ) ? $this->config['charts'] : [];
if ( empty( $charts ) ) {
return;
}
// Group consecutive non-full-width charts into rows of two; full-width gets its own row.
$full = [];
$half = [];
foreach ( $charts as $chart ) {
if ( ! empty( $chart['full_width'] ) ) {
$full[] = $chart;
} else {
$half[] = $chart;
}
}
// Full-width charts each on their own row.
foreach ( $full as $chart ) {
?>
render_chart_container( $chart ); ?>
render_chart_container( $chart );
}
?>
collect_chart( $chart );
$title = $chart['title'] ?? '';
$date_label = $chart['date_label'] ?? '';
?>
render_chart_canvas( $canvas_id ); ?>
charts_js[] = [
'canvasId' => $canvas_id,
'type' => $chart['type'] ?? 'line',
'data' => array_values( $chart['data'] ?? [] ),
'xKey' => $chart['x_key'] ?? 'date',
'series' => array_values( $chart['series'] ?? [] ),
'labelKey' => $chart['label_key'] ?? 'label',
'valueKey' => $chart['value_key'] ?? 'value',
'valueFormat' => $chart['value_format'] ?? 'number',
'empty' => [
'title' => $chart['empty']['title'] ?? esc_html__( 'No data available', 'affiliates-for-woocommerce' ),
'desc' => $chart['empty']['desc'] ?? esc_html__( 'Data will appear here once activity is recorded.', 'affiliates-for-woocommerce' ),
],
];
return $canvas_id;
}
/**
* Output a chart canvas wrapper for a previously collected chart.
*
* @param string $canvas_id Canvas element id.
* @return void
*/
protected function render_chart_canvas( $canvas_id ) {
?>
config['widgets'] ) && is_array( $this->config['widgets'] ) ? $this->config['widgets'] : [];
if ( empty( $widgets ) ) {
return;
}
?>
render_chart_canvas( $this->collect_chart( $widget['chart'] ) );
} elseif ( ! empty( $widget['render'] ) && is_callable( $widget['render'] ) ) {
call_user_func( $widget['render'] );
}
?>
tag ( `$product->get_image()` ) or an image URL.
* @type string $shape 'square' ( products, the default ) or 'round' ( people ).
* @type string $value Optional value on the right ( HTML: `wc_price()`, a status badge ).
* @type string $value_class Optional 'positive' or 'negative' for a tinted signed value.
* }
* @param string $empty Message shown when there are no rows.
* @param bool $image Whether rows carry an image column. Rows without one get the placeholder tile.
* @return void
*/
public static function render_list( $items, $empty = '', $image = true ) {
if ( empty( $items ) ) {
?>
< class="ddfw-dash-list-item">
' : wp_kses_post( $source ); ?>
>
html_entity_decode( get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8' ),
'position' => get_option( 'woocommerce_currency_pos', 'left' ),
'decimals' => wc_get_price_decimals(),
'decimalSeparator' => wc_get_price_decimal_separator(),
'thousandSeparator' => wc_get_price_thousand_separator(),
]
: [ 'symbol' => '$' ];
$currency = apply_filters( 'ddfw_dashboard_currency', $currency );
wp_localize_script(
'ddfw-dashboard-analytics-script',
'ddfwDashboardAnalytics',
[
'charts' => $this->charts_js,
'dateRange' => $this->date_range,
'currency' => $currency,
'currencySymbol' => $currency['symbol'],
'today' => current_time( 'Y-m-d' ),
'startOfWeek' => (int) get_option( 'start_of_week', 1 ),
'i18n' => [
'noData' => esc_html__( 'No data available', 'affiliates-for-woocommerce' ),
'total' => esc_html__( 'Total', 'affiliates-for-woocommerce' ),
'pickStart' => esc_html__( 'Pick a start date', 'affiliates-for-woocommerce' ),
'pickEnd' => esc_html__( 'Now pick the end date', 'affiliates-for-woocommerce' ),
'previousMonth' => esc_html__( 'Previous month', 'affiliates-for-woocommerce' ),
'nextMonth' => esc_html__( 'Next month', 'affiliates-for-woocommerce' ),
/* translators: %s: the period still being counted, e.g. "Oct 19 – 25, 2026". */
'soFar' => esc_html__( '%s (so far)', 'affiliates-for-woocommerce' ),
'busiestDay' => esc_html__( 'Busiest day', 'affiliates-for-woocommerce' ),
'busiestHour' => esc_html__( 'Busiest hour', 'affiliates-for-woocommerce' ),
'less' => esc_html__( 'Less', 'affiliates-for-woocommerce' ),
'more' => esc_html__( 'More', 'affiliates-for-woocommerce' ),
'day' => esc_html__( 'Day', 'affiliates-for-woocommerce' ),
'week' => esc_html__( 'Week', 'affiliates-for-woocommerce' ),
'month' => esc_html__( 'Month', 'affiliates-for-woocommerce' ),
'line' => esc_html__( 'Line', 'affiliates-for-woocommerce' ),
'bar' => esc_html__( 'Bar', 'affiliates-for-woocommerce' ),
],
]
);
}
}
}