-
Notifications
You must be signed in to change notification settings - Fork 13
feat: add tracking link schemas for creation, update, and querying #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
558e054
60bdc55
ad98fae
46e1e15
475e643
8d25601
877d03c
1be9493
88489e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| }), | ||
| }) | ||
|
|
||
| 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, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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())
NODERepository: reqcore-inc/reqcore Length of output: 275 🏁 Script executed: git ls-files | grep -E "(dashboard|index\.vue)" | head -20Repository: reqcore-inc/reqcore Length of output: 883 🏁 Script executed: head -60 app/pages/dashboard/index.vue | tail -30Repository: reqcore-inc/reqcore Length of output: 921 Use calendar arithmetic instead of fixed milliseconds for +7 days. At line 47, adding 🛠️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| const { interviews: upcomingInterviews } = useInterviews({ | ||||||||
| status: 'scheduled', | ||||||||
| from: now.toISOString(), | ||||||||
| to: weekFromNow.toISOString(), | ||||||||
| from: today.toISOString(), | ||||||||
| to: weekFromToday.toISOString(), | ||||||||
| limit: 5, | ||||||||
| }) | ||||||||
|
|
||||||||
|
|
||||||||
Uh oh!
There was an error while loading. Please reload this page.