Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ JOB_RETRY_MAX_SECONDS=300
JOB_TIMEOUT_SECONDS=600
JOB_RESULT_TTL_SECONDS=3600
GIG_RECRUITING_STALE_DAYS=7
GIG_CONTACTED_REMINDER_DAYS=5
GIG_RECRUITING_REMINDER_MAX_AGE_DAYS=90

# Internal transfer storage (optional defaults for host-run app services)
Expand Down
65 changes: 42 additions & 23 deletions apps/admin_dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ type DashboardNotification = {

type DashboardNotificationsResponse = {
stale_days: number
contacted_reminder_days: number
notifications: DashboardNotification[]
}

Expand Down Expand Up @@ -927,6 +928,7 @@ function App() {
const [projectQuery, setProjectQuery] = useState("")
const [projectStatus, setProjectStatus] = useState(initialProjectDetailId ? "" : "Open")
const [staleRecruitingDays, setStaleRecruitingDays] = useState(7)
const [contactedReminderDays, setContactedReminderDays] = useState(5)
const [peopleQuery, setPeopleQuery] = useState("")
const [peopleMember, setPeopleMember] = useState("")
const [peopleFilters, setPeopleFilters] = useState<FilterState>({})
Expand Down Expand Up @@ -1757,6 +1759,7 @@ function App() {
"/dashboard/api/notifications?limit=20",
)
setStaleRecruitingDays(payload.stale_days || 7)
setContactedReminderDays(payload.contacted_reminder_days || 5)
setNotifications(payload.notifications || [])
} catch (error) {
showError(error, "Unable to load notifications")
Expand Down Expand Up @@ -2550,16 +2553,18 @@ function App() {
)

function openNotification(notification: DashboardNotification) {
if (notification.type === "stale_recruiting_gig") {
if (notification.type === "stale_recruiting_gig" || notification.type === "contacted_gig") {
const isContacted = notification.type === "contacted_gig"
const notificationPrefix = isContacted ? "contacted-gig:" : "stale-recruiting:"
const gigId =
notification.engagement_id ||
(notification.id.startsWith("stale-recruiting:")
? notification.id.slice("stale-recruiting:".length)
(notification.id.startsWith(notificationPrefix)
? notification.id.slice(notificationPrefix.length)
: "")
if (gigId) {
openGigDetail(gigId)
} else {
setGigStatus("recruiting")
setGigStatus(isContacted ? "contacted" : "recruiting")
navigate("gigs", true)
}
}
Expand Down Expand Up @@ -2797,6 +2802,7 @@ function App() {
includeHistorical={gigIncludeHistorical}
limit={gigLimit}
staleDays={staleRecruitingDays}
contactedReminderDays={contactedReminderDays}
canWrite={can("gigs:write")}
canSearchCandidates={can("people:read")}
canManageLeads={can("people:read")}
Expand Down Expand Up @@ -3302,6 +3308,7 @@ function FilterChips({
const gigStatuses = [
"lead",
"recruiting",
"contacted",
"filled",
"unknown",
"lost",
Expand Down Expand Up @@ -3339,11 +3346,16 @@ function gigActivityTimestamp(gig: Gig) {
return activityTimes.length > 0 ? new Date(Math.max(...activityTimes)).toISOString() : ""
}

function staleRecruitingAge(gig: Gig, staleDays: number) {
if (gig.status !== "recruiting") return null
const latestActivity = gigActivityTimestamp(gig)
const age = daysSince(latestActivity)
if (age === null || age < staleDays) return null
function staleGigAge(gig: Gig, recruitingDays: number, contactedDays: number) {
const isContacted = gig.status === "contacted"
if (gig.status !== "recruiting" && !isContacted) return null
const age = daysSince(
isContacted
? gig.last_status_changed_at || gig.updated_at || gig.created_at
: gigActivityTimestamp(gig),
)
const reminderDays = isContacted ? contactedDays : recruitingDays
if (age === null || age < reminderDays) return null
return age
}

Expand Down Expand Up @@ -5172,6 +5184,7 @@ function GigsView(props: {
includeHistorical: boolean
limit: number
staleDays: number
contactedReminderDays: number
canWrite: boolean
canSearchCandidates: boolean
canManageLeads: boolean
Expand Down Expand Up @@ -5207,7 +5220,9 @@ function GigsView(props: {
acc.total += 1
acc.applications += Number(gig.application_count || 0)
acc.interested += Number(gig.interested_count || 0)
if (staleRecruitingAge(gig, props.staleDays) !== null) acc.stale += 1
if (staleGigAge(gig, props.staleDays, props.contactedReminderDays) !== null) {
acc.stale += 1
}
return acc
},
{ total: 0, applications: 0, interested: 0, stale: 0 },
Expand Down Expand Up @@ -5459,6 +5474,7 @@ function GigsView(props: {
crmContactUrl={props.crmContactUrl}
crmAttachmentUrl={props.crmAttachmentUrl}
staleDays={props.staleDays}
contactedReminderDays={props.contactedReminderDays}
onBack={props.onCloseGig}
onUpdateStatus={props.onUpdateStatus}
onAddApplication={props.onAddApplication}
Expand All @@ -5476,7 +5492,7 @@ function GigsView(props: {
<Metric id="gigMetricTotal" label="Gigs" value={counts.total} />
<Metric id="gigMetricCandidates" label="Candidates" value={counts.applications} />
<Metric id="gigMetricInterested" label="Interested" value={counts.interested} />
<Metric id="gigMetricStale" label="Stale recruiting" value={counts.stale} />
<Metric id="gigMetricStale" label="Needs update" value={counts.stale} />
</section>

<Card>
Expand Down Expand Up @@ -5516,6 +5532,7 @@ function GigsView(props: {
loading={props.loading}
canWrite={props.canWrite}
staleDays={props.staleDays}
contactedReminderDays={props.contactedReminderDays}
onOpenGig={props.onOpenGig}
onUpdateStatus={props.onUpdateStatus}
/>
Expand Down Expand Up @@ -5879,28 +5896,30 @@ function GigListItem({
onOpenGig,
onUpdateStatus,
staleDays,
contactedReminderDays,
}: {
gig: Gig
loading: Record<string, boolean>
canWrite: boolean
onOpenGig: (gigId: string) => void
onUpdateStatus: (gigId: string, status: string) => void
staleDays: number
contactedReminderDays: number
}) {
const applications = Array.isArray(gig.applications) ? gig.applications : []
const isRecruiting = gig.status === "recruiting"
const isActive = gig.status === "recruiting" || gig.status === "contacted"
const threadUrl =
gig.discord_guild_id && gig.discord_thread_id
? `https://discord.com/channels/${encodeURIComponent(
gig.discord_guild_id,
)}/${encodeURIComponent(gig.discord_thread_id)}`
: ""
const staleAge = staleRecruitingAge(gig, staleDays)
const staleAge = staleGigAge(gig, staleDays, contactedReminderDays)
return (
<article
className={cn(
"grid gap-4 rounded-md border bg-background p-4 lg:grid-cols-[minmax(0,1fr)_220px_180px] lg:items-start",
!isRecruiting && "border-l-4 border-l-muted-foreground/60 bg-secondary/45",
!isActive && "border-l-4 border-l-muted-foreground/60 bg-secondary/45",
)}
>
<div className="min-w-0">
Expand All @@ -5921,14 +5940,14 @@ function GigListItem({
? "succeeded"
: gig.status === "lost" || gig.status === "duplicate"
? "failed"
: isRecruiting
: isActive
? "queued"
: "neutral"
}
>
{gig.status_label || titleCase(gig.status)}
</Badge>
{!isRecruiting ? <Badge variant="neutral">Not recruiting</Badge> : null}
{!isActive ? <Badge variant="neutral">Not active</Badge> : null}
{staleAge !== null ? <Badge variant="running">{staleAge}d stale</Badge> : null}
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
Expand Down Expand Up @@ -6027,6 +6046,7 @@ function GigDetailPage({
crmContactUrl,
crmAttachmentUrl,
staleDays,
contactedReminderDays,
onBack,
onUpdateStatus,
onAddApplication,
Expand All @@ -6039,6 +6059,7 @@ function GigDetailPage({
crmContactUrl: (contactId?: string) => string
crmAttachmentUrl: (attachmentId?: string) => string
staleDays: number
contactedReminderDays: number
onBack: () => void
onUpdateStatus: (gigId: string, status: string) => void
onAddApplication: (gigId: string, crmProfile: string) => Promise<boolean>
Expand All @@ -6050,7 +6071,7 @@ function GigDetailPage({
const [candidateSearchError, setCandidateSearchError] = useState("")
const [crmProfile, setCrmProfile] = useState("")
const applications = Array.isArray(gig.applications) ? gig.applications : []
const isRecruiting = gig.status === "recruiting"
const isActive = gig.status === "recruiting" || gig.status === "contacted"
const candidateQueryReady = candidateQuery.trim().length >= 2
const selectedCandidateId = selectedCandidate?.crm_contact_id || ""
const candidateProfile = selectedCandidateId
Expand All @@ -6063,7 +6084,7 @@ function GigDetailPage({
gig.discord_guild_id,
)}/${encodeURIComponent(gig.discord_thread_id)}`
: ""
const staleAge = staleRecruitingAge(gig, staleDays)
const staleAge = staleGigAge(gig, staleDays, contactedReminderDays)

useEffect(() => {
if (!canWrite || !canSearchCandidates) return
Expand Down Expand Up @@ -6116,9 +6137,7 @@ function GigDetailPage({

return (
<div className="grid gap-5">
<Card
className={cn(!isRecruiting && "border-l-4 border-l-muted-foreground/60 bg-secondary/35")}
>
<Card className={cn(!isActive && "border-l-4 border-l-muted-foreground/60 bg-secondary/35")}>
<CardHeader className="items-start">
<div className="grid gap-2">
<Button type="button" variant="ghost" size="sm" className="w-fit" onClick={onBack}>
Expand All @@ -6134,14 +6153,14 @@ function GigDetailPage({
? "succeeded"
: gig.status === "lost" || gig.status === "duplicate"
? "failed"
: isRecruiting
: isActive
? "queued"
: "neutral"
}
>
{gig.status_label || titleCase(gig.status)}
</Badge>
{!isRecruiting ? <Badge variant="neutral">Not recruiting</Badge> : null}
{!isActive ? <Badge variant="neutral">Not active</Badge> : null}
{staleAge !== null ? <Badge variant="running">{staleAge}d stale</Badge> : null}
{gig.posting_type ? (
<Badge variant="neutral">{titleCase(gig.posting_type)}</Badge>
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/five08/backend/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4659,12 +4659,14 @@ async def dashboard_notifications_handler(
viewer_discord_user_id=session.subject,
include_all=include_all,
stale_days=settings.gig_recruiting_stale_days,
contacted_reminder_days=settings.gig_contacted_reminder_days,
max_age_days=settings.gig_recruiting_reminder_max_age_days,
limit=limit,
)
return JSONResponse(
{
"stale_days": settings.gig_recruiting_stale_days,
"contacted_reminder_days": settings.gig_contacted_reminder_days,
"notifications": notifications,
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"index.html": {
"file": "assets/index-BRLPZTtg.js",
"file": "assets/index-CB3HLLMv.js",
"name": "index",
"src": "index.html",
"isEntry": true,
Expand Down

This file was deleted.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/api/src/five08/backend/static/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>508 Operations Dashboard</title>
<script type="module" crossorigin src="/dashboard/assets/index-BRLPZTtg.js"></script>
<script type="module" crossorigin src="/dashboard/assets/index-CB3HLLMv.js"></script>
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-2UypYUrJ.css">
</head>
<body>
Expand Down
Loading