diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts index 072e541bd..c40d37b0a 100644 --- a/apps/client/src/common/api/ontimeApi.ts +++ b/apps/client/src/common/api/ontimeApi.ts @@ -265,3 +265,14 @@ export const postPreviewSheet = async (sheetId: string, worksheet: string, optio }); return response.data.data; }; + +export const postPushSheet = async (sheetId: string, worksheet: string, options?: ExcelImportMap) => { + const response = await axios.post(`${ontimeURL}/sheet-push`, { + sheetid: sheetId, + worksheet: worksheet, + options: options, + }); + return response.data.data; +}; + + diff --git a/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx b/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx index caadb1ee6..b7516a604 100644 --- a/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx +++ b/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx @@ -17,6 +17,7 @@ import { postPreviewSheet, uploadSheetClientFile, patchData, + postPushSheet, } from '../../../common/api/ontimeApi'; import { OntimeRundown, ProjectData, UserFields } from 'ontime-types'; @@ -88,6 +89,12 @@ export default function SheetsModal(props: SheetsModalProps) { }); }; + const handlePushData = () => { + postPushSheet(sheetid.current?.value ?? '', worksheet.current?.value ?? '').then((data) => { + console.log(data); + }); + }; + const handleFinalise = async () => { // this step is currently only used for excel files, after preview if (rundown && userFields && project) { @@ -181,6 +188,9 @@ export default function SheetsModal(props: SheetsModalProps) { + )} diff --git a/apps/server/src/controllers/ontimeController.ts b/apps/server/src/controllers/ontimeController.ts index 4007cffbd..b2f1e0232 100644 --- a/apps/server/src/controllers/ontimeController.ts +++ b/apps/server/src/controllers/ontimeController.ts @@ -405,6 +405,19 @@ export async function previewSheet(req, res) { } } +/** + * downloads and parses an sheet + * @returns parsed result + */ +export async function pushSheet(req, res) { + try { + const data = await Sheet.push(req.body.sheetid, req.body.worksheet); + res.status(200).send('ok'); + } catch (error) { + res.status(500).send({ message: error.toString() }); + } +} + /** * uploads Client secrets file * @returns parsed result diff --git a/apps/server/src/routes/ontimeRouter.ts b/apps/server/src/routes/ontimeRouter.ts index a16a11f9d..fe2caf131 100644 --- a/apps/server/src/routes/ontimeRouter.ts +++ b/apps/server/src/routes/ontimeRouter.ts @@ -23,6 +23,7 @@ import { sheetAuthState, sheetClientFile, previewSheet, + pushSheet, } from '../controllers/ontimeController.js'; import { @@ -103,4 +104,7 @@ router.get('/sheet-authstatus', sheetAuthState); router.get('/sheet-authurl', sheetAuthUrl); // create route between controller and '/ontime/preview-sheet' endpoint -router.post('/sheet-preview', validateSheetPreview, previewSheet); \ No newline at end of file +router.post('/sheet-preview', validateSheetPreview, previewSheet); + +// create route between controller and '/ontime/preview-sheet' endpoint +router.post('/sheet-push', pushSheet); \ No newline at end of file diff --git a/apps/server/src/utils/sheetsAuth.ts b/apps/server/src/utils/sheetsAuth.ts index 0ce8c6d3f..1de920a08 100644 --- a/apps/server/src/utils/sheetsAuth.ts +++ b/apps/server/src/utils/sheetsAuth.ts @@ -6,10 +6,11 @@ import { URL } from 'url'; import { logger } from '../classes/Logger.js'; import { getAppDataPath } from '../setup.js'; import { DatabaseModel, LogOrigin, OntimeRundownEntry, isOntimeEvent } from 'ontime-types'; -import { ExcelImportOptions, isExcelImportMap, defaultExcelImportMap } from 'ontime-utils'; +import { ExcelImportOptions, isExcelImportMap, defaultExcelImportMap, millisToString } from 'ontime-utils'; import { parseExcel } from './parser.js'; import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js'; import { ensureDirectory } from './fileManagement.js'; +import { DataProvider } from '../classes/data-provider/DataProvider.js'; type ResponseOK = { data: Partial; @@ -47,6 +48,71 @@ class sheet { if (!isExcelImportMap(options)) { throw new Error('Got incorrect options to excel import', JSON.parse(options)); } + const rq = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.values.get({ + spreadsheetId: sheetId, + valueRenderOption: 'FORMATTED_VALUE', + majorDimension: 'ROWS', + range: worksheet + '!A:Z', //FIXME: this is an abitrary range + }); + if (rq.status === 200) { + const { projectMetadata, rundownMetadata } = parseExcel(rq.data.values, options); + const rundown = DataProvider.getRundown(); + const titleRow = Object.values(rundownMetadata)[0]['row']; + + const updateRundown = Array(); + + // we can't delete the last unflozzen row so we create an empty one + updateRundown.push({ + insertDimension: { + inheritFromBefore: false, + range: { dimension: 'ROWS', startIndex: titleRow + 1, endIndex: titleRow + 2, sheetId: 0 }, + }, + }); + //and delete the rest + updateRundown.push({ + deleteDimension: { range: { dimension: 'ROWS', startIndex: titleRow + 2, sheetId: 0 } }, + }); + // insert the lenght of the rundown + updateRundown.push({ + insertDimension: { + inheritFromBefore: false, + range: { + dimension: 'ROWS', + startIndex: titleRow + 1, + endIndex: titleRow + rundown.length, + sheetId: 0, + }, + }, + }); + + //TODO: this is a mess + const titleCol = Object.values(rundownMetadata) + // .filter(([key, value]) => value !== undefined) + .reduce((accumulator: number, val) => Math.min(accumulator, val['col']), Number.MAX_VALUE); + if (titleCol == Number.MAX_VALUE) { + throw new Error(`could not finde starting title column`); + } + //update the corespunding row with event data + rundown.forEach((entry, index) => + updateRundown.push(this.cellRequenstFromEvent(entry, index, rundownMetadata, titleCol as number)), + ); + const writeResponds = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.batchUpdate({ + spreadsheetId: sheetId, + requestBody: { + includeSpreadsheetInResponse: false, + responseRanges: [worksheet + '!A:Z'], //FIXME: + requests: updateRundown, + }, + }); + + if (writeResponds.status == 200) { + logger.info(LogOrigin.Server, `Sheet write: ${writeResponds.statusText}`); + } else { + throw new Error(`Sheet write faild: ${writeResponds.statusText}`); + } + } else { + throw new Error(`Sheet read faild: ${rq.statusText}`); + } } /** @@ -149,51 +215,68 @@ class sheet { } /** - * @param index - the index of the event in Sheet dimentions + * @param index - the index of the event in rundown dimentions */ - private cellRequenstFromEvent(event: OntimeRundownEntry, index: number): sheets_v4.Schema$Request['updateCells'] { - const titelCol = 1 - // index += titelRow; + private cellRequenstFromEvent( + event: OntimeRundownEntry, + index: number, + metadata, + titleCol: number, + ): sheets_v4.Schema$Request { let r: sheets_v4.Schema$CellData[] = []; - Object.entries(this.metadata['rundown']) + const tmp = Object.entries(metadata) .filter(([key, value]) => value !== undefined) - .sort(([aKey, a], [bKey, b]) => a['col'] - b['col']) - .forEach(([key, value]) => { - if (isOntimeEvent(event)) { - if (key === 'colour') { - r.push({ - userEnteredValue: { stringValue: event.colour }, - userEnteredFormat: { backgroundColor: hexToRgb(event?.colour ?? null) }, - }); - } else if (typeof event[key] === 'number') { - r.push({ - userEnteredValue: { stringValue: millisToString(event[key], true) }, - }); - } else if (typeof event[key] === 'string') { - r.push({ - userEnteredValue: { stringValue: event[key] }, - }); - } else if (typeof event[key] === 'boolean') { - r.push({ - userEnteredValue: { stringValue: event[key] ? 'x' : '' }, - }); - } else { - r.push({}); - } + .sort(([aKey, a], [bKey, b]) => a['col'] - b['col']); + + tmp.forEach(([key, value], index, arr) => { + if (index != 0) { + if (arr[index - 1][1]['col'] + 1 < value['col']) { + console.log('missing', key, value['col'], arr[index - 1][1]['col']); + arr.splice(index, 0, ['blank', { col: arr[index - 1][1]['col'] + 1 }]); } - }); + } + }); + console.log(tmp); + + tmp.forEach(([key, value]) => { + if (isOntimeEvent(event)) { + if (key === 'blank') { + r.push({}); + } else if (key === 'colour') { + r.push({ + userEnteredValue: { stringValue: event.colour }, + }); + } else if (typeof event[key] === 'number') { + r.push({ + userEnteredValue: { stringValue: millisToString(event[key], true) }, + }); + } else if (typeof event[key] === 'string') { + r.push({ + userEnteredValue: { stringValue: event[key] }, + }); + } else if (typeof event[key] === 'boolean') { + r.push({ + userEnteredValue: { stringValue: event[key] ? 'x' : '' }, + }); + } else { + r.push({}); + } + } + }); return { - start: { - sheetId: 0, //FIXME: get from smalest medata col - rowIndex: index, - columnIndex: titelCol, - }, - fields: 'userEnteredValue,userEnteredFormat.backgroundColor', - rows: [ - { - values: r, + updateCells: { + start: { + sheetId: 0, //FIXME: + rowIndex: index + Object.values(metadata)[0]['row'] + 1, + columnIndex: titleCol, }, - ], + fields: 'userEnteredValue', + rows: [ + { + values: r, + }, + ], + }, }; }