File size: 12,427 Bytes
7ff6662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Google Sheets + Google Drive integration for Job Automation Agent.

Writes all assessed jobs to:
  Sheet: https://docs.google.com/spreadsheets/d/SHEET_ID
  Drive: "Job Automation Agent β€” Resumes" folder

Each run APPENDS rows (never overwrites), so you keep a history of all searches.

Authentication:
  Option A (Service Account β€” for servers/automation):
    β†’ Place google_credentials.json (service account key) in project root
    β†’ Share the Google Sheet with the service account email
  Option B (OAuth β€” for personal use, easiest):
    β†’ Run setup_google.py once for browser login
    β†’ Token cached in google_token.json

See setup_google.py for step-by-step instructions.
"""

import os
import json
import logging
import re
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)

# ── Column definitions ────────────────────────────────────────────────────────
HEADERS = [
    "Batch Date",
    "Rank",
    "Job Title",
    "Company",
    "Location",
    "Platform",
    "Salary Range",
    "Experience",
    "Relevance Score",
    "ATS Before (%)",      # JD match on original resume
    "ATS After (%)",       # JD match on tailored resume
    "ATS Improvement",     # After - Before
    "Resume Quality",      # Resume-ATS structural quality (independent of JD)
    "Priority",
    "Matching Skills",
    "Missing Skills",
    "AI Recommendation",
    "Apply Link",
    "Resume File",         # Filename of tailored resume
    "Resume Folder",       # Local folder path β€” open in File Explorer
    "Application Status",
    "Date Applied",
    "Notes",
]

STATUS_OPTIONS = ["Not Applied", "Applied", "Shortlisted", "Interview", "Offer", "Rejected"]

# Row colors based on score
def _score_color(score: int):
    if score >= 8:  return {"red": 0.88, "green": 0.98, "blue": 0.88}  # light green
    if score >= 6:  return {"red": 1.00, "green": 0.97, "blue": 0.85}  # light yellow
    if score >= 4:  return {"red": 1.00, "green": 0.93, "blue": 0.88}  # light orange
    return             {"red": 1.00, "green": 0.90, "blue": 0.90}      # light red


def _get_client():
    """Return authenticated gspread client (service account or OAuth)."""
    import gspread
    from google.oauth2.service_account import Credentials as SACredentials
    from google.oauth2.credentials import Credentials as OAuthCredentials
    from google_auth_oauthlib.flow import InstalledAppFlow
    from google.auth.transport.requests import Request

    SCOPES = [
        "https://www.googleapis.com/auth/spreadsheets",
        "https://www.googleapis.com/auth/drive",
    ]

    sa_file = Path("google_credentials.json")
    token_file = Path("google_token.json")
    oauth_file = Path("google_oauth_client.json")

    # ── Option A: Service Account ──
    if sa_file.exists():
        try:
            creds = SACredentials.from_service_account_file(str(sa_file), scopes=SCOPES)
            return gspread.authorize(creds)
        except Exception as e:
            logger.warning(f"Service account auth failed: {e}")

    # ── Option B: OAuth2 ──
    creds = None
    if token_file.exists():
        try:
            creds = OAuthCredentials.from_authorized_user_file(str(token_file), SCOPES)
        except Exception:
            pass

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        elif oauth_file.exists():
            flow = InstalledAppFlow.from_client_secrets_file(str(oauth_file), SCOPES)
            creds = flow.run_local_server(port=0)
        else:
            raise FileNotFoundError(
                "\n\n❌ Google credentials not found!\n"
                "Run: python setup_google.py\n"
                "for step-by-step setup instructions.\n"
            )
        # Save token for next run
        token_file.write_text(creds.to_json())

    return gspread.authorize(creds)


def _get_or_create_drive_folder(drive_service, folder_name: str) -> str:
    """Get Google Drive folder ID, creating it if needed."""
    query = (
        f"name='{folder_name}' and mimeType='application/vnd.google-apps.folder' "
        f"and trashed=false"
    )
    results = drive_service.files().list(q=query, fields="files(id, name)").execute()
    files = results.get("files", [])
    if files:
        return files[0]["id"]
    # Create folder
    meta = {
        "name": folder_name,
        "mimeType": "application/vnd.google-apps.folder",
    }
    folder = drive_service.files().create(body=meta, fields="id").execute()
    return folder["id"]


def upload_resume_to_drive(
    file_path: str,
    folder_name: str = "Job Automation Agent β€” Resumes",
) -> str:
    """
    Upload a DOCX resume to Google Drive and return a public shareable link.
    Returns empty string if Drive upload is unavailable.
    """
    try:
        from googleapiclient.discovery import build
        from googleapiclient.http import MediaFileUpload
        import gspread

        client = _get_client()
        creds  = client.auth

        drive_service = build("drive", "v3", credentials=creds)
        folder_id     = _get_or_create_drive_folder(drive_service, folder_name)

        fname = Path(file_path).name
        media = MediaFileUpload(
            file_path,
            mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        )
        file_meta = {
            "name":    fname,
            "parents": [folder_id],
        }
        uploaded = drive_service.files().create(
            body=file_meta, media_body=media, fields="id"
        ).execute()
        file_id = uploaded["id"]

        # Make publicly accessible (anyone with link can view)
        drive_service.permissions().create(
            fileId=file_id,
            body={"type": "anyone", "role": "reader"},
        ).execute()

        return f"https://drive.google.com/file/d/{file_id}/view?usp=sharing"

    except Exception as e:
        logger.warning(f"Drive upload failed for {file_path}: {e}")
        return ""


def write_jobs_to_sheet(
    jobs: List[Dict],
    sheet_id: str,
    tab_name: str = "Job Applications",
    batch_label: str = "",
) -> bool:
    """
    Write assessed jobs to Google Sheet.
    - Creates the tab if it doesn't exist
    - Adds header row if sheet is empty
    - APPENDS new rows (never overwrites existing data)
    - Returns True on success
    """
    try:
        client = _get_client()
        sh     = client.open_by_key(sheet_id)

        # Get or create tab
        try:
            ws = sh.worksheet(tab_name)
        except Exception:
            ws = sh.add_worksheet(title=tab_name, rows=2000, cols=len(HEADERS))

        # Check if header row exists
        existing = ws.get_all_values()
        has_headers = bool(existing and existing[0] and existing[0][0] == "Batch Date")

        if not existing:
            # Empty sheet β€” write header row
            ws.append_row(HEADERS, value_input_option="USER_ENTERED")
            _format_header(ws)
            first_data_row = 2
        elif not has_headers:
            # Data exists but no header β€” insert header at row 1
            ws.insert_row(HEADERS, index=1, value_input_option="USER_ENTERED")
            _format_header(ws)
            first_data_row = len(existing) + 2  # existing rows shifted down by 1
        else:
            first_data_row = len(existing) + 1

        if not batch_label:
            batch_label = datetime.now().strftime("%Y-%m-%d %H:%M")

        # Build rows
        rows = []
        for rank, job in enumerate(jobs, 1):
            job_url   = job.get("url", "")
            resume_path = job.get("resume_path", "")
            resume_link = job.get("drive_resume_link", "")

            # Format job URL as clickable hyperlink formula
            apply_cell = f'=HYPERLINK("{job_url}","Apply β†’")' if job_url else ""

            # Resume file and folder cells
            resume_file_cell   = ""
            resume_folder_cell = ""
            if resume_path and os.path.exists(resume_path):
                p = Path(resume_path).resolve()
                resume_file_cell   = p.name                   # just filename
                resume_folder_cell = str(p.parent)            # full Windows path to folder

            score  = job.get("relevance_score", 0)
            before  = job.get("ats_score_before")   # None if not set
            after   = job.get("ats_score_after")    # None if not set
            improv  = job.get("ats_improvement", 0) or 0
            quality = job.get("resume_quality_score")

            def _pct(val):
                """Format an ATS percentage value for the sheet."""
                if val is None or val == "":
                    return "β€”"
                return f"{int(val)}%"

            improv_str = f"+{improv}pp" if improv > 0 else (f"{improv}pp" if improv < 0 else "β€”")

            row = [
                batch_label,
                rank,
                job.get("title", ""),
                job.get("company", ""),
                job.get("location", ""),
                job.get("platform", ""),
                job.get("salary", "Not specified"),
                job.get("experience_required", ""),
                f"{score}/10",
                _pct(before),
                _pct(after),
                improv_str,
                _pct(quality),
                job.get("application_priority", ""),
                job.get("matching_skills", ""),
                job.get("missing_skills", ""),
                (job.get("recommendation", "") or "")[:200],
                apply_cell,
                resume_file_cell,
                resume_folder_cell,
                "Not Applied",
                "",
                "",
            ]
            rows.append(row)

        if rows:
            # Append all rows at once
            ws.append_rows(rows, value_input_option="USER_ENTERED")
            logger.info(f"Wrote {len(rows)} jobs to Google Sheet tab '{tab_name}'")

        # Apply conditional formatting for score column (col I = index 9)
        _apply_score_formatting(ws, first_data_row, first_data_row + len(rows) - 1, sheet_id)

        return True

    except FileNotFoundError as e:
        print(str(e))
        return False
    except Exception as e:
        logger.error(f"Google Sheet write failed: {e}")
        print(f"⚠ Google Sheet update failed: {e}")
        return False


def _format_header(ws):
    """Bold + freeze header row, auto-resize columns."""
    try:
        from gspread.utils import rowcol_to_a1

        ws.format("A1:W1", {
            "textFormat":        {"bold": True, "foregroundColor": {"red": 1, "green": 1, "blue": 1}},
            "backgroundColor":   {"red": 0.086, "green": 0.282, "blue": 0.745},
            "horizontalAlignment": "CENTER",
        })
        ws.freeze(rows=1)
        # Set column widths via batchUpdate
        try:
            sh = ws.spreadsheet
            sh.batch_update({
                "requests": [
                    {"updateDimensionProperties": {
                        "range": {"sheetId": ws.id, "dimension": "COLUMNS",
                                  "startIndex": 0, "endIndex": len(HEADERS)},
                        "properties": {"pixelSize": 150},
                        "fields": "pixelSize",
                    }},
                    # Wider columns
                    {"updateDimensionProperties": {
                        "range": {"sheetId": ws.id, "dimension": "COLUMNS",
                                  "startIndex": 2, "endIndex": 3},   # Job Title
                        "properties": {"pixelSize": 260},
                        "fields": "pixelSize",
                    }},
                ]
            })
        except Exception:
            pass
    except Exception as e:
        logger.warning(f"Header formatting failed: {e}")


def _apply_score_formatting(ws, start_row: int, end_row: int, sheet_id: str):
    """Color rows based on relevance score column (col I)."""
    pass  # gspread conditional formatting is complex; skipped for now


def get_sheet_url(sheet_id: str) -> str:
    return f"https://docs.google.com/spreadsheets/d/{sheet_id}/edit"