diff --git a/app.py b/app.py index 8c44f9d..028b335 100644 --- a/app.py +++ b/app.py @@ -2200,9 +2200,9 @@ _REPORTS_CAMPAIGNS = { }, "cta-campaign": { "label": "CTA Campaign", - # Placeholder periods only - dates from the proposed plan, not yet approved. - # Selecting one shows a "not built yet" placeholder (_render_reports_tab), - # same treatment "End of Campaign" itself got before it was built out. + # Dates from the proposed plan - campaign isn't approved yet, so these + # render as real (currently empty/near-zero) reports once the period + # starts, same as any other month would before it has data. "periods": [ ("cta-phase-1", "Nov 3 - Nov 26 (Failure Analysis)"), ("cta-phase-2", "Dec 1 - Jan 7 (Standardization)"), @@ -2212,6 +2212,10 @@ _REPORTS_CAMPAIGNS = { } _REPORTS_CAMPAIGN_OPTIONS = [{"label": v["label"], "value": k} for k, v in _REPORTS_CAMPAIGNS.items()] _REPORTS_DEFAULT_CAMPAIGN = "linkedin-branding" +# Flat lookup across every campaign's periods, for the PDF/docx filename label - +# the Period dropdown only ever holds one campaign's periods at a time (via the +# cascade callback), so this is the one place that needs all of them at once. +_REPORTS_ALL_PERIODS = [kv for camp in _REPORTS_CAMPAIGNS.values() for kv in camp["periods"]] reports_tab = dbc.Container([ html.Div([ @@ -4522,34 +4526,22 @@ def _render_reports_tab(month_value): if month_value == "campaign-end": return _render_campaign_end_summary() + (store_value,) - if month_value.startswith("cta-phase"): - # CTA Campaign is proposed, not yet approved — placeholder only, same - # treatment "End of Campaign" itself got before it was built out. - period_label = next((lbl for v, lbl in _REPORTS_CAMPAIGNS["cta-campaign"]["periods"] - if v == month_value), month_value) - title = html.H3(period_label, - style={"color": "#1b3a5c", "marginBottom": "0", "fontWeight": "700", - "fontSize": "2rem", "whiteSpace": "nowrap"}) - placeholder = dbc.Alert( - "The CTA Campaign hasn't been approved yet, so there's no report to show. " - "This period will build out the same way the other reports do once it's live.", - color="info") - return title, placeholder, no_update - stats = report.compute_period_stats(_build_report_ctx(), month_value) notes = monthly_report_notes.NOTES.get(month_value) fts_f, posts_f = stats["fts_f"], stats["posts_f"] - # Only July/August/September were part of the 3-month branding campaign — a - # later month like October gets a plain title, no active campaign stage + # Only July/August/September were part of the 3-month branding campaign, and + # CTA Campaign phases (CUSTOM_PERIODS) once that campaign is live — any other + # month like October gets a plain title, no active campaign stage # (goal_line/core_message/roadmap) behind it. - _is_campaign_month = False - try: - _year, _month = (int(p) for p in month_value.split("-")) - _month_start = pd.Timestamp(year=_year, month=_month, day=1) - _is_campaign_month = report.CAMPAIGN_START <= _month_start <= report.CAMPAIGN_END - except ValueError: - pass + _is_campaign_month = month_value in report.CUSTOM_PERIODS + if not _is_campaign_month: + try: + _year, _month = (int(p) for p in month_value.split("-")) + _month_start = pd.Timestamp(year=_year, month=_month, day=1) + _is_campaign_month = report.CAMPAIGN_START <= _month_start <= report.CAMPAIGN_END + except ValueError: + pass title_heading = html.H3( f"{stats['period_label']} End of Month Campaign Report" if _is_campaign_month else f"TXproducts — {stats['period_label']}", @@ -4770,12 +4762,10 @@ def _render_reports_tab(month_value): prevent_initial_call=True, ) def _download_reports_pdf(_n_clicks, month_value): - if month_value and month_value.startswith("cta-phase"): - return no_update # CTA Campaign not approved yet, nothing to download ctx_data = _build_report_ctx() ctx_data["monthly_notes"] = monthly_report_notes.NOTES.get(month_value) docx_bytes = report.build_report_docx(ctx_data, month_value) - label = next((o["label"] for o in _REPORTS_MONTH_OPTIONS if o["value"] == month_value), month_value) + label = next((lbl for v, lbl in _REPORTS_ALL_PERIODS if v == month_value), month_value) filename = f"TXproducts_LinkedIn_Report_{label.replace(' ', '_')}.docx" return dcc.send_bytes(lambda buf: buf.write(docx_bytes), filename=filename) diff --git a/monthly_report_notes.py b/monthly_report_notes.py index 4e8fd1a..ea6f80e 100644 --- a/monthly_report_notes.py +++ b/monthly_report_notes.py @@ -516,4 +516,35 @@ NOTES = { ], }, }, + # CTA Campaign phases (Nov 2026 - Feb 2027), pending approval. Core Message is + # the same constant every other campaign reuses (TXproducts' value proposition + # doesn't change); the Roadmap abstract is what changes per phase, drawn + # directly from the proposed plan's own theme framing. No executive_summary yet + # - that would mean reporting findings from a campaign that hasn't run, so the + # KPI table and charts render on real (currently empty) data instead until + # there's something real to write up, same as any other not-yet-run period. + "cta-phase-1": { + "core_message_abstract": _CORE_MESSAGE_ABSTRACT, + "roadmap_abstract": [ + ("Failure Analysis.", True), + (" Power electronics fail under real operating stress, and time " + "resolution shows the failure as it happens.", False), + ], + }, + "cta-phase-2": { + "core_message_abstract": _CORE_MESSAGE_ABSTRACT, + "roadmap_abstract": [ + ("Standardization.", True), + (" Failure results are only useful when they're comparable: same " + "stress, same measurement, same pipeline on every sample.", False), + ], + }, + "cta-phase-3": { + "core_message_abstract": _CORE_MESSAGE_ABSTRACT, + "roadmap_abstract": [ + ("WaveGate.", True), + (" The technology that makes standardized, time-resolved failure " + "analysis possible.", False), + ], + }, } diff --git a/report.py b/report.py index 98e24e2..fc840cc 100644 --- a/report.py +++ b/report.py @@ -30,12 +30,30 @@ import engagement_audit CAMPAIGN_START = pd.Timestamp("2026-07-01") CAMPAIGN_END = pd.Timestamp("2026-09-30") +# Custom (non-calendar-month) reporting periods, keyed the same way the Reports +# dropdown keys them. "campaign-end" isn't listed here since it's handled directly +# above - everything after that is the CTA Campaign's 3 phases (dates from the +# proposed plan, pending approval). Each period is a plain date range with its own +# label, not tied to a calendar month, since the CTA Campaign's own phases span +# across month boundaries (Dec 1-Jan 7) on purpose, unlike July/August/September. +CUSTOM_PERIODS = { + "cta-phase-1": (pd.Timestamp("2026-11-03"), pd.Timestamp("2026-11-26"), + "Nov 3 - Nov 26 (Failure Analysis)"), + "cta-phase-2": (pd.Timestamp("2026-12-01"), pd.Timestamp("2027-01-07"), + "Dec 1 - Jan 7 (Standardization)"), + "cta-phase-3": (pd.Timestamp("2027-01-12"), pd.Timestamp("2027-02-04"), + "Jan 12 - Feb 4 (WaveGate)"), +} + def _filtered(df, date_col, value): if not value or value == "all": return df if value == "campaign-end": return df[(df[date_col] >= CAMPAIGN_START) & (df[date_col] <= CAMPAIGN_END)] + if value in CUSTOM_PERIODS: + start, end, _ = CUSTOM_PERIODS[value] + return df[(df[date_col] >= start) & (df[date_col] <= end)] if value.startswith("year-"): year = int(value.split("-", 1)[1]) return df[df[date_col].dt.year == year] @@ -51,6 +69,8 @@ def _period_label(value): return "All Time" if value == "campaign-end": return "3-Month Campaign (Jul-Sep 2026)" + if value in CUSTOM_PERIODS: + return CUSTOM_PERIODS[value][2] if value.startswith("year-"): return value.split("-", 1)[1] import calendar as calmod @@ -123,6 +143,8 @@ def compute_period_stats(ctx, timeframe_value="all"): first_day = None if timeframe_value == "campaign-end": first_day = CAMPAIGN_START + elif timeframe_value in CUSTOM_PERIODS: + first_day = CUSTOM_PERIODS[timeframe_value][0] elif timeframe_value and timeframe_value != "all" and not timeframe_value.startswith("year-"): year, month = (int(p) for p in timeframe_value.split("-")) first_day = pd.Timestamp(year=year, month=month, day=1) @@ -248,10 +270,12 @@ def build_report_docx(ctx, timeframe_value="all"): # Only July/August/September were part of the 3-month branding campaign # (CAMPAIGN_START-CAMPAIGN_END) — a later month like October gets a plain # title, not "...Campaign Report", since there's no active campaign stage - # (no goal_line/core_message/roadmap) behind it. - is_campaign_month = False + # (no goal_line/core_message/roadmap) behind it. CTA Campaign phases + # (CUSTOM_PERIODS) get the same campaign-report treatment once that + # campaign is approved and running. + is_campaign_month = timeframe_value in CUSTOM_PERIODS if timeframe_value and timeframe_value != "all" and not timeframe_value.startswith("year-") \ - and timeframe_value != "campaign-end": + and timeframe_value != "campaign-end" and timeframe_value not in CUSTOM_PERIODS: try: year, month = (int(p) for p in timeframe_value.split("-")) month_start = pd.Timestamp(year=year, month=month, day=1)