/ scripts / reexport-sheet.js
reexport-sheet.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * One-time script: Re-export pending outreaches to the persistent sheet
  5   * with the new 12-column layout (Country + Google Domain instead of Score)
  6   */
  7  
  8  import 'dotenv/config';
  9  import { google } from 'googleapis';
 10  import { createDatabaseConnection } from '../src/utils/db.js';
 11  
 12  const dbPath = process.env.DATABASE_PATH || './db/sites.db';
 13  const spreadsheetId = process.env.GOOGLE_SHEETS_SPREADSHEET_ID;
 14  
 15  if (!spreadsheetId) {
 16    console.error('GOOGLE_SHEETS_SPREADSHEET_ID not set in .env');
 17    process.exit(1);
 18  }
 19  
 20  const auth = new google.auth.JWT({
 21    email: process.env.GOOGLE_SHEETS_CLIENT_EMAIL,
 22    key: process.env.GOOGLE_SHEETS_PRIVATE_KEY?.replace(/\\n/g, '\n'),
 23    scopes: ['https://www.googleapis.com/auth/spreadsheets'],
 24  });
 25  const sheets = google.sheets({ version: 'v4', auth });
 26  const db = createDatabaseConnection(dbPath);
 27  
 28  try {
 29    const spreadsheet = await sheets.spreadsheets.get({ spreadsheetId });
 30    const sheetName = spreadsheet.data.sheets[0].properties.title;
 31    const tabId = spreadsheet.data.sheets[0].properties.sheetId;
 32  
 33    const outreaches = db
 34      .prepare(
 35        `SELECT o.id, s.domain, s.keyword,
 36          COALESCE(s.country_code, 'AU') as country_code,
 37          COALESCE(s.google_domain, 'google.com.au') as google_domain,
 38          o.contact_method, o.contact_uri, o.subject_line, o.proposal_text
 39        FROM outreaches o JOIN sites s ON o.site_id = s.id
 40        WHERE o.status = 'pending'
 41        ORDER BY o.id ASC`
 42      )
 43      .all();
 44  
 45    console.log(`Found ${outreaches.length} pending outreaches`);
 46  
 47    const headers = [
 48      'ID',
 49      'Domain',
 50      'Keyword',
 51      'Country',
 52      'Google Domain',
 53      'Method',
 54      'Contact',
 55      'Subject',
 56      'Proposal Text',
 57      'Action',
 58      'Rework Instructions',
 59      'Processed',
 60    ];
 61  
 62    const rows = outreaches.map(o => [
 63      o.id,
 64      o.domain,
 65      o.keyword,
 66      o.country_code,
 67      o.google_domain,
 68      o.contact_method,
 69      o.contact_uri,
 70      o.subject_line || '',
 71      o.proposal_text,
 72      '',
 73      '',
 74      '',
 75    ]);
 76  
 77    // Clear and rewrite
 78    await sheets.spreadsheets.values.clear({ spreadsheetId, range: sheetName });
 79    await sheets.spreadsheets.values.update({
 80      spreadsheetId,
 81      range: `${sheetName}!A1`,
 82      valueInputOption: 'RAW',
 83      requestBody: { values: [headers, ...rows] },
 84    });
 85  
 86    // Format sheet
 87    await sheets.spreadsheets.batchUpdate({
 88      spreadsheetId,
 89      requestBody: {
 90        requests: [
 91          // Header styling
 92          {
 93            repeatCell: {
 94              range: { sheetId: tabId, startRowIndex: 0, endRowIndex: 1 },
 95              cell: {
 96                userEnteredFormat: {
 97                  backgroundColor: { red: 0.2, green: 0.2, blue: 0.2 },
 98                  textFormat: { foregroundColor: { red: 1, green: 1, blue: 1 }, bold: true },
 99                  horizontalAlignment: 'CENTER',
100                },
101              },
102              fields: 'userEnteredFormat(backgroundColor,textFormat,horizontalAlignment)',
103            },
104          },
105          // Widen Proposal Text (col I, index 8)
106          {
107            updateDimensionProperties: {
108              range: { sheetId: tabId, dimension: 'COLUMNS', startIndex: 8, endIndex: 9 },
109              properties: { pixelSize: 400 },
110              fields: 'pixelSize',
111            },
112          },
113          // Wrap Proposal Text
114          {
115            repeatCell: {
116              range: { sheetId: tabId, startColumnIndex: 8, endColumnIndex: 9, startRowIndex: 1 },
117              cell: { userEnteredFormat: { wrapStrategy: 'WRAP' } },
118              fields: 'userEnteredFormat.wrapStrategy',
119            },
120          },
121          // Action dropdown (col J, index 9)
122          {
123            setDataValidation: {
124              range: {
125                sheetId: tabId,
126                startColumnIndex: 9,
127                endColumnIndex: 10,
128                startRowIndex: 1,
129                endRowIndex: rows.length + 1,
130              },
131              rule: {
132                condition: {
133                  type: 'ONE_OF_LIST',
134                  values: [
135                    { userEnteredValue: 'approve' },
136                    { userEnteredValue: 'rework' },
137                    { userEnteredValue: 'reject' },
138                  ],
139                },
140                showCustomUi: true,
141                strict: true,
142              },
143            },
144          },
145          // Green for approve
146          {
147            addConditionalFormatRule: {
148              rule: {
149                ranges: [
150                  { sheetId: tabId, startColumnIndex: 9, endColumnIndex: 10, startRowIndex: 1 },
151                ],
152                booleanRule: {
153                  condition: { type: 'TEXT_EQ', values: [{ userEnteredValue: 'approve' }] },
154                  format: { backgroundColor: { red: 0.7, green: 1, blue: 0.7 } },
155                },
156              },
157              index: 0,
158            },
159          },
160          // Yellow for rework
161          {
162            addConditionalFormatRule: {
163              rule: {
164                ranges: [
165                  { sheetId: tabId, startColumnIndex: 9, endColumnIndex: 10, startRowIndex: 1 },
166                ],
167                booleanRule: {
168                  condition: { type: 'TEXT_EQ', values: [{ userEnteredValue: 'rework' }] },
169                  format: { backgroundColor: { red: 1, green: 1, blue: 0.7 } },
170                },
171              },
172              index: 1,
173            },
174          },
175          // Red for reject
176          {
177            addConditionalFormatRule: {
178              rule: {
179                ranges: [
180                  { sheetId: tabId, startColumnIndex: 9, endColumnIndex: 10, startRowIndex: 1 },
181                ],
182                booleanRule: {
183                  condition: { type: 'TEXT_EQ', values: [{ userEnteredValue: 'reject' }] },
184                  format: { backgroundColor: { red: 1, green: 0.7, blue: 0.7 } },
185                },
186              },
187              index: 2,
188            },
189          },
190        ],
191      },
192    });
193  
194    // Mark all as exported
195    const stmt = db.prepare("UPDATE outreaches SET exported_at = datetime('now') WHERE id = ?");
196    const tx = db.transaction(ids => {
197      for (const id of ids) stmt.run(id);
198    });
199    tx(outreaches.map(o => o.id));
200  
201    console.log(`Done! Sheet updated with ${rows.length} rows and new 12-column layout.`);
202  } finally {
203    db.close();
204  }