added all aplicants
parent
8b4ebb1bb8
commit
52b76bb1cf
|
|
@ -1,9 +1,11 @@
|
||||||
|
from typing import Any
|
||||||
from fastapi import APIRouter,Depends, Query
|
from fastapi import APIRouter,Depends, Query
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from db_setup import get_session
|
from db_setup import get_session
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from inbox.views import Email
|
from inbox.views import Email
|
||||||
|
import uuid
|
||||||
from users.permissions import PermissionTag, require_permission
|
from users.permissions import PermissionTag, require_permission
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
@ -111,3 +113,18 @@ async def get_inbox_read_status(
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500,detail=str(e))
|
raise HTTPException(status_code=500,detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/inbox/all-applications")
|
||||||
|
async def get_all_applications(
|
||||||
|
app_id:uuid.UUID|int=Query(None),
|
||||||
|
current_user:dict=Depends(require_permission(PermissionTag.INBOX_VIEW)),
|
||||||
|
session:AsyncSession=Depends(get_session),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
service=Email(session=session)
|
||||||
|
data=await service.get_all_applications(app_id)
|
||||||
|
return JSONResponse(content={"data":data,"total":1,"status_code":200})
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500,detail=str(e))
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, func, or_, update
|
from sqlalchemy import Column, DateTime, func, or_, update
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
@ -82,6 +83,17 @@ class Inbox_Messages(SQLModel, table=True):
|
||||||
return email_data.get("bodyPreview") or ""
|
return email_data.get("bodyPreview") or ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
async def get_all_applications(cls,session:AsyncSession,message_id:uuid.UUID|int|None=None):
|
||||||
|
try:
|
||||||
|
qry=select(cls.message_id,cls.full_email_response,cls.message_subject,cls.message_from,cls.message_to,cls.message_sent_time,cls.message_read,cls.attachment)
|
||||||
|
if message_id:
|
||||||
|
qry=qry.where(cls.message_id==message_id)
|
||||||
|
result=await session.execute(qry)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500,detail=str(e))
|
||||||
|
@classmethod
|
||||||
async def set_match_result(
|
async def set_match_result(
|
||||||
cls,
|
cls,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,16 @@ class Email:
|
||||||
self.token=token or EMAIL_API_TOKEN
|
self.token=token or EMAIL_API_TOKEN
|
||||||
self.pending_match_ids:list[str]=[]
|
self.pending_match_ids:list[str]=[]
|
||||||
|
|
||||||
|
async def get_all_applications(self,app_id=None):
|
||||||
|
try:
|
||||||
|
if app_id:
|
||||||
|
application_lst=await Inbox_Messages.get_all_applications(self.session,message_id=app_id)
|
||||||
|
else:
|
||||||
|
application_lst=await Inbox_Messages.get_all_applications(self.session)
|
||||||
|
return application_lst
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500,detail=str(e))
|
||||||
|
|
||||||
async def service_email(self,top,skip):
|
async def service_email(self,top,skip):
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,26 @@ const TABS = ['All Applications', 'Unread', 'Imported', 'Processed', 'Rejected',
|
||||||
/** The prototype computed "time ago" against a fixed 2026-07-09T20:00. */
|
/** The prototype computed "time ago" against a fixed 2026-07-09T20:00. */
|
||||||
const NOW = new Date('2026-07-09T20:00')
|
const NOW = new Date('2026-07-09T20:00')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The seed candidate record importEmail() writes needs a number. The agent
|
||||||
|
* returns a verdict, not a score, so there is nothing on the wire to use —
|
||||||
|
* named here so the fabricated value is visible at its point of use instead of
|
||||||
|
* arriving disguised as a server field on every message.
|
||||||
|
*/
|
||||||
|
const SEED_ATS_SCORE = 70
|
||||||
|
|
||||||
|
/**
|
||||||
|
* message_received_time / message_sent_time are plain string columns
|
||||||
|
* (backend/inbox/models.py:54-56), not timestamps. An unparseable value yields
|
||||||
|
* an Invalid Date that every fmt* helper renders as the literal "Invalid Date",
|
||||||
|
* so return null instead and let the call sites decide what to show.
|
||||||
|
*/
|
||||||
|
function parseDate(value) {
|
||||||
|
if (!value) return null
|
||||||
|
const d = new Date(value)
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
function resumeText(i) {
|
function resumeText(i) {
|
||||||
return `${i.name.toUpperCase()}
|
return `${i.name.toUpperCase()}
|
||||||
${i.email} · ${i.phone}
|
${i.email} · ${i.phone}
|
||||||
|
|
@ -87,11 +107,18 @@ export default function Inbox() {
|
||||||
fromEmail: row.fromEmail || '',
|
fromEmail: row.fromEmail || '',
|
||||||
subject: row.subject || '',
|
subject: row.subject || '',
|
||||||
body: row.body || '',
|
body: row.body || '',
|
||||||
when: row.when ? new Date(row.when) : new Date(),
|
when: parseDate(row.when) ?? parseDate(row.message_sent_time),
|
||||||
unread: Boolean(row.unread),
|
unread: Boolean(row.unread),
|
||||||
attachment: row.attachment_name || 'Resume.pdf',
|
attachment: row.attachment_name || 'Resume.pdf',
|
||||||
attachmentSize: '—',
|
attachmentSize: '—',
|
||||||
atsScore: 70,
|
// The agent's verdict, straight off backend/inbox/serializers.py:44-48.
|
||||||
|
// suggested_job_post_ids is deliberately NOT carried: job posts stay
|
||||||
|
// dark to the inbox.
|
||||||
|
matchStatus: row.match_status || null,
|
||||||
|
matchSummary: row.match_summary || '',
|
||||||
|
matchReasoning: row.match_reasoning || '',
|
||||||
|
matchError: row.match_error || '',
|
||||||
|
matchedAt: parseDate(row.matched_at),
|
||||||
imported: false,
|
imported: false,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
|
@ -444,13 +471,18 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
onError: (err) => toast(friendlyAuthError(err, 'Could not mark message read.'), 'error'),
|
onError: (err) => toast(friendlyAuthError(err, 'Could not mark message read.'), 'error'),
|
||||||
})
|
})
|
||||||
|
|
||||||
async function sync() {
|
// Refetching the list alone only re-reads rows already in our DB. GET
|
||||||
toast('Fetching from Outlook…', 'info')
|
// /email/fetch is the Graph proxy pull that inserts new mail and enqueues the
|
||||||
const res = await qc.refetchQueries({ queryKey: qk.mailbox.messages() })
|
// matching agent, so it has to run FIRST — then the list is invalidated to
|
||||||
if (query.isError) toast('Sync failed', 'error')
|
// pick up whatever it wrote.
|
||||||
else toast('Mailbox synced', 'success')
|
const sync = useMutation({
|
||||||
return res
|
mutationFn: () => inboxApi.syncMailbox(),
|
||||||
}
|
onSuccess: async () => {
|
||||||
|
await qc.invalidateQueries({ queryKey: qk.mailbox.all() })
|
||||||
|
toast('Mailbox synced', 'success')
|
||||||
|
},
|
||||||
|
onError: (err) => toast(friendlyAuthError(err, 'Sync failed'), 'error'),
|
||||||
|
})
|
||||||
|
|
||||||
function importEmail(e) {
|
function importEmail(e) {
|
||||||
const job = jobs[0]
|
const job = jobs[0]
|
||||||
|
|
@ -463,12 +495,12 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
jobId: job.id, jobTitle: job.title, department: job.department,
|
jobId: job.id, jobTitle: job.title, department: job.department,
|
||||||
experience: int(2, 10), currentCompany: pick(companies), currentTitle: job.title,
|
experience: int(2, 10), currentCompany: pick(companies), currentTitle: job.title,
|
||||||
location: pick(locations), stage: 'Applied', status: 'Applied',
|
location: pick(locations), stage: 'Applied', status: 'Applied',
|
||||||
aiScore: e.atsScore, source: 'Microsoft Outlook', recruiter: job.recruiter, recruiterId: '',
|
aiScore: SEED_ATS_SCORE, source: 'Microsoft Outlook', recruiter: job.recruiter, recruiterId: '',
|
||||||
applied: new Date(TODAY), education: "Bachelor's Degree",
|
applied: new Date(TODAY), education: "Bachelor's Degree",
|
||||||
skills: job.skills.slice(0, 4), rating: '4.0', salary: 130000,
|
skills: job.skills.slice(0, 4), rating: '4.0', salary: 130000,
|
||||||
matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3),
|
matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3),
|
||||||
recommendation: 'Potential Match',
|
recommendation: 'Potential Match',
|
||||||
subScores: { skills: e.atsScore, experience: 80, education: 80, keywords: e.atsScore, location: 100, salary: 90 },
|
subScores: { skills: SEED_ATS_SCORE, experience: 80, education: 80, keywords: SEED_ATS_SCORE, location: 100, salary: 90 },
|
||||||
noticePeriod: '1 month', availability: '2 weeks', certifications: [],
|
noticePeriod: '1 month', availability: '2 weeks', certifications: [],
|
||||||
favorite: false, interviewStatus: 'Not Scheduled',
|
favorite: false, interviewStatus: 'Not Scheduled',
|
||||||
},
|
},
|
||||||
|
|
@ -492,8 +524,13 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
<span className="text-muted text-sm">
|
<span className="text-muted text-sm">
|
||||||
{query.isPending ? 'Loading…' : query.isError ? 'Sync failed' : `${emails.length} messages · ${unread} unread`}
|
{query.isPending ? 'Loading…' : query.isError ? 'Sync failed' : `${emails.length} messages · ${unread} unread`}
|
||||||
</span>
|
</span>
|
||||||
<button className="btn btn-secondary btn-sm" style={{ marginLeft: 'auto' }} onClick={sync}>
|
<button
|
||||||
<Icon name="refresh" /> Sync Mailbox
|
className="btn btn-secondary btn-sm"
|
||||||
|
style={{ marginLeft: 'auto' }}
|
||||||
|
disabled={sync.isPending}
|
||||||
|
onClick={() => { toast('Fetching from Outlook…', 'info'); sync.mutate() }}
|
||||||
|
>
|
||||||
|
<Icon name="refresh" /> {sync.isPending ? 'Syncing…' : 'Sync Mailbox'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -525,7 +562,7 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
{isImported(e) && <Badge className="b-green">Imported</Badge>}
|
{isImported(e) && <Badge className="b-green">Imported</Badge>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ii-time">{fmtShort(e.when)}</div>
|
<div className="ii-time">{e.when ? fmtShort(e.when) : '—'}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,7 +584,9 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
<Avatar name={selected.from} />
|
<Avatar name={selected.from} />
|
||||||
<div>
|
<div>
|
||||||
<div className="fw-600">{selected.from}</div>
|
<div className="fw-600">{selected.from}</div>
|
||||||
<div className="cell-sub">{selected.fromEmail} · {fmtDate(selected.when)}</div>
|
<div className="cell-sub">
|
||||||
|
{selected.fromEmail} · {selected.when ? fmtDate(selected.when) : 'Date unavailable'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -564,7 +603,6 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
||||||
<div className="cell-sub">{selected.attachmentSize} · PDF</div>
|
<div className="cell-sub">{selected.attachmentSize} · PDF</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
<ScoreChip score={selected.atsScore} />
|
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => toast('Opening attachment preview', 'info')}>
|
<button className="btn btn-secondary btn-sm" onClick={() => toast('Opening attachment preview', 'info')}>
|
||||||
<Icon name="eye" /> Preview
|
<Icon name="eye" /> Preview
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue