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

View File

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

View File

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

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. */ /** 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>