Reports
One screen lists every report a user may open, builds its filter form and opens it. Adding a report never adds a screen — it is two declarations and one class.
The three pieces
| Piece | Where | What it says |
|---|---|---|
| the catalogue entry | config/apps/<app>.php → 'reports' | its name, the module that gates it, its category, which filters it takes |
| the filter dictionary | config/apps/<app>.php → 'report_filters' | what each filter code is: control, label, option list |
| the handler | App\Reports\<StudlyCode> | where the rows come from |
The code is the link, and the only one: report item_list is handled by
App\Reports\ItemList. There is no register to add it to — deploy the
class and the code works. The code is also permanent: the console matches on it, so
a new code is a new report.
'reports' => [
'item_list' => [
'name' => 'Item List',
'module' => 'RPC', // the XAuth module that gates it
'category' => 'Examples', // must be in 'report_categories'
'params' => 'item_id|start_date!|end_date', // ! required, =value default
'actions' => ['print', 'export_excel', 'export_csv'],
'display_order' => 1,
'live_filters' => true,
],
],
'report_filters' => [
'item_id' => ['type' => 'select', 'label' => 'Item', 'width' => 'full',
'options' => 'items', 'value' => 'id', 'text' => 'name'],
'start_date' => ['type' => 'date', 'label' => 'From'],
'end_date' => ['type' => 'date', 'label' => 'To'],
],
'report_categories' => ['Examples'], // in display order
A dictionary entry may also name depends — the parent filters whose
values scope its option list. A dependent select is filled only once every parent
has a value, refilled when one changes, and emptied with its own children when one
is cleared, so a stale child can never be submitted. lazy keeps a long
list out of the first form payload and fetches it when the control is opened.
vsa check holds all three together: a category that is not declared,
a filter the dictionary does not define, or a report without a module is an error —
and the console refuses to save a report naming a filter it has never been told
about, so the check catches it before the push does.
The commands
| Command | What it does |
|---|---|
make:report <app> <code> | The catalogue entry and a handler stub. Takes --name=, --module=, --category=, --params=a|b, --permission=MOD.NAME or --open-access, --actions=print,export, --hidden, --no-handler |
reports [app] | The reports the code declares |
rename:report <app> <code> "<New name>" | The display name. The code never changes |
report:permission <app> <code> <MOD.NAME | --own | --open-access> | Gate it by an existing permission, by its own report.<code>, or not at all |
xauth:sync | Pushes the catalogue and the dictionary to the console. Nothing is live until this runs |
xauth:pull | Takes console-side presentation edits back into config/apps |
The handler writes a query, not a screen
View, Print, Excel and CSV are the base class's and all four render the same rows, so an export can never disagree with what was on screen. A filter map applies only the filters the user actually gave, so one query serves every combination without an if-chain; casts format cells and make money and integer columns summable; the layout declares grouping, subtotals, totals, signatures and orientation. Only a report none of that can express gets its own blade.
The ticket
A report is not opened by a link carrying its filters. The browser POSTs to
/api/report-center/open; the server checks access now
— this user, this report, these values — and answers with a signed, short-lived
URL. GET /report/{ticket}, optionally
/print, /xlsx, /csv or /pdf,
then renders with no cookie, so a mobile app or a PDF service
opens exactly the URL it was handed.
The ticket carries the decision, not a credential: an access token embedded in one went stale inside the ticket's own lifetime, so a link a minute old answered 401. Because access was decided when the ticket was issued, its TTL is the revocation window — keep it to minutes.
Who owns what, once it is live
vsa xauth:sync pushes the catalogue and the dictionary to the XAuth
console, and from then on there are two owners:
- the console owns presentation — name, description, icon, category, order, hidden, which filters and which are required, which actions a role may be granted. An engineer changes a live report there without a deploy.
- the code owns data — the report code, the handler class, the filter dictionary. None of it can be typed into a console.
The sync is a three-way merge, not an overwrite: it records what the code last
pushed, so a field only the console changed is kept, a field only the code changed
is updated, and a field both changed is reported with the console's value kept.
vsa xauth:pull writes those console edits back into
config/apps so the two agree again. A catalogue row may exist before
its class does — the report shows as "no handler" to engineers only, and lights up
when the class deploys.
The actions a report offers beyond View are inherited from the application's Report Actions unless the entry names its own; both are set in the console under Catalog → Reports → Report actions.
The screen
ReportCenterComponent comes from vsd/report-center, lives
in public/js/pages/common/ and is never edited. It is an ordinary page
in the app file, gated by its own module, so it appears in the menu like any other.