recieved time implemented
parent
e7907a194e
commit
8380be0d8a
|
|
@ -13,6 +13,7 @@
|
||||||
**/.env
|
**/.env
|
||||||
**/.env.*
|
**/.env.*
|
||||||
!**/.env.example
|
!**/.env.example
|
||||||
|
backend/credentials/*.json
|
||||||
|
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
**/*.py[cod]
|
**/*.py[cod]
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,9 @@ frontend/dist/
|
||||||
# Uploaded content — user data, never in git
|
# Uploaded content — user data, never in git
|
||||||
backend/uploads/
|
backend/uploads/
|
||||||
|
|
||||||
|
# Google OAuth ADC / Desktop client secrets — never commit
|
||||||
|
backend/credentials/*.json
|
||||||
|
|
||||||
**.pdf
|
**.pdf
|
||||||
# Per-machine alembic autogen revisions only — the old bare `**_**_**.py`
|
# Per-machine alembic autogen revisions only — the old bare `**_**_**.py`
|
||||||
# also swallowed any module with two underscores (e.g. test_talent_plugins.py).
|
# also swallowed any module with two underscores (e.g. test_talent_plugins.py).
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"account": "",
|
"type": "authorized_user",
|
||||||
"client_id": "679334897177-ufal3rogbg8cgm3qcren6pv20phqd7nl.apps.googleusercontent.com",
|
"client_id": "679334897177-ufal3rogbg8cgm3qcren6pv20phqd7nl.apps.googleusercontent.com",
|
||||||
"client_secret": "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",
|
"client_secret": "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",
|
||||||
"refresh_token": "1//03C8LMkk-9mSyCgYIARAAGAMSNwF-L9IrAcPhShsp3cDeprSlqI-P6lArpmbyzu-PcKiLfQ5gC3H_MEk930IaKfewy3cxP3T0Oo8",
|
"refresh_token": "1//03C8LMkk-9mSyCgYIARAAGAMSNwF-L9IrAcPhShsp3cDeprSlqI-P6lArpmbyzu-PcKiLfQ5gC3H_MEk930IaKfewy3cxP3T0Oo8",
|
||||||
"type": "authorized_user",
|
"universe_domain": "googleapis.com",
|
||||||
"universe_domain": "googleapis.com"
|
"account": "ahmed.mujtaba@utopiabrands.com",
|
||||||
|
"token": "ya29.a0AdMD6Eg_6meQs84gTmiyhzZp7C-JlZeJU6-ECm6twwAcMqvfvRvyvs5LQGbAhHarHzZF-jiU-sicebJmxXIN4l6hNDXoaHcrojuhq--hj2oSBWojiEKaGIgLKPM8frdspz_wVANrwkFwpIKhN3RpWID9mJCt7N6IFaNrZtgStakdF0sVCKKVttE7qWK0vIvJT3HZHpVbaCgYKAX8SARASFQHGX2MiQvJqWStYQDgYFK4E6GJ9Zw0207",
|
||||||
|
"expiry": "2026-08-31T09:52:09Z",
|
||||||
|
"quota_project_id": "hrms-ats-portal"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@ family and lets g_sheet/views.py translate that into HTTPException.
|
||||||
|
|
||||||
Auth reuses the credentials already on disk (authorized_user ADC + a valid refresh
|
Auth reuses the credentials already on disk (authorized_user ADC + a valid refresh
|
||||||
token). Nothing here launches a browser, runs InstalledAppFlow, or reads stdin.
|
token). Nothing here launches a browser, runs InstalledAppFlow, or reads stdin.
|
||||||
|
After a successful refresh, store_authorized_session writes the ADC JSON back so
|
||||||
|
the session can be copied to Linux prod. Re-auth lives in g_sheet/store_session.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
|
@ -31,12 +34,11 @@ from g_sheet.enums import (
|
||||||
MonthNormalisation,
|
MonthNormalisation,
|
||||||
)
|
)
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
logger=logging.getLogger("g_sheet.plugins")
|
logger=logging.getLogger("g_sheet.plugins")
|
||||||
|
|
||||||
# backend/ — GOOGLE_APPLICATION_CREDENTIALS is stored relative to it ("credentials/...").
|
# backend/ — GOOGLE_APPLICATION_CREDENTIALS is stored relative to it ("credentials/...").
|
||||||
ROOT=Path(__file__).resolve().parent.parent
|
ROOT=Path(__file__).resolve().parent.parent
|
||||||
|
load_dotenv(ROOT/".env")
|
||||||
|
|
||||||
SCOPES=[
|
SCOPES=[
|
||||||
"https://www.googleapis.com/auth/spreadsheets",
|
"https://www.googleapis.com/auth/spreadsheets",
|
||||||
|
|
@ -47,6 +49,9 @@ SPREADSHEET_ID=os.getenv("SPREADSHEET_ID")
|
||||||
SPREADSHEET_NAME=os.getenv("SPREADSHEET_NAME")
|
SPREADSHEET_NAME=os.getenv("SPREADSHEET_NAME")
|
||||||
SPREADSHEET_URL=os.getenv("SPREADSHEET_URL")
|
SPREADSHEET_URL=os.getenv("SPREADSHEET_URL")
|
||||||
GOOGLE_APPLICATION_CREDENTIALS=os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
GOOGLE_APPLICATION_CREDENTIALS=os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
||||||
|
GOOGLE_OAUTH_CLIENT_ID_FILE=os.getenv("GOOGLE_OAUTH_CLIENT_ID_FILE")
|
||||||
|
GOOGLE_CLOUD_PROJECT=os.getenv("GOOGLE_CLOUD_PROJECT")
|
||||||
|
GOOGLE_ACCOUNT=os.getenv("GOOGLE_ACCOUNT")
|
||||||
|
|
||||||
# 429 and 5xx are transient; every other 4xx is a bad request that a retry repeats.
|
# 429 and 5xx are transient; every other 4xx is a bad request that a retry repeats.
|
||||||
RETRY_ATTEMPTS=3
|
RETRY_ATTEMPTS=3
|
||||||
|
|
@ -94,6 +99,73 @@ def resolve_credentials_path(credentials_path=None):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_client_secret_path(client_secret_path=None):
|
||||||
|
"""Absolute path to the Desktop OAuth client json (credentials/client_secret.json)."""
|
||||||
|
raw=client_secret_path or GOOGLE_OAUTH_CLIENT_ID_FILE
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
path=Path(raw)
|
||||||
|
if not path.is_absolute():
|
||||||
|
path=ROOT/path
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _expiry_iso(expiry):
|
||||||
|
if expiry is None:
|
||||||
|
return None
|
||||||
|
if expiry.tzinfo is None:
|
||||||
|
return expiry.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
return expiry.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
|
def _authorized_user_adc(credentials):
|
||||||
|
"""gcloud-compatible authorized_user payload. google.auth.default() requires type."""
|
||||||
|
payload={
|
||||||
|
"type":"authorized_user",
|
||||||
|
"client_id":credentials.client_id,
|
||||||
|
"client_secret":credentials.client_secret,
|
||||||
|
"refresh_token":credentials.refresh_token,
|
||||||
|
"universe_domain":getattr(credentials,"universe_domain",None) or "googleapis.com",
|
||||||
|
"account":getattr(credentials,"account",None) or GOOGLE_ACCOUNT or "",
|
||||||
|
}
|
||||||
|
token=getattr(credentials,"token",None)
|
||||||
|
if token:
|
||||||
|
payload["token"]=token
|
||||||
|
expiry=_expiry_iso(getattr(credentials,"expiry",None))
|
||||||
|
if expiry:
|
||||||
|
payload["expiry"]=expiry
|
||||||
|
if GOOGLE_CLOUD_PROJECT:
|
||||||
|
payload["quota_project_id"]=GOOGLE_CLOUD_PROJECT
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def store_authorized_session(credentials,credentials_path=None):
|
||||||
|
"""Persist an authorized_user session to GOOGLE_APPLICATION_CREDENTIALS.
|
||||||
|
|
||||||
|
Service-account key files are left untouched (no refresh_token to rotate).
|
||||||
|
A persist failure is logged, never raised — the in-memory token still works.
|
||||||
|
"""
|
||||||
|
path=resolve_credentials_path(credentials_path)
|
||||||
|
if path is None:
|
||||||
|
logger.warning("GOOGLE_APPLICATION_CREDENTIALS is not configured; session not stored")
|
||||||
|
return None
|
||||||
|
if not getattr(credentials,"refresh_token",None) or not getattr(credentials,"client_id",None):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
path.parent.mkdir(parents=True,exist_ok=True)
|
||||||
|
tmp=path.with_name(path.name+".tmp")
|
||||||
|
tmp.write_text(json.dumps(_authorized_user_adc(credentials),indent=2)+"\n",encoding="utf-8")
|
||||||
|
tmp.replace(path)
|
||||||
|
try:
|
||||||
|
os.chmod(path,0o600)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
logger.warning("could not persist Google authorized session: %s",e)
|
||||||
|
return None
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def load_credentials(credentials_path=None,scopes=None):
|
def load_credentials(credentials_path=None,scopes=None):
|
||||||
"""Build scoped ADC credentials and refresh them once. Never prompts."""
|
"""Build scoped ADC credentials and refresh them once. Never prompts."""
|
||||||
path=resolve_credentials_path(credentials_path)
|
path=resolve_credentials_path(credentials_path)
|
||||||
|
|
@ -108,10 +180,11 @@ def load_credentials(credentials_path=None,scopes=None):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SheetsAuthError(f"Google credential refresh failed: {e}")
|
raise SheetsAuthError(f"Google credential refresh failed: {e}")
|
||||||
|
store_authorized_session(credentials,credentials_path)
|
||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
def ensure_fresh(credentials):
|
def ensure_fresh(credentials,credentials_path=None):
|
||||||
"""Refresh only when the token has actually gone stale — not on every call."""
|
"""Refresh only when the token has actually gone stale — not on every call."""
|
||||||
if credentials is None:
|
if credentials is None:
|
||||||
raise SheetsAuthError("Google credentials are not initialised")
|
raise SheetsAuthError("Google credentials are not initialised")
|
||||||
|
|
@ -121,6 +194,7 @@ def ensure_fresh(credentials):
|
||||||
credentials.refresh(Request())
|
credentials.refresh(Request())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SheetsAuthError(f"Google credential refresh failed: {e}")
|
raise SheetsAuthError(f"Google credential refresh failed: {e}")
|
||||||
|
store_authorized_session(credentials,credentials_path)
|
||||||
return credentials
|
return credentials
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
"""Capture a Google authorized_user session into credentials/.
|
||||||
|
|
||||||
|
Run on a machine with a browser (Windows/macOS). Copy the resulting JSON to
|
||||||
|
Linux prod — the API never opens a browser.
|
||||||
|
|
||||||
|
cd backend
|
||||||
|
python g_sheet/store_session.py
|
||||||
|
python g_sheet/store_session.py --force # re-consent, mint a new refresh token
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# `python g_sheet/store_session.py` puts this file's dir on sys.path, not backend/.
|
||||||
|
_BACKEND=Path(__file__).resolve().parent.parent
|
||||||
|
if str(_BACKEND) not in sys.path:
|
||||||
|
sys.path.insert(0,str(_BACKEND))
|
||||||
|
|
||||||
|
from g_sheet.plugins import (
|
||||||
|
SCOPES,
|
||||||
|
SheetsAuthError,
|
||||||
|
load_credentials,
|
||||||
|
resolve_client_secret_path,
|
||||||
|
resolve_credentials_path,
|
||||||
|
store_authorized_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _authorize_browser(client_secret_path):
|
||||||
|
try:
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
except ImportError as e:
|
||||||
|
raise SystemExit(
|
||||||
|
"google-auth-oauthlib is required for browser login. "
|
||||||
|
"pip install google-auth-oauthlib==1.4.0"
|
||||||
|
) from e
|
||||||
|
if client_secret_path is None or not client_secret_path.exists():
|
||||||
|
raise SystemExit(
|
||||||
|
"OAuth client file not found. Set GOOGLE_OAUTH_CLIENT_ID_FILE "
|
||||||
|
"(credentials/client_secret.json)."
|
||||||
|
)
|
||||||
|
flow=InstalledAppFlow.from_client_secrets_file(str(client_secret_path),SCOPES)
|
||||||
|
return flow.run_local_server(port=0,prompt="consent",access_type="offline")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser=argparse.ArgumentParser(description="Store a Google authorized_user session on disk.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Ignore the existing ADC file and open a browser consent screen.",
|
||||||
|
)
|
||||||
|
args=parser.parse_args(argv)
|
||||||
|
path=resolve_credentials_path()
|
||||||
|
if path is None:
|
||||||
|
raise SystemExit("GOOGLE_APPLICATION_CREDENTIALS is not set.")
|
||||||
|
credentials=None
|
||||||
|
if not args.force:
|
||||||
|
try:
|
||||||
|
credentials=load_credentials()
|
||||||
|
except SheetsAuthError as e:
|
||||||
|
print(f"existing session unusable ({e}); opening browser…",file=sys.stderr)
|
||||||
|
if credentials is None:
|
||||||
|
credentials=_authorize_browser(resolve_client_secret_path())
|
||||||
|
stored=store_authorized_session(credentials)
|
||||||
|
else:
|
||||||
|
stored=path
|
||||||
|
if stored is None:
|
||||||
|
raise SystemExit("failed to write the authorized session file")
|
||||||
|
print(f"stored authorized session: {stored}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -85,13 +85,13 @@ class SheetClient(Sheet):
|
||||||
their own client.
|
their own client.
|
||||||
"""
|
"""
|
||||||
if self.client is not None:
|
if self.client is not None:
|
||||||
return ensure_fresh(self.credentials) and self.client
|
return ensure_fresh(self.credentials,self.credentials_path) and self.client
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
self.credentials=load_credentials(self.credentials_path,self.scopes)
|
self.credentials=load_credentials(self.credentials_path,self.scopes)
|
||||||
self.client=build_sheets_client(self.credentials)
|
self.client=build_sheets_client(self.credentials)
|
||||||
else:
|
else:
|
||||||
ensure_fresh(self.credentials)
|
ensure_fresh(self.credentials,self.credentials_path)
|
||||||
return self.client
|
return self.client
|
||||||
|
|
||||||
async def _values(self):
|
async def _values(self):
|
||||||
|
|
|
||||||
|
|
@ -820,7 +820,7 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
):
|
):
|
||||||
|
|
||||||
statement = cls._apply_filters(
|
statement = cls._apply_filters(
|
||||||
select(cls).order_by(cls.created_at.desc(),cls.id.desc()),
|
select(cls).order_by(cls.message_received_time.desc(),cls.id.desc()),
|
||||||
search, isread, application_status, assigned, is_duplicate,
|
search, isread, application_status, assigned, is_duplicate,
|
||||||
no_suggestions, processing_state,
|
no_suggestions, processing_state,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -864,7 +864,10 @@ class Interviews(SQLModel, table=True):
|
||||||
def _with_inbox_message(cls):
|
def _with_inbox_message(cls):
|
||||||
from inbox.models import Inbox
|
from inbox.models import Inbox
|
||||||
|
|
||||||
return selectinload(cls.inbox).selectinload(Inbox.messages)
|
return selectinload(cls.inbox).options(
|
||||||
|
selectinload(Inbox.messages),
|
||||||
|
selectinload(Inbox.user),
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_interview_by_id(cls, session: AsyncSession, record_id):
|
async def get_interview_by_id(cls, session: AsyncSession, record_id):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
def serialize_interview(row, *, job_title=None) -> dict:
|
def serialize_interview(row, *, job_title=None) -> dict:
|
||||||
inbox=getattr(row,"inbox",None)
|
inbox=getattr(row,"inbox",None)
|
||||||
user=getattr(inbox,"user",None) if inbox else None
|
user=getattr(inbox,"user",None) if inbox else None
|
||||||
|
uid=getattr(user,"id",None) or getattr(row,"user_id",None)
|
||||||
return {
|
return {
|
||||||
"id": str(row.id),
|
"id": str(row.id),
|
||||||
"inbox_id": row.inbox_id,
|
"inbox_id": row.inbox_id,
|
||||||
|
|
@ -9,6 +10,7 @@ def serialize_interview(row, *, job_title=None) -> dict:
|
||||||
"interview_type": row.interview_type,
|
"interview_type": row.interview_type,
|
||||||
"interview_status": row.interview_status,
|
"interview_status": row.interview_status,
|
||||||
"candidate_name": user.name if user else None,
|
"candidate_name": user.name if user else None,
|
||||||
|
"user_id": str(uid) if uid else None,
|
||||||
"job_title": job_title or None,
|
"job_title": job_title or None,
|
||||||
"graph_event_id": row.graph_event_id or None,
|
"graph_event_id": row.graph_event_id or None,
|
||||||
"web_link": row.web_link or None,
|
"web_link": row.web_link or None,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ openpyxl==3.1.5
|
||||||
google-api-python-client==2.198.0 # Sheets v4 client in g_sheet/plugins.py
|
google-api-python-client==2.198.0 # Sheets v4 client in g_sheet/plugins.py
|
||||||
google-auth==2.56.3 # ADC + refresh in g_sheet/plugins.py
|
google-auth==2.56.3 # ADC + refresh in g_sheet/plugins.py
|
||||||
google-auth-httplib2==0.4.1 # transport used by googleapiclient
|
google-auth-httplib2==0.4.1 # transport used by googleapiclient
|
||||||
|
google-auth-oauthlib==1.4.0 # InstalledAppFlow in g_sheet/store_session.py only
|
||||||
|
|
||||||
# --- AWS S3 (s3/) ----------------------------------------------------------
|
# --- AWS S3 (s3/) ----------------------------------------------------------
|
||||||
boto3==1.40.49 # S3 PutObject / DeleteObject in s3/plugins.py
|
boto3==1.40.49 # S3 PutObject / DeleteObject in s3/plugins.py
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,18 @@ x-backend-env: &backend-env
|
||||||
|
|
||||||
# Shared CV storage. Named volume so API + workers see the same files.
|
# Shared CV storage. Named volume so API + workers see the same files.
|
||||||
# Optional docker-compose.dev.yml remounts ./backend/inbox/decoded_attachments.
|
# Optional docker-compose.dev.yml remounts ./backend/inbox/decoded_attachments.
|
||||||
|
# Google authorized_user ADC is bind-mounted so a refresh-token rotation on
|
||||||
|
# disk survives image rebuilds. Copy the JSON onto the host; do not bake it.
|
||||||
x-attachments: &attachments
|
x-attachments: &attachments
|
||||||
- attachments-data:/app/inbox/decoded_attachments
|
- attachments-data:/app/inbox/decoded_attachments
|
||||||
|
- ./backend/credentials:/app/credentials
|
||||||
|
|
||||||
x-backend-service: &backend-service
|
x-backend-service: &backend-service
|
||||||
build: *backend-build
|
build: *backend-build
|
||||||
image: hrms-backend:local
|
image: hrms-backend:local
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
volumes:
|
||||||
|
- ./backend/credentials:/app/credentials
|
||||||
env_file:
|
env_file:
|
||||||
# backend/.env is the source of truth (plain DB_* + PROD_ENV).
|
# backend/.env is the source of truth (plain DB_* + PROD_ENV).
|
||||||
- ./backend/.env
|
- ./backend/.env
|
||||||
|
|
|
||||||
|
|
@ -95,10 +95,12 @@ export function update(interviewId, { instant, type, status } = {}) {
|
||||||
* render.
|
* render.
|
||||||
*
|
*
|
||||||
* serialize_interview returns the interview columns plus optional calendar sync
|
* serialize_interview returns the interview columns plus optional calendar sync
|
||||||
* fields (`graph_event_id`, `web_link`) and `job_title`. Meeting mode, duration,
|
* fields (`graph_event_id`, `web_link`), `job_title`, and `user_id` (inbox.user
|
||||||
* interviewer list and feedback verdict still have no source — they stay absent
|
* id, falling back to the denorm column). Meeting mode, duration, interviewer
|
||||||
* rather than defaulted. Screens that already hydrate `jobTitle` from the
|
* list and feedback verdict still have no source — they stay absent rather than
|
||||||
* application row keep doing so as a fallback.
|
* defaulted. Screens that already hydrate `jobTitle` from the application row
|
||||||
|
* keep doing so as a fallback; `userId` prefers the interview row so unassigned
|
||||||
|
* applications still open a profile.
|
||||||
*/
|
*/
|
||||||
export function toInterviewView(row) {
|
export function toInterviewView(row) {
|
||||||
const whenRaw = row.interview_date || row.interview_time
|
const whenRaw = row.interview_date || row.interview_time
|
||||||
|
|
@ -111,6 +113,7 @@ export function toInterviewView(row) {
|
||||||
status: row.interview_status || 'Scheduled',
|
status: row.interview_status || 'Scheduled',
|
||||||
when: when && !Number.isNaN(when.getTime()) ? when : null,
|
when: when && !Number.isNaN(when.getTime()) ? when : null,
|
||||||
jobTitle: row.job_title || null,
|
jobTitle: row.job_title || null,
|
||||||
|
userId: row.user_id ?? null,
|
||||||
graphEventId: row.graph_event_id || null,
|
graphEventId: row.graph_event_id || null,
|
||||||
webLink: row.web_link || null,
|
webLink: row.web_link || null,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,9 @@ export default function Calendar() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/* Job title and the candidate's user id are not on the interview row; the
|
/* Job title is hydrated from the pipeline board; user id prefers the
|
||||||
application supplies both. One extra request for the whole screen. */
|
interview row so unassigned applications (absent from the board) still
|
||||||
|
open a profile. One extra request for the whole screen. */
|
||||||
const appsQuery = useApplications()
|
const appsQuery = useApplications()
|
||||||
const appByInbox = useMemo(() => byInboxId(appsQuery.data), [appsQuery.data])
|
const appByInbox = useMemo(() => byInboxId(appsQuery.data), [appsQuery.data])
|
||||||
|
|
||||||
|
|
@ -84,7 +85,7 @@ export default function Calendar() {
|
||||||
return {
|
return {
|
||||||
...iv,
|
...iv,
|
||||||
jobTitle: iv.jobTitle || app?.jobTitle || null,
|
jobTitle: iv.jobTitle || app?.jobTitle || null,
|
||||||
userId: app?.userId ?? null,
|
userId: iv.userId ?? app?.userId ?? null,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[monthQuery.data, appByInbox],
|
[monthQuery.data, appByInbox],
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export default function Interviews() {
|
||||||
return {
|
return {
|
||||||
...iv,
|
...iv,
|
||||||
jobTitle: iv.jobTitle || app?.jobTitle || null,
|
jobTitle: iv.jobTitle || app?.jobTitle || null,
|
||||||
userId: app?.userId ?? null,
|
userId: iv.userId ?? app?.userId ?? null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[appByInbox],
|
[appByInbox],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue