diff --git a/apps/client/src/common/api/excel.ts b/apps/client/src/common/api/excel.ts index 67f6c896c..a25275be7 100644 --- a/apps/client/src/common/api/excel.ts +++ b/apps/client/src/common/api/excel.ts @@ -1,5 +1,5 @@ import axios, { AxiosResponse } from 'axios'; -import { CustomFields, Rundown } from 'ontime-types'; +import { CustomFields, Rundown, RundownSummary } from 'ontime-types'; import { ImportMap } from 'ontime-utils'; import { apiEntryUrl } from './constants'; @@ -25,6 +25,7 @@ export async function upload(file: File): Promise { type PreviewSpreadsheetResponse = { rundown: Rundown; customFields: CustomFields; + summary: RundownSummary; }; export async function importRundownPreview(options: ImportMap): Promise { const response: AxiosResponse = await axios.post(`${excelPath}/preview`, { diff --git a/apps/client/src/common/api/sheets.ts b/apps/client/src/common/api/sheets.ts index 231f543db..fa070b8f5 100644 --- a/apps/client/src/common/api/sheets.ts +++ b/apps/client/src/common/api/sheets.ts @@ -1,5 +1,5 @@ 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 { apiEntryUrl } from './constants'; @@ -56,6 +56,7 @@ export const previewRundown = async ( ): Promise<{ rundown: Rundown; customFields: CustomFields; + summary: RundownSummary; }> => { const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options }); return response.data; diff --git a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/ImportReview.tsx b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/ImportReview.tsx index b84e71786..5a7ac166f 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/ImportReview.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/ImportReview.tsx @@ -1,9 +1,10 @@ import { useState } from 'react'; -import { CustomFields, Rundown } from 'ontime-types'; -import { getFirstEventNormal, getLastEventNormal, millisToString } from 'ontime-utils'; +import { CustomFields, Rundown, RundownSummary } from 'ontime-types'; +import { millisToString } from 'ontime-utils'; import Button from '../../../../../common/components/buttons/Button'; import useRundown from '../../../../../common/hooks-query/useRundown'; +import { formatDuration } from '../../../../../common/utils/time'; import * as Panel from '../../../panel-utils/PanelUtils'; import PreviewSpreadsheet from './preview/PreviewRundown'; @@ -13,12 +14,20 @@ import { useSheetStore } from './useSheetStore'; interface ImportReviewProps { rundown: Rundown; customFields: CustomFields; + summary: RundownSummary; onFinished: () => void; onCancel: () => 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 [loading, setLoading] = useState(false); const { importRundown } = useGoogleSheet(); @@ -44,9 +53,6 @@ export default function ImportReview({ rundown, customFields, onFinished, onCanc onFinished(); }; - const { firstEvent } = getFirstEventNormal(rundown.entries, rundown.flatOrder); - const { lastEvent } = getLastEventNormal(rundown.entries, rundown.flatOrder); - return ( @@ -70,16 +76,15 @@ export default function ImportReview({ rundown, customFields, onFinished, onCanc Number of entries {rundown.flatOrder.length} - {firstEvent && ( - - Start Time {millisToString(firstEvent.timeStart)} - - )} - {lastEvent && ( - - End Time {millisToString(lastEvent.timeEnd)} - - )} + + Start time {millisToString(summary.start)} + + + End time {millisToString(summary.end)} + + + Total duration {formatDuration(summary.duration)} + diff --git a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/SourcesPanel.tsx b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/SourcesPanel.tsx index 329891b11..9d683b9a2 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/SourcesPanel.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/SourcesPanel.tsx @@ -36,6 +36,8 @@ export default function SourcesPanel() { const setRundown = useSheetStore((state) => state.setRundown); const customFields = useSheetStore((state) => state.customFields); 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 sheetId = useSheetStore((state) => state.sheetId); const resetPreview = useSheetStore((state) => state.resetPreview); @@ -76,6 +78,7 @@ export default function SourcesPanel() { setHasFile('none'); setWorksheets(null); setCustomFields(null); + setSummary(null); setError(''); setSheetId(null); }; @@ -109,6 +112,7 @@ export default function SourcesPanel() { const previewData = await importRundownPreviewExcel(importMap); setRundown(previewData.rundown); setCustomFields(previewData.customFields); + setSummary(previewData.summary); } catch (error) { setError(maybeAxiosError(error)); } @@ -151,7 +155,7 @@ export default function SourcesPanel() { const showCompleted = importFlow === 'finished'; const showAuth = isGSheetFlow && !isAuthenticated; const showImportMap = (isGSheetFlow && isAuthenticated) || (isExcelFlow && hasFile === 'done'); - const showReview = rundown !== null && customFields !== null; + const showReview = rundown !== null && customFields !== null && summary !== null; return ( @@ -218,6 +222,7 @@ export default function SourcesPanel() { state.patchStepData); const setRundown = useSheetStore((state) => state.setRundown); const setCustomFields = useSheetStore((state) => state.setCustomFields); + const setSummary = useSheetStore((state) => state.setSummary); /** whether the current session has been authenticated */ const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus; sheetId: string } | void> => { @@ -58,6 +59,7 @@ export default function useGoogleSheet() { const data = await previewRundown(sheetId, fileOptions); setRundown(data.rundown); setCustomFields(data.customFields); + setSummary(data.summary); } catch (error) { patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } }); } diff --git a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/useSheetStore.ts b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/useSheetStore.ts index c2e49ae54..e323d4d57 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/useSheetStore.ts +++ b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/useSheetStore.ts @@ -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 { create } from 'zustand'; @@ -17,10 +17,10 @@ type SheetStore = { // we get this from a preview response rundown: Rundown | null; setRundown: (rundown: Rundown | null) => void; - - // we get this from a preview response customFields: CustomFields | null; setCustomFields: (customFields: CustomFields | null) => void; + summary: RundownSummary | null; + setSummary: (metadata: RundownSummary | null) => void; spreadsheetImportMap: ImportMap; patchSpreadsheetImportMap: (field: T, value: ImportMap[T]) => void; @@ -43,6 +43,7 @@ const initialState = { authenticationStatus: 'not_authenticated' as AuthenticationStatus, rundown: null, customFields: null, + summary: null, spreadsheetImportMap: defaultImportMap, }; @@ -64,6 +65,8 @@ export const useSheetStore = create((set, get) => ({ setCustomFields: (customFields: CustomFields | null) => set({ customFields }), + setSummary: (summary: RundownSummary | null) => set({ summary }), + patchSpreadsheetImportMap: (field: T, value: ImportMap[T]) => { const currentImportMap = get().spreadsheetImportMap; if (currentImportMap[field] !== value) { @@ -72,5 +75,5 @@ export const useSheetStore = create((set, get) => ({ }, reset: () => set(initialState), - resetPreview: () => set({ rundown: null, customFields: null }), + resetPreview: () => set({ rundown: null, customFields: null, summary: null }), })); diff --git a/apps/server/src/api-data/excel/excel.router.ts b/apps/server/src/api-data/excel/excel.router.ts index 1ca89eb09..f69c20c1c 100644 --- a/apps/server/src/api-data/excel/excel.router.ts +++ b/apps/server/src/api-data/excel/excel.router.ts @@ -1,6 +1,6 @@ import express 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'; @@ -32,7 +32,10 @@ router.post( router.post( '/preview', validateImportMapOptions, - (req: Request, res: Response<{ rundown: Rundown; customFields: CustomFields } | ErrorResponse>) => { + ( + req: Request, + res: Response<{ rundown: Rundown; customFields: CustomFields; summary: RundownSummary } | ErrorResponse>, + ) => { try { const { options } = req.body; const data = generateRundownPreview(options); diff --git a/apps/server/src/api-data/excel/excel.service.ts b/apps/server/src/api-data/excel/excel.service.ts index 73d4a6575..d3a47d8f1 100644 --- a/apps/server/src/api-data/excel/excel.service.ts +++ b/apps/server/src/api-data/excel/excel.service.ts @@ -3,7 +3,7 @@ * Google Sheets */ -import { CustomFields, Rundown } from 'ontime-types'; +import { CustomFields, Rundown, RundownSummary } from 'ontime-types'; import { type ImportMap } from 'ontime-utils'; import { extname } from 'path'; @@ -14,7 +14,7 @@ import type { WorkBook } from 'xlsx'; import { deleteFile } from '../../utils/fileManagement.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 { parseExcel } from './excel.parser.js'; @@ -43,7 +43,11 @@ export async function readExcelFile(filePath: string): Promise { 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]; 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 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, + }; } /** diff --git a/apps/server/src/api-data/sheets/sheets.controller.ts b/apps/server/src/api-data/sheets/sheets.controller.ts index df5d1a442..faded981e 100644 --- a/apps/server/src/api-data/sheets/sheets.controller.ts +++ b/apps/server/src/api-data/sheets/sheets.controller.ts @@ -3,7 +3,7 @@ * 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 { Request, Response } from 'express'; @@ -86,6 +86,7 @@ export async function readFromSheet( | { rundown: Rundown; customFields: CustomFields; + summary: RundownSummary; } | ErrorResponse >, diff --git a/apps/server/src/services/sheet-service/SheetService.ts b/apps/server/src/services/sheet-service/SheetService.ts index a89776ef2..703e9711a 100644 --- a/apps/server/src/services/sheet-service/SheetService.ts +++ b/apps/server/src/services/sheet-service/SheetService.ts @@ -15,6 +15,7 @@ import { MaybeString, OntimeGroup, Rundown, + RundownSummary, SupportedEntry, } from 'ontime-types'; import { ImportMap, getErrorMessage } from 'ontime-utils'; @@ -24,7 +25,7 @@ import { Credentials, OAuth2Client } from 'google-auth-library'; import { logger } from '../../classes/Logger.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 { parseCustomFields } from '../../api-data/custom-fields/customFields.parser.js'; import { consoleSubdued } from '../../utils/console.js'; @@ -455,6 +456,7 @@ export async function download( ): Promise<{ rundown: Rundown; customFields: CustomFields; + summary: RundownSummary; }> { if (!currentAuthClient) { throw new Error('Not authenticated'); @@ -488,9 +490,9 @@ export async function download( }; const customFields = parseCustomFields(dataModel); - const rundowns = parseRundowns(dataModel, customFields); + const parsedRundown = parseRundowns(dataModel, customFields); - const importedRundown = rundowns[rundownId]; + const importedRundown = parsedRundown[rundownId]; if (!importedRundown) { 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'); } - 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, + }; } diff --git a/packages/types/src/api/rundown-controller/BackendResponse.type.ts b/packages/types/src/api/rundown-controller/BackendResponse.type.ts index c44428879..af815b6d6 100644 --- a/packages/types/src/api/rundown-controller/BackendResponse.type.ts +++ b/packages/types/src/api/rundown-controller/BackendResponse.type.ts @@ -1,4 +1,5 @@ import type { EntryId, OntimeEntry } from '../../definitions/core/OntimeEntry.js'; +import type { MaybeNumber } from '../../utils/utils.type.js'; export type PatchWithId = Partial & { id: EntryId }; @@ -23,3 +24,9 @@ export type ProjectRundownsList = { loaded: string; rundowns: ProjectRundown[]; }; + +export type RundownSummary = { + duration: number; + start: MaybeNumber; + end: MaybeNumber; +}; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 24c921943..753701688 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -83,6 +83,7 @@ export type { ProjectRundown, ProjectRundownsList, TransientEventPayload, + RundownSummary, } from './api/rundown-controller/BackendResponse.type.js'; export type { LinkOptions } from './api/session-controller/BackendResponse.type.js';