added all aplicants

pull/5/head
ahmed.mujtaba 2026-08-07 19:03:23 +05:00
parent 8b4ebb1bb8
commit 52b76bb1cf
4 changed files with 93 additions and 16 deletions

View File

@ -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))

View File

@ -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,

View File

@ -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:

View File

@ -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>