Osy#betaRun it on Osyrin: osyrin.com

Kits · Data

Reports — build them, share them

Reports a team builds for itself — start from one thing the app keeps, pick fields by following its relations, group, measure (counts, totals, averages, medians and percentiles) and filter, all computed in the database as the reader may read it — read as figures, a table and a chart, exported as CSV, saved and shared by name.

Package
Osysharp.Reports
Install
use Osysharp.Reports@0;
Group
Data
Tests
57 in the kit’s own suite
Builds on
Identity, Clock — the team's time zone and week, Charts, CSV
Used by
Inbox reports — what the desk did

Reports your team builds for itself, and the overview it opens every morning. Start from one thing your app keeps (an order, a ticket, a candidate), pick fields by following its relations (Order › Café › Region), group, measure and filter, and read the answer as figures, a table and a chart. It updates as you pick. Export it as a CSV that says what the screen says, and save it by name for yourself or for the whole team.

Every report is computed in the database, as the person reading it may read it. However many orders it covers, a report reads only the aggregate rows it shows, never the orders themselves.

osy

use Osysharp.Reports@0;
use Osysharp.Explore;
use Osysharp.Clock@0;               // the time zone a day and a week are counted in

policy ReadsReports => IsStaff;     // who opens reports
policy SharesReports => IsLead;     // who shares a saved report with the team
policy ManagesClock => IsAdmin;     // who sets the app's time zone (Settings › Time and date)

public class OrderReports : IReportSource {
  public ReportSource Describe() {
    var placed = Explore.Path<Order>(o => o.PlacedAt);
    var shipped = Explore.Path<Order>(o => o.ShippedAt);
    return new ReportSource {
      Key = "orders", Title = "Orders", Root = Explore.Root<Order>(), Noun = "order", Nouns = "orders", Date = placed,
      Measures = [
        new ReportMeasure { Label = "Orders", Measure = new ExploreMeasure { Name = "orders" } },
        new ReportMeasure { Label = "Time to ship", Format = FigureFormat.Duration, HigherIsBetter = false,
                            Measure = new ExploreMeasure { Name = "toShip", Kind = MeasureKind.Median, Field = shipped, Since = placed } },
      ],
      Dimensions = [new ReportDimension { Field = Explore.Path<Order>(o => o.Cafe.Region), Label = "Region" }],
      // What "Show the rows" lists of each order a figure counted.
      RowFields = [Explore.Path<Order>(o => o.Cafe), placed],
    };
  }
}

[Route("/reports")] [Authorize(ReadsReports)]
component OrdersOverview() {
  render { ReportOverview("orders", ["orders", "toShip"], build: "/reports/build"); }
}

[Route("/reports/build")] [Authorize(ReadsReports)]
component OrdersBuilder() {
  render { ReportBuilder("orders"); }
}

Your app says what it reports on once, in its own words: a class per root, found without being listed. Every other field a reader may reach from that root is offered too, from your model.

What it does

Fields by relationone root, and every field reachable from it within three steps, with its trail ("Order › Café › Region"). Search across all of them. A path that crosses a collection says so ("one per line") and never counts a row twice
Measurescount, total, average, median, any percentile (the 90th is one click), count of different values, earliest and latest; a time between two dates (shipped since placed); a share of one count in another. Your app's own measures come first, in its words
Filters you build by pointingpick a field and the filter fits its kind: tick choices, pick records with a search, a number or a duration more than, less than or between, a date range in your app's time zone, words that contain or start with, yes or no, empty or not. Conditions read back as sentences ("Courier is Bike courier or Freight"), and nest in "all of" and "any of" groups
Show groups whereconditions on the groups themselves, by their measures ("cafés with more than ten orders"), kept apart from the conditions on rows
Periods in your time zonetoday, 7, 30 and 90 days, this week, month, quarter and year, last month, or two days you choose. A day, a week and a month start at midnight in the app's time zone, picked from a list in Settings › Time and date (Osysharp.Clock), and follow summer time. A trend has a bar for every day, even a quiet one
The overviewheadline figures with their change against the period before (in the success colour when it is good news, whichever way that is), what stands open now, the trend, and a breakdown by any of your groupings. A figure from a second source joins the breakdown on the grouping of the same name
The rows behind every figurepress a figure, or a row of a grouped table, and see the rows it counted: "Shipped" for one region lists that region's shipped orders, a day of a trend that day's. Give the source the fields to list (RowFields)
CSVthe same period, the same rows and the same words as the screen, written by one function, so the file and the page cannot disagree
Saved reportsa report keeps the question, never the numbers. Whoever opens it sees the rows they may read, as they are then. A private one is its owner's; a shared one is everybody's who reads reports
Custom fields as columnsrows a kit keeps one per record and key, a custom field's answers, are columns of the record: "Ticket › Customer › Their plan" groups, filters and sums as its kind reads. With Osysharp.Fields it takes one relation in your model
Only what the reader may reada field masked for the reader, or a path into something they may read none of, is never offered, and a saved report naming one says so rather than answering a smaller question. A row they may not read counts for nothing
The overviewFilters in nested groups
an overview of orders: figures, a daily trend and a breakdown by regiona report with an "all of" group holding an "any of" group
Grouped by a relation, in the darkOn a phone
orders grouped by the café's region, dark themethe overview on a phone, figures two to a line

Its tests

The kit's test app is a coffee roaster's wholesale orders: cafés in three regions, a courier, a price list only the owner may read, and a margin masked from the sales reps. Every figure is checked against an answer worked out by hand: medians and percentiles, a month in Stockholm across the change to winter time, a week that starts on Sunday, filters of every kind, nested groups, conditions on groups, the CSV against the screen, saved reports shared and private, and each reader seeing only their own orders. The builder and the overview are driven as a person would use them, wide and on a phone, light and dark, with osy test, and photographed in a real browser with osy test --pixels.

Get it

Add the kit to your app’s manifest:

osy

app MyApp {
  use Osysharp.Reports@0;
}

Then run osy lock: it downloads the release, verifies it by its content address and pins that version in osyrin.lock. osy kits Osysharp.Reports shows what the kit is for, from inside your app.

Reference: osy docs ui-reports-kit.