Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion app/components/AppTopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
{ label: 'Applications', to: '/dashboard/applications', icon: FileText, exact: false },
{ label: 'Interviews', to: '/dashboard/interviews', icon: Calendar, exact: false },
{ label: 'Timeline', to: '/dashboard/timeline', icon: History, exact: true },
{ label: 'Source Tracking', to: '/dashboard/source-tracking', icon: Radio, exact: true, comingSoon: true },
{ label: 'Source Tracking', to: '/dashboard/source-tracking', icon: Radio, exact: true },
{ label: 'AI Analysis', to: '/dashboard/ai-analysis', icon: Sparkles, exact: true },
{ label: 'Settings', to: '/dashboard/settings', icon: Settings, exact: false },
]
Expand Down Expand Up @@ -191,7 +191,7 @@
<component :is="item.icon" class="size-4" />
{{ item.label }}
<span
v-if="item.comingSoon"

Check failure on line 194 in app/components/AppTopBar.vue

View workflow job for this annotation

GitHub Actions / Build, typecheck, and test

Property 'comingSoon' does not exist on type '{ label: string; to: string; icon: FunctionalComponent<LucideProps, {}, any, {}>; exact: boolean; }'.
class="ml-0.5 inline-flex items-center rounded-full bg-amber-50 dark:bg-amber-950/40 px-1.5 py-0.5 text-[9px] font-semibold leading-none text-amber-700 dark:text-amber-400 ring-1 ring-inset ring-amber-200/60 dark:ring-amber-800/40"
>
Soon
Expand Down Expand Up @@ -364,7 +364,7 @@
<component :is="item.icon" class="size-4" />
{{ item.label }}
<span
v-if="item.comingSoon"

Check failure on line 367 in app/components/AppTopBar.vue

View workflow job for this annotation

GitHub Actions / Build, typecheck, and test

Property 'comingSoon' does not exist on type '{ label: string; to: string; icon: FunctionalComponent<LucideProps, {}, any, {}>; exact: boolean; }'.
class="ml-auto inline-flex items-center rounded-full bg-amber-50 dark:bg-amber-950/40 px-1.5 py-0.5 text-[9px] font-semibold leading-none text-amber-700 dark:text-amber-400 ring-1 ring-inset ring-amber-200/60 dark:ring-amber-800/40"
>
Soon
Expand Down Expand Up @@ -487,7 +487,7 @@
<component :is="item.icon" class="size-4" />
{{ item.label }}
<span
v-if="item.comingSoon"

Check failure on line 490 in app/components/AppTopBar.vue

View workflow job for this annotation

GitHub Actions / Build, typecheck, and test

Property 'comingSoon' does not exist on type '{ label: string; to: string; icon: FunctionalComponent<LucideProps, {}, any, {}>; exact: boolean; }'.
class="ml-auto inline-flex items-center rounded-full bg-amber-50 dark:bg-amber-950/40 px-1.5 py-0.5 text-[9px] font-semibold leading-none text-amber-700 dark:text-amber-400 ring-1 ring-inset ring-amber-200/60 dark:ring-amber-800/40"
>
Soon
Expand Down
200 changes: 200 additions & 0 deletions app/composables/useSourceTracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* Composable for source tracking analytics and tracking link management.
* Provides data for the source tracking dashboard and link CRUD operations.
*/

interface TrackingLink {
id: string
jobId: string | null
jobTitle: string | null
channel: string
name: string
code: string
utmSource: string | null
utmMedium: string | null
utmCampaign: string | null
utmTerm: string | null
utmContent: string | null
clickCount: number
applicationCount: number
isActive: boolean
createdAt: string
updatedAt: string
}

interface SourceStats {
channelBreakdown: { channel: string; count: number }[]
topLinks: {
id: string
name: string
channel: string
code: string
jobTitle: string | null
clickCount: number
applicationCount: number
isActive: boolean
}[]
funnel: Record<string, Record<string, number>>
dailyTrend: { date: string; channel: string; count: number }[]
recentAttributed: {
applicationId: string
channel: string
utmSource: string | null
utmCampaign: string | null
referrerDomain: string | null
trackingLinkName: string | null
candidateFirstName: string
candidateLastName: string
candidateEmail: string
jobTitle: string
status: string
appliedAt: string
}[]
topReferrerDomains: { domain: string | null; count: number }[]
summary: {
totalTracked: number
totalUntracked: number
attributionRate: number
}
}

export function useSourceTracking(options?: {
jobId?: Ref<string | undefined> | string
from?: Ref<string | undefined> | string
to?: Ref<string | undefined> | string
}) {
const jobId = computed(() => toValue(options?.jobId))
const from = computed(() => toValue(options?.from))
const to = computed(() => toValue(options?.to))

// ─── Source stats ─────────────────────────
const {
data: stats,
status: statsStatus,
error: statsError,
refresh: refreshStats,
} = useFetch<SourceStats>('/api/source-tracking/stats', {
key: 'source-stats',
headers: useRequestHeaders(['cookie']),
query: computed(() => {
const q: Record<string, string> = {}
if (jobId.value) q.jobId = jobId.value
if (from.value) q.from = from.value
if (to.value) q.to = to.value
return q
}),
})

const channelBreakdown = computed(() => stats.value?.channelBreakdown ?? [])
const topLinks = computed(() => stats.value?.topLinks ?? [])
const funnel = computed(() => stats.value?.funnel ?? {})
const dailyTrend = computed(() => stats.value?.dailyTrend ?? [])
const recentAttributed = computed(() => stats.value?.recentAttributed ?? [])
const topReferrerDomains = computed(() => stats.value?.topReferrerDomains ?? [])
const summary = computed(() => stats.value?.summary ?? { totalTracked: 0, totalUntracked: 0, attributionRate: 0 })

return {
channelBreakdown,
topLinks,
funnel,
dailyTrend,
recentAttributed,
topReferrerDomains,
summary,
statsStatus,
statsError,
refreshStats,
}
}

/**
* Composable for managing tracking links (CRUD operations).
*/
export function useTrackingLinks(options?: {
jobId?: Ref<string | undefined> | string
channel?: Ref<string | undefined> | string
}) {
const toast = useToast()

const jobId = computed(() => toValue(options?.jobId))
const channel = computed(() => toValue(options?.channel))

const {
data,
status: fetchStatus,
error,
refresh,
} = useFetch<{ data: TrackingLink[]; total: number }>('/api/tracking-links', {
key: 'tracking-links',
headers: useRequestHeaders(['cookie']),
query: computed(() => {
const q: Record<string, string> = {}
if (jobId.value) q.jobId = jobId.value
if (channel.value) q.channel = channel.value
return q
}),
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const links = computed<TrackingLink[]>(() => data.value?.data ?? [])
const total = computed(() => data.value?.total ?? 0)

async function createLink(payload: {
jobId?: string
channel?: string
name: string
utmSource?: string
utmMedium?: string
utmCampaign?: string
utmTerm?: string
utmContent?: string
}) {
const created = await $fetch('/api/tracking-links', {
method: 'POST',
body: payload,
})
await refresh()
toast.success('Tracking link created')
return created
}

async function updateLink(id: string, payload: {
name?: string
channel?: string
utmSource?: string
utmMedium?: string
utmCampaign?: string
utmTerm?: string
utmContent?: string
isActive?: boolean
}) {
const updated = await $fetch(`/api/tracking-links/${id}`, {
method: 'PATCH',
body: payload,
})
await refresh()
return updated
}

async function deleteLink(id: string) {
await $fetch(`/api/tracking-links/${id}`, { method: 'DELETE' })
await refresh()
toast.success('Tracking link deleted')
}

async function toggleLink(id: string, isActive: boolean) {
await updateLink(id, { isActive })
toast.success(isActive ? 'Link activated' : 'Link deactivated')
}

return {
links,
total,
fetchStatus,
error,
refresh,
createLink,
updateLink,
deleteLink,
toggleLink,
}
}
9 changes: 6 additions & 3 deletions app/pages/dashboard/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ const {
// ─────────────────────────────────────────────

const now = new Date()
const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
// Truncate to start-of-day so the useFetch key is identical on server & client
// (prevents SSR/hydration mismatch from sub-second timestamp drift)
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const weekFromToday = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify DST edge behavior in JS Date arithmetic.
TZ=America/New_York node <<'NODE'
const today = new Date(2026, 9, 31, 0, 0, 0, 0) // Oct 31, 2026 local
const fixedMs = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)
const calendar = new Date(today)
calendar.setDate(calendar.getDate() + 7)

console.log('today      :', today.toString())
console.log('fixed ms +7:', fixedMs.toString())
console.log('setDate +7 :', calendar.toString())
NODE

Repository: reqcore-inc/reqcore

Length of output: 275


🏁 Script executed:

git ls-files | grep -E "(dashboard|index\.vue)" | head -20

Repository: reqcore-inc/reqcore

Length of output: 883


🏁 Script executed:

head -60 app/pages/dashboard/index.vue | tail -30

Repository: reqcore-inc/reqcore

Length of output: 921


Use calendar arithmetic instead of fixed milliseconds for +7 days.

At line 47, adding 7 * 24 * 60 * 60 * 1000 milliseconds shifts the boundary by an hour when crossing DST transitions (e.g., Nov 6 23:00 instead of Nov 7 00:00), causing the date range query to include or exclude interviews incorrectly.

🛠️ Proposed fix
-const weekFromToday = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)
+const weekFromToday = new Date(today)
+weekFromToday.setDate(weekFromToday.getDate() + 7)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const weekFromToday = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)
const weekFromToday = new Date(today)
weekFromToday.setDate(weekFromToday.getDate() + 7)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/index.vue` at line 47, Replace the millisecond addition
with calendar-safe arithmetic: create a copy of today and call setDate(getDate()
+ 7) so weekFromToday advances by 7 calendar days (respecting DST) instead of
adding a fixed millisecond offset; update the expression that defines
weekFromToday (currently using today.getTime() + 7 * 24 * 60 * 60 * 1000) to use
the copy/setDate/getDate approach to avoid DST shifts.


const { interviews: upcomingInterviews } = useInterviews({
status: 'scheduled',
from: now.toISOString(),
to: weekFromNow.toISOString(),
from: today.toISOString(),
to: weekFromToday.toISOString(),
limit: 5,
})

Expand Down
Loading
Loading