- {hasPlan && (
-
-
- {didReachEnd ? (
- <>
-
}
- className={style.verdictLabel}
- >
- {verdictLabel(offsets.duringShow)}
-
-
- {formatOffset(offsets.duringShow)}
-
- >
- ) : (
- <>
-
Show incomplete
-
- The show did not reach the end of the rundown, so there is nothing to compare the finish against.
-
- >
+
+ }
+ >
+ {deviationBreakdown(offsets.startOffset, offsets.duringShow)}
+
+ ) : (
+ 'The show did not reach the end of the rundown, so there is nothing to compare the finish against.'
+ )
+ }
+ footer={
+ <>
+
+
+ {summary.eventsRun} of {summary.eventsPlanned}
+ {' '}
+ events run
+
+ {summary.worstOverrun !== null && (
+
+ longest overrun {worstOverrunTitle || 'an event'}{' '}
+ +{formatDuration(summary.worstOverrun.delta, false)}
+
)}
-
-
-
-
- {didReachEnd && (
-
- )}
-
-
- )}
-
-
-
-
- {summary.eventsRun} of {summary.eventsPlanned}
- {' '}
- events run
-
-
-
-
-
+ >
+ }
+ >
+ {hasPlan && (
+
+ )}
+ {hasPlan && didReachEnd && (
+
+ )}
+
);
}
-function verdictLabel(duringShow: MaybeNumber): string {
- if (duringShow === null || Math.abs(duringShow) < MILLIS_PER_SECOND) {
- return 'Held schedule during the show';
- }
- return duringShow > 0 ? 'Lost during the show' : 'Recovered during the show';
-}
-
-function TimeLine({
- label,
- planned,
- actual,
- offset,
-}: {
- label: string;
- planned: MaybeNumber;
- actual: MaybeNumber;
- offset: MaybeNumber;
-}) {
- return (
-
- {label}
-
- {planned === null ? enDash : formatTime(planned)}
- →
- {actual === null ? enDash : formatTime(actual)}
-
- {formatOffset(offset)}
-
- );
-}
-
-function Count({ label, value }: { label: string; value: number }) {
- return (
- <>
-
·
-
- {value} {label}
-
- >
- );
+function formatMaybeTime(value: MaybeNumber): string {
+ return value === null ? enDash : formatTime(value);
}
diff --git a/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportSummaryCard.module.scss b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportSummaryCard.module.scss
new file mode 100644
index 000000000..8e9ec4c10
--- /dev/null
+++ b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportSummaryCard.module.scss
@@ -0,0 +1,193 @@
+// an inset surface within the panel card, matching how Info nests inside one
+.card {
+ padding: 1.25rem;
+ background-color: $gray-1200;
+ border-radius: 3px;
+
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+// a subordinate tier carries the same shape at less weight, so a group heading
+// reads as belonging to the show summary rather than competing with it
+.compact {
+ padding: 0.875rem 1rem;
+ gap: 0.625rem;
+
+ // at this weight the conclusion fits beside its label rather than under it,
+ // keeping a group heading to two lines however many of them the table has
+ .headline {
+ flex-direction: row;
+ align-items: baseline;
+ gap: 0.625rem;
+ }
+
+ .headlineValue {
+ font-size: 1.25rem;
+ }
+
+ .body {
+ align-items: baseline;
+ }
+
+ .footer {
+ padding-top: 0.625rem;
+ }
+}
+
+.identity {
+ display: flex;
+ align-items: baseline;
+ flex-wrap: wrap;
+ gap: 0.25rem 0.75rem;
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+
+ color: $ui-white;
+ font-weight: 600;
+}
+
+.subtitle {
+ color: $gray-500;
+ font-size: calc(1rem - 2px);
+ font-variant-numeric: tabular-nums;
+}
+
+.body {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 1rem 2.5rem;
+}
+
+// the conclusion, given the most weight in the card
+.headline {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ min-width: 0;
+}
+
+.headlineLabel {
+ font-size: calc(1rem - 3px);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: $gray-300;
+}
+
+.headlineValue {
+ font-size: 1.75rem;
+ font-weight: 600;
+ line-height: 1.1;
+ font-variant-numeric: tabular-nums;
+
+ &.over {
+ color: $playback-over;
+ }
+
+ &.under {
+ color: $playback-under;
+ }
+
+ &.none {
+ color: $ui-white;
+ }
+}
+
+.note {
+ max-width: 32rem;
+ color: $gray-300;
+ font-size: calc(1rem - 2px);
+ line-height: 1.4;
+
+ // nothing to conclude yet is a caveat on the report, not a detail
+ &.unavailable {
+ color: $warning-orange;
+ }
+}
+
+// label | planned -> actual | offset, so every offset lands in one column
+.metrics {
+ display: grid;
+ grid-template-columns: auto auto auto;
+ align-items: baseline;
+ gap: 0.375rem 1.5rem;
+}
+
+.metricLabel {
+ color: $gray-300;
+ font-size: calc(1rem - 2px);
+}
+
+.metricValue {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ font-variant-numeric: tabular-nums;
+}
+
+.planned {
+ color: $gray-300;
+}
+
+.arrow {
+ color: $gray-500;
+}
+
+.actual {
+ color: $ui-white;
+ font-weight: 600;
+}
+
+.metricOffset {
+ justify-self: end;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+
+ &.over {
+ color: $playback-over;
+ }
+
+ &.under {
+ color: $playback-under;
+ }
+
+ &.none {
+ color: $gray-300;
+ }
+}
+
+.footer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.75rem;
+
+ padding-top: 0.875rem;
+ border-top: 1px solid $white-10;
+
+ color: $gray-300;
+ font-size: calc(1rem - 2px);
+}
+
+.footerItem {
+ white-space: nowrap;
+
+ // a dot between neighbours, rather than a separator element per gap
+ & + &::before {
+ content: "·";
+ margin-right: 0.75rem;
+ color: $gray-500;
+ }
+
+ b {
+ color: $ui-white;
+ font-weight: 600;
+ }
+}
diff --git a/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportSummaryCard.tsx b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportSummaryCard.tsx
new file mode 100644
index 000000000..d81d4a6ab
--- /dev/null
+++ b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportSummaryCard.tsx
@@ -0,0 +1,103 @@
+import { MaybeNumber } from 'ontime-types';
+import { PropsWithChildren, ReactNode } from 'react';
+
+import { cx } from '../../../../../common/utils/styleUtils';
+import { formatOffset, offsetTone } from '../reportSettings.utils';
+
+import style from './ReportSummaryCard.module.scss';
+
+interface ReportSummaryCardProps {
+ /** what is being summarised: a rundown name, a group title */
+ title: ReactNode;
+ /** optional context for the title, eg the window the group ran in */
+ subtitle?: ReactNode;
+ /** what the headline figure measures, eg "Total plan deviation" */
+ headlineLabel: string;
+ /** the conclusion. Null renders the note instead, for a run with nothing to conclude */
+ headline: MaybeNumber;
+ /** shown under the headline: the workings when there is a figure, the reason when there is not */
+ note?: ReactNode;
+ footer?: ReactNode;
+ /** a subordinate tier, eg a group inside the show it belongs to */
+ compact?: boolean;
+}
+
+/**
+ * One shape for every tier of the report.
+ *
+ * A group header is a summary of the events inside it exactly as the show
+ * summary is a summary of the groups, so both are read the same way: the
+ * conclusion on the left at full weight, the workings behind it on the right,
+ * and how much of it ran along the bottom.
+ */
+export default function ReportSummaryCard({
+ title,
+ subtitle,
+ headlineLabel,
+ headline,
+ note,
+ footer,
+ compact,
+ children,
+}: PropsWithChildren
) {
+ return (
+
+
+ {title}
+ {subtitle && {subtitle}}
+
+
+
+
+ {headlineLabel}
+ {headline !== null && (
+ {formatOffset(headline)}
+ )}
+ {note && {note}}
+
+
+
{children}
+
+
+ {footer &&
{footer}
}
+
+ );
+}
+
+interface MetricProps {
+ label: string;
+ /** the value it was measured against, omitted for a figure which stands alone */
+ planned?: string;
+ /** what actually happened */
+ actual: string;
+ offset?: MaybeNumber;
+}
+
+/**
+ * A row of the workings: what was planned, what happened, and the distance
+ * between them. Every offset lands in the same column so they read as a stack.
+ */
+export function Metric({ label, planned, actual, offset }: MetricProps) {
+ return (
+ <>
+ {label}
+
+ {planned !== undefined && (
+ <>
+ {planned}
+ →
+ >
+ )}
+ {actual}
+
+
+ {offset === undefined ? '' : formatOffset(offset)}
+
+ >
+ );
+}
+
+/** A fact in the card footer, separated from its neighbours by a dot */
+export function FooterItem({ children }: PropsWithChildren) {
+ return {children};
+}
diff --git a/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.module.scss b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.module.scss
index 9ee8c08a1..3abcfb00b 100644
--- a/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.module.scss
+++ b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.module.scss
@@ -6,30 +6,16 @@ th.under {
color: $playback-under;
}
+// the group heading is a card rather than a table row treatment, so the cell
+// only has to give it room and stay out of the table's striping
.groupRow {
- background-color: $white-3;
+ background-color: transparent;
td {
- padding-top: 0.75rem;
- padding-bottom: 0.75rem;
+ padding: 1.25rem 0 0.5rem;
}
}
-.groupHeading {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 0.5rem 0.75rem;
-}
-
-.groupTitle {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- font-weight: 600;
- color: $ui-white;
-}
-
.groupColour {
width: 0.25rem;
height: 1rem;
@@ -38,44 +24,6 @@ th.under {
flex-shrink: 0;
}
-.groupStats {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 0.375rem 0.625rem;
- color: $gray-300;
- font-variant-numeric: tabular-nums;
-}
-
-.stat b {
- color: $ui-white;
- font-weight: 600;
-}
-
-.variance {
- font-weight: 600;
-
- &.over {
- color: $playback-over;
- }
-
- &.under {
- color: $playback-under;
- }
-
- &.none {
- color: $gray-300;
- }
-}
-
-.separator {
- color: $gray-500;
-}
-
-.incomplete {
- color: $warning-orange;
-}
-
.eventCue,
.eventIndex {
color: $gray-300;
diff --git a/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.tsx b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.tsx
index 99d6aa4f6..a93fadce7 100644
--- a/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.tsx
+++ b/apps/client/src/features/app-settings/panel/feature-panel/composite/ReportTable.tsx
@@ -5,7 +5,8 @@ import Tooltip from '../../../../../common/components/tooltip/Tooltip';
import { cx, enDash } from '../../../../../common/utils/styleUtils';
import { formatDuration, formatTime } from '../../../../../common/utils/time';
import * as Panel from '../../../panel-utils/PanelUtils';
-import { CombinedReport, formatOffset, offsetTone } from '../reportSettings.utils';
+import { CombinedReport } from '../reportSettings.utils';
+import ReportSummaryCard, { FooterItem, Metric } from './ReportSummaryCard';
import style from './ReportTable.module.scss';
@@ -50,69 +51,68 @@ export default function ReportTable({ rows, groups }: ReportTableProps) {
);
}
+/**
+ * A group read the same way as the show above it: what it was measured
+ * against, what it actually did, and how much of it ran.
+ */
function GroupRow({ group }: { group: GroupReport }) {
const isComplete = group.eventsRun === group.eventsPlanned;
- const measure = group.targetDuration === null ? 'scheduled' : 'target';
- const measureValue = group.targetDuration ?? group.scheduledDuration;
+ const hasTarget = group.targetDuration !== null;
+ const measuredAgainst = group.targetDuration ?? group.scheduledDuration;
return (
|
-
-
-
- {group.title || 'Untitled group'}
-
-
-
- }
- className={style.stat}
- >
- {formatDuration(measureValue, false)} {measure}
-
-
- ·
-
- {group.elapsed === null ? enDash : formatDuration(group.elapsed, false)} actual
-
-
- {group.variance !== null && (
- <>
- ·
-
- {formatOffset(group.variance)}
-
- >
- )}
-
- {group.changeover !== null && group.changeover > 0 && (
- <>
- ·
- }
- className={style.stat}
- >
- {formatDuration(group.changeover, false)} changeover
-
- >
- )}
-
- ·
-
-
- {group.eventsRun}/{group.eventsPlanned}
- {' '}
- {isComplete ? 'events' : 'events run'}
-
-
-
+
+
+ {group.title || 'Untitled group'}
+ >
+ }
+ subtitle={
+ group.actualStart !== null && group.actualEnd !== null
+ ? `ran ${formatTime(group.actualStart)} to ${formatTime(group.actualEnd)}`
+ : undefined
+ }
+ compact
+ headlineLabel={hasTarget ? 'Against target' : 'Against schedule'}
+ headline={group.variance}
+ note={
+ group.variance !== null
+ ? undefined
+ : isComplete
+ ? 'This group has not run.'
+ : 'Still to finish, so there is nothing to measure the group against yet.'
+ }
+ footer={
+ <>
+
+
+ {group.eventsRun} of {group.eventsPlanned}
+ {' '}
+ events run
+
+ {group.untimed !== null && group.untimed > 0 && (
+
+ }
+ >
+ {formatDuration(group.untimed, false)} untimed
+
+
+ )}
+ >
+ }
+ >
+ {/* the headline already carries the variance, so this is the workings behind it */}
+
+
|
);
@@ -141,7 +141,11 @@ function punctuality(actual: number | null, scheduled: number): 'under' | 'over'
return actual <= scheduled ? 'under' : 'over';
}
-type Section = { key: string; group: GroupReport | null; rows: CombinedReport[] };
+type Section = {
+ key: string;
+ group: GroupReport | null;
+ rows: CombinedReport[];
+};
/**
* Splits the rows into the blocks they belong to, keeping rundown order and
diff --git a/apps/client/src/features/app-settings/panel/feature-panel/reportSettings.utils.ts b/apps/client/src/features/app-settings/panel/feature-panel/reportSettings.utils.ts
index 1ede84fa1..e23d8aa84 100644
--- a/apps/client/src/features/app-settings/panel/feature-panel/reportSettings.utils.ts
+++ b/apps/client/src/features/app-settings/panel/feature-panel/reportSettings.utils.ts
@@ -79,6 +79,26 @@ export function formatOffset(value: MaybeNumber): string {
return `${value > 0 ? '+' : '-'}${formatDuration(Math.abs(value), false)}`;
}
+/**
+ * Reads a show's total plan deviation back as the two parts it is made of, so
+ * a show which finished late because it began late is not mistaken for one
+ * which overran.
+ */
+export function deviationBreakdown(startOffset: MaybeNumber, duringShow: MaybeNumber): string {
+ if (startOffset === null || duringShow === null) {
+ return '';
+ }
+
+ const fromStart =
+ Math.abs(startOffset) < MILLIS_PER_SECOND ? 'started on time' : `started ${formatOffset(startOffset)}`;
+ const inShow =
+ Math.abs(duringShow) < MILLIS_PER_SECOND
+ ? 'held schedule from there'
+ : `${formatOffset(duringShow)} during the show`;
+
+ return `${fromStart}, ${inShow}`;
+}
+
/** Whether an offset is behind, ahead, or neither, for colouring */
export function offsetTone(value: MaybeNumber): 'over' | 'under' | 'none' {
if (value === null || Math.abs(value) < MILLIS_PER_SECOND) return 'none';
@@ -90,16 +110,7 @@ function csvTime(value: MaybeNumber): string {
return value === null ? '' : formatTime(value);
}
-const csvHeader = [
- 'Index',
- 'Group',
- 'Cue',
- 'Title',
- 'Scheduled Start',
- 'Actual Start',
- 'Scheduled End',
- 'Actual End',
-];
+const csvHeader = ['Index', 'Group', 'Cue', 'Title', 'Scheduled Start', 'Actual Start', 'Scheduled End', 'Actual End'];
/**
* Transforms a CombinedReport into a CSV string.
diff --git a/packages/types/src/definitions/core/Report.type.ts b/packages/types/src/definitions/core/Report.type.ts
index 862068f26..40b954242 100644
--- a/packages/types/src/definitions/core/Report.type.ts
+++ b/packages/types/src/definitions/core/Report.type.ts
@@ -63,23 +63,32 @@ export type GroupReport = {
/** wall time from the first event starting to the last one ending */
elapsed: MaybeNumber;
/**
- * Time inside the block not covered by a running event, ie changeovers.
- * elapsed minus the sum of the events' own durations.
+ * Time inside the block with no event running: elapsed minus the sum of the
+ * events' own durations. Deliberately named for what was measured rather
+ * than for a cause, since the same gap can be a changeover, a timer started
+ * late, or an event run without the timer at all.
+ */
+ untimed: MaybeNumber;
+ /**
+ * Measured against targetDuration where set, otherwise scheduledDuration.
+ * Null while the group is incomplete: the events still to run would make a
+ * partial group read as a large underrun.
*/
- changeover: MaybeNumber;
- /** measured against targetDuration where set, otherwise scheduledDuration */
variance: MaybeNumber;
eventsRun: number;
eventsPlanned: number;
};
-/** Counts across the events in the report */
+/**
+ * Counts across the events in the report.
+ *
+ * Deliberately not a tally of events over, under and on time: events rarely
+ * land on the exact second, so those buckets describe rounding more than they
+ * describe the show. The single worst overrun is the fact that acts on.
+ */
export type RunSummary = {
eventsRun: number;
eventsPlanned: number;
- eventsOver: number;
- eventsUnder: number;
- eventsOnTime: number;
/** largest single overrun, answers "what blew the schedule" */
worstOverrun: { id: EntryId; delta: number } | null;
};
diff --git a/packages/utils/src/report-utils/reportUtils.test.ts b/packages/utils/src/report-utils/reportUtils.test.ts
index f2f3bef12..6358624d8 100644
--- a/packages/utils/src/report-utils/reportUtils.test.ts
+++ b/packages/utils/src/report-utils/reportUtils.test.ts
@@ -172,8 +172,8 @@ describe('getGroupReports()', () => {
});
});
- it('reports the time a block spent not running an event', () => {
- // 5 minute changeover between the two events
+ it('reports the time a block spent with no event running', () => {
+ // 5 minutes between one event ending and the next starting
const report: OntimeReport = {
a: makeEntry({ startedAt: 0, endedAt: 10 * MIN, scheduledDuration: 10 * MIN }),
b: makeEntry({ startedAt: 15 * MIN, endedAt: 25 * MIN, scheduledDuration: 10 * MIN }),
@@ -182,7 +182,7 @@ describe('getGroupReports()', () => {
const [group] = getGroupReports(report, entries, ['act1']);
expect(group.elapsed).toBe(25 * MIN);
- expect(group.changeover).toBe(5 * MIN);
+ expect(group.untimed).toBe(5 * MIN);
});
it('falls back to what was scheduled when no target was set', () => {
@@ -200,7 +200,9 @@ describe('getGroupReports()', () => {
expect(group.variance).toBe(4 * MIN);
});
- it('reads a partly run block as incomplete rather than as an overrun', () => {
+ it('withholds variance while a block still has events to run', () => {
+ // one of two events run: the block has not spent its 30 minute budget yet,
+ // reporting it now would read as a 20 minute underrun
const report: OntimeReport = {
a: makeEntry({ startedAt: 0, endedAt: 10 * MIN, scheduledDuration: 10 * MIN }),
};
@@ -209,12 +211,15 @@ describe('getGroupReports()', () => {
expect(group.eventsRun).toBe(1);
expect(group.eventsPlanned).toBe(2);
+ expect(group.variance).toBeNull();
+ // what did happen is still reported
+ expect(group.elapsed).toBe(10 * MIN);
});
it('has no actuals for a block which never ran', () => {
const [group] = getGroupReports({}, entries, ['act1']);
- expect(group).toMatchObject({ elapsed: null, changeover: null, variance: null, eventsRun: 0 });
+ expect(group).toMatchObject({ elapsed: null, untimed: null, variance: null, eventsRun: 0 });
});
it('leaves skipped events out of the block', () => {
@@ -232,20 +237,14 @@ describe('getGroupReports()', () => {
});
describe('getRunSummary()', () => {
- it('counts how events landed', () => {
+ it('counts how much of the show ran', () => {
const report: OntimeReport = {
- a: makeEntry({ endedAt: 15000 }), // over
- b: makeEntry({ endedAt: 6000 }), // under
- c: makeEntry({ endedAt: 10000 }), // on time
+ a: makeEntry({ endedAt: 15000 }),
+ b: makeEntry({ endedAt: 6000 }),
+ c: makeEntry({ endedAt: 10000 }),
};
- expect(getRunSummary(report, 4)).toMatchObject({
- eventsRun: 3,
- eventsPlanned: 4,
- eventsOver: 1,
- eventsUnder: 1,
- eventsOnTime: 1,
- });
+ expect(getRunSummary(report, 4)).toMatchObject({ eventsRun: 3, eventsPlanned: 4 });
});
it('identifies the worst overrun', () => {
@@ -258,6 +257,11 @@ describe('getRunSummary()', () => {
expect(getRunSummary(report, 3).worstOverrun).toEqual({ id: 'b', delta: 20000 });
});
+ it('has no worst overrun when nothing ran long', () => {
+ const report: OntimeReport = { a: makeEntry({ endedAt: 6000 }), b: makeEntry({ endedAt: 10000 }) };
+ expect(getRunSummary(report, 2).worstOverrun).toBeNull();
+ });
+
it('ignores events which did not complete', () => {
const report: OntimeReport = { a: makeEntry({ endedAt: 15000 }), b: makeEntry({ endedAt: null }) };
expect(getRunSummary(report, 2).eventsRun).toBe(1);
diff --git a/packages/utils/src/report-utils/reportUtils.ts b/packages/utils/src/report-utils/reportUtils.ts
index e33fd4299..6fadc666d 100644
--- a/packages/utils/src/report-utils/reportUtils.ts
+++ b/packages/utils/src/report-utils/reportUtils.ts
@@ -164,6 +164,9 @@ export function getGroupReports(report: OntimeReport, entries: RundownEntries, o
const elapsed = actualStart === null || actualEnd === null ? null : elapsedBetween(actualStart, actualEnd);
// the budget if the user set one, otherwise what they scheduled into it
const measuredAgainst = group.targetDuration ?? scheduledDuration;
+ // a group with events still to run has not spent its budget yet: comparing
+ // what it has used so far against the whole would report a large underrun
+ const isComplete = eventsRun > 0 && eventsRun === eventsPlanned;
groups.push({
id: group.id,
@@ -174,9 +177,9 @@ export function getGroupReports(report: OntimeReport, entries: RundownEntries, o
actualStart,
actualEnd,
elapsed,
- // whatever the block spent not running an event
- changeover: elapsed === null ? null : Math.max(0, elapsed - ranDuration),
- variance: elapsed === null ? null : elapsed - measuredAgainst,
+ // whatever the block spent with no event running
+ untimed: elapsed === null ? null : Math.max(0, elapsed - ranDuration),
+ variance: elapsed === null || !isComplete ? null : elapsed - measuredAgainst,
eventsRun,
eventsPlanned,
});
@@ -185,14 +188,17 @@ export function getGroupReports(report: OntimeReport, entries: RundownEntries, o
return groups;
}
-/** Counts across the events in the report */
+/**
+ * Counts across the events in the report.
+ *
+ * Only how much of the show ran, and which single event ran longest over its
+ * schedule. Bucketing every event as over, under or on time reads as precision
+ * the data does not have: almost nothing lands on the exact second.
+ */
export function getRunSummary(report: OntimeReport, eventsPlanned: number): RunSummary {
const summary: RunSummary = {
eventsRun: 0,
eventsPlanned,
- eventsOver: 0,
- eventsUnder: 0,
- eventsOnTime: 0,
worstOverrun: null,
};
@@ -204,15 +210,8 @@ export function getRunSummary(report: OntimeReport, eventsPlanned: number): RunS
summary.eventsRun += 1;
- if (variance.status === 'over') {
- summary.eventsOver += 1;
- if (summary.worstOverrun === null || variance.delta > summary.worstOverrun.delta) {
- summary.worstOverrun = { id, delta: variance.delta };
- }
- } else if (variance.status === 'under') {
- summary.eventsUnder += 1;
- } else {
- summary.eventsOnTime += 1;
+ if (variance.status === 'over' && (summary.worstOverrun === null || variance.delta > summary.worstOverrun.delta)) {
+ summary.worstOverrun = { id, delta: variance.delta };
}
}