Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/Apps/W1/EssentialBusinessHeadlines/App/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Essential Business Headlines

Displays newspaper-style business insight headlines on Role Center pages.
Shows dynamic messages like "Most popular item sold," "Largest sale amount,"
and "Recently overdue invoices" with drill-down detail.

## Quick reference

- **27 objects**: 3 codeunits, 2 tables, 3 queries, 1 page, 7 page extensions, 11 permissions
- **ID range**: 1-49999 allocation
- **No dependencies** -- extends base app Role Center pages only
- **9 headline types**: MostPopularItem, BusiestResource, LargestSale, LargestOrder, SalesIncrease, TopCustomer, OpenVATReturn, OverdueVATReturn, RecentlyOverdueInvoices
- **Role-based**: Business Manager (9 headlines), Order Processor (2), Accountant (3), Relationship Mgr (1)

## How it works

**Trigger**: Computation starts when a user opens a Role Center page. The
RC Headlines Executor raises OnComputeHeadlines event, which headline
computation codeunits subscribe to.

**Time-window strategy**: Headlines attempt to find data in 7-day window
first, then 30 days, then 90 days. First window with sufficient data is
used. If no window has enough data, headline is skipped.

**Data thresholds**: Require minimum data volume to show a headline --
3+ items/resources or 5+ orders/invoices. Prevents showing meaningless
results from sparse data.

**Caching**: Computed headlines stored per-user in "Ess. Business Headline
Per Usr" table (composite PK: HeadlineName option + UserId GUID). Cache
invalidated when user changes language or work date.

**Drill-down**: Each headline stores detail records in HeadlineDetailsPerUser
table. Cleared and rebuilt on every computation. User can click headline
to see underlying data.

**SQL queries**: Three AL queries provide efficient aggregation --
BestSoldItemHeadline, TopCustomerHeadline, SalesIncreaseHeadline. Used
instead of iterating filtered records.

## Structure

```
App/BCApps/src/Apps/W1/EssentialBusinessHeadlines/App/
├── src/
│ ├── codeunits/
│ │ ├── EssBusHeadlinesCompute.Codeunit.al -- Computation engine + time window logic
│ │ ├── EssBusHeadlinesInstall.Codeunit.al -- Install subscriber (sets up defaults)
│ │ └── [headline-specific computation codeunits]
│ ├── tables/
│ │ ├── EssBusHeadlinePerUsr.Table.al -- Per-user cache (PK: HeadlineName + UserId)
│ │ └── HeadlineDetailsPerUser.Table.al -- Drill-down detail records
│ ├── queries/
│ │ ├── BestSoldItemHeadline.Query.al -- Aggregates item sales by quantity
│ │ ├── TopCustomerHeadline.Query.al -- Aggregates customer sales by amount
│ │ └── SalesIncreaseHeadline.Query.al -- Compares sales periods
│ ├── pages/
│ │ ├── [headline detail page]
│ │ └── [7 RC page extensions inject headlines via addlast(Content)]
│ └── Permissions/
│ └── [11 permission objects -- Read/Insert/Modify/Delete per object]
└── app.json
```

## Documentation

No external docs -- code is self-documenting. See inline comments in
EssBusHeadlinesCompute codeunit for time-window algorithm and threshold
logic.

## Things to know

**Role Center injection**: Page extensions use `addlast(Content)` to
insert headline parts into 7 base app Role Centers. Changes to RC layout
in base app may require extension updates.

**Event-driven computation**: Does not poll or use background tasks.
Computation is synchronous when user opens RC page -- first load may
show delay if cache empty.

**Language/date invalidation**: Changing language or work date clears
all cached headlines for that user. Next RC open recomputes from scratch.

**No telemetry**: App does not emit usage telemetry. Cannot track which
headlines are most viewed or clicked.

**Query performance**: Three AL queries hit potentially large tables
(Item Ledger, Cust. Ledger, Sales Header/Line). Time-window filter
helps but computation can be slow on large datasets.

**No configuration UI**: Thresholds (3 items, 5 orders) and time windows
(7/30/90 days) are hard-coded. Users cannot customize sensitivity or
lookback period.

**Drill-down limitation**: HeadlineDetailsPerUser stores max 20 detail
records per headline. If more exist, user sees "top 20" without indication
of total count.
142 changes: 142 additions & 0 deletions src/Apps/W1/EssentialBusinessHeadlines/App/docs/business-logic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Business Logic

This document describes the computation flow, time window strategy, and event routing for Essential Business Headlines.

## Computation Trigger

The RC Headlines Executor fires `OnComputeHeadlines` events when a role center page opens. The `EssBusHeadlineSubscribers` codeunit subscribes to this event and routes headline computation based on the `RoleCenterPageID`:

- **Business Manager** -- all 9 headline types
- **Order Processor** -- Largest Order + Largest Sale
- **Accountant** -- Largest Order + Largest Sale + Sales Increase
- **Relationship Manager** -- Top Customer

```mermaid
flowchart TD
Start([RC Page Opens]) --> Executor[RC Headlines Executor]
Executor -->|OnComputeHeadlines| Sub[EssBusHeadlineSubscribers]
Sub --> Guard{CanRunSubscribers?}
Guard -->|No| Stop([Exit])
Guard -->|Yes| Route{RoleCenterPageID}

Route -->|Business Manager| BM[Compute All 9 Headlines]
Route -->|Order Processor| OP[Largest Order<br/>Largest Sale]
Route -->|Accountant| Acct[Largest Order<br/>Largest Sale<br/>Sales Increase]
Route -->|Relationship Mgr| RM[Top Customer]

BM --> Compute
OP --> Compute
Acct --> Compute
RM --> Compute

Compute[Per-Headline Computation] --> End([Headlines Displayed])
```

## Time Window Strategy

Most headlines use a fallback strategy to find sufficient data. The system tries progressively longer time windows until it finds enough data or exhausts all options.

**Standard window sequence:**
1. Try 7-day window (WorkDate - 7 days to WorkDate)
2. If insufficient data, try 30-day window
3. If still insufficient, try 90-day window
4. First successful window is cached (WorkDate + Period stored in headline record)

**Data thresholds:**
- **Items/Resources** -- 3 or more required
- **Orders/Invoices** -- 5 or more required

```mermaid
flowchart TD
Start([GetOrCreateHeadline]) --> Init[Set Visible = false]
Init --> Check{Check Prerequisites}
Check -->|Not Met| Exit([Exit - Headline Hidden])
Check -->|Met| Try7[TryHandleHeadline<br/>7-Day Window]

Try7 --> Has7{Enough Data?}
Has7 -->|Yes| Success7[Build Headline Text<br/>Store Period=7]
Has7 -->|No| Try30[TryHandleHeadline<br/>30-Day Window]

Try30 --> Has30{Enough Data?}
Has30 -->|Yes| Success30[Build Headline Text<br/>Store Period=30]
Has30 -->|No| Try90[TryHandleHeadline<br/>90-Day Window]

Try90 --> Has90{Enough Data?}
Has90 -->|Yes| Success90[Build Headline Text<br/>Store Period=90]
Has90 -->|No| Exit

Success7 --> Finalize
Success30 --> Finalize
Success90 --> Finalize

Finalize[Delete Old Details<br/>Insert New Details<br/>Set Visible=true<br/>Modify Record] --> Complete([Headline Ready])
```

## Per-Headline Computation Flow

Each headline follows this pattern (using Most Popular Item as example):

1. **GetOrCreateHeadline()** -- Retrieve existing headline record or create new one
2. **Set visible = false** -- Assume failure until proven otherwise
3. **Check prerequisites** -- For Most Popular Item, verify item count >= 3
4. **Try time windows** -- Call `TryHandleMostPopularItem(7)`, then `TryHandleMostPopularItem(30)`, then `TryHandleMostPopularItem(90)`
5. **Execute query** -- Run `BestSoldItemHeadline` query with date filter for the current window
6. **Validate results** -- Ensure 2+ results with different quantities (proves variety)
7. **Build headline text** -- Use `Headlines` codeunit to generate display text
8. **Update details** -- Delete old `HeadlineDetails` records, insert new ones
9. **Finalize** -- Set visible = true, store period, call `Modify()`

If all time windows fail, the headline remains invisible (visible = false).

## Sales Increase Headline

The Sales Increase headline compares invoice counts between two periods:
- **Current period** -- WorkDate back N days (7, 30, or 90)
- **Same period last year** -- Same date range shifted back one year

**Display condition:** Current period invoice count > Last year invoice count

**Requirements:** Both periods must have sales data. If either period has zero invoices, the headline is not shown.

## VAT Headlines

VAT headlines use the reminder period defined in VAT Report Setup:

- **OpenVATReturn** -- VAT return is due soon (within reminder period)
- **OverdueVATReturn** -- VAT return is past due

Each VAT headline stores the `VAT Return Period` RecordId in the headline details table for drill-down navigation.

## Recently Overdue Invoices

This headline finds sales invoices that became overdue yesterday:
- **Due Date** = Yesterday (WorkDate - 1)
- **Amount <> 0** (still open, not fully paid)

## Drilldowns

Each headline has a corresponding `OnDrillDownXXX` method in `EssBusHeadlineSubscribers`. The drill-down flow:

1. Retrieve stored computation period from headline record
2. Rebuild date filter using the same period (7, 30, or 90 days)
3. Apply filter to underlying query or table
4. Open filtered list page (e.g., Item List, Sales Invoice List)

This ensures the drill-down shows exactly the data that was used to compute the headline.

## Invalidation

The `EssBusHeadlineSubscribers` codeunit subscribes to `User Settings.OnUpdateUserSettings`. When language or work date changes:

1. Call `DeleteAll` to remove all headlines for the current user
2. Force recomputation on next role center open

This prevents stale headlines from being displayed after user settings change.

## Guard Conditions

All event subscribers check `CanRunSubscribers()` before executing:
- Verify `GetExecutionContext() = Normal` (not upgrade, not install)
- Check permissions on relevant tables

If guards fail, the subscriber exits silently without computing headlines.
87 changes: 87 additions & 0 deletions src/Apps/W1/EssentialBusinessHeadlines/App/docs/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Data Model

The Essential Business Headlines app uses a per-user caching architecture with efficient SQL aggregation queries to compute and store headline metrics.

## Core Tables

### Headline Cache

The app maintains two user-specific tables that store computed headlines and their drill-down details:

```mermaid
erDiagram
ESS_BUSINESS_HEADLINE_PER_USR {
Option Headline_Name PK
GUID User_Id PK
Text250 Headline_Text
Boolean Headline_Visible
Date Headline_Computation_WorkDate
Integer Headline_Computation_Period
RecordId VAT_Return_Period_Record_Id
}

HEADLINE_DETAILS_PER_USER {
Code20 No PK
Option Type PK
GUID User_Id PK
Text Name
Decimal Quantity
Code10 UOM
Decimal Amount_LCY
}

ESS_BUSINESS_HEADLINE_PER_USR ||--o{ HEADLINE_DETAILS_PER_USER : "computed by"
```

**Ess. Business Headline Per Usr (1436)** stores the computed headline text and visibility state for each user. The table uses a composite primary key of Headline Name (Option 0-8) and User Id (GUID). Each headline tracks its computation date, period (7, 30, or 90 days), and optionally links to a VAT Return Period via RecordId. The GetOrCreateHeadline() method automatically creates records on first access.

**Headline Details Per User (1437)** provides drill-down detail rows for headlines. The primary key combines No (Code[20]), Type (Option: Item/Resource/Customer), and User Id. The Type field drives which detail fields are relevant -- Quantity and UOM display for Item and Resource types, while Amount(LCY) shows for Customer type. This table is ephemeral, deleted and rebuilt during each headline computation.

## Aggregation Queries

The app uses three queries that translate to efficient SQL GROUP BY operations against transactional tables:

```mermaid
erDiagram
SALES_INVOICE_HEADER ||--o{ SALES_INVOICE_LINE : contains
CUSTOMER ||--o{ CUST_LEDGER_ENTRY : "has entries"

SALES_INVOICE_HEADER {
Code20 No
Date Posting_Date
Boolean Cancelled
}

SALES_INVOICE_LINE {
Code20 Document_No FK
Option Type
Code20 No
Decimal Quantity_Base
Decimal Amount
}

CUSTOMER {
Code20 No
}

CUST_LEDGER_ENTRY {
Code20 Customer_No FK
Date Posting_Date
Decimal Amount_LCY
Boolean Reversed
}
```

**Best Sold Item Headline (1440)** joins SalesInvoiceHeader to SalesInvoiceLine and computes SUM(Quantity Base) grouped by ProductNo. The query filters out cancelled invoices and zero/negative amounts. It serves double duty for both Item and Resource headlines through polymorphic filtering on ProductType.

**Top Customer Headline (1441)** joins Customer to CustLedgerEntry and computes SUM(Amount LCY) grouped by Customer. It excludes reversed entries and zero/negative amounts to ensure only real revenue appears in rankings.

**Sales Increase Headline (1442)** aggregates SalesInvoiceHeader only, computing COUNT(*) of invoices. The app invokes this query twice -- once for the current period and once for the same period last year -- to calculate year-over-year growth percentages.

## User Isolation

Both cache tables key on User Id, ensuring complete per-user isolation. No headline or detail data crosses user boundaries. Each user sees only their own computed metrics, even when multiple users compute the same headline types.

## External References

The VAT Return Period Record Id field in the headline cache provides an optional link to the base app's VAT Return Period table. This field remains nullable and unused for most headline types.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Extensibility Guide

The Essential Business Headlines app provides an open framework for displaying contextual insights on Business Central role center pages. This guide describes the integration events and patterns for extending headlines with custom data.

## Integration Events Published

The app publishes events on page extensions to control headline visibility and content. These events allow subscribers to inject custom headline text and visibility logic.

### Business Manager Role Center

**HeadlinesRCBusMgrExt** publishes:

- `OnSetVisibility` -- controls 14 headline variables for sales, purchasing, activities, and cash flow insights
- `OnSetVisibilityOpenVATReturn` -- controls open VAT return headline
- `OnSetVisibilityOverdueVATReturn` -- controls overdue VAT return headline

### Accountant Role Center

**HeadlinesRCAccountantExt** publishes:

- `OnSetVisibility` -- controls 6 headline variables for accounting-specific insights

### Order Processor Role Center

**HeadlinesRCOrderProcExt** publishes:

- `OnSetVisibility` -- controls 4 headline variables for order processing insights

### Relationship Manager Role Center

**HeadlinesRCRelMgtExt** publishes:

- `OnSetVisibility` -- controls 2 headline variables for relationship management insights

### Extensibility Stubs

The following page extensions provide minimal stub implementations designed for future extension:

- **HeadlinesRCAdminExt** -- `OnSetVisibility` with 2 generic headline variables
- **HeadlinesRCProjectMgrExt** -- `OnSetVisibility` with 2 generic headline variables
- **HeadlinesRCTeamMemberExt** -- `OnSetVisibility` with 2 generic headline variables

These stubs are deliberately minimal to allow other apps to provide headline content without modifying the base app.

## Event Subscriptions Consumed

The app subscribes to the following events from other apps:

- `RC Headlines Executor.OnComputeHeadlines` -- main computation trigger, routed by RoleCenterPageID. Subscribe here to compute your custom headlines.
- `RC Headlines Page Common.OnIsAnyExtensionHeadlineVisible` -- aggregate visibility check. Only sets true, never false. Subscribe here to report whether your extension has visible headlines.
- `User Settings.OnUpdateUserSettings` -- cache invalidation on language or work date change.
- `Company-Initialize.OnCompanyInitialize` -- install-time privacy classification registration.
- Page visibility events (`OnSetVisibilityXXX`) on each role center page extension.

## Extension Pattern

To add custom headlines to a role center:

1. **Compute headlines** -- Subscribe to `OnComputeHeadlines` from the RC Headlines Executor codeunit. Check the `RoleCenterPageID` parameter to determine which role center is requesting headlines. Compute your headline data and store it in your own table.

2. **Populate visibility and text** -- Subscribe to the relevant page extension's `OnSetVisibility` event. Read your stored headline data and set the appropriate visibility and text variables passed by reference.

3. **Report visibility** -- Subscribe to `OnIsAnyExtensionHeadlineVisible` from the RC Headlines Page Common codeunit. Set the `IsVisible` parameter to true if your extension has any visible headlines for the current role center. Do not set it to false -- the event aggregates visibility across all subscribers.

## Design Philosophy

This is an open framework. The Admin, Project Manager, and Team Member page extensions are deliberately minimal stubs with generic `Headline1Visible`, `Headline1Text`, `Headline2Visible`, and `Headline2Text` variables. These are designed for other apps to extend without requiring changes to the base app.

The framework separates computation (via `OnComputeHeadlines`) from presentation (via `OnSetVisibility`). This allows expensive headline calculations to run once and be reused across multiple page refreshes, while still allowing fine-grained control over what is displayed.
Loading