Skip to content

Commit 7545be8

Browse files
authored
Merge pull request #343 from codeunia-dev/fix/companyhackathons
feat(company-dashboard): Integrate real hackathon & event metrics into analytics
2 parents beab5aa + 9a0c590 commit 7545be8

File tree

4 files changed

+483
-38
lines changed

4 files changed

+483
-38
lines changed

app/api/hackathons/[id]/register/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export async function POST(
8282
const fullName = profile ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() : ''
8383

8484
// Register user in master_registrations table
85+
// Note: hackathon.id is an integer, convert to string for activity_id
8586
const { error: registrationError } = await supabase
8687
.from('master_registrations')
8788
.insert({
@@ -92,6 +93,11 @@ export async function POST(
9293
full_name: fullName || undefined,
9394
email: profile?.email || user.email,
9495
phone: profile?.phone || undefined,
96+
metadata: {
97+
hackathon_id: hackathon.id,
98+
hackathon_slug: hackathon.slug,
99+
hackathon_title: hackathon.title
100+
}
95101
})
96102

97103
if (registrationError) {

components/dashboard/AnalyticsCharts.tsx

Lines changed: 216 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,114 @@ interface AnalyticsChartsProps {
3030

3131
export function AnalyticsCharts({ analytics, dateRange, onExport }: AnalyticsChartsProps) {
3232
const [chartType, setChartType] = useState<'line' | 'bar' | 'area'>('line')
33+
const [actualStats, setActualStats] = React.useState<{
34+
views: number
35+
clicks: number
36+
registrations: number
37+
eventsPublished: number
38+
eventsCreated: number
39+
hackathonsPublished: number
40+
hackathonsCreated: number
41+
} | null>(null)
42+
43+
// Fetch actual stats from events and hackathons tables
44+
React.useEffect(() => {
45+
const fetchActualStats = async () => {
46+
try {
47+
const pathParts = window.location.pathname.split('/')
48+
const companySlug = pathParts[pathParts.indexOf('company') + 1]
49+
50+
// Fetch events
51+
const eventsRes = await fetch(`/api/companies/${companySlug}/events?status=all&limit=100`)
52+
const eventsData = await eventsRes.json()
53+
/* eslint-disable @typescript-eslint/no-explicit-any */
54+
const allEvents = eventsData.events || []
55+
const approvedEvents = allEvents.filter((e: any) => e.approval_status === 'approved')
56+
const eventViews = approvedEvents.reduce((sum: number, e: any) => sum + (e.views || 0), 0)
57+
const eventClicks = approvedEvents.reduce((sum: number, e: any) => sum + (e.clicks || 0), 0)
58+
const eventRegs = approvedEvents.reduce((sum: number, e: any) => sum + (e.registered || 0), 0)
59+
60+
// Fetch hackathons
61+
const hackathonsRes = await fetch(`/api/companies/${companySlug}/hackathons?status=all&limit=100`)
62+
const hackathonsData = await hackathonsRes.json()
63+
const allHackathons = hackathonsData.hackathons || []
64+
const approvedHackathons = allHackathons.filter((h: any) => h.approval_status === 'approved')
65+
const hackathonViews = approvedHackathons.reduce((sum: number, h: any) => sum + (h.views || 0), 0)
66+
const hackathonClicks = approvedHackathons.reduce((sum: number, h: any) => sum + (h.clicks || 0), 0)
67+
const hackathonRegs = approvedHackathons.reduce((sum: number, h: any) => sum + (h.registered || 0), 0)
68+
/* eslint-enable @typescript-eslint/no-explicit-any */
69+
70+
setActualStats({
71+
views: eventViews + hackathonViews,
72+
clicks: eventClicks + hackathonClicks,
73+
registrations: eventRegs + hackathonRegs,
74+
eventsPublished: approvedEvents.length,
75+
eventsCreated: allEvents.length,
76+
hackathonsPublished: approvedHackathons.length,
77+
hackathonsCreated: allHackathons.length,
78+
})
79+
} catch (error) {
80+
console.error('Error fetching actual stats:', error)
81+
}
82+
}
83+
84+
fetchActualStats()
85+
}, [])
86+
87+
// Calculate scaling factor to adjust historical data to match current reality
88+
const analyticsHistoricalTotal = analytics.reduce((sum, record) => sum + record.total_views, 0)
89+
const actualCurrentTotal = actualStats?.views ?? analyticsHistoricalTotal
90+
91+
const analyticsHistoricalClicks = analytics.reduce((sum, record) => sum + record.total_clicks, 0)
92+
const actualCurrentClicks = actualStats?.clicks ?? analyticsHistoricalClicks
93+
94+
// Transform analytics data for charts with proportional distribution
95+
// First pass: calculate proportions and floor values
96+
const chartDataWithRemainder = analytics.map((record) => {
97+
const viewsProportion = analyticsHistoricalTotal > 0 ? (record.total_views / analyticsHistoricalTotal) * actualCurrentTotal : 0
98+
const clicksProportion = analyticsHistoricalClicks > 0 ? (record.total_clicks / analyticsHistoricalClicks) * actualCurrentClicks : 0
99+
100+
return {
101+
date: format(new Date(record.date), 'MMM dd'),
102+
fullDate: record.date,
103+
views: Math.floor(viewsProportion),
104+
viewsRemainder: viewsProportion - Math.floor(viewsProportion),
105+
clicks: Math.floor(clicksProportion),
106+
clicksRemainder: clicksProportion - Math.floor(clicksProportion),
107+
registrations: record.total_registrations,
108+
eventsCreated: record.events_created,
109+
eventsPublished: record.events_published,
110+
hackathonsCreated: record.hackathons_created,
111+
hackathonsPublished: record.hackathons_published,
112+
}
113+
})
114+
115+
// Second pass: distribute remaining views to days with highest remainders
116+
const totalFlooredViews = chartDataWithRemainder.reduce((sum, d) => sum + d.views, 0)
117+
const viewsToDistribute = actualCurrentTotal - totalFlooredViews
118+
119+
const totalFlooredClicks = chartDataWithRemainder.reduce((sum, d) => sum + d.clicks, 0)
120+
const clicksToDistribute = actualCurrentClicks - totalFlooredClicks
121+
122+
// Sort by remainder and add 1 to top N days
123+
const sortedByViewsRemainder = [...chartDataWithRemainder].sort((a, b) => b.viewsRemainder - a.viewsRemainder)
124+
for (let i = 0; i < viewsToDistribute && i < sortedByViewsRemainder.length; i++) {
125+
const index = chartDataWithRemainder.indexOf(sortedByViewsRemainder[i])
126+
chartDataWithRemainder[index].views += 1
127+
}
128+
129+
const sortedByClicksRemainder = [...chartDataWithRemainder].sort((a, b) => b.clicksRemainder - a.clicksRemainder)
130+
for (let i = 0; i < clicksToDistribute && i < sortedByClicksRemainder.length; i++) {
131+
const index = chartDataWithRemainder.indexOf(sortedByClicksRemainder[i])
132+
chartDataWithRemainder[index].clicks += 1
133+
}
33134

34-
// Transform analytics data for charts
35-
const chartData = analytics.map((record) => ({
36-
date: format(new Date(record.date), 'MMM dd'),
37-
fullDate: record.date,
38-
views: record.total_views,
39-
clicks: record.total_clicks,
40-
registrations: record.total_registrations,
41-
eventsCreated: record.events_created,
42-
eventsPublished: record.events_published,
43-
hackathonsCreated: record.hackathons_created,
44-
hackathonsPublished: record.hackathons_published,
45-
}))
46-
47-
// Calculate totals
48-
const totals = analytics.reduce(
135+
// Final chart data without remainder fields
136+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
137+
const chartData = chartDataWithRemainder.map(({ viewsRemainder, clicksRemainder, ...rest }) => rest)
138+
139+
// Calculate totals from analytics (for published counts)
140+
const analyticsTotals = analytics.reduce(
49141
(acc, record) => ({
50142
views: acc.views + record.total_views,
51143
clicks: acc.clicks + record.total_clicks,
@@ -58,6 +150,17 @@ export function AnalyticsCharts({ analytics, dateRange, onExport }: AnalyticsCha
58150
{ views: 0, clicks: 0, registrations: 0, eventsCreated: 0, eventsPublished: 0, hackathonsCreated: 0, hackathonsPublished: 0 }
59151
)
60152

153+
// Use actual stats if available, otherwise fall back to analytics
154+
const totals = {
155+
views: actualStats?.views ?? analyticsTotals.views,
156+
clicks: actualStats?.clicks ?? analyticsTotals.clicks,
157+
registrations: actualStats?.registrations ?? analyticsTotals.registrations,
158+
eventsCreated: actualStats?.eventsCreated ?? analyticsTotals.eventsCreated,
159+
eventsPublished: actualStats?.eventsPublished ?? analyticsTotals.eventsPublished,
160+
hackathonsCreated: actualStats?.hackathonsCreated ?? analyticsTotals.hackathonsCreated,
161+
hackathonsPublished: actualStats?.hackathonsPublished ?? analyticsTotals.hackathonsPublished,
162+
}
163+
61164
// Calculate averages
62165
const avgViews = analytics.length > 0 ? Math.round(totals.views / analytics.length) : 0
63166
const avgClicks = analytics.length > 0 ? Math.round(totals.clicks / analytics.length) : 0
@@ -515,11 +618,57 @@ interface EventPerformanceComparisonProps {
515618
}
516619

517620
function EventPerformanceComparison({ analytics }: EventPerformanceComparisonProps) {
518-
// Calculate performance metrics
519-
const totalEvents = analytics.reduce((sum, record) => sum + record.events_published, 0)
520-
const totalViews = analytics.reduce((sum, record) => sum + record.total_views, 0)
521-
const totalClicks = analytics.reduce((sum, record) => sum + record.total_clicks, 0)
522-
const totalRegistrations = analytics.reduce((sum, record) => sum + record.total_registrations, 0)
621+
const [eventStats, setEventStats] = React.useState<{
622+
totalEvents: number
623+
totalViews: number
624+
totalClicks: number
625+
totalRegistrations: number
626+
} | null>(null)
627+
628+
React.useEffect(() => {
629+
// Fetch actual event data from the API
630+
const fetchEventStats = async () => {
631+
try {
632+
// Get company slug from URL
633+
const pathParts = window.location.pathname.split('/')
634+
const companySlug = pathParts[pathParts.indexOf('company') + 1]
635+
636+
const response = await fetch(`/api/companies/${companySlug}/events?status=all&limit=100`)
637+
if (!response.ok) return
638+
639+
const data = await response.json()
640+
/* eslint-disable @typescript-eslint/no-explicit-any */
641+
const approvedEvents = data.events?.filter((e: any) => e.approval_status === 'approved') || []
642+
643+
// Calculate actual totals from events table
644+
const totalViews = approvedEvents.reduce((sum: number, e: any) => sum + (e.views || 0), 0)
645+
const totalClicks = approvedEvents.reduce((sum: number, e: any) => sum + (e.clicks || 0), 0)
646+
647+
// Get registrations count
648+
const regResponse = await fetch(`/api/companies/${companySlug}/events?status=all&limit=100`)
649+
const regData = await regResponse.json()
650+
const totalRegistrations = regData.events?.reduce((sum: number, e: any) => sum + (e.registered || 0), 0) || 0
651+
/* eslint-enable @typescript-eslint/no-explicit-any */
652+
653+
setEventStats({
654+
totalEvents: approvedEvents.length,
655+
totalViews,
656+
totalClicks,
657+
totalRegistrations
658+
})
659+
} catch (error) {
660+
console.error('Error fetching event stats:', error)
661+
}
662+
}
663+
664+
fetchEventStats()
665+
}, [])
666+
667+
// Use fetched stats if available, otherwise fall back to analytics data
668+
const totalEvents = eventStats?.totalEvents ?? analytics.reduce((sum, record) => sum + record.events_published, 0)
669+
const totalViews = eventStats?.totalViews ?? 0
670+
const totalClicks = eventStats?.totalClicks ?? 0
671+
const totalRegistrations = eventStats?.totalRegistrations ?? 0
523672

524673
// Calculate averages per event
525674
const avgViewsPerEvent = totalEvents > 0 ? Math.round(totalViews / totalEvents) : 0
@@ -621,11 +770,53 @@ interface HackathonPerformanceComparisonProps {
621770
}
622771

623772
function HackathonPerformanceComparison({ analytics }: HackathonPerformanceComparisonProps) {
624-
// Calculate performance metrics
625-
const totalHackathons = analytics.reduce((sum, record) => sum + record.hackathons_published, 0)
626-
const totalViews = analytics.reduce((sum, record) => sum + record.total_views, 0)
627-
const totalClicks = analytics.reduce((sum, record) => sum + record.total_clicks, 0)
628-
const totalRegistrations = analytics.reduce((sum, record) => sum + record.total_registrations, 0)
773+
const [hackathonStats, setHackathonStats] = React.useState<{
774+
totalHackathons: number
775+
totalViews: number
776+
totalClicks: number
777+
totalRegistrations: number
778+
} | null>(null)
779+
780+
React.useEffect(() => {
781+
// Fetch actual hackathon data from the API
782+
const fetchHackathonStats = async () => {
783+
try {
784+
// Get company slug from URL
785+
const pathParts = window.location.pathname.split('/')
786+
const companySlug = pathParts[pathParts.indexOf('company') + 1]
787+
788+
const response = await fetch(`/api/companies/${companySlug}/hackathons?status=all&limit=100`)
789+
if (!response.ok) return
790+
791+
const data = await response.json()
792+
/* eslint-disable @typescript-eslint/no-explicit-any */
793+
const approvedHackathons = data.hackathons?.filter((h: any) => h.approval_status === 'approved') || []
794+
795+
// Calculate actual totals from hackathons table
796+
const totalViews = approvedHackathons.reduce((sum: number, h: any) => sum + (h.views || 0), 0)
797+
const totalClicks = approvedHackathons.reduce((sum: number, h: any) => sum + (h.clicks || 0), 0)
798+
const totalRegistrations = approvedHackathons.reduce((sum: number, h: any) => sum + (h.registered || 0), 0)
799+
/* eslint-enable @typescript-eslint/no-explicit-any */
800+
801+
setHackathonStats({
802+
totalHackathons: approvedHackathons.length,
803+
totalViews,
804+
totalClicks,
805+
totalRegistrations
806+
})
807+
} catch (error) {
808+
console.error('Error fetching hackathon stats:', error)
809+
}
810+
}
811+
812+
fetchHackathonStats()
813+
}, [])
814+
815+
// Use fetched stats if available, otherwise fall back to analytics data
816+
const totalHackathons = hackathonStats?.totalHackathons ?? analytics.reduce((sum, record) => sum + record.hackathons_published, 0)
817+
const totalViews = hackathonStats?.totalViews ?? 0
818+
const totalClicks = hackathonStats?.totalClicks ?? 0
819+
const totalRegistrations = hackathonStats?.totalRegistrations ?? 0
629820

630821
// Calculate averages per hackathon
631822
const avgViewsPerHackathon = totalHackathons > 0 ? Math.round(totalViews / totalHackathons) : 0

components/dashboard/CompanyDashboard.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,16 +189,32 @@ export function CompanyDashboard({ company }: CompanyDashboardProps) {
189189
const totalRegistrations = eventRegistrations + hackathonRegistrations
190190
/* eslint-enable @typescript-eslint/no-explicit-any */
191191

192-
// Calculate separate metrics for events and hackathons from analytics
193-
// Note: The analytics API doesn't currently separate views/clicks by type
194-
// For now, we'll use the registration counts from the actual data
195-
// and split views/clicks proportionally based on the number of each type
196-
const totalItems = approvedEvents.length + approvedHackathons.length
197-
const eventRatio = totalItems > 0 ? approvedEvents.length / totalItems : 0.5
198-
const hackathonRatio = totalItems > 0 ? approvedHackathons.length / totalItems : 0.5
192+
// Calculate actual views and clicks from events and hackathons
193+
/* eslint-disable @typescript-eslint/no-explicit-any */
194+
const eventViews = eventsData.events?.reduce(
195+
(sum: number, e: any) => sum + (e.views || 0),
196+
0
197+
) || 0
198+
199+
const eventClicks = eventsData.events?.reduce(
200+
(sum: number, e: any) => sum + (e.clicks || 0),
201+
0
202+
) || 0
203+
204+
const hackathonViews = hackathonsData.hackathons?.reduce(
205+
(sum: number, h: any) => sum + (h.views || 0),
206+
0
207+
) || 0
208+
209+
const hackathonClicks = hackathonsData.hackathons?.reduce(
210+
(sum: number, h: any) => sum + (h.clicks || 0),
211+
0
212+
) || 0
213+
/* eslint-enable @typescript-eslint/no-explicit-any */
199214

200-
const totalViews = analyticsData.summary?.total_views || 0
201-
const totalClicks = analyticsData.summary?.total_clicks || 0
215+
// Use actual views and clicks from events/hackathons tables, not analytics
216+
const totalViews = eventViews + hackathonViews
217+
const totalClicks = eventClicks + hackathonClicks
202218

203219
setStats({
204220
totalEvents: approvedEvents.length,
@@ -208,14 +224,14 @@ export function CompanyDashboard({ company }: CompanyDashboardProps) {
208224
totalClicks: totalClicks,
209225
pendingApprovals: pendingEvents.length,
210226
eventMetrics: {
211-
views: Math.round(totalViews * eventRatio),
227+
views: eventViews,
212228
registrations: eventRegistrations,
213-
clicks: Math.round(totalClicks * eventRatio),
229+
clicks: eventClicks,
214230
},
215231
hackathonMetrics: {
216-
views: Math.round(totalViews * hackathonRatio),
232+
views: hackathonViews,
217233
registrations: hackathonRegistrations,
218-
clicks: Math.round(totalClicks * hackathonRatio),
234+
clicks: hackathonClicks,
219235
},
220236
recentChange: {
221237
events: 0, // Could calculate from analytics

0 commit comments

Comments
 (0)