mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-24 16:39:13 +00:00
Report (#1469)
* create report service * write report from runtimeState * use in UI * clear report * update types * clear all from settings menu * rearence rightclik menu * refactor styling * also report roll events * ontime/under time is same colour * refactor reporter * add target to ontime-refetch * remove menu * fectch only on message from server * memo useGetEventReport * refactor * use staleTime * dont add to menu yet * implement review * clear all from menu * fix merge * start end show * combine test for the go button text and action * add report to menu * extract csv utility * report management * unneeded async * small refacort of triggerReportEntry --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
21877e4bff
commit
b6507c27a6
@@ -14,6 +14,7 @@ export const SHEET_STATE = ['sheetState'];
|
|||||||
export const URL_PRESETS = ['urlpresets'];
|
export const URL_PRESETS = ['urlpresets'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const CLIENT_LIST = ['clientList'];
|
export const CLIENT_LIST = ['clientList'];
|
||||||
|
export const REPORT = ['report'];
|
||||||
|
|
||||||
// API URLs
|
// API URLs
|
||||||
export const apiEntryUrl = `${serverURL}/data`;
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
import { DatabaseModel, MessageResponse, ProjectData, ProjectFileListResponse, QuickStartData } from 'ontime-types';
|
||||||
|
|
||||||
import { makeCSV, makeTable } from '../../views/cuesheet/cuesheet.utils';
|
import { makeTable } from '../../views/cuesheet/cuesheet.utils';
|
||||||
|
import { makeCSVFromArrayOfArrays } from '../utils/csv';
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
import { createBlob, downloadBlob } from './utils';
|
import { createBlob, downloadBlob } from './utils';
|
||||||
@@ -42,7 +43,7 @@ export async function downloadCSV(fileName: string = 'rundown') {
|
|||||||
const { project, rundown, customFields } = data;
|
const { project, rundown, customFields } = data;
|
||||||
|
|
||||||
const sheetData = makeTable(project, rundown, customFields);
|
const sheetData = makeTable(project, rundown, customFields);
|
||||||
const fileContent = makeCSV(sheetData);
|
const fileContent = makeCSVFromArrayOfArrays(sheetData);
|
||||||
|
|
||||||
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
const blob = createBlob(fileContent, 'text/csv;charset=utf-8;');
|
||||||
downloadBlob(blob, `${name}.csv`);
|
downloadBlob(blob, `${name}.csv`);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { OntimeReport } from 'ontime-types';
|
||||||
|
|
||||||
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
|
|
||||||
|
import { apiEntryUrl, REPORT } from './constants';
|
||||||
|
|
||||||
|
export const reportUrl = `${apiEntryUrl}/report`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to fetch all reports
|
||||||
|
*/
|
||||||
|
export async function fetchReport(): Promise<OntimeReport> {
|
||||||
|
const res = await axios.get(`${reportUrl}/`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteReport(id: string) {
|
||||||
|
await axios.delete(`${reportUrl}/${id}`);
|
||||||
|
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAllReport() {
|
||||||
|
await axios.delete(`${reportUrl}/all`);
|
||||||
|
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { OntimeReport } from 'ontime-types';
|
||||||
|
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { REPORT } from '../api/constants';
|
||||||
|
import { fetchReport } from '../api/report';
|
||||||
|
|
||||||
|
export default function useReport() {
|
||||||
|
const { data, refetch } = useQuery<OntimeReport>({
|
||||||
|
queryKey: REPORT,
|
||||||
|
queryFn: fetchReport,
|
||||||
|
placeholderData: (previousData, _previousQuery) => previousData,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
|
networkMode: 'always',
|
||||||
|
staleTime: MILLIS_PER_HOUR,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { data: data ?? {}, refetch };
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { makeCSVFromArrayOfArrays } from '../csv';
|
||||||
|
|
||||||
|
describe('makeCSVFromArrayOfArrays()', () => {
|
||||||
|
it('joins an array of arrays with commas and newlines', () => {
|
||||||
|
const testdata = [['field'], ['after newline', 'after comma'], ['', 'after empty']];
|
||||||
|
expect(makeCSVFromArrayOfArrays(testdata)).toMatchInlineSnapshot(`
|
||||||
|
"field
|
||||||
|
after newline,after comma
|
||||||
|
,after empty
|
||||||
|
"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { stringify } from 'csv-stringify/browser/esm/sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Converts an array of arrays to a CSV file
|
||||||
|
* @param {string[][]} arrayOfArrays
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function makeCSVFromArrayOfArrays(arrayOfArrays: string[][]): string {
|
||||||
|
return stringify(arrayOfArrays);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Log, RundownCached, RuntimeStore } from 'ontime-types';
|
import { Log, RundownCached, RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
import { isProduction, websocketUrl } from '../../externals';
|
import { isProduction, websocketUrl } from '../../externals';
|
||||||
import { CLIENT_LIST, CUSTOM_FIELDS, RUNDOWN, RUNTIME } from '../api/constants';
|
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
|
||||||
import { invalidateAllCaches } from '../api/utils';
|
import { invalidateAllCaches } from '../api/utils';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
import {
|
import {
|
||||||
@@ -198,19 +198,23 @@ export const connectSocket = () => {
|
|||||||
}
|
}
|
||||||
case 'ontime-refetch': {
|
case 'ontime-refetch': {
|
||||||
// the refetch message signals that the rundown has changed in the server side
|
// the refetch message signals that the rundown has changed in the server side
|
||||||
const { revision, reload } = payload;
|
const { reload, target } = payload;
|
||||||
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;
|
|
||||||
|
|
||||||
if (reload) {
|
if (reload) {
|
||||||
invalidateAllCaches();
|
invalidateAllCaches();
|
||||||
} else if (revision > currentRevision) {
|
} else if (target === 'RUNDOWN') {
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
const { revision } = payload;
|
||||||
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;
|
||||||
|
if (revision > currentRevision) {
|
||||||
|
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
|
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||||
|
}
|
||||||
|
} else if (target === 'REPORT') {
|
||||||
|
ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ontime-flush': {
|
case 'ontime-flush': {
|
||||||
flushBatchUpdates()
|
flushBatchUpdates();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ $inner-padding: 1rem;
|
|||||||
color: $muted-gray;
|
color: $muted-gray;
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding-block: 1rem;
|
padding-block: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|||||||
@@ -67,9 +67,17 @@ export function TableEmpty({ label, handleClick }: { label?: string; handleClick
|
|||||||
<tr className={style.empty}>
|
<tr className={style.empty}>
|
||||||
<td colSpan={99}>
|
<td colSpan={99}>
|
||||||
<div>{label ?? 'No data yet'}</div>
|
<div>{label ?? 'No data yet'}</div>
|
||||||
<Button onClick={handleClick} isDisabled={!handleClick} variant='ontime-filled' rightIcon={<IoAdd />} size='sm'>
|
{handleClick && (
|
||||||
New
|
<Button
|
||||||
</Button>
|
onClick={handleClick}
|
||||||
|
isDisabled={!handleClick}
|
||||||
|
variant='ontime-filled'
|
||||||
|
rightIcon={<IoAdd />}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
+6
@@ -3,11 +3,13 @@ import type { PanelBaseProps } from '../../panel-list/PanelList';
|
|||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
import CustomFields from './custom-fields/CustomFields';
|
import CustomFields from './custom-fields/CustomFields';
|
||||||
|
import ReportSettings from './ReportSettings';
|
||||||
import UrlPresetsForm from './UrlPresetsForm';
|
import UrlPresetsForm from './UrlPresetsForm';
|
||||||
|
|
||||||
export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
|
export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
|
||||||
const customFieldsRef = useScrollIntoView<HTMLDivElement>('custom', location);
|
const customFieldsRef = useScrollIntoView<HTMLDivElement>('custom', location);
|
||||||
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
|
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
|
||||||
|
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -19,6 +21,10 @@ export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
|
|||||||
<div ref={urlPresetsRef}>
|
<div ref={urlPresetsRef}>
|
||||||
<UrlPresetsForm />
|
<UrlPresetsForm />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div ref={reportRef}>
|
||||||
|
<ReportSettings />
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
th.over {
|
||||||
|
color: $ontime-delay-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.under {
|
||||||
|
color: $playback-ahead;
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Button } from '@chakra-ui/react';
|
||||||
|
import { IoTrashBin } from '@react-icons/all-files/io5/IoTrashBin';
|
||||||
|
|
||||||
|
import { deleteAllReport } from '../../../../common/api/report';
|
||||||
|
import { createBlob, downloadBlob } from '../../../../common/api/utils';
|
||||||
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
|
import useRundown from '../../../../common/hooks-query/useRundown';
|
||||||
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
|
import { formatTime } from '../../../../common/utils/time';
|
||||||
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
|
|
||||||
|
import { CombinedReport, getCombinedReport, makeReportCSV } from './reportSettings.utils';
|
||||||
|
|
||||||
|
import style from './ReportSettings.module.scss';
|
||||||
|
|
||||||
|
export default function ReportSettings() {
|
||||||
|
const { data: reportData } = useReport();
|
||||||
|
const { data } = useRundown();
|
||||||
|
|
||||||
|
const clearReport = async () => await deleteAllReport();
|
||||||
|
const downloadCSV = (combinedReport: CombinedReport[]) => {
|
||||||
|
if (!combinedReport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const csv = makeReportCSV(combinedReport);
|
||||||
|
const blob = createBlob(csv, 'text/csv;charset=utf-8;');
|
||||||
|
downloadBlob(blob, 'ontime-report.csv');
|
||||||
|
};
|
||||||
|
|
||||||
|
const combinedReport = useMemo(() => {
|
||||||
|
return getCombinedReport(reportData, data.rundown, data.order);
|
||||||
|
}, [reportData, data.rundown, data.order]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Card>
|
||||||
|
<Panel.SubHeader>Report</Panel.SubHeader>
|
||||||
|
<Panel.Divider />
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Title>
|
||||||
|
Manage report
|
||||||
|
<Panel.InlineElements>
|
||||||
|
<Button
|
||||||
|
variant='ontime-subtle'
|
||||||
|
leftIcon={<IoTrashBin />}
|
||||||
|
size='sm'
|
||||||
|
onClick={() => downloadCSV(combinedReport)}
|
||||||
|
isDisabled={combinedReport.length === 0}
|
||||||
|
>
|
||||||
|
Export CSV
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='ontime-subtle'
|
||||||
|
leftIcon={<IoTrashBin />}
|
||||||
|
size='sm'
|
||||||
|
color='#FA5656'
|
||||||
|
onClick={clearReport}
|
||||||
|
isDisabled={combinedReport.length === 0}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
</Panel.InlineElements>
|
||||||
|
</Panel.Title>
|
||||||
|
</Panel.Section>
|
||||||
|
<Panel.Section>
|
||||||
|
<Panel.Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Cue</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Scheduled Start</th>
|
||||||
|
<th>Actual Start</th>
|
||||||
|
<th>Scheduled End</th>
|
||||||
|
<th>Actual End</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{combinedReport.length === 0 && (
|
||||||
|
<Panel.TableEmpty label='Reports are generated when running through the show.' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{combinedReport.map((entry) => {
|
||||||
|
const start = (() => {
|
||||||
|
if (entry.actualStart === null) return null;
|
||||||
|
if (entry.actualStart <= entry.scheduledStart) return 'under';
|
||||||
|
return 'over';
|
||||||
|
})();
|
||||||
|
const end = (() => {
|
||||||
|
if (entry.actualEnd === null) return null;
|
||||||
|
if (entry.actualEnd <= entry.scheduledEnd) return 'under';
|
||||||
|
return 'over';
|
||||||
|
})();
|
||||||
|
return (
|
||||||
|
<tr key={entry.index}>
|
||||||
|
<th>{entry.index}</th>
|
||||||
|
<th>{entry.cue}</th>
|
||||||
|
<th>{entry.title}</th>
|
||||||
|
<th className={cx([start && style[start]])}>{formatTime(entry.scheduledStart)}</th>
|
||||||
|
<th className={cx([start && style[start]])}>{formatTime(entry.actualStart)}</th>
|
||||||
|
<th className={cx([end && style[end]])}>{formatTime(entry.scheduledEnd)}</th>
|
||||||
|
<th className={cx([end && style[end]])}>{formatTime(entry.actualEnd)}</th>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Panel.Table>
|
||||||
|
</Panel.Section>
|
||||||
|
</Panel.Card>
|
||||||
|
</Panel.Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import { isOntimeEvent, MaybeNumber, NormalisedRundown, OntimeReport } from 'ontime-types';
|
||||||
|
|
||||||
|
import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
|
||||||
|
import { formatTime } from '../../../../common/utils/time';
|
||||||
|
|
||||||
|
export type CombinedReport = {
|
||||||
|
index: number;
|
||||||
|
title: string;
|
||||||
|
cue: string;
|
||||||
|
scheduledStart: number;
|
||||||
|
actualStart: MaybeNumber;
|
||||||
|
scheduledEnd: number;
|
||||||
|
actualEnd: MaybeNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a combined report with the rundown data
|
||||||
|
*/
|
||||||
|
export function getCombinedReport(report: OntimeReport, rundown: NormalisedRundown, order: string[]): CombinedReport[] {
|
||||||
|
if (Object.keys(report).length === 0) return [];
|
||||||
|
if (order.length === 0) return [];
|
||||||
|
|
||||||
|
const combinedReport: CombinedReport[] = [];
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(report)) {
|
||||||
|
if (!rundown[key] || !isOntimeEvent(rundown[key])) continue;
|
||||||
|
|
||||||
|
combinedReport.push({
|
||||||
|
index: order.findIndex((id) => id === key),
|
||||||
|
title: rundown[key].title,
|
||||||
|
cue: rundown[key].cue,
|
||||||
|
scheduledStart: rundown[key].timeStart,
|
||||||
|
actualEnd: value.endedAt,
|
||||||
|
scheduledEnd: rundown[key].timeEnd,
|
||||||
|
actualStart: value.startedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvHeader = ['Index', 'Title', 'Cue', 'Scheduled Start', 'Actual Start', 'Scheduled End', 'Actual End'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a CombinedReport into a CSV string
|
||||||
|
*/
|
||||||
|
export function makeReportCSV(combinedReport: CombinedReport[]) {
|
||||||
|
const csv: string[][] = [];
|
||||||
|
csv.push(csvHeader);
|
||||||
|
|
||||||
|
for (const entry of combinedReport) {
|
||||||
|
csv.push([
|
||||||
|
String(entry.index),
|
||||||
|
entry.title,
|
||||||
|
entry.cue,
|
||||||
|
formatTime(entry.scheduledStart),
|
||||||
|
formatTime(entry.actualStart),
|
||||||
|
formatTime(entry.scheduledEnd),
|
||||||
|
formatTime(entry.actualEnd),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeCSVFromArrayOfArrays(csv);
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ const staticOptions = [
|
|||||||
secondary: [
|
secondary: [
|
||||||
{ id: 'feature_settings__custom', label: 'Custom fields' },
|
{ id: 'feature_settings__custom', label: 'Custom fields' },
|
||||||
{ id: 'feature_settings__urlpresets', label: 'URL Presets' },
|
{ id: 'feature_settings__urlpresets', label: 'URL Presets' },
|
||||||
|
{ id: 'feature_settings__report', label: 'Report' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
import { IoPause } from '@react-icons/all-files/io5/IoPause';
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
@@ -34,7 +35,7 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
const isLast = selectedEventIndex === numEvents - 1;
|
const isLast = selectedEventIndex === numEvents - 1;
|
||||||
const noEvents = numEvents === 0;
|
const noEvents = numEvents === 0;
|
||||||
|
|
||||||
const disableGo = isRolling || noEvents || (isLast && !isArmed);
|
const disableGo = isRolling || noEvents;
|
||||||
const disableNext = isRolling || noEvents || isLast;
|
const disableNext = isRolling || noEvents || isLast;
|
||||||
const disablePrev = isRolling || noEvents || isFirst;
|
const disablePrev = isRolling || noEvents || isFirst;
|
||||||
|
|
||||||
@@ -45,14 +46,16 @@ export default function PlaybackButtons(props: PlaybackButtonsProps) {
|
|||||||
const disableStop = !playbackCan.stop;
|
const disableStop = !playbackCan.stop;
|
||||||
const disableReload = !playbackCan.reload;
|
const disableReload = !playbackCan.reload;
|
||||||
|
|
||||||
const goModeText = selectedEventIndex === null || isArmed ? 'Start' : 'Next';
|
const [goModeAction, goModeText] = useMemo(() => {
|
||||||
const goModeAction = () => {
|
|
||||||
if (isArmed) {
|
if (isArmed) {
|
||||||
setPlayback.start();
|
return [setPlayback.start, 'Start'];
|
||||||
} else {
|
} else if (isLast) {
|
||||||
setPlayback.startNext();
|
return [setPlayback.stop, 'Finish'];
|
||||||
|
} else if (selectedEventIndex === null) {
|
||||||
|
return [setPlayback.startNext, 'Start'];
|
||||||
}
|
}
|
||||||
};
|
return [setPlayback.startNext, 'Next'];
|
||||||
|
}, [isArmed, isLast, selectedEventIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.buttonContainer}>
|
<div className={style.buttonContainer}>
|
||||||
|
|||||||
@@ -76,3 +76,10 @@
|
|||||||
color: $ontime-roll;
|
color: $ontime-roll;
|
||||||
font-size: $text-body-size;
|
font-size: $text-body-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reportLink {
|
||||||
|
color: $label-gray;
|
||||||
|
font-size: $text-body-size;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { Playback, TimerPhase } from 'ontime-types';
|
import { MaybeNumber, Playback, TimerPhase } from 'ontime-types';
|
||||||
import { dayInMs, millisToString } from 'ontime-utils';
|
import { dayInMs, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { useTimer } from '../../../../common/hooks/useSocket';
|
import { useTimer } from '../../../../common/hooks/useSocket';
|
||||||
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import { formatDuration } from '../../../../common/utils/time';
|
import { formatDuration } from '../../../../common/utils/time';
|
||||||
import TimerDisplay from '../timer-display/TimerDisplay';
|
import TimerDisplay from '../timer-display/TimerDisplay';
|
||||||
|
|
||||||
@@ -29,10 +30,6 @@ export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProp
|
|||||||
const { playback, children } = props;
|
const { playback, children } = props;
|
||||||
const timer = useTimer();
|
const timer = useTimer();
|
||||||
|
|
||||||
const started = millisToString(timer.startedAt);
|
|
||||||
const expectedFinish = timer.expectedFinish !== null ? timer.expectedFinish % dayInMs : null;
|
|
||||||
const finish = millisToString(expectedFinish);
|
|
||||||
|
|
||||||
const isRolling = playback === Playback.Roll;
|
const isRolling = playback === Playback.Roll;
|
||||||
const isWaiting = timer.phase === TimerPhase.Pending;
|
const isWaiting = timer.phase === TimerPhase.Pending;
|
||||||
const isOvertime = timer.phase === TimerPhase.Overtime;
|
const isOvertime = timer.phase === TimerPhase.Overtime;
|
||||||
@@ -58,19 +55,55 @@ export default function PlaybackTimer(props: PropsWithChildren<PlaybackTimerProp
|
|||||||
{isWaiting ? (
|
{isWaiting ? (
|
||||||
<span className={style.rolltag}>Roll: Countdown to start</span>
|
<span className={style.rolltag}>Roll: Countdown to start</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<RunningStatus startedAt={timer.startedAt} expectedFinish={timer.expectedFinish} playback={playback} />
|
||||||
<span className={style.start}>
|
|
||||||
<span className={style.tag}>Started at</span>
|
|
||||||
<span className={style.time}>{started}</span>
|
|
||||||
</span>
|
|
||||||
<span className={style.finish}>
|
|
||||||
<span className={style.tag}>Expect end</span>
|
|
||||||
<span className={style.time}>{finish}</span>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RunningStatusProps {
|
||||||
|
startedAt: MaybeNumber;
|
||||||
|
expectedFinish: MaybeNumber;
|
||||||
|
playback: Playback;
|
||||||
|
}
|
||||||
|
function RunningStatus(props: RunningStatusProps) {
|
||||||
|
const { startedAt, expectedFinish, playback } = props;
|
||||||
|
|
||||||
|
if (playback === Playback.Stop) {
|
||||||
|
return <StoppedStatus />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = millisToString(startedAt);
|
||||||
|
const finishedMs = expectedFinish !== null ? expectedFinish % dayInMs : null;
|
||||||
|
const finish = millisToString(finishedMs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className={style.start}>
|
||||||
|
<span className={style.tag}>Started at</span>
|
||||||
|
<span className={style.time}>{started}</span>
|
||||||
|
</span>
|
||||||
|
<span className={style.finish}>
|
||||||
|
<span className={style.tag}>Expect end</span>
|
||||||
|
<span className={style.time}>{finish}</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StoppedStatus() {
|
||||||
|
const { data } = useReport();
|
||||||
|
const hasReport = Object.keys(data).length > 0;
|
||||||
|
|
||||||
|
if (hasReport) {
|
||||||
|
return (
|
||||||
|
<a className={style.reportLink} href='/editor?settings=feature_settings__report'>
|
||||||
|
Go to report management
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ahead {
|
.ahead {
|
||||||
color: $green-500;
|
color: $playback-ahead;
|
||||||
}
|
}
|
||||||
|
|
||||||
.behind {
|
.behind {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export type EventItemActions =
|
|||||||
| 'delete'
|
| 'delete'
|
||||||
| 'clone'
|
| 'clone'
|
||||||
| 'update'
|
| 'update'
|
||||||
| 'swap';
|
| 'swap'
|
||||||
|
| 'clear-report';
|
||||||
|
|
||||||
interface RundownEntryProps {
|
interface RundownEntryProps {
|
||||||
type: SupportedEvent;
|
type: SupportedEvent;
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
},
|
},
|
||||||
isDisabled: selectedEventId == null || selectedEventId === eventId,
|
isDisabled: selectedEventId == null || selectedEventId === eventId,
|
||||||
},
|
},
|
||||||
{ withDivider: true, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
|
{ withDivider: false, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
|
||||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
|
|||||||
isLoaded={loaded}
|
isLoaded={loaded}
|
||||||
totalGap={totalGap}
|
totalGap={totalGap}
|
||||||
isLinkedAndNext={isNext && linkStart !== null}
|
isLinkedAndNext={isNext && linkStart !== null}
|
||||||
|
duration={duration}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
&.over {
|
&.over {
|
||||||
color: $playback-negative;
|
color: $ontime-delay-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.under {
|
&.under {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle';
|
||||||
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';
|
||||||
|
|
||||||
import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket';
|
import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket';
|
||||||
|
import useReport from '../../../../common/hooks-query/useReport';
|
||||||
import { cx } from '../../../../common/utils/styleUtils';
|
import { cx } from '../../../../common/utils/styleUtils';
|
||||||
import { formatDuration } from '../../../../common/utils/time';
|
import { formatDuration, formatTime } from '../../../../common/utils/time';
|
||||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||||
|
|
||||||
import style from './EventBlockChip.module.scss';
|
import style from './EventBlockChip.module.scss';
|
||||||
@@ -16,10 +19,11 @@ interface EventBlockChipProps {
|
|||||||
className: string;
|
className: string;
|
||||||
totalGap: number;
|
totalGap: number;
|
||||||
isLinkedAndNext: boolean;
|
isLinkedAndNext: boolean;
|
||||||
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventBlockChip(props: EventBlockChipProps) {
|
export default function EventBlockChip(props: EventBlockChipProps) {
|
||||||
const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext } = props;
|
const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext, id, duration } = props;
|
||||||
const { playback } = usePlayback();
|
const { playback } = usePlayback();
|
||||||
|
|
||||||
if (isLoaded) {
|
if (isLoaded) {
|
||||||
@@ -29,7 +33,7 @@ export default function EventBlockChip(props: EventBlockChipProps) {
|
|||||||
const playbackActive = isPlaybackActive(playback);
|
const playbackActive = isPlaybackActive(playback);
|
||||||
|
|
||||||
if (!playbackActive || isPast) {
|
if (!playbackActive || isPast) {
|
||||||
return null; //TODO: Event report will go here
|
return <EventReport className={className} id={id} duration={duration} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackActive) {
|
if (playbackActive) {
|
||||||
@@ -65,3 +69,55 @@ function EventUntil(props: EventUntilProps) {
|
|||||||
|
|
||||||
return <div className={cx([style.chip, isDue && style.due])}>{timeUntilString}</div>;
|
return <div className={cx([style.chip, isDue && style.due])}>{timeUntilString}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EventReportProps {
|
||||||
|
className: string;
|
||||||
|
id: string;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventReport(props: EventReportProps) {
|
||||||
|
const { className, id, duration } = props;
|
||||||
|
const { data } = useReport();
|
||||||
|
const currentReport = data[id];
|
||||||
|
|
||||||
|
const [value, overUnderStyle, tooltip] = useMemo(() => {
|
||||||
|
if (!currentReport) {
|
||||||
|
return [null, 'none', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startedAt, endedAt } = currentReport;
|
||||||
|
if (!startedAt || !endedAt) {
|
||||||
|
return [null, 'none', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualDuration = endedAt - startedAt;
|
||||||
|
const difference = actualDuration - duration;
|
||||||
|
const absDifference = Math.abs(difference);
|
||||||
|
|
||||||
|
if (absDifference < MILLIS_PER_SECOND) {
|
||||||
|
return ['ontime', 'ontime', 'Event finished ontime'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOver = difference > 0;
|
||||||
|
|
||||||
|
const fullTimeValue = formatTime(absDifference);
|
||||||
|
|
||||||
|
const tooltip = `Event ran ${isOver ? 'over' : 'under'} time by ${fullTimeValue}`;
|
||||||
|
|
||||||
|
const value = `${isOver ? '+' : '-'}${formatDuration(absDifference, absDifference > 2 * MILLIS_PER_MINUTE)}`;
|
||||||
|
return [value, isOver ? 'over' : 'under', tooltip];
|
||||||
|
}, [currentReport, duration]);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={tooltip} openDelay={tooltipDelayFast}>
|
||||||
|
<div className={cx([style.chip, style[overUnderStyle], className])}>
|
||||||
|
{value === 'ontime' ? <IoCheckmarkCircle size='1.1rem' /> : value}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ProjectData } from 'ontime-types';
|
import { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import { makeCSV, makeTable, parseField } from '../cuesheet.utils';
|
import { makeTable, parseField } from '../cuesheet.utils';
|
||||||
|
|
||||||
describe('parseField()', () => {
|
describe('parseField()', () => {
|
||||||
it('returns a string from given millis on timeStart, TimeEnd and duration', () => {
|
it('returns a string from given millis on timeStart, TimeEnd and duration', () => {
|
||||||
@@ -114,15 +114,3 @@ describe('makeTable()', () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('make CSV()', () => {
|
|
||||||
it('joins an array of arrays with commas and newlines', () => {
|
|
||||||
const testdata = [['field'], ['after newline', 'after comma'], ['', 'after empty']];
|
|
||||||
expect(makeCSV(testdata)).toMatchInlineSnapshot(`
|
|
||||||
"field
|
|
||||||
after newline,after comma
|
|
||||||
,after empty
|
|
||||||
"
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { stringify } from 'csv-stringify/browser/esm/sync';
|
|
||||||
import {
|
import {
|
||||||
CustomFields,
|
CustomFields,
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
@@ -103,13 +102,3 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, custo
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Converts an array of arrays to a csv file
|
|
||||||
* @param {string[][]} arrayOfArrays
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export const makeCSV = (arrayOfArrays: string[][]): string => {
|
|
||||||
const stringifiedData = stringify(arrayOfArrays);
|
|
||||||
return stringifiedData;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -234,6 +234,10 @@ function makeSettingsMenu(redirectWindow) {
|
|||||||
label: 'URL presets',
|
label: 'URL presets',
|
||||||
click: () => redirectWindow('/editor?settings=feature_settings__urlpresets'),
|
click: () => redirectWindow('/editor?settings=feature_settings__urlpresets'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Report',
|
||||||
|
click: () => redirectWindow('/editor?settings=feature_settings__report'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { router as sheetsRouter } from './sheets/sheets.router.js';
|
|||||||
import { router as excelRouter } from './excel/excel.router.js';
|
import { router as excelRouter } from './excel/excel.router.js';
|
||||||
import { router as sessionRouter } from './session/session.router.js';
|
import { router as sessionRouter } from './session/session.router.js';
|
||||||
import { router as viewSettingsRouter } from './view-settings/viewSettings.router.js';
|
import { router as viewSettingsRouter } from './view-settings/viewSettings.router.js';
|
||||||
|
import { router as reportRouter } from './report/report.router.js';
|
||||||
|
|
||||||
export const appRouter = express.Router();
|
export const appRouter = express.Router();
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ appRouter.use('/excel', excelRouter);
|
|||||||
appRouter.use('/url-presets', urlPresetsRouter);
|
appRouter.use('/url-presets', urlPresetsRouter);
|
||||||
appRouter.use('/session', sessionRouter);
|
appRouter.use('/session', sessionRouter);
|
||||||
appRouter.use('/view-settings', viewSettingsRouter);
|
appRouter.use('/view-settings', viewSettingsRouter);
|
||||||
|
appRouter.use('/report', reportRouter);
|
||||||
|
|
||||||
//we don't want to redirect to react index when using api routes
|
//we don't want to redirect to react index when using api routes
|
||||||
appRouter.all('/*', (_req, res) => {
|
appRouter.all('/*', (_req, res) => {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import type { OntimeReport } from 'ontime-types';
|
||||||
|
import * as report from './report.service.js';
|
||||||
|
|
||||||
|
export function getAll(_req: Request, res: Response<OntimeReport>) {
|
||||||
|
res.json(report.generate());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAll(_req: Request, res: Response<OntimeReport>) {
|
||||||
|
report.clear();
|
||||||
|
res.status(200).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteWithId(req: Request, res: Response<OntimeReport>) {
|
||||||
|
const { eventId } = req.params;
|
||||||
|
report.clear(eventId);
|
||||||
|
res.status(200).send();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { getAll, deleteWithId, deleteAll } from './report.controller.js';
|
||||||
|
import { paramsMustHaveEventId } from '../rundown/rundown.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getAll);
|
||||||
|
|
||||||
|
router.delete('/all', deleteAll);
|
||||||
|
router.delete('/:eventId', paramsMustHaveEventId, deleteWithId);
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { OntimeReport, OntimeEventReport, TimerLifeCycle } from 'ontime-types';
|
||||||
|
import { RuntimeState } from '../../stores/runtimeState.js';
|
||||||
|
import { sendRefetch } from '../../adapters/websocketAux.js';
|
||||||
|
import { DeepReadonly } from 'ts-essentials';
|
||||||
|
|
||||||
|
const report = new Map<string, OntimeEventReport>();
|
||||||
|
|
||||||
|
let formattedReport: OntimeReport | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generates a full report
|
||||||
|
* @returns full report
|
||||||
|
*/
|
||||||
|
export function generate(): OntimeReport {
|
||||||
|
if (formattedReport === null) {
|
||||||
|
formattedReport = Object.fromEntries(report);
|
||||||
|
}
|
||||||
|
return formattedReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear report
|
||||||
|
* @param id optional id of a event report to clear
|
||||||
|
*/
|
||||||
|
export function clear(id?: string) {
|
||||||
|
formattedReport = null;
|
||||||
|
if (id) {
|
||||||
|
report.delete(id);
|
||||||
|
} else {
|
||||||
|
report.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* trigger report entry
|
||||||
|
* @param cycle
|
||||||
|
* @param state
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function triggerReportEntry(
|
||||||
|
cycle: TimerLifeCycle.onStart | TimerLifeCycle.onStop,
|
||||||
|
state: DeepReadonly<RuntimeState>,
|
||||||
|
) {
|
||||||
|
if (!state.eventNow?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = state.eventNow.id;
|
||||||
|
|
||||||
|
if (cycle === TimerLifeCycle.onStart) {
|
||||||
|
report.set(eventId, { startedAt: state.timer.startedAt, endedAt: null });
|
||||||
|
formattedReport = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycle === TimerLifeCycle.onStop) {
|
||||||
|
const startedAt = report.get(eventId)?.startedAt ?? null;
|
||||||
|
report.set(eventId, { startedAt, endedAt: state.clock });
|
||||||
|
formattedReport = null;
|
||||||
|
sendRefetch({
|
||||||
|
target: 'REPORT',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -266,6 +266,7 @@ function notifyChanges(options: NotifyChangesOptions) {
|
|||||||
if (options.external) {
|
if (options.external) {
|
||||||
// advice socket subscribers of change
|
// advice socket subscribers of change
|
||||||
const payload = {
|
const payload = {
|
||||||
|
target: 'RUNDOWN',
|
||||||
changes: Array.isArray(options.timer) ? options.timer : undefined,
|
changes: Array.isArray(options.timer) ? options.timer : undefined,
|
||||||
reload: options.reload,
|
reload: options.reload,
|
||||||
revision: cache.getMetadata().revision,
|
revision: cache.getMetadata().revision,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import type { RuntimeState } from '../../stores/runtimeState.js';
|
|||||||
import { timerConfig } from '../../config/config.js';
|
import { timerConfig } from '../../config/config.js';
|
||||||
import { eventStore } from '../../stores/EventStore.js';
|
import { eventStore } from '../../stores/EventStore.js';
|
||||||
|
|
||||||
|
import { triggerReportEntry } from '../../api-data/report/report.service.js';
|
||||||
|
|
||||||
import { EventTimer } from '../EventTimer.js';
|
import { EventTimer } from '../EventTimer.js';
|
||||||
import { RestorePoint, restoreService } from '../RestoreService.js';
|
import { RestorePoint, restoreService } from '../RestoreService.js';
|
||||||
import {
|
import {
|
||||||
@@ -288,14 +290,17 @@ class RuntimeService {
|
|||||||
logger.warning(LogOrigin.Playback, `Refused skipped event with ID ${event.id}`);
|
logger.warning(LogOrigin.Playback, `Refused skipped event with ID ${event.id}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const previousState = runtimeState.getState();
|
||||||
|
|
||||||
const rundown = getRundown();
|
const rundown = getRundown();
|
||||||
const success = runtimeState.load(event, rundown, initialData);
|
const success = runtimeState.load(event, rundown, initialData);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
logger.info(LogOrigin.Playback, `Loaded event with ID ${event.id}`);
|
||||||
|
const newState = runtimeState.getState();
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
triggerAutomations(TimerLifeCycle.onLoad, runtimeState.getState());
|
triggerReportEntry(TimerLifeCycle.onStop, previousState);
|
||||||
|
triggerAutomations(TimerLifeCycle.onLoad, newState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
@@ -474,6 +479,7 @@ class RuntimeService {
|
|||||||
|
|
||||||
if (didStart) {
|
if (didStart) {
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
triggerReportEntry(TimerLifeCycle.onStart, newState);
|
||||||
triggerAutomations(TimerLifeCycle.onStart, newState);
|
triggerAutomations(TimerLifeCycle.onStart, newState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -536,8 +542,8 @@ class RuntimeService {
|
|||||||
*/
|
*/
|
||||||
@broadcastResult
|
@broadcastResult
|
||||||
public stop(): boolean {
|
public stop(): boolean {
|
||||||
const state = runtimeState.getState();
|
const previousState = runtimeState.getState();
|
||||||
const canStop = validatePlayback(state.timer.playback, state.timer.phase).stop;
|
const canStop = validatePlayback(previousState.timer.playback, previousState.timer.phase).stop;
|
||||||
if (!canStop) {
|
if (!canStop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -546,6 +552,7 @@ class RuntimeService {
|
|||||||
const newState = runtimeState.getState();
|
const newState = runtimeState.getState();
|
||||||
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
|
logger.info(LogOrigin.Playback, `Play Mode ${newState.timer.playback.toUpperCase()}`);
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
triggerReportEntry(TimerLifeCycle.onStop, previousState);
|
||||||
triggerAutomations(TimerLifeCycle.onStop, newState);
|
triggerAutomations(TimerLifeCycle.onStop, newState);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -598,12 +605,14 @@ class RuntimeService {
|
|||||||
if (result.eventId !== previousState.eventNow?.id) {
|
if (result.eventId !== previousState.eventNow?.id) {
|
||||||
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
|
logger.info(LogOrigin.Playback, `Loaded event with ID ${result.eventId}`);
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
triggerReportEntry(TimerLifeCycle.onStop, previousState);
|
||||||
triggerAutomations(TimerLifeCycle.onLoad, newState);
|
triggerAutomations(TimerLifeCycle.onLoad, newState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.didStart) {
|
if (result.didStart) {
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
|
triggerReportEntry(TimerLifeCycle.onStart, newState);
|
||||||
triggerAutomations(TimerLifeCycle.onStart, newState);
|
triggerAutomations(TimerLifeCycle.onStart, newState);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,7 +391,6 @@ export function start(state: RuntimeState = runtimeState): boolean {
|
|||||||
// update offset
|
// update offset
|
||||||
state.runtime.offset = getRuntimeOffset(state);
|
state.runtime.offset = getRuntimeOffset(state);
|
||||||
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
|
state.runtime.expectedEnd = state.runtime.plannedEnd - state.runtime.offset;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +409,6 @@ export function stop(state: RuntimeState = runtimeState): boolean {
|
|||||||
if (state.timer.playback === Playback.Stop) {
|
if (state.timer.playback === Playback.Stop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
runtimeState.runtime.actualStart = null;
|
runtimeState.runtime.actualStart = null;
|
||||||
runtimeState.runtime.expectedEnd = null;
|
runtimeState.runtime.expectedEnd = null;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { MaybeNumber } from '../../utils/utils.type.js';
|
||||||
|
|
||||||
|
export type OntimeEventReport = {
|
||||||
|
startedAt: MaybeNumber;
|
||||||
|
endedAt: MaybeNumber;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OntimeReport = Record<string, OntimeEventReport>;
|
||||||
@@ -16,6 +16,9 @@ export type { OntimeEntryCommonKeys, OntimeRundown, OntimeRundownEntry } from '.
|
|||||||
export { TimeStrategy } from './definitions/TimeStrategy.type.js';
|
export { TimeStrategy } from './definitions/TimeStrategy.type.js';
|
||||||
export { TimerType } from './definitions/TimerType.type.js';
|
export { TimerType } from './definitions/TimerType.type.js';
|
||||||
|
|
||||||
|
// ---> Report
|
||||||
|
export type { OntimeReport, OntimeEventReport } from './definitions/core/Report.type.js';
|
||||||
|
|
||||||
// ---> Automations
|
// ---> Automations
|
||||||
export type {
|
export type {
|
||||||
Automation,
|
Automation,
|
||||||
|
|||||||
Reference in New Issue
Block a user