-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreconcile-all.js
More file actions
277 lines (235 loc) · 8.43 KB
/
Copy pathreconcile-all.js
File metadata and controls
277 lines (235 loc) · 8.43 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
const { google } = require('googleapis');
const path = require('path');
async function main() {
const auth = new google.auth.GoogleAuth({
keyFile: path.join(__dirname, 'credentials.json'),
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth: await auth.getClient() });
const MASTER_ID = '1hSzGYowH0qFmODYJF08vrcvfdcCycs7ztSALjTSDpYs';
console.log('=== FULL RECONCILIATION ===\n');
// Get crew mappings
const mappings = await sheets.spreadsheets.values.get({
spreadsheetId: MASTER_ID,
range: 'Crew Mappings!A:G'
});
const mData = mappings.data.values;
const crewNameCol = mData[0].indexOf('Crew');
const urlCol = mData[0].indexOf('Sheet');
const crews = [];
for (let i = 1; i < mData.length; i++) {
const name = mData[i][crewNameCol];
const url = mData[i][urlCol];
if (name && url) {
const match = url.match(/\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/);
if (match) crews.push({ name, id: match[1] });
}
}
console.log('Crews:', crews.map(c => c.name).join(', '));
// Read Master Tasks
const master = await sheets.spreadsheets.values.get({
spreadsheetId: MASTER_ID,
range: "'Master Tasks'!A1:Z500"
});
const masterData = master.data.values || [];
let headerRow = -1;
for (let r = 0; r < masterData.length; r++) {
if (masterData[r] && masterData[r].includes('Task')) {
headerRow = r;
break;
}
}
const headers = masterData[headerRow];
const idCol = headers.indexOf('TaskID');
const taskCol = headers.indexOf('Task');
const crewsCol = headers.indexOf('Crews');
// Build master task index
const masterTasks = {};
for (let r = headerRow + 1; r < masterData.length; r++) {
const row = masterData[r];
if (!row || !row[taskCol]) continue;
const id = row[idCol];
if (!id) continue;
masterTasks[id] = {
row: r + 1,
data: row,
crews: (row[crewsCol] || '').split(',').map(c => c.trim().toLowerCase()).filter(Boolean)
};
}
console.log('Master has', Object.keys(masterTasks).length, 'tasks with IDs\n');
// Process each crew sheet
for (const crew of crews) {
console.log('--- Processing', crew.name, '---');
const crewKey = crew.name.toLowerCase();
const crewSheetName = crew.name + ' Crew';
try {
// Get sheet metadata to find the correct sheet ID
const metadata = await sheets.spreadsheets.get({
spreadsheetId: crew.id
});
const sheetInfo = metadata.data.sheets.find(s => s.properties.title === crewSheetName);
if (!sheetInfo) {
console.log(' Sheet not found:', crewSheetName);
continue;
}
const sheetId = sheetInfo.properties.sheetId;
// Read crew data
const crewResp = await sheets.spreadsheets.values.get({
spreadsheetId: crew.id,
range: `'${crewSheetName}'!A1:Z500`
});
const crewData = crewResp.data.values || [];
let crewHeaderRow = -1;
for (let r = 0; r < crewData.length; r++) {
if (crewData[r] && crewData[r].includes('Task')) {
crewHeaderRow = r;
break;
}
}
if (crewHeaderRow === -1) {
console.log(' No Tasks table found');
continue;
}
const crewHeaders = crewData[crewHeaderRow];
const crewIdCol = crewHeaders.indexOf('TaskID');
const crewTaskCol = crewHeaders.indexOf('Task');
// Build crew task index
const crewTasks = {};
const rowsToDelete = [];
for (let r = crewHeaderRow + 1; r < crewData.length; r++) {
const row = crewData[r];
if (!row || !row[crewTaskCol]) continue;
const id = row[crewIdCol];
if (!id) {
console.log(' Row', r + 1, 'has no TaskID - will skip');
continue;
}
// Check for duplicates
if (crewTasks[id]) {
console.log(' Duplicate TaskID at rows', crewTasks[id].row, 'and', r + 1, '- marking later one for delete');
rowsToDelete.push(r); // 0-indexed
continue;
}
// Check if this task should be in this crew
const masterTask = masterTasks[id];
if (!masterTask) {
console.log(' Task ID', id, 'not in Master - marking for delete');
rowsToDelete.push(r);
continue;
}
if (!masterTask.crews.includes(crewKey)) {
console.log(' Task "' + (row[crewTaskCol] || '').substring(0, 30) + '" should not be in', crew.name, '- marking for delete');
rowsToDelete.push(r);
continue;
}
crewTasks[id] = { row: r + 1, dataRowIndex: r };
}
// Delete rows that shouldn't be there (in reverse order to preserve indices)
if (rowsToDelete.length > 0) {
console.log(' Deleting', rowsToDelete.length, 'rows...');
const requests = rowsToDelete
.sort((a, b) => b - a) // Reverse order
.map(r => ({
deleteDimension: {
range: {
sheetId: sheetId,
dimension: 'ROWS',
startIndex: r,
endIndex: r + 1
}
}
}));
await sheets.spreadsheets.batchUpdate({
spreadsheetId: crew.id,
requestBody: { requests }
});
}
// Re-read crew data after deletions
const crewResp2 = await sheets.spreadsheets.values.get({
spreadsheetId: crew.id,
range: `'${crewSheetName}'!A1:Z500`
});
const crewData2 = crewResp2.data.values || [];
// Rebuild crew task index after deletions
const crewTasks2 = {};
for (let r = crewHeaderRow + 1; r < crewData2.length; r++) {
const row = crewData2[r];
if (!row || !row[crewTaskCol]) continue;
const id = row[crewIdCol];
if (id) crewTasks2[id] = { row: r + 1 };
}
// Find tasks that should be added
const tasksToAdd = [];
for (const [id, masterTask] of Object.entries(masterTasks)) {
if (masterTask.crews.includes(crewKey) && !crewTasks2[id]) {
tasksToAdd.push(masterTask);
}
}
if (tasksToAdd.length > 0) {
console.log(' Adding', tasksToAdd.length, 'missing tasks...');
// Build rows to add
const rowsToAdd = tasksToAdd.map(t => {
const newRow = [];
for (const h of crewHeaders) {
const masterIdx = headers.indexOf(h);
newRow.push(masterIdx >= 0 ? (t.data[masterIdx] || '') : '');
}
return newRow;
});
// Append rows
await sheets.spreadsheets.values.append({
spreadsheetId: crew.id,
range: `'${crewSheetName}'!A${crewHeaderRow + 2}`,
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
requestBody: { values: rowsToAdd }
});
}
// Re-read again after additions
const crewResp3 = await sheets.spreadsheets.values.get({
spreadsheetId: crew.id,
range: `'${crewSheetName}'!A1:Z500`
});
const crewData3 = crewResp3.data.values || [];
// Build batch update for all existing tasks to match Master
const batchData = [];
let updatedCount = 0;
for (let r = crewHeaderRow + 1; r < crewData3.length; r++) {
const row = crewData3[r];
if (!row || !row[crewTaskCol]) continue;
const id = row[crewIdCol];
if (!id) continue;
const masterTask = masterTasks[id];
if (!masterTask) continue;
// Build the expected row from Master
const expectedRow = [];
for (const h of crewHeaders) {
const masterIdx = headers.indexOf(h);
expectedRow.push(masterIdx >= 0 ? (masterTask.data[masterIdx] || '') : '');
}
const lastCol = String.fromCharCode(65 + crewHeaders.length - 1);
batchData.push({
range: `'${crewSheetName}'!A${r + 1}:${lastCol}${r + 1}`,
values: [expectedRow]
});
updatedCount++;
}
// Execute batch update
if (batchData.length > 0) {
await sheets.spreadsheets.values.batchUpdate({
spreadsheetId: crew.id,
requestBody: {
valueInputOption: 'RAW',
data: batchData
}
});
}
console.log(' Updated', updatedCount, 'tasks to match Master');
console.log(' Done!\n');
} catch (err) {
console.log(' Error:', err.message, '\n');
}
}
console.log('=== RECONCILIATION COMPLETE ===');
}
main().catch(console.error);