Skip to content

Commit 2fc88f0

Browse files
Add jobs API with Azure DB persistence
1 parent 55efe2a commit 2fc88f0

8 files changed

Lines changed: 566 additions & 9 deletions

File tree

backend/app/api/routes/jobs.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from sqlalchemy import select
3+
from sqlalchemy.ext.asyncio import AsyncSession
4+
5+
from app.database import get_db
6+
from app.db_models import JobDB
7+
from app.models import Job, JobCreate, new_id, utc_now_iso
8+
9+
router = APIRouter(tags=["jobs"])
10+
11+
12+
def _to_model(db_job: JobDB) -> Job:
13+
return Job(
14+
id=db_job.id,
15+
title=db_job.title,
16+
budget=db_job.budget,
17+
skill=db_job.skill,
18+
description=db_job.description or "",
19+
urgent=db_job.urgent,
20+
location=db_job.location or "",
21+
posted_at=db_job.posted_at.isoformat(),
22+
)
23+
24+
25+
@router.get("/jobs")
26+
async def list_jobs(db: AsyncSession = Depends(get_db)) -> list[Job]:
27+
result = await db.execute(select(JobDB).order_by(JobDB.posted_at.desc()))
28+
return [_to_model(j) for j in result.scalars().all()]
29+
30+
31+
@router.post("/jobs", status_code=201)
32+
async def create_job(payload: JobCreate, db: AsyncSession = Depends(get_db)) -> Job:
33+
db_job = JobDB(
34+
id=new_id("job"),
35+
title=payload.title,
36+
budget=payload.budget,
37+
skill=payload.skill,
38+
description=payload.description,
39+
urgent=payload.urgent,
40+
location=payload.location,
41+
)
42+
db.add(db_job)
43+
await db.commit()
44+
await db.refresh(db_job)
45+
return _to_model(db_job)
46+
47+
48+
@router.delete("/jobs/{job_id}", status_code=204)
49+
async def delete_job(job_id: str, db: AsyncSession = Depends(get_db)):
50+
result = await db.execute(select(JobDB).where(JobDB.id == job_id))
51+
db_job = result.scalar_one_or_none()
52+
if not db_job:
53+
raise HTTPException(status_code=404, detail="Job not found")
54+
await db.delete(db_job)
55+
await db.commit()

backend/app/db_models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@
66
from app.database import Base
77

88

9+
class JobDB(Base):
10+
__tablename__ = "jobs"
11+
12+
id: Mapped[str] = mapped_column(String(50), primary_key=True)
13+
title: Mapped[str] = mapped_column(String(200), nullable=False)
14+
budget: Mapped[str] = mapped_column(String(100), nullable=False)
15+
skill: Mapped[str] = mapped_column(String(100), nullable=False)
16+
description: Mapped[str] = mapped_column(Text, default="")
17+
urgent: Mapped[bool] = mapped_column(Boolean, default=False)
18+
location: Mapped[str] = mapped_column(String(200), default="")
19+
posted_at: Mapped[datetime] = mapped_column(
20+
DateTime(timezone=True),
21+
default=lambda: datetime.now(timezone.utc),
22+
nullable=False,
23+
)
24+
25+
926
class WorkerDB(Base):
1027
__tablename__ = "workers"
1128

backend/app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from app.api.routes.call import router as call_router
77
from app.api.routes.health import router as health_router
8+
from app.api.routes.jobs import router as jobs_router
89
from app.api.routes.karma import router as karma_router
910
from app.api.routes.sessions import router as sessions_router
1011
from app.api.routes.speech import router as speech_router
@@ -34,4 +35,5 @@ async def lifespan(app: FastAPI):
3435
app.include_router(sessions_router, prefix="/api")
3536
app.include_router(speech_router, prefix="/api")
3637
app.include_router(call_router, prefix="/api")
38+
app.include_router(jobs_router, prefix="/api")
3739
app.include_router(karma_router, prefix="/api")

backend/app/models.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,28 @@ class IntegrityEventResponse(BaseModel):
225225
pause_reason: Optional[Literal["multiface", "face_absent", "face_change"]] = None
226226

227227

228+
class JobCreate(BaseModel):
229+
title: str = Field(min_length=3, max_length=200)
230+
budget: str = Field(min_length=1, max_length=100)
231+
skill: str = Field(min_length=2, max_length=100)
232+
description: str = Field(default="", max_length=1000)
233+
urgent: bool = False
234+
location: str = Field(default="", max_length=200)
235+
236+
237+
class Job(BaseModel):
238+
model_config = ConfigDict(extra="ignore")
239+
240+
id: str
241+
title: str
242+
budget: str
243+
skill: str
244+
description: str = ""
245+
urgent: bool = False
246+
location: str = ""
247+
posted_at: str
248+
249+
228250
def utc_now_iso() -> str:
229251
return datetime.now(timezone.utc).isoformat()
230252

backend/scripts/__init__.py

Whitespace-only changes.

backend/scripts/seed_demo.py

Lines changed: 438 additions & 0 deletions
Large diffs are not rendered by default.

frontend/src/pages/WorkersBoardPage.jsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,22 @@ const DEMO_WORKERS = [
6666
},
6767
];
6868

69-
const JOBS = [
70-
{ id: 1, title: "Bridal Lehenga Embroidery", budget: "Rs. 2000-4000", skill: "Embroidery", postedAgo: "2h", urgent: true },
71-
{ id: 2, title: "10 Suit Alterations (Corporate)", budget: "Rs. 1500", skill: "Alterations", postedAgo: "5h", urgent: false },
72-
{ id: 3, title: "Pattern Making - SS25 Collection", budget: "Rs. 5000-8000", skill: "Pattern Making", postedAgo: "1d", urgent: false },
73-
{ id: 4, title: "Leather Jacket Repair and Restoration", budget: "Rs. 800-1200", skill: "Leather Work", postedAgo: "3h", urgent: true },
74-
{ id: 5, title: "Children's School Uniform x 30 pcs", budget: "Rs. 600-800", skill: "Children's Wear", postedAgo: "2d", urgent: false },
69+
const FALLBACK_JOBS = [
70+
{ id: "f1", title: "Bridal Lehenga Embroidery", budget: "Rs. 2000-4000", skill: "Embroidery", posted_at: new Date(Date.now() - 2 * 3600000).toISOString(), urgent: true },
71+
{ id: "f2", title: "10 Suit Alterations (Corporate)", budget: "Rs. 1500", skill: "Alterations", posted_at: new Date(Date.now() - 5 * 3600000).toISOString(), urgent: false },
72+
{ id: "f3", title: "Leather Jacket Repair and Restoration", budget: "Rs. 800-1200", skill: "Leather Work", posted_at: new Date(Date.now() - 3 * 3600000).toISOString(), urgent: true },
7573
];
7674

75+
function postedAgo(isoString) {
76+
const diff = Date.now() - new Date(isoString).getTime();
77+
const mins = Math.floor(diff / 60000);
78+
if (mins < 2) return "just now";
79+
if (mins < 60) return `${mins}m`;
80+
const hrs = Math.floor(mins / 60);
81+
if (hrs < 24) return `${hrs}h`;
82+
return `${Math.floor(hrs / 24)}d`;
83+
}
84+
7785
const inputStyle = {
7886
width: "100%",
7987
padding: "10px 12px",
@@ -402,7 +410,7 @@ function JobCard({ job, copy, skillLabel }) {
402410
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
403411
<span style={{ fontSize: 11, padding: "5px 10px", borderRadius: 999, background: palette.soft, color: palette.primary, fontWeight: 700 }}>{skillLabel(job.skill)}</span>
404412
<span style={{ fontSize: 13, color: palette.text, fontWeight: 700, marginLeft: "auto" }}>{job.budget}</span>
405-
<span style={{ fontSize: 12, color: palette.muted }}>{job.postedAgo}</span>
413+
<span style={{ fontSize: 12, color: palette.muted }}>{job.posted_at ? postedAgo(job.posted_at) : (job.postedAgo || "")}</span>
406414
</div>
407415
</article>
408416
);
@@ -479,11 +487,18 @@ export default function WorkersBoardPage() {
479487
const [selectedWorker, setSelectedWorker] = useState(null);
480488
const [activeTab, setActiveTab] = useState("tailors");
481489
const [postForm, setPostForm] = useState({ title: "", skill: "Alterations", budget: "", description: "" });
482-
const [postedJobs, setPostedJobs] = useState(JOBS);
490+
const [postedJobs, setPostedJobs] = useState([]);
483491
const [jobPosted, setJobPosted] = useState(false);
484492

485493
const skillLabel = (skill) => copy.skillLabels[skill] || skill;
486494

495+
// Fetch jobs from API
496+
useEffect(() => {
497+
screeningApi.listJobs()
498+
.then((jobs) => setPostedJobs(jobs && jobs.length > 0 ? jobs : FALLBACK_JOBS))
499+
.catch(() => setPostedJobs(FALLBACK_JOBS));
500+
}, []);
501+
487502
// Fetch real workers from API
488503
useEffect(() => {
489504
screeningApi.listWorkers()
@@ -526,10 +541,14 @@ export default function WorkersBoardPage() {
526541

527542
const handlePostJob = () => {
528543
if (!postForm.title || !postForm.description) return;
529-
setPostedJobs((prev) => [{ id: Date.now(), title: postForm.title, budget: postForm.budget || copy.openBudget, skill: postForm.skill, postedAgo: copy.justNow, urgent: false }, ...prev]);
544+
const optimistic = { id: `opt_${Date.now()}`, title: postForm.title, budget: postForm.budget || copy.openBudget, skill: postForm.skill, posted_at: new Date().toISOString(), urgent: false };
545+
setPostedJobs((prev) => [optimistic, ...prev]);
530546
setJobPosted(true);
531547
setPostForm({ title: "", skill: "Alterations", budget: "", description: "" });
532548
setTimeout(() => setJobPosted(false), 3000);
549+
screeningApi.createJob({ title: optimistic.title, budget: optimistic.budget, skill: optimistic.skill, description: postForm.description })
550+
.then((created) => setPostedJobs((prev) => prev.map((j) => j.id === optimistic.id ? created : j)))
551+
.catch(() => {});
533552
};
534553

535554
const totalKarma = Object.values(karmaMap);

frontend/src/services/api.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,8 @@ export const screeningApi = {
5555
(await client.get("/review/queue")).data,
5656
submitReviewDecision: async (sessionId, payload) =>
5757
(await client.post(`/review/${sessionId}/decision`, payload)).data,
58+
59+
// Jobs
60+
listJobs: async () => (await client.get("/jobs")).data,
61+
createJob: async (payload) => (await client.post("/jobs", payload)).data,
5862
};

0 commit comments

Comments
 (0)