-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGoogleAppsScript.gs
More file actions
1012 lines (873 loc) · 31.5 KB
/
GoogleAppsScript.gs
File metadata and controls
1012 lines (873 loc) · 31.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Main entry point for web requests
* This function delegates to the appropriate handler based on the HTTP method
*/
/*
****************************************************************************************************************************
Replace SPREADSHEET_ID below with your spreadsheet ID with edit access enabled otherwise, it won't work
*/
// Set your spreadsheet ID here - only need to change in one place
const SPREADSHEET_ID = "YOUR_SPREADSHEET_ID";
/**
* Main entry point for web requests
* This function delegates to the appropriate handler based on the HTTP method
*/
function doRequest(request) {
const method = request.method || 'GET';
if (method === 'POST') {
return doPost(request);
} else if (method === 'OPTIONS') {
return doOptions(request);
} else {
return doGet(request);
}
}
/**
* Handles OPTIONS requests for CORS preflight
*/
function doOptions(e) {
// Create a proper CORS response for preflight requests
// This uses the HtmlService which correctly handles headers
return HtmlService.createHtmlOutput('')
.addHeader('Access-Control-Allow-Origin', '*')
.addHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
.addHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
.addHeader('Access-Control-Max-Age', '3600');
}
/**
* Main entry point for web requests
* This function delegates to the appropriate handler based on the HTTP method
*/
function doGet(e) {
// Handle OPTIONS requests as well - this is needed because Google Apps Script doesn't properly support OPTIONS
// This workaround detects if X-HTTP-Method-Override is set to OPTIONS
const headers = {};
if (e && e.parameter && e.parameter['X-HTTP-Method-Override'] === 'OPTIONS') {
return handleOptionsRequest();
}
// Normal GET request handling
if (e && e.parameter && e.parameter.action === 'getProblems') {
return getAllProblems();
}
if (e && e.parameter && e.parameter.action === 'getDueReviews') {
return getDueReviews();
}
// Create a response with necessary CORS headers
const response = {
"status": "online",
"message": "Google Sheets API is running"
};
return createJsonResponse(response);
}
/**
* Handle OPTIONS requests (for CORS preflight)
* This is a workaround for Google Apps Script's limited OPTIONS support
*/
function handleOptionsRequest() {
const output = ContentService.createTextOutput(JSON.stringify({
status: "success",
message: "CORS preflight handled"
}));
output.setMimeType(ContentService.MimeType.JSON);
return output;
}
/**
* Returns all problem data as JSON for the web UI
* This is used by the Next.js application to display problems
*/
function getAllProblems() {
try {
// Use the centralized spreadsheet ID variable
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getActiveSheet();
const data = sheet.getDataRange().getValues();
if (data.length <= 1) {
// No data or only headers
const response = {
"success": true,
"problems": []
};
return createJsonResponse(response);
}
const headerRow = data[0];
// Convert sheet data to JSON
const problems = [];
for (let i = 1; i < data.length; i++) {
const problem = {};
for (let j = 0; j < headerRow.length; j++) {
// Handle date objects for the Timestamp column
if (j === 0 && data[i][j] instanceof Date) {
problem[headerRow[j]] = data[i][j].toISOString();
} else {
problem[headerRow[j]] = data[i][j];
}
}
problems.push(problem);
}
const response = {
"success": true,
"problems": problems
};
return createJsonResponse(response);
} catch (error) {
const errorResponse = {
"success": false,
"error": error.toString()
};
return createJsonResponse(errorResponse);
}
}
/**
* Gets problems due for review
*/
function getDueReviews() {
try {
// Use the centralized spreadsheet ID variable
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const revisionSheet = ss.getSheetByName("Revision Schedule");
if (!revisionSheet) {
return createJsonResponse({
"success": false,
"error": "Revision Schedule sheet not found. SRS system may not be set up."
});
}
// Get the current date
const today = new Date();
today.setHours(0, 0, 0, 0);
// Get all data from the revision sheet
const data = revisionSheet.getDataRange().getValues();
if (data.length <= 1) {
return createJsonResponse({
"success": true,
"dueReviews": []
});
}
const headerRow = data[0];
// Find indices for key columns
const idIndex = headerRow.findIndex(col => col === "Problem ID");
const titleIndex = headerRow.findIndex(col => col === "Title");
const nextReviewIndex = headerRow.findIndex(col => col === "Next Review Date");
const reviewCountIndex = headerRow.findIndex(col => col === "Review Count");
const statusIndex = headerRow.findIndex(col => col === "Status");
if (idIndex === -1 || nextReviewIndex === -1 || statusIndex === -1) {
return createJsonResponse({
"success": false,
"error": "Required columns not found in Revision Schedule sheet"
});
}
// Find problems due today or earlier
const dueReviews = [];
for (let i = 1; i < data.length; i++) {
const nextReviewDate = new Date(data[i][nextReviewIndex]);
nextReviewDate.setHours(0, 0, 0, 0);
if (nextReviewDate <= today && data[i][statusIndex] === "Pending") {
// Get main sheet data for this problem ID
const problemDetails = getProblemDetails(data[i][idIndex]);
dueReviews.push({
id: data[i][idIndex],
title: data[i][titleIndex] || "",
reviewCount: data[i][reviewCountIndex] || 0,
nextReviewDate: nextReviewDate.toISOString(),
details: problemDetails // Additional details from main sheet
});
}
}
return createJsonResponse({
"success": true,
"dueReviews": dueReviews
});
} catch (error) {
return createJsonResponse({
"success": false,
"error": error.toString()
});
}
}
/**
* Handles POST requests from the userscript
* Adds the problem data to a Google Sheet
*/
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
// Handle review completion
if (data.action === 'completeReview') {
return completeReview(data);
}
// Handle adding problem to review schedule
if (data.action === 'addToReview') {
return addProblemToReview(data);
}
// Handle bulk adding problems to review schedule
if (data.action === 'bulkAddToReview') {
return bulkAddToReview(data);
}
// Use the centralized spreadsheet ID variable
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getActiveSheet();
// Validate required fields
if (!data.id || !data.title) {
const errorResponse = {
"error": "Missing required fields",
"result": "error"
};
return createJsonResponse(errorResponse);
}
// Check if this problem already exists in the sheet
const existingEntry = findExistingProblem(sheet, data.id);
let statusMessage = "";
if (existingEntry && existingEntry.rowIndex > 0) {
// Problem already exists, update the row
if (data.update !== false) { // Allow forcing a new entry if update=false
updateExistingProblem(sheet, existingEntry, data);
statusMessage = "Entry already found - updated existing record for Problem #" + data.id;
Logger.log(statusMessage);
} else {
// Add as a new entry if the caller explicitly requests it
addNewProblemRow(sheet, data);
statusMessage = "Entry found but new entry requested - inserted new record for Problem #" + data.id;
Logger.log(statusMessage);
}
} else {
// Problem doesn't exist, add a new row
addNewProblemRow(sheet, data);
statusMessage = "Entry not found - inserted new record for Problem #" + data.id;
Logger.log(statusMessage);
}
const successResponse = {
"result": "success",
"message": statusMessage,
"wasUpdated": !!(existingEntry && data.update !== false)
};
return createJsonResponse(successResponse);
} catch (error) {
const errorResponse = {
"error": error.toString(),
"result": "error",
"stack": error.stack
};
return createJsonResponse(errorResponse);
}
}
/**
* Adds a problem to the review schedule
*/
function addProblemToReview(data) {
try {
// Validate required data
if (!data.id) {
return createJsonResponse({
"success": false,
"error": "Problem ID is required"
});
}
// Get Revision Schedule sheet
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const revisionSheet = ss.getSheetByName("Revision Schedule");
if (!revisionSheet) {
return createJsonResponse({
"success": false,
"error": "Revision Schedule sheet not found. Please set up SRS system first."
});
}
// Calculate next review date
const daysUntilReview = parseInt(data.days) || 1;
if (isNaN(daysUntilReview) || daysUntilReview < 1) {
return createJsonResponse({
"success": false,
"error": "Days until review must be a positive number"
});
}
const reviewDate = new Date();
reviewDate.setDate(reviewDate.getDate() + daysUntilReview);
// Get the problem title (from data or lookup in main sheet)
let title = data.title || "";
if (!title) {
title = getProblemTitle(data.id);
}
// Check if problem already exists in revision schedule
const existingProblem = findProblemInRevisionSchedule(revisionSheet, data.id);
if (existingProblem) {
if (data.updateIfExists === true) {
// Update existing problem's review date
revisionSheet.getRange(existingProblem.rowIndex, 4, 1, 2).setValues([[
reviewDate, // Next Review Date
daysUntilReview // Interval (days)
]]);
return createJsonResponse({
"success": true,
"message": `Problem ${data.id} already exists in revision schedule. Updated review date.`,
"wasUpdated": true,
"nextReviewDate": reviewDate.toISOString()
});
} else {
return createJsonResponse({
"success": false,
"error": `Problem ${data.id} already exists in revision schedule.`,
"exists": true
});
}
}
// Ensure revision sheet has headers
let hasHeaders = true;
if (revisionSheet.getLastRow() === 0) {
// Sheet is empty, add headers
revisionSheet.getRange(1, 1, 1, 7).setValues([[
"Problem ID", "Title", "Last Review Date", "Next Review Date", "Review Count", "Interval", "Status"
]]);
revisionSheet.getRange("A1:G1").setFontWeight("bold");
}
// Add new problem to revision schedule
const nextRow = revisionSheet.getLastRow() + 1;
revisionSheet.getRange(nextRow, 1, 1, 7).setValues([[
data.id, // Problem ID
title, // Title
new Date(), // Last Review Date (today)
reviewDate, // Next Review Date
0, // Review Count
daysUntilReview, // Interval (days)
data.status || "Pending" // Status
]]);
return createJsonResponse({
"success": true,
"message": `Problem ${data.id} added to revision schedule for review in ${daysUntilReview} day(s).`,
"id": data.id,
"nextReviewDate": reviewDate.toISOString()
});
} catch (error) {
return createJsonResponse({
"success": false,
"error": error.toString()
});
}
}
/**
* Handles bulk adding problems to the review schedule
* Supports different types of bulk operations:
* - 'all': Add all problems not already in revision schedule
* - 'today': Add problems from today
* - 'selected': Add specific problems by IDs
*/
function bulkAddToReview(data) {
try {
// Validate bulk type
if (!data.bulkType) {
return createJsonResponse({
"success": false,
"error": "Bulk type is required (all, today, or selected)"
});
}
// Get the spreadsheet
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
if (!ss) {
return createJsonResponse({
"success": false,
"error": "Could not open spreadsheet. Check if the SPREADSHEET_ID is correct."
});
}
// Get main and revision sheets
const mainSheet = ss.getActiveSheet();
const revisionSheet = ss.getSheetByName("Revision Schedule");
if (!revisionSheet) {
return createJsonResponse({
"success": false,
"error": "Revision Schedule sheet not found. Please set up SRS system first."
});
}
// Ensure days parameter is valid
const daysUntilReview = parseInt(data.days) || 1;
if (isNaN(daysUntilReview) || daysUntilReview < 1) {
return createJsonResponse({
"success": false,
"error": "Days until review must be a positive number"
});
}
// Calculate next review date
const reviewDate = new Date();
reviewDate.setDate(reviewDate.getDate() + daysUntilReview);
// Get all problems from main sheet
const problemData = mainSheet.getDataRange().getValues();
// Find the ID and title columns
const headers = problemData[0].map(header => String(header).trim().toLowerCase());
let idIndex = -1;
let titleIndex = -1;
let timestampIndex = -1;
for (let i = 0; i < headers.length; i++) {
const header = headers[i].toLowerCase();
if (header === 'id') {
idIndex = i;
} else if (header === 'title') {
titleIndex = i;
} else if (header === 'timestamp' || header === 'date' || header === 'time') {
timestampIndex = i;
}
}
if (idIndex === -1 || titleIndex === -1) {
return createJsonResponse({
"success": false,
"error": "Could not find ID or Title columns in the main sheet"
});
}
// Get existing problem IDs in revision schedule
let existingProblems = [];
if (revisionSheet.getLastRow() > 1) {
const existingData = revisionSheet.getRange(2, 1, Math.max(1, revisionSheet.getLastRow() - 1), 1).getValues();
existingProblems = existingData.map(row => String(row[0])).filter(id => id !== "");
}
// Ensure revision sheet has headers
if (revisionSheet.getLastRow() === 0) {
// Sheet is empty, add headers
revisionSheet.getRange(1, 1, 1, 7).setValues([[
"Problem ID", "Title", "Last Review Date", "Next Review Date", "Review Count", "Interval", "Status"
]]);
revisionSheet.getRange("A1:G1").setFontWeight("bold");
}
// Variables to track results
let addedCount = 0;
const problemsAdded = [];
let errors = [];
// Handle different bulk types
switch (data.bulkType) {
case 'all':
// Add all problems not already in revision schedule
for (let i = 1; i < problemData.length; i++) {
const row = problemData[i];
const id = String(row[idIndex]);
const title = String(row[titleIndex] || "");
if (id && !existingProblems.includes(id)) {
try {
const nextRow = revisionSheet.getLastRow() + 1;
revisionSheet.getRange(nextRow, 1, 1, 7).setValues([[
id, // Problem ID
title, // Title
new Date(), // Last Review Date (today)
reviewDate, // Next Review Date
0, // Review Count
daysUntilReview, // Interval (days)
"Pending" // Status
]]);
addedCount++;
problemsAdded.push(id);
existingProblems.push(id); // Update tracking array to avoid duplicates
} catch (e) {
errors.push(`Error adding problem ${id}: ${e.toString()}`);
}
}
}
break;
case 'today':
// Get today's date for comparison
const today = new Date();
today.setHours(0, 0, 0, 0);
// Add today's problems to revision schedule
for (let i = 1; i < problemData.length; i++) {
const row = problemData[i];
if (!row) continue;
const id = String(row[idIndex] || "");
const title = String(row[titleIndex] || "");
// Skip if ID is missing or already in schedule
if (!id || existingProblems.includes(id)) continue;
// Should we add this problem? Default depends on if we found timestamp column
let addProblem = timestampIndex === -1;
// If we have timestamp column, check if problem is from today
if (timestampIndex !== -1 && row.length > timestampIndex) {
const timestamp = row[timestampIndex];
if (timestamp && timestamp instanceof Date) {
// Check if the problem was added today
const problemDate = new Date(timestamp);
problemDate.setHours(0, 0, 0, 0);
if (problemDate.getTime() === today.getTime()) {
addProblem = true;
}
}
}
if (addProblem) {
try {
// Calculate next row safely (1-based index in Sheets)
const nextRow = revisionSheet.getLastRow() + 1;
// Add the problem to revision schedule
revisionSheet.getRange(nextRow, 1, 1, 7).setValues([[
id, // Problem ID
title, // Title
new Date(), // Last Review Date (today)
reviewDate, // Next Review Date
0, // Review Count
daysUntilReview, // Interval (days)
"Pending" // Status
]]);
addedCount++;
problemsAdded.push(id);
existingProblems.push(id); // Update tracking array to avoid duplicates
} catch (e) {
errors.push(`Error adding problem ${id}: ${e.toString()}`);
}
}
}
break;
case 'selected':
// Validate selected IDs array
if (!data.ids || !Array.isArray(data.ids) || data.ids.length === 0) {
return createJsonResponse({
"success": false,
"error": "Selected problem IDs array is required"
});
}
// Add selected problems to revision schedule
for (let i = 0; i < data.ids.length; i++) {
const id = String(data.ids[i]);
// Skip if ID is already in schedule
if (existingProblems.includes(id)) {
errors.push(`Problem ${id} already exists in review schedule`);
continue;
}
// Find the problem in the main sheet
let title = '';
for (let j = 1; j < problemData.length; j++) {
if (String(problemData[j][idIndex]) === id) {
title = String(problemData[j][titleIndex] || "");
break;
}
}
if (!title) {
errors.push(`Problem ${id} not found in main sheet`);
continue;
}
try {
const nextRow = revisionSheet.getLastRow() + 1;
revisionSheet.getRange(nextRow, 1, 1, 7).setValues([[
id, // Problem ID
title, // Title
new Date(), // Last Review Date (today)
reviewDate, // Next Review Date
0, // Review Count
daysUntilReview, // Interval (days)
"Pending" // Status
]]);
addedCount++;
problemsAdded.push(id);
existingProblems.push(id); // Update tracking array to avoid duplicates
} catch (e) {
errors.push(`Error adding problem ${id}: ${e.toString()}`);
}
}
break;
default:
return createJsonResponse({
"success": false,
"error": `Unknown bulk type: ${data.bulkType}`
});
}
return createJsonResponse({
"success": true,
"message": `Added ${addedCount} problems to review in ${daysUntilReview} day(s)`,
"addedCount": addedCount,
"addedProblems": problemsAdded,
"errors": errors.length > 0 ? errors : undefined
});
} catch (error) {
return createJsonResponse({
"success": false,
"error": error.toString()
});
}
}
/**
* Updates a problem's review status
*/
function completeReview(data) {
try {
// Use the centralized spreadsheet ID variable
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const revisionSheet = ss.getSheetByName("Revision Schedule");
if (!revisionSheet) {
return createJsonResponse({
"success": false,
"error": "Revision Schedule sheet not found"
});
}
// Validate required fields
if (!data.id) {
return createJsonResponse({
"success": false,
"error": "Problem ID is required"
});
}
// Find the problem row
const sheetData = revisionSheet.getDataRange().getValues();
let problemRow = -1;
for (let i = 1; i < sheetData.length; i++) {
if (sheetData[i][0] == data.id) {
problemRow = i + 1; // Convert to 1-indexed row number
break;
}
}
if (problemRow === -1) {
return createJsonResponse({
"success": false,
"error": `Problem ID ${data.id} not found in revision schedule`
});
}
// Get current data for the problem
const rowData = sheetData[problemRow - 1];
const reviewCount = rowData[4];
// Process based on difficulty rating or custom interval
let nextInterval = 1; // Default: review tomorrow
if (data.difficulty === "custom" && data.nextInterval) {
// Use the user-specified interval
nextInterval = parseInt(data.nextInterval);
if (isNaN(nextInterval) || nextInterval < 1) nextInterval = 1;
} else if (data.difficulty === "easy") {
nextInterval = Math.min(30, reviewCount * 2 + 1); // Exponential growth, max 30 days
} else if (data.difficulty === "medium") {
nextInterval = Math.min(14, reviewCount + 2); // Linear growth, max 14 days
} else if (data.difficulty === "hard") {
nextInterval = 1; // Always tomorrow for hard problems
} else if (data.nextInterval) {
// Use provided interval if difficulty not specified
nextInterval = parseInt(data.nextInterval);
if (isNaN(nextInterval) || nextInterval < 1) nextInterval = 1;
}
// Calculate next review date
const nextReviewDate = new Date();
if (data.complete === false) {
// If marked as not complete, schedule for tomorrow
nextReviewDate.setDate(nextReviewDate.getDate() + 1);
nextInterval = 1;
} else {
// Normal schedule based on difficulty
nextReviewDate.setDate(nextReviewDate.getDate() + nextInterval);
}
// Update the problem row
revisionSheet.getRange(problemRow, 3, 1, 5).setValues([[
new Date(), // Last Review Date (today)
nextReviewDate, // Next Review Date
reviewCount + 1, // Review Count
nextInterval, // Interval (days)
"Pending" // Status
]]);
// Log the review in history if we have a history sheet
const historySheet = ss.getSheetByName("Review History");
if (historySheet) {
const nextRow = historySheet.getLastRow() + 1;
historySheet.getRange(nextRow, 1, 1, 4).setValues([[
data.id,
new Date(),
data.difficulty || "medium",
nextInterval
]]);
}
return createJsonResponse({
"success": true,
"message": `Review completed for problem ${data.id}. Next review scheduled in ${nextInterval} days.`,
"nextReviewDate": nextReviewDate.toISOString(),
"nextInterval": nextInterval
});
} catch (error) {
return createJsonResponse({
"success": false,
"error": error.toString()
});
}
}
/**
* Helper function to find a problem in the revision schedule
*/
function findProblemInRevisionSchedule(sheet, problemId) {
const data = sheet.getDataRange().getValues();
if (data.length <= 1) return null; // Empty or just headers
// Search for the problem ID (first column)
for (let i = 1; i < data.length; i++) {
if (data[i][0] == problemId) {
return {
rowData: data[i],
rowIndex: i + 1 // +1 because sheet rows are 1-indexed
};
}
}
return null;
}
/**
* Helper function to get problem title from main sheet
*/
function getProblemTitle(problemId) {
try {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getActiveSheet();
const problem = findExistingProblem(sheet, problemId);
if (problem && problem.rowData) {
// Title is in the third column (index 2) in the main sheet
return problem.rowData[2] || "";
}
return "";
} catch (error) {
Logger.log("Error getting problem title: " + error.toString());
return "";
}
}
/**
* Utility to get problem details from main sheet
*/
function getProblemDetails(problemId) {
try {
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getActiveSheet();
const data = sheet.getDataRange().getValues();
if (data.length <= 1) return null;
const headers = data[0];
const idIndex = headers.findIndex(col => col === "ID");
if (idIndex === -1) return null;
for (let i = 1; i < data.length; i++) {
if (data[i][idIndex] == problemId) {
const problem = {};
for (let j = 0; j < headers.length; j++) {
problem[headers[j]] = data[i][j];
}
return problem;
}
}
return null;
} catch (error) {
Logger.log("Error getting problem details: " + error.toString());
return null;
}
}
/**
* Helper for creating JSON responses with CORS headers
*/
function createJsonResponse(obj) {
// Since setHeader isn't available in older Google Apps Script versions,
// we include the CORS headers directly in the JSON response
// Add CORS headers manually to response object
obj.cors = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
};
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}
/**
* Finds an existing problem entry by ID
* @param {Sheet} sheet The Google Sheet to search
* @param {string} problemId The problem ID to search for
* @return {Object|null} The row data and index if found, null otherwise
*/
function findExistingProblem(sheet, problemId) {
// Get all data from the sheet
const data = sheet.getDataRange().getValues();
if (data.length <= 1) return null; // Empty or just headers
// Find the index of the ID column (should be column 1 - the second column)
const idColIndex = 1;
// Search for the problem ID
for (let i = 1; i < data.length; i++) { // Start at 1 to skip headers
if (data[i][idColIndex] == problemId) {
return {
rowData: data[i],
rowIndex: i + 1 // +1 because sheet rows are 1-indexed
};
}
}
return null;
}
/**
* Updates an existing problem entry with new data
*/
function updateExistingProblem(sheet, existingEntry, newData) {
// Process the tags array
let tagsString = "";
if (newData.tags && Array.isArray(newData.tags)) {
tagsString = newData.tags.join(", ");
}
// Format first attempt result as text (if available)
let firstAttemptText = "Not recorded";
if (newData.firstAttemptSuccess === true) {
firstAttemptText = "Success";
} else if (newData.firstAttemptSuccess === false) {
firstAttemptText = "Failed";
}
// Prepare updated row data
const updatedRow = [
new Date(), // Timestamp (always update)
newData.id || "", // Problem ID
newData.title || "", // Problem title
newData.difficulty || "", // Difficulty
tagsString, // Tags
newData.url || "", // URL (not Link)
newData.status || "Pending", // Status
newData.remarks || existingEntry.rowData[7] || "" // Remarks (not Remark)
];
// Add performance metrics
if (sheet.getLastColumn() >= 12) { // Check if we have the performance columns
updatedRow.push(
newData.solveTime || existingEntry.rowData[8] || "",
newData.solveTimeSeconds || existingEntry.rowData[9] || "",
newData.firstAttemptSuccess !== undefined ? firstAttemptText : existingEntry.rowData[10] || "Not recorded",
newData.confidence || existingEntry.rowData[11] || ""
);
}
// Update the row
sheet.getRange(existingEntry.rowIndex, 1, 1, updatedRow.length).setValues([updatedRow]);
}
/**
* Adds a new problem row to the sheet
*/
function addNewProblemRow(sheet, data) {
// Process the tags array
let tagsString = "";
if (data.tags && Array.isArray(data.tags)) {
tagsString = data.tags.join(", ");
}
// Format first attempt result as text
let firstAttemptText = "Not recorded";
if (data.firstAttemptSuccess === true) {
firstAttemptText = "Success";
} else if (data.firstAttemptSuccess === false) {
firstAttemptText = "Failed";
}
// Check if the sheet has headers
let hasHeaders = false;
let headerRow = [];
try {
headerRow = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
hasHeaders = headerRow.length > 0;
} catch (err) {
hasHeaders = false;
}
// If sheet is empty or doesn't have headers yet, create them
if (!hasHeaders || sheet.getLastRow() === 0) {
// Use the exact column names from user's spreadsheet
const headers = [
"Timestamp", "ID", "Title", "Difficulty", "Tags", "URL", "Status", "Remarks",
"Solve Time", "Solve Time (sec)", "First Attempt", "Confidence"
];
sheet.appendRow(headers);
}
// Prepare the data row
const rowData = [
new Date(), // Timestamp
data.id || "", // Problem ID
data.title || "", // Problem title
data.difficulty || "", // Difficulty
tagsString, // Tags
data.url || "", // URL (not Link)
data.status || "Pending", // Status
data.remarks || "", // Remarks (not Remark)
data.solveTime || "", // Solve time (formatted)
data.solveTimeSeconds || "", // Solve time in seconds
firstAttemptText, // First attempt result
data.confidence || "" // Confidence rating
];
// Append the row once
sheet.appendRow(rowData);
}
/**
* Utility function to ensure required columns exist in the sheet
* This can be used for future expansion