Skip to content

Commit 8951a74

Browse files
authored
feat: add contacted gig status reminders (#387)
* feat: add contacted gig status reminders * fix: address contacted status review feedback
1 parent 24608c4 commit 8951a74

19 files changed

Lines changed: 496 additions & 163 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ JOB_RETRY_MAX_SECONDS=300
3636
JOB_TIMEOUT_SECONDS=600
3737
JOB_RESULT_TTL_SECONDS=3600
3838
GIG_RECRUITING_STALE_DAYS=7
39+
GIG_CONTACTED_REMINDER_DAYS=5
3940
GIG_RECRUITING_REMINDER_MAX_AGE_DAYS=90
4041

4142
# Internal transfer storage (optional defaults for host-run app services)

apps/admin_dashboard/src/main.tsx

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ type DashboardNotification = {
283283

284284
type DashboardNotificationsResponse = {
285285
stale_days: number
286+
contacted_reminder_days: number
286287
notifications: DashboardNotification[]
287288
}
288289

@@ -927,6 +928,7 @@ function App() {
927928
const [projectQuery, setProjectQuery] = useState("")
928929
const [projectStatus, setProjectStatus] = useState(initialProjectDetailId ? "" : "Open")
929930
const [staleRecruitingDays, setStaleRecruitingDays] = useState(7)
931+
const [contactedReminderDays, setContactedReminderDays] = useState(5)
930932
const [peopleQuery, setPeopleQuery] = useState("")
931933
const [peopleMember, setPeopleMember] = useState("")
932934
const [peopleFilters, setPeopleFilters] = useState<FilterState>({})
@@ -1757,6 +1759,7 @@ function App() {
17571759
"/dashboard/api/notifications?limit=20",
17581760
)
17591761
setStaleRecruitingDays(payload.stale_days || 7)
1762+
setContactedReminderDays(payload.contacted_reminder_days || 5)
17601763
setNotifications(payload.notifications || [])
17611764
} catch (error) {
17621765
showError(error, "Unable to load notifications")
@@ -2550,16 +2553,18 @@ function App() {
25502553
)
25512554

25522555
function openNotification(notification: DashboardNotification) {
2553-
if (notification.type === "stale_recruiting_gig") {
2556+
if (notification.type === "stale_recruiting_gig" || notification.type === "contacted_gig") {
2557+
const isContacted = notification.type === "contacted_gig"
2558+
const notificationPrefix = isContacted ? "contacted-gig:" : "stale-recruiting:"
25542559
const gigId =
25552560
notification.engagement_id ||
2556-
(notification.id.startsWith("stale-recruiting:")
2557-
? notification.id.slice("stale-recruiting:".length)
2561+
(notification.id.startsWith(notificationPrefix)
2562+
? notification.id.slice(notificationPrefix.length)
25582563
: "")
25592564
if (gigId) {
25602565
openGigDetail(gigId)
25612566
} else {
2562-
setGigStatus("recruiting")
2567+
setGigStatus(isContacted ? "contacted" : "recruiting")
25632568
navigate("gigs", true)
25642569
}
25652570
}
@@ -2797,6 +2802,7 @@ function App() {
27972802
includeHistorical={gigIncludeHistorical}
27982803
limit={gigLimit}
27992804
staleDays={staleRecruitingDays}
2805+
contactedReminderDays={contactedReminderDays}
28002806
canWrite={can("gigs:write")}
28012807
canSearchCandidates={can("people:read")}
28022808
canManageLeads={can("people:read")}
@@ -3302,6 +3308,7 @@ function FilterChips({
33023308
const gigStatuses = [
33033309
"lead",
33043310
"recruiting",
3311+
"contacted",
33053312
"filled",
33063313
"unknown",
33073314
"lost",
@@ -3339,11 +3346,16 @@ function gigActivityTimestamp(gig: Gig) {
33393346
return activityTimes.length > 0 ? new Date(Math.max(...activityTimes)).toISOString() : ""
33403347
}
33413348

3342-
function staleRecruitingAge(gig: Gig, staleDays: number) {
3343-
if (gig.status !== "recruiting") return null
3344-
const latestActivity = gigActivityTimestamp(gig)
3345-
const age = daysSince(latestActivity)
3346-
if (age === null || age < staleDays) return null
3349+
function staleGigAge(gig: Gig, recruitingDays: number, contactedDays: number) {
3350+
const isContacted = gig.status === "contacted"
3351+
if (gig.status !== "recruiting" && !isContacted) return null
3352+
const age = daysSince(
3353+
isContacted
3354+
? gig.last_status_changed_at || gig.updated_at || gig.created_at
3355+
: gigActivityTimestamp(gig),
3356+
)
3357+
const reminderDays = isContacted ? contactedDays : recruitingDays
3358+
if (age === null || age < reminderDays) return null
33473359
return age
33483360
}
33493361

@@ -5172,6 +5184,7 @@ function GigsView(props: {
51725184
includeHistorical: boolean
51735185
limit: number
51745186
staleDays: number
5187+
contactedReminderDays: number
51755188
canWrite: boolean
51765189
canSearchCandidates: boolean
51775190
canManageLeads: boolean
@@ -5207,7 +5220,9 @@ function GigsView(props: {
52075220
acc.total += 1
52085221
acc.applications += Number(gig.application_count || 0)
52095222
acc.interested += Number(gig.interested_count || 0)
5210-
if (staleRecruitingAge(gig, props.staleDays) !== null) acc.stale += 1
5223+
if (staleGigAge(gig, props.staleDays, props.contactedReminderDays) !== null) {
5224+
acc.stale += 1
5225+
}
52115226
return acc
52125227
},
52135228
{ total: 0, applications: 0, interested: 0, stale: 0 },
@@ -5459,6 +5474,7 @@ function GigsView(props: {
54595474
crmContactUrl={props.crmContactUrl}
54605475
crmAttachmentUrl={props.crmAttachmentUrl}
54615476
staleDays={props.staleDays}
5477+
contactedReminderDays={props.contactedReminderDays}
54625478
onBack={props.onCloseGig}
54635479
onUpdateStatus={props.onUpdateStatus}
54645480
onAddApplication={props.onAddApplication}
@@ -5476,7 +5492,7 @@ function GigsView(props: {
54765492
<Metric id="gigMetricTotal" label="Gigs" value={counts.total} />
54775493
<Metric id="gigMetricCandidates" label="Candidates" value={counts.applications} />
54785494
<Metric id="gigMetricInterested" label="Interested" value={counts.interested} />
5479-
<Metric id="gigMetricStale" label="Stale recruiting" value={counts.stale} />
5495+
<Metric id="gigMetricStale" label="Needs update" value={counts.stale} />
54805496
</section>
54815497

54825498
<Card>
@@ -5516,6 +5532,7 @@ function GigsView(props: {
55165532
loading={props.loading}
55175533
canWrite={props.canWrite}
55185534
staleDays={props.staleDays}
5535+
contactedReminderDays={props.contactedReminderDays}
55195536
onOpenGig={props.onOpenGig}
55205537
onUpdateStatus={props.onUpdateStatus}
55215538
/>
@@ -5879,28 +5896,30 @@ function GigListItem({
58795896
onOpenGig,
58805897
onUpdateStatus,
58815898
staleDays,
5899+
contactedReminderDays,
58825900
}: {
58835901
gig: Gig
58845902
loading: Record<string, boolean>
58855903
canWrite: boolean
58865904
onOpenGig: (gigId: string) => void
58875905
onUpdateStatus: (gigId: string, status: string) => void
58885906
staleDays: number
5907+
contactedReminderDays: number
58895908
}) {
58905909
const applications = Array.isArray(gig.applications) ? gig.applications : []
5891-
const isRecruiting = gig.status === "recruiting"
5910+
const isActive = gig.status === "recruiting" || gig.status === "contacted"
58925911
const threadUrl =
58935912
gig.discord_guild_id && gig.discord_thread_id
58945913
? `https://discord.com/channels/${encodeURIComponent(
58955914
gig.discord_guild_id,
58965915
)}/${encodeURIComponent(gig.discord_thread_id)}`
58975916
: ""
5898-
const staleAge = staleRecruitingAge(gig, staleDays)
5917+
const staleAge = staleGigAge(gig, staleDays, contactedReminderDays)
58995918
return (
59005919
<article
59015920
className={cn(
59025921
"grid gap-4 rounded-md border bg-background p-4 lg:grid-cols-[minmax(0,1fr)_220px_180px] lg:items-start",
5903-
!isRecruiting && "border-l-4 border-l-muted-foreground/60 bg-secondary/45",
5922+
!isActive && "border-l-4 border-l-muted-foreground/60 bg-secondary/45",
59045923
)}
59055924
>
59065925
<div className="min-w-0">
@@ -5921,14 +5940,14 @@ function GigListItem({
59215940
? "succeeded"
59225941
: gig.status === "lost" || gig.status === "duplicate"
59235942
? "failed"
5924-
: isRecruiting
5943+
: isActive
59255944
? "queued"
59265945
: "neutral"
59275946
}
59285947
>
59295948
{gig.status_label || titleCase(gig.status)}
59305949
</Badge>
5931-
{!isRecruiting ? <Badge variant="neutral">Not recruiting</Badge> : null}
5950+
{!isActive ? <Badge variant="neutral">Not active</Badge> : null}
59325951
{staleAge !== null ? <Badge variant="running">{staleAge}d stale</Badge> : null}
59335952
</div>
59345953
<div className="mt-2 flex flex-wrap gap-1.5">
@@ -6027,6 +6046,7 @@ function GigDetailPage({
60276046
crmContactUrl,
60286047
crmAttachmentUrl,
60296048
staleDays,
6049+
contactedReminderDays,
60306050
onBack,
60316051
onUpdateStatus,
60326052
onAddApplication,
@@ -6039,6 +6059,7 @@ function GigDetailPage({
60396059
crmContactUrl: (contactId?: string) => string
60406060
crmAttachmentUrl: (attachmentId?: string) => string
60416061
staleDays: number
6062+
contactedReminderDays: number
60426063
onBack: () => void
60436064
onUpdateStatus: (gigId: string, status: string) => void
60446065
onAddApplication: (gigId: string, crmProfile: string) => Promise<boolean>
@@ -6050,7 +6071,7 @@ function GigDetailPage({
60506071
const [candidateSearchError, setCandidateSearchError] = useState("")
60516072
const [crmProfile, setCrmProfile] = useState("")
60526073
const applications = Array.isArray(gig.applications) ? gig.applications : []
6053-
const isRecruiting = gig.status === "recruiting"
6074+
const isActive = gig.status === "recruiting" || gig.status === "contacted"
60546075
const candidateQueryReady = candidateQuery.trim().length >= 2
60556076
const selectedCandidateId = selectedCandidate?.crm_contact_id || ""
60566077
const candidateProfile = selectedCandidateId
@@ -6063,7 +6084,7 @@ function GigDetailPage({
60636084
gig.discord_guild_id,
60646085
)}/${encodeURIComponent(gig.discord_thread_id)}`
60656086
: ""
6066-
const staleAge = staleRecruitingAge(gig, staleDays)
6087+
const staleAge = staleGigAge(gig, staleDays, contactedReminderDays)
60676088

60686089
useEffect(() => {
60696090
if (!canWrite || !canSearchCandidates) return
@@ -6116,9 +6137,7 @@ function GigDetailPage({
61166137

61176138
return (
61186139
<div className="grid gap-5">
6119-
<Card
6120-
className={cn(!isRecruiting && "border-l-4 border-l-muted-foreground/60 bg-secondary/35")}
6121-
>
6140+
<Card className={cn(!isActive && "border-l-4 border-l-muted-foreground/60 bg-secondary/35")}>
61226141
<CardHeader className="items-start">
61236142
<div className="grid gap-2">
61246143
<Button type="button" variant="ghost" size="sm" className="w-fit" onClick={onBack}>
@@ -6134,14 +6153,14 @@ function GigDetailPage({
61346153
? "succeeded"
61356154
: gig.status === "lost" || gig.status === "duplicate"
61366155
? "failed"
6137-
: isRecruiting
6156+
: isActive
61386157
? "queued"
61396158
: "neutral"
61406159
}
61416160
>
61426161
{gig.status_label || titleCase(gig.status)}
61436162
</Badge>
6144-
{!isRecruiting ? <Badge variant="neutral">Not recruiting</Badge> : null}
6163+
{!isActive ? <Badge variant="neutral">Not active</Badge> : null}
61456164
{staleAge !== null ? <Badge variant="running">{staleAge}d stale</Badge> : null}
61466165
{gig.posting_type ? (
61476166
<Badge variant="neutral">{titleCase(gig.posting_type)}</Badge>

apps/api/src/five08/backend/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4659,12 +4659,14 @@ async def dashboard_notifications_handler(
46594659
viewer_discord_user_id=session.subject,
46604660
include_all=include_all,
46614661
stale_days=settings.gig_recruiting_stale_days,
4662+
contacted_reminder_days=settings.gig_contacted_reminder_days,
46624663
max_age_days=settings.gig_recruiting_reminder_max_age_days,
46634664
limit=limit,
46644665
)
46654666
return JSONResponse(
46664667
{
46674668
"stale_days": settings.gig_recruiting_stale_days,
4669+
"contacted_reminder_days": settings.gig_contacted_reminder_days,
46684670
"notifications": notifications,
46694671
}
46704672
)

apps/api/src/five08/backend/static/dashboard/.vite/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"index.html": {
3-
"file": "assets/index-BRLPZTtg.js",
3+
"file": "assets/index-CB3HLLMv.js",
44
"name": "index",
55
"src": "index.html",
66
"isEntry": true,

apps/api/src/five08/backend/static/dashboard/assets/index-BRLPZTtg.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

apps/api/src/five08/backend/static/dashboard/assets/index-CB3HLLMv.js

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/api/src/five08/backend/static/dashboard/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>508 Operations Dashboard</title>
7-
<script type="module" crossorigin src="/dashboard/assets/index-BRLPZTtg.js"></script>
7+
<script type="module" crossorigin src="/dashboard/assets/index-CB3HLLMv.js"></script>
88
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-2UypYUrJ.css">
99
</head>
1010
<body>

0 commit comments

Comments
 (0)