@@ -239,9 +239,10 @@ public function getSubmissionsData(Form $form, string $fileFormat, ?File $file =
239239
240240 // Process initial header
241241 $ header = [];
242- $ header [] = $ this ->l10n ->t ('User ID ' );
243- $ header [] = $ this ->l10n ->t ('User display name ' );
244- $ header [] = $ this ->l10n ->t ('Timestamp ' );
242+ $ header [] = ['id ' => 'submission_id ' , 'title ' => $ this ->l10n ->t ('Submission ID ' )];
243+ $ header [] = ['id ' => 'user_id ' , 'title ' => $ this ->l10n ->t ('User ID ' )];
244+ $ header [] = ['id ' => 'user_display_name ' , 'title ' => $ this ->l10n ->t ('User display name ' )];
245+ $ header [] = ['id ' => 'timestamp ' , 'title ' => $ this ->l10n ->t ('Timestamp ' )];
245246 /** @var array<int, Question> $questionPerQuestionId */
246247 $ questionPerQuestionId = [];
247248 /** @var array<int, array<int, string>> $gridRowsPerQuestionId */
@@ -269,12 +270,12 @@ public function getSubmissionsData(Form $form, string $fileFormat, ?File $file =
269270
270271 foreach ($ gridRowsPerQuestionId [$ question ->getId ()] as $ rowId ) {
271272 if ($ gridCellType === Constants::ANSWER_GRID_TYPE_CHECKBOX || $ gridCellType === Constants::ANSWER_GRID_TYPE_RADIO ) {
272- $ header [] = $ question ->getText () . ' ( ' . $ optionPerOptionId [$ rowId ]->getText () . ') ' ;
273+ $ header [] = [ ' id ' => ' question-id- ' . $ question ->getId () . ' - ' . $ rowId , ' title ' => $ question -> getText () . ' ( ' . $ optionPerOptionId [$ rowId ]->getText () . ') ' ] ;
273274 }
274275
275276 if ($ gridCellType === Constants::ANSWER_GRID_TYPE_NUMBER ) {
276277 foreach ($ gridColumnsPerQuestionId [$ question ->getId ()] as $ columnId ) {
277- $ header [] = $ question ->getText () . ' ( ' . $ optionPerOptionId [$ rowId ]->getText () . ' - ' . $ optionPerOptionId [$ columnId ]->getText () . ') ' ;
278+ $ header [] = [ ' id ' => ' question-id- ' . $ question ->getId () . ' - ' . $ rowId . ' - ' . $ columnId , ' title ' => $ question -> getText () . ' ( ' . $ optionPerOptionId [$ rowId ]->getText () . ' - ' . $ optionPerOptionId [$ columnId ]->getText () . ') ' ] ;
278279 }
279280 }
280281 }
@@ -285,10 +286,10 @@ public function getSubmissionsData(Form $form, string $fileFormat, ?File $file =
285286 $ rankingOptionsPerQuestionId [$ question ->getId ()][] = $ option ->getId ();
286287 }
287288 foreach ($ rankingOptionsPerQuestionId [$ question ->getId ()] as $ optionId ) {
288- $ header [] = $ question ->getText () . ' ( ' . $ optionPerOptionId [$ optionId ]->getText () . ') ' ;
289+ $ header [] = [ ' id ' => ' question-id- ' . $ question ->getId () . ' - ' . $ optionId , ' title ' => $ question -> getText () . ' ( ' . $ optionPerOptionId [$ optionId ]->getText () . ') ' ] ;
289290 }
290291 } else {
291- $ header [] = $ question ->getText ();
292+ $ header [] = [ ' id ' => ' question-id- ' . $ question ->getId (), ' title ' => $ question -> getText ()] ;
292293 }
293294
294295 $ questionPerQuestionId [$ question ->getId ()] = $ question ;
@@ -301,6 +302,8 @@ public function getSubmissionsData(Form $form, string $fileFormat, ?File $file =
301302 foreach ($ submissionEntities as $ submission ) {
302303 $ row = [];
303304
305+ $ row [] = $ submission ->getId ();
306+
304307 // User
305308 $ user = $ this ->userManager ->get ($ submission ->getUserId ());
306309 if ($ user === null ) {
@@ -388,7 +391,7 @@ function (array $carry, Answer $answer) use ($questionPerQuestionId, $gridRowsPe
388391 }
389392
390393 /**
391- * @param array<int, string> $header
394+ * @param array<int, array{id: string, title: string} > $header
392395 * @param list<non-empty-list<array{columns?: list<mixed|string>, label?: string, url?: string}|mixed|null|string>> $data
393396 */
394397 private function exportData (array $ header , array $ data , string $ fileFormat , ?File $ file = null ): string {
@@ -411,34 +414,120 @@ private function exportData(array $header, array $data, string $fileFormat, ?Fil
411414 }
412415
413416 $ activeWorksheet = $ spreadsheet ->getSheet (0 );
414- foreach ($ header as $ columnIndex => $ value ) {
415- $ activeWorksheet ->setCellValue ([$ columnIndex + 1 , 1 ], $ value );
417+
418+ // Set column IDs in a hidden row
419+ $ activeWorksheet ->getRowDimension (2 )->setVisible (false );
420+
421+ // Get existing header
422+ $ existingHeaderIds = [];
423+ $ highestColumn = $ activeWorksheet ->getHighestColumn ();
424+ $ highestColumnIndex = \PhpOffice \PhpSpreadsheet \Cell \Coordinate::columnIndexFromString ($ highestColumn );
425+ for ($ col = 1 ; $ col <= $ highestColumnIndex ; $ col ++) {
426+ $ id = $ activeWorksheet ->getCell ([$ col , 2 ])->getValue ();
427+ if ($ id ) {
428+ $ existingHeaderIds [$ id ] = $ col ;
429+ }
430+ }
431+
432+ $ newHeaderIds = array_column ($ header , 'id ' );
433+ $ newHeaderTitles = array_column ($ header , 'title ' );
434+
435+ // Sync Columns
436+ $ colsToDelete = array_diff (array_keys ($ existingHeaderIds ), $ newHeaderIds );
437+ // Sort columns to delete by index descending
438+ $ colsToDeleteIndices = [];
439+ foreach ($ colsToDelete as $ colId ) {
440+ if (isset ($ existingHeaderIds [$ colId ])) {
441+ $ colsToDeleteIndices [] = $ existingHeaderIds [$ colId ];
442+ }
443+ }
444+ rsort ($ colsToDeleteIndices );
445+
446+ foreach ($ colsToDeleteIndices as $ colIndex ) {
447+ $ activeWorksheet ->removeColumnByIndex ($ colIndex , 1 );
448+ }
449+
450+ // Write header
451+ foreach ($ header as $ columnIndex => $ headerItem ) {
452+ $ activeWorksheet ->setCellValue ([$ columnIndex + 1 , 2 ], $ headerItem ['id ' ]);
453+ $ activeWorksheet ->setCellValue ([$ columnIndex + 1 , 1 ], $ headerItem ['title ' ]);
454+ }
455+
456+ // Get existing submissions
457+ $ existingSubmissionIds = [];
458+ $ highestRow = $ activeWorksheet ->getHighestRow ();
459+ $ submissionIdColIndex = array_search ('submission_id ' , $ newHeaderIds );
460+ $ submissionIdCol = $ submissionIdColIndex !== false ? $ submissionIdColIndex + 1 : 0 ;
461+
462+ if ($ submissionIdCol ) {
463+ for ($ row = 3 ; $ row <= $ highestRow ; $ row ++) {
464+ $ submissionId = $ activeWorksheet ->getCell ([$ submissionIdCol , $ row ])->getValue ();
465+ if ($ submissionId ) {
466+ $ existingSubmissionIds [(string )$ submissionId ] = $ row ;
467+ }
468+ }
416469 }
417- foreach ($ data as $ rowIndex => $ rowData ) {
418- $ column = 1 ;
419- foreach ($ rowData as $ value ) {
420- $ row = $ rowIndex + 2 ;
470+
471+ $ newSubmissionIds = [];
472+ if ($ submissionIdColIndex !== false ) {
473+ $ newSubmissionIds = array_map ('strval ' , array_column ($ data , $ submissionIdColIndex ));
474+ }
475+
476+ // Sync Rows
477+ $ rowsToDelete = array_diff (array_keys ($ existingSubmissionIds ), $ newSubmissionIds );
478+ $ deletedCount = 0 ;
479+ foreach ($ rowsToDelete as $ submissionId ) {
480+ $ rowIndex = $ existingSubmissionIds [$ submissionId ];
481+ $ activeWorksheet ->removeRow ($ rowIndex - $ deletedCount , 1 );
482+ $ deletedCount ++;
483+ }
484+
485+ // Re-map existing submission rows after deletion
486+ $ existingSubmissionIds = [];
487+ $ highestRow = $ activeWorksheet ->getHighestRow ();
488+ if ($ submissionIdCol ) {
489+ for ($ row = 3 ; $ row <= $ highestRow ; $ row ++) {
490+ $ submissionId = $ activeWorksheet ->getCell ([$ submissionIdCol , $ row ])->getValue ();
491+ if ($ submissionId ) {
492+ $ existingSubmissionIds [(string )$ submissionId ] = $ row ;
493+ }
494+ }
495+ }
496+
497+ // Update/Append data
498+ foreach ($ data as $ rowData ) {
499+ $ submissionId = (string )$ rowData [$ submissionIdColIndex ];
500+ $ row = $ existingSubmissionIds [$ submissionId ] ?? null ;
501+
502+ if ($ row === null ) {
503+ // Append new row
504+ $ row = $ activeWorksheet ->getHighestRow () + 1 ;
505+ }
506+
507+ $ dataIndex = 0 ;
508+ $ columnIndex = 1 ;
509+ while ($ dataIndex < count ($ rowData )) {
510+ $ value = $ rowData [$ dataIndex ];
421511
422512 if (is_array ($ value ) && isset ($ value ['label ' ])) { // file question type
423- $ activeWorksheet ->getCell ([$ column , $ row ])
513+ $ activeWorksheet ->getCell ([$ columnIndex , $ row ])
424514 ->setValueExplicit ($ value ['label ' ])
425515 ->getHyperlink ()
426516 ->setUrl ($ value ['url ' ]);
427-
428- $ activeWorksheet ->getStyle ([$ column , $ row ])
517+ $ activeWorksheet ->getStyle ([$ columnIndex , $ row ])
429518 ->getAlignment ()
430519 ->setWrapText (true );
520+ $ columnIndex ++;
431521 } elseif (is_array ($ value ) && isset ($ value ['columns ' ])) { // grid question type
432522 foreach ($ value ['columns ' ] as $ nestedValue ) {
433- $ this ->setCellValue ($ activeWorksheet , $ column , $ row , $ nestedValue , $ fileFormat );
434- $ column ++;
523+ $ this ->setCellValue ($ activeWorksheet , $ columnIndex , $ row , $ nestedValue , $ fileFormat );
524+ $ columnIndex ++;
435525 }
436- continue ; // no need to increment the column one more time
437526 } else {
438- $ this ->setCellValue ($ activeWorksheet , $ column , $ row , $ value , $ fileFormat );
527+ $ this ->setCellValue ($ activeWorksheet , $ columnIndex , $ row , $ value , $ fileFormat );
528+ $ columnIndex ++;
439529 }
440-
441- $ column ++;
530+ $ dataIndex ++;
442531 }
443532 }
444533
0 commit comments