added all aplicants
parent
8b4ebb1bb8
commit
52b76bb1cf
|
|
@ -1,9 +1,11 @@
|
|||
from typing import Any
|
||||
from fastapi import APIRouter,Depends, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import HTTPException
|
||||
from db_setup import get_session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from inbox.views import Email
|
||||
import uuid
|
||||
from users.permissions import PermissionTag, require_permission
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
|
@ -111,3 +113,18 @@ async def get_inbox_read_status(
|
|||
raise
|
||||
except Exception as 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
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Optional
|
||||
from fastapi import HTTPException
|
||||
|
||||
from sqlalchemy import Column, DateTime, func, or_, update
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
|
@ -82,6 +83,17 @@ class Inbox_Messages(SQLModel, table=True):
|
|||
return email_data.get("bodyPreview") or ""
|
||||
|
||||
@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(
|
||||
cls,
|
||||
session: AsyncSession,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,16 @@ class Email:
|
|||
self.token=token or EMAIL_API_TOKEN
|
||||
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 with httpx.AsyncClient() as client:
|
||||
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. */
|
||||
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) {
|
||||
return `${i.name.toUpperCase()}
|
||||
${i.email} · ${i.phone}
|
||||
|
|
@ -87,11 +107,18 @@ export default function Inbox() {
|
|||
fromEmail: row.fromEmail || '',
|
||||
subject: row.subject || '',
|
||||
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),
|
||||
attachment: row.attachment_name || 'Resume.pdf',
|
||||
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,
|
||||
}))
|
||||
},
|
||||
|
|
@ -444,13 +471,18 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
|||
onError: (err) => toast(friendlyAuthError(err, 'Could not mark message read.'), 'error'),
|
||||
})
|
||||
|
||||
async function sync() {
|
||||
toast('Fetching from Outlook…', 'info')
|
||||
const res = await qc.refetchQueries({ queryKey: qk.mailbox.messages() })
|
||||
if (query.isError) toast('Sync failed', 'error')
|
||||
else toast('Mailbox synced', 'success')
|
||||
return res
|
||||
}
|
||||
// Refetching the list alone only re-reads rows already in our DB. GET
|
||||
// /email/fetch is the Graph proxy pull that inserts new mail and enqueues the
|
||||
// matching agent, so it has to run FIRST — then the list is invalidated to
|
||||
// pick up whatever it wrote.
|
||||
const sync = useMutation({
|
||||
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) {
|
||||
const job = jobs[0]
|
||||
|
|
@ -463,12 +495,12 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
|||
jobId: job.id, jobTitle: job.title, department: job.department,
|
||||
experience: int(2, 10), currentCompany: pick(companies), currentTitle: job.title,
|
||||
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",
|
||||
skills: job.skills.slice(0, 4), rating: '4.0', salary: 130000,
|
||||
matchedSkills: job.skills.slice(0, 3), missingSkills: job.skills.slice(3),
|
||||
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: [],
|
||||
favorite: false, interviewStatus: 'Not Scheduled',
|
||||
},
|
||||
|
|
@ -492,8 +524,13 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
|||
<span className="text-muted text-sm">
|
||||
{query.isPending ? 'Loading…' : query.isError ? 'Sync failed' : `${emails.length} messages · ${unread} unread`}
|
||||
</span>
|
||||
<button className="btn btn-secondary btn-sm" style={{ marginLeft: 'auto' }} onClick={sync}>
|
||||
<Icon name="refresh" /> Sync Mailbox
|
||||
<button
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
|
@ -525,7 +562,7 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
|||
{isImported(e) && <Badge className="b-green">Imported</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ii-time">{fmtShort(e.when)}</div>
|
||||
<div className="ii-time">{e.when ? fmtShort(e.when) : '—'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -547,7 +584,9 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
|||
<Avatar name={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>
|
||||
|
||||
|
|
@ -564,7 +603,6 @@ function EmailTab({ query, jobs, updateCandidates, toast }) {
|
|||
<div className="cell-sub">{selected.attachmentSize} · PDF</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<ScoreChip score={selected.atsScore} />
|
||||
<button className="btn btn-secondary btn-sm" onClick={() => toast('Opening attachment preview', 'info')}>
|
||||
<Icon name="eye" /> Preview
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue