refactor: add rundown summary to import preview

This commit is contained in:
Carlos Valente
2025-11-02 12:46:26 +01:00
committed by Carlos Valente
parent 3918664945
commit e6070e6efd
12 changed files with 103 additions and 35 deletions
+2 -1
View File
@@ -1,5 +1,5 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { CustomFields, Rundown } from 'ontime-types'; import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { ImportMap } from 'ontime-utils'; import { ImportMap } from 'ontime-utils';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
@@ -25,6 +25,7 @@ export async function upload(file: File): Promise<string[]> {
type PreviewSpreadsheetResponse = { type PreviewSpreadsheetResponse = {
rundown: Rundown; rundown: Rundown;
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary;
}; };
export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> { export async function importRundownPreview(options: ImportMap): Promise<PreviewSpreadsheetResponse> {
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, { const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(`${excelPath}/preview`, {
+2 -1
View File
@@ -1,5 +1,5 @@
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types'; import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { ImportMap } from 'ontime-utils'; import { ImportMap } from 'ontime-utils';
import { apiEntryUrl } from './constants'; import { apiEntryUrl } from './constants';
@@ -56,6 +56,7 @@ export const previewRundown = async (
): Promise<{ ): Promise<{
rundown: Rundown; rundown: Rundown;
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary;
}> => { }> => {
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options }); const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
return response.data; return response.data;
@@ -1,9 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { CustomFields, Rundown } from 'ontime-types'; import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { getFirstEventNormal, getLastEventNormal, millisToString } from 'ontime-utils'; import { millisToString } from 'ontime-utils';
import Button from '../../../../../common/components/buttons/Button'; import Button from '../../../../../common/components/buttons/Button';
import useRundown from '../../../../../common/hooks-query/useRundown'; import useRundown from '../../../../../common/hooks-query/useRundown';
import { formatDuration } from '../../../../../common/utils/time';
import * as Panel from '../../../panel-utils/PanelUtils'; import * as Panel from '../../../panel-utils/PanelUtils';
import PreviewSpreadsheet from './preview/PreviewRundown'; import PreviewSpreadsheet from './preview/PreviewRundown';
@@ -13,12 +14,20 @@ import { useSheetStore } from './useSheetStore';
interface ImportReviewProps { interface ImportReviewProps {
rundown: Rundown; rundown: Rundown;
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary;
onFinished: () => void; onFinished: () => void;
onCancel: () => void; onCancel: () => void;
onBack: () => void; onBack: () => void;
} }
export default function ImportReview({ rundown, customFields, onFinished, onCancel, onBack }: ImportReviewProps) { export default function ImportReview({
rundown,
customFields,
summary,
onFinished,
onCancel,
onBack,
}: ImportReviewProps) {
const { data: currentRundown } = useRundown(); const { data: currentRundown } = useRundown();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { importRundown } = useGoogleSheet(); const { importRundown } = useGoogleSheet();
@@ -44,9 +53,6 @@ export default function ImportReview({ rundown, customFields, onFinished, onCanc
onFinished(); onFinished();
}; };
const { firstEvent } = getFirstEventNormal(rundown.entries, rundown.flatOrder);
const { lastEvent } = getLastEventNormal(rundown.entries, rundown.flatOrder);
return ( return (
<Panel.Section> <Panel.Section>
<Panel.Title> <Panel.Title>
@@ -70,16 +76,15 @@ export default function ImportReview({ rundown, customFields, onFinished, onCanc
<Panel.ListItem> <Panel.ListItem>
<b>Number of entries</b> {rundown.flatOrder.length} <b>Number of entries</b> {rundown.flatOrder.length}
</Panel.ListItem> </Panel.ListItem>
{firstEvent && ( <Panel.ListItem>
<Panel.ListItem> <b>Start time</b> {millisToString(summary.start)}
<b>Start Time</b> {millisToString(firstEvent.timeStart)} </Panel.ListItem>
</Panel.ListItem> <Panel.ListItem>
)} <b>End time</b> {millisToString(summary.end)}
{lastEvent && ( </Panel.ListItem>
<Panel.ListItem> <Panel.ListItem>
<b>End Time</b> {millisToString(lastEvent.timeEnd)} <b>Total duration</b> {formatDuration(summary.duration)}
</Panel.ListItem> </Panel.ListItem>
)}
</Panel.ListGroup> </Panel.ListGroup>
<PreviewSpreadsheet rundown={rundown} customFields={customFields} /> <PreviewSpreadsheet rundown={rundown} customFields={customFields} />
</Panel.Section> </Panel.Section>
@@ -36,6 +36,8 @@ export default function SourcesPanel() {
const setRundown = useSheetStore((state) => state.setRundown); const setRundown = useSheetStore((state) => state.setRundown);
const customFields = useSheetStore((state) => state.customFields); const customFields = useSheetStore((state) => state.customFields);
const setCustomFields = useSheetStore((state) => state.setCustomFields); const setCustomFields = useSheetStore((state) => state.setCustomFields);
const summary = useSheetStore((state) => state.summary);
const setSummary = useSheetStore((state) => state.setSummary);
const setSheetId = useSheetStore((state) => state.setSheetId); const setSheetId = useSheetStore((state) => state.setSheetId);
const sheetId = useSheetStore((state) => state.sheetId); const sheetId = useSheetStore((state) => state.sheetId);
const resetPreview = useSheetStore((state) => state.resetPreview); const resetPreview = useSheetStore((state) => state.resetPreview);
@@ -76,6 +78,7 @@ export default function SourcesPanel() {
setHasFile('none'); setHasFile('none');
setWorksheets(null); setWorksheets(null);
setCustomFields(null); setCustomFields(null);
setSummary(null);
setError(''); setError('');
setSheetId(null); setSheetId(null);
}; };
@@ -109,6 +112,7 @@ export default function SourcesPanel() {
const previewData = await importRundownPreviewExcel(importMap); const previewData = await importRundownPreviewExcel(importMap);
setRundown(previewData.rundown); setRundown(previewData.rundown);
setCustomFields(previewData.customFields); setCustomFields(previewData.customFields);
setSummary(previewData.summary);
} catch (error) { } catch (error) {
setError(maybeAxiosError(error)); setError(maybeAxiosError(error));
} }
@@ -151,7 +155,7 @@ export default function SourcesPanel() {
const showCompleted = importFlow === 'finished'; const showCompleted = importFlow === 'finished';
const showAuth = isGSheetFlow && !isAuthenticated; const showAuth = isGSheetFlow && !isAuthenticated;
const showImportMap = (isGSheetFlow && isAuthenticated) || (isExcelFlow && hasFile === 'done'); const showImportMap = (isGSheetFlow && isAuthenticated) || (isExcelFlow && hasFile === 'done');
const showReview = rundown !== null && customFields !== null; const showReview = rundown !== null && customFields !== null && summary !== null;
return ( return (
<Panel.Section> <Panel.Section>
@@ -218,6 +222,7 @@ export default function SourcesPanel() {
<ImportReview <ImportReview
rundown={rundown} rundown={rundown}
customFields={customFields} customFields={customFields}
summary={summary}
onFinished={handleFinished} onFinished={handleFinished}
onCancel={cancelImportMap} onCancel={cancelImportMap}
onBack={resetPreview} onBack={resetPreview}
@@ -21,6 +21,7 @@ export default function useGoogleSheet() {
const patchStepData = useSheetStore((state) => state.patchStepData); const patchStepData = useSheetStore((state) => state.patchStepData);
const setRundown = useSheetStore((state) => state.setRundown); const setRundown = useSheetStore((state) => state.setRundown);
const setCustomFields = useSheetStore((state) => state.setCustomFields); const setCustomFields = useSheetStore((state) => state.setCustomFields);
const setSummary = useSheetStore((state) => state.setSummary);
/** whether the current session has been authenticated */ /** whether the current session has been authenticated */
const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus; sheetId: string } | void> => { const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus; sheetId: string } | void> => {
@@ -58,6 +59,7 @@ export default function useGoogleSheet() {
const data = await previewRundown(sheetId, fileOptions); const data = await previewRundown(sheetId, fileOptions);
setRundown(data.rundown); setRundown(data.rundown);
setCustomFields(data.customFields); setCustomFields(data.customFields);
setSummary(data.summary);
} catch (error) { } catch (error) {
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } }); patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
} }
@@ -1,4 +1,4 @@
import { AuthenticationStatus, CustomFields, Rundown } from 'ontime-types'; import { AuthenticationStatus, CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { defaultImportMap, ImportMap } from 'ontime-utils'; import { defaultImportMap, ImportMap } from 'ontime-utils';
import { create } from 'zustand'; import { create } from 'zustand';
@@ -17,10 +17,10 @@ type SheetStore = {
// we get this from a preview response // we get this from a preview response
rundown: Rundown | null; rundown: Rundown | null;
setRundown: (rundown: Rundown | null) => void; setRundown: (rundown: Rundown | null) => void;
// we get this from a preview response
customFields: CustomFields | null; customFields: CustomFields | null;
setCustomFields: (customFields: CustomFields | null) => void; setCustomFields: (customFields: CustomFields | null) => void;
summary: RundownSummary | null;
setSummary: (metadata: RundownSummary | null) => void;
spreadsheetImportMap: ImportMap; spreadsheetImportMap: ImportMap;
patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => void; patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => void;
@@ -43,6 +43,7 @@ const initialState = {
authenticationStatus: 'not_authenticated' as AuthenticationStatus, authenticationStatus: 'not_authenticated' as AuthenticationStatus,
rundown: null, rundown: null,
customFields: null, customFields: null,
summary: null,
spreadsheetImportMap: defaultImportMap, spreadsheetImportMap: defaultImportMap,
}; };
@@ -64,6 +65,8 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
setCustomFields: (customFields: CustomFields | null) => set({ customFields }), setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
setSummary: (summary: RundownSummary | null) => set({ summary }),
patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => { patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => {
const currentImportMap = get().spreadsheetImportMap; const currentImportMap = get().spreadsheetImportMap;
if (currentImportMap[field] !== value) { if (currentImportMap[field] !== value) {
@@ -72,5 +75,5 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
}, },
reset: () => set(initialState), reset: () => set(initialState),
resetPreview: () => set({ rundown: null, customFields: null }), resetPreview: () => set({ rundown: null, customFields: null, summary: null }),
})); }));
@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { CustomFields, ErrorResponse, Rundown } from 'ontime-types'; import { CustomFields, ErrorResponse, Rundown, RundownSummary } from 'ontime-types';
import { getDataProvider } from '../../classes/data-provider/DataProvider.js'; import { getDataProvider } from '../../classes/data-provider/DataProvider.js';
@@ -32,7 +32,10 @@ router.post(
router.post( router.post(
'/preview', '/preview',
validateImportMapOptions, validateImportMapOptions,
(req: Request, res: Response<{ rundown: Rundown; customFields: CustomFields } | ErrorResponse>) => { (
req: Request,
res: Response<{ rundown: Rundown; customFields: CustomFields; summary: RundownSummary } | ErrorResponse>,
) => {
try { try {
const { options } = req.body; const { options } = req.body;
const data = generateRundownPreview(options); const data = generateRundownPreview(options);
@@ -3,7 +3,7 @@
* Google Sheets * Google Sheets
*/ */
import { CustomFields, Rundown } from 'ontime-types'; import { CustomFields, Rundown, RundownSummary } from 'ontime-types';
import { type ImportMap } from 'ontime-utils'; import { type ImportMap } from 'ontime-utils';
import { extname } from 'path'; import { extname } from 'path';
@@ -14,7 +14,7 @@ import type { WorkBook } from 'xlsx';
import { deleteFile } from '../../utils/fileManagement.js'; import { deleteFile } from '../../utils/fileManagement.js';
import { parseRundown } from '../rundown/rundown.parser.js'; import { parseRundown } from '../rundown/rundown.parser.js';
import { getProjectCustomFields } from '../rundown/rundown.dao.js'; import { getProjectCustomFields, processRundown } from '../rundown/rundown.dao.js';
import { parseCustomFields } from '../custom-fields/customFields.parser.js'; import { parseCustomFields } from '../custom-fields/customFields.parser.js';
import { parseExcel } from './excel.parser.js'; import { parseExcel } from './excel.parser.js';
@@ -43,7 +43,11 @@ export async function readExcelFile(filePath: string): Promise<string[]> {
return excelData.SheetNames; return excelData.SheetNames;
} }
export function generateRundownPreview(options: ImportMap): { rundown: Rundown; customFields: CustomFields } { export function generateRundownPreview(options: ImportMap): {
rundown: Rundown;
summary: RundownSummary;
customFields: CustomFields;
} {
const data = excelData.Sheets[options.worksheet]; const data = excelData.Sheets[options.worksheet];
if (!data) { if (!data) {
@@ -59,9 +63,25 @@ export function generateRundownPreview(options: ImportMap): { rundown: Rundown;
// we run the parsed data through an extra step to ensure the objects shape // we run the parsed data through an extra step to ensure the objects shape
const customFields = parseCustomFields(dataFromExcel); const customFields = parseCustomFields(dataFromExcel);
const rundown = parseRundown(dataFromExcel.rundown, customFields); const parsedRundown = parseRundown(dataFromExcel.rundown, customFields);
const processedRundown = processRundown(parsedRundown, customFields);
return { rundown, customFields }; return {
rundown: {
id: parsedRundown.id,
title: parsedRundown.title,
order: processedRundown.order,
flatOrder: processedRundown.flatEntryOrder,
entries: processedRundown.entries,
revision: 0,
},
summary: {
duration: processedRundown.totalDuration,
start: processedRundown.firstStart,
end: processedRundown.lastEnd,
},
customFields,
};
} }
/** /**
@@ -3,7 +3,7 @@
* Google Sheets * Google Sheets
*/ */
import type { AuthenticationStatus, CustomFields, ErrorResponse, Rundown } from 'ontime-types'; import type { AuthenticationStatus, CustomFields, ErrorResponse, Rundown, RundownSummary } from 'ontime-types';
import { getErrorMessage } from 'ontime-utils'; import { getErrorMessage } from 'ontime-utils';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
@@ -86,6 +86,7 @@ export async function readFromSheet(
| { | {
rundown: Rundown; rundown: Rundown;
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary;
} }
| ErrorResponse | ErrorResponse
>, >,
@@ -15,6 +15,7 @@ import {
MaybeString, MaybeString,
OntimeGroup, OntimeGroup,
Rundown, Rundown,
RundownSummary,
SupportedEntry, SupportedEntry,
} from 'ontime-types'; } from 'ontime-types';
import { ImportMap, getErrorMessage } from 'ontime-utils'; import { ImportMap, getErrorMessage } from 'ontime-utils';
@@ -24,7 +25,7 @@ import { Credentials, OAuth2Client } from 'google-auth-library';
import { logger } from '../../classes/Logger.js'; import { logger } from '../../classes/Logger.js';
import { parseRundowns } from '../../api-data/rundown/rundown.parser.js'; import { parseRundowns } from '../../api-data/rundown/rundown.parser.js';
import { getCurrentRundown, getProjectCustomFields } from '../../api-data/rundown/rundown.dao.js'; import { getCurrentRundown, getProjectCustomFields, processRundown } from '../../api-data/rundown/rundown.dao.js';
import { parseExcel } from '../../api-data/excel/excel.parser.js'; import { parseExcel } from '../../api-data/excel/excel.parser.js';
import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js'; import { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js';
import { consoleSubdued } from '../../utils/console.js'; import { consoleSubdued } from '../../utils/console.js';
@@ -455,6 +456,7 @@ export async function download(
): Promise<{ ): Promise<{
rundown: Rundown; rundown: Rundown;
customFields: CustomFields; customFields: CustomFields;
summary: RundownSummary;
}> { }> {
if (!currentAuthClient) { if (!currentAuthClient) {
throw new Error('Not authenticated'); throw new Error('Not authenticated');
@@ -488,9 +490,9 @@ export async function download(
}; };
const customFields = parseCustomFields(dataModel); const customFields = parseCustomFields(dataModel);
const rundowns = parseRundowns(dataModel, customFields); const parsedRundown = parseRundowns(dataModel, customFields);
const importedRundown = rundowns[rundownId]; const importedRundown = parsedRundown[rundownId];
if (!importedRundown) { if (!importedRundown) {
throw new Error(`Sheet: Rundown with ID ${rundownId} not found in the worksheet`); throw new Error(`Sheet: Rundown with ID ${rundownId} not found in the worksheet`);
} }
@@ -499,5 +501,22 @@ export async function download(
throw new Error('Sheet: Could not find data to import in the worksheet'); throw new Error('Sheet: Could not find data to import in the worksheet');
} }
return { rundown: rundowns[rundownId], customFields }; const processedRundown = processRundown(importedRundown, customFields);
return {
rundown: {
id: importedRundown.id,
title: importedRundown.title,
order: processedRundown.order,
flatOrder: processedRundown.flatEntryOrder,
entries: processedRundown.entries,
revision: 0,
},
summary: {
duration: processedRundown.totalDuration,
start: processedRundown.firstStart,
end: processedRundown.lastEnd,
},
customFields,
};
} }
@@ -1,4 +1,5 @@
import type { EntryId, OntimeEntry } from '../../definitions/core/OntimeEntry.js'; import type { EntryId, OntimeEntry } from '../../definitions/core/OntimeEntry.js';
import type { MaybeNumber } from '../../utils/utils.type.js';
export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: EntryId }; export type PatchWithId<T extends OntimeEntry = OntimeEntry> = Partial<T> & { id: EntryId };
@@ -23,3 +24,9 @@ export type ProjectRundownsList = {
loaded: string; loaded: string;
rundowns: ProjectRundown[]; rundowns: ProjectRundown[];
}; };
export type RundownSummary = {
duration: number;
start: MaybeNumber;
end: MaybeNumber;
};
+1
View File
@@ -83,6 +83,7 @@ export type {
ProjectRundown, ProjectRundown,
ProjectRundownsList, ProjectRundownsList,
TransientEventPayload, TransientEventPayload,
RundownSummary,
} from './api/rundown-controller/BackendResponse.type.js'; } from './api/rundown-controller/BackendResponse.type.js';
export type { LinkOptions } from './api/session-controller/BackendResponse.type.js'; export type { LinkOptions } from './api/session-controller/BackendResponse.type.js';