Skip to content

Commit 18bcdbe

Browse files
mrautela365claude
andauthored
fix(dashboards): show no-data state instead of performing well (#682)
* fix(dashboards): show no-data state instead of performing well When all campaign data is zero the catch-all action showed "Performing Well — Maintain current momentum" which is misleading. Now detects zero email/paid/attribution activity and shows "No active campaigns detected" under the attention section instead. LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> * fix(dashboards): improve no-data message to guide users Update the no-data description to suggest reaching out to marketing ops instead of passively waiting for data. LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> * fix(dashboards): reword no-data message for campaigns Emphasize engaging marketing ops for momentum rather than setting up tracking. LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> * fix(dashboards): use neutral card for no-data state Replace the red attention-section approach with a neutral blue info card. When all campaign data is zero the drawer now skips both "Needs Your Attention" and "Performing Well" and shows a neutral informational card guiding EDs to engage marketing ops. LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> * fix(dashboards): broaden attribution check in hasNoData Include firstTouchRevenue, lastTouchRevenue, and timeDecayRevenue in the hasAttributionActivity predicate so channels with non-zero revenue from any attribution model are correctly detected. LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> * fix(dashboards): add a11y attrs and soften no-data copy Extract dataResolved signal to DRY triple-gate check. Add role=status and aria-live=polite to no-data card. Remove actionable CTA copy. LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> * fix(dashboards): extract hasNoData to initHasNoData, fix a11y - Extract inline hasNoData computed to private initHasNoData() per component organization rules - Drop redundant aria-live="polite" (implicit in role="status") - Use h3 for no-data card title to match heading outline - Use border-l-blue-600 to match design system -600 shade pattern LFXV2-1644 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> --------- Signed-off-by: Misha Rautela <mrautela@linuxfoundation.org> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7b0247b commit 18bcdbe

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

apps/lfx-one/src/app/modules/dashboards/executive-director/components/email-ctr-drawer/email-ctr-drawer.component.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ <h3 class="text-sm font-semibold text-green-700">Performing Well</h3>
9999
</div>
100100
}
101101

102+
@if (hasNoData()) {
103+
<div
104+
class="flex flex-col rounded-lg border border-gray-200 border-l-4 border-l-blue-600 bg-white overflow-hidden"
105+
role="status"
106+
data-testid="email-ctr-drawer-no-data">
107+
<div class="flex items-start gap-4 px-4 py-4">
108+
<i class="fa-light fa-circle-info text-blue-500 mt-0.5" aria-hidden="true"></i>
109+
<div class="flex flex-col gap-1 flex-1 min-w-0">
110+
<h3 class="text-sm font-semibold text-gray-900">No active campaigns detected</h3>
111+
<p class="text-sm text-gray-500">No email, paid, or attribution activity is currently available for this foundation</p>
112+
</div>
113+
</div>
114+
</div>
115+
}
116+
102117
@if (attributionData().channels.length > 0) {
103118
<div class="flex flex-col gap-5" data-testid="email-ctr-drawer-attribution-section">
104119
<div class="flex items-center gap-2">

apps/lfx-one/src/app/modules/dashboards/executive-director/components/email-ctr-drawer/email-ctr-drawer.component.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export class EmailCtrDrawerComponent {
5454
protected readonly performingActions: Signal<MarketingRecommendedAction[]> = computed(() => this.split().performingActions);
5555

5656
protected readonly performingInsights: Signal<MarketingKeyInsight[]> = computed(() => this.split().performingInsights);
57+
58+
// True once all three data sources have resolved (not still loading)
59+
private readonly dataResolved: Signal<boolean> = computed(() => !this.drawerLoading() && this.paidDataResolved() && this.attributionDataResolved());
60+
61+
protected readonly hasNoData: Signal<boolean> = this.initHasNoData();
62+
5763
protected readonly expandedTypes = signal<Set<string>>(new Set());
5864

5965
protected readonly emailTotalSends: Signal<string> = computed(() => {
@@ -212,6 +218,23 @@ export class EmailCtrDrawerComponent {
212218
return channel.sessions > 0 ? `$${(channel.linearRevenue / channel.sessions).toFixed(2)}` : '—';
213219
}
214220

221+
private initHasNoData(): Signal<boolean> {
222+
return computed(() => {
223+
if (!this.dataResolved()) {
224+
return false;
225+
}
226+
const email = this.drawerData();
227+
const paid = this.paidData();
228+
const attribution = this.attributionData();
229+
const hasEmailActivity = email.currentCtr > 0 || email.monthlySends.some((s) => s > 0);
230+
const hasPaidActivity = paid.totalReach > 0 || paid.totalSpend > 0;
231+
const hasAttributionActivity =
232+
attribution.channels.length > 0 &&
233+
attribution.channels.some((c) => c.sessions > 0 || c.linearRevenue > 0 || c.firstTouchRevenue > 0 || c.lastTouchRevenue > 0 || c.timeDecayRevenue > 0);
234+
return !hasEmailActivity && !hasPaidActivity && !hasAttributionActivity;
235+
});
236+
}
237+
215238
private initDrawerData(): Signal<EmailCtrResponse> {
216239
const defaultValue: EmailCtrResponse = {
217240
currentCtr: 0,
@@ -255,7 +278,7 @@ export class EmailCtrDrawerComponent {
255278
return computed(() => {
256279
// Gate on all three data sources having resolved — avoid misleading
257280
// "Maintain current momentum" while paid/attribution are still in-flight.
258-
if (this.drawerLoading() || !this.paidDataResolved() || !this.attributionDataResolved()) {
281+
if (!this.dataResolved()) {
259282
return [];
260283
}
261284

@@ -392,7 +415,7 @@ export class EmailCtrDrawerComponent {
392415
// Combine — 1 per section, max 3
393416
const actions = [...attrActions.slice(0, 1), ...paidActions.slice(0, 1), ...emailActions.slice(0, 1)];
394417

395-
if (actions.length === 0) {
418+
if (actions.length === 0 && !this.hasNoData()) {
396419
actions.push({
397420
title: 'Maintain current momentum',
398421
description: 'All channels performing well — continue current strategy and monitor for shifts',
@@ -408,7 +431,7 @@ export class EmailCtrDrawerComponent {
408431
private initKeyInsights(): Signal<MarketingKeyInsight[]> {
409432
return computed(() => {
410433
// Gate on all three data sources — same rationale as initRecommendedActions.
411-
if (this.drawerLoading() || !this.paidDataResolved() || !this.attributionDataResolved()) {
434+
if (!this.dataResolved()) {
412435
return [];
413436
}
414437

0 commit comments

Comments
 (0)