diff --git a/backend/database/campaignStats.go b/backend/database/campaignStats.go index 818ee29..1e2aef1 100644 --- a/backend/database/campaignStats.go +++ b/backend/database/campaignStats.go @@ -37,12 +37,6 @@ type CampaignStats struct { DataSubmissions int `gorm:"not null;default:0" json:"dataSubmissions"` // Form submissions Reported int `gorm:"not null;default:0" json:"reported"` // Reported phishing - // Success rates (as percentages for quick display) - OpenRate float64 `gorm:"not null;default:0" json:"openRate"` - ClickRate float64 `gorm:"not null;default:0" json:"clickRate"` - SubmissionRate float64 `gorm:"not null;default:0" json:"submissionRate"` - ReportRate float64 `gorm:"not null;default:0" json:"reportRate"` - // Campaign metadata TemplateName string `gorm:"" json:"templateName"` CampaignType string `gorm:"" json:"campaignType"` // 'scheduled', 'self-managed' diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 2e23834..85c7ee0 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -3733,19 +3733,6 @@ func (c *Campaign) GenerateCampaignStats(ctx context.Context, session *model.Ses } c.Logger.Debugf("retrieved result stats", "campaignID", campaignID.String(), "recipients", resultStats.Recipients) - // Calculate rates - openRate := float64(0) - clickRate := float64(0) - submissionRate := float64(0) - reportRate := float64(0) - - if resultStats.Recipients > 0 { - openRate = (float64(resultStats.TrackingPixelLoaded) / float64(resultStats.Recipients)) * 100 - clickRate = (float64(resultStats.WebsiteLoaded) / float64(resultStats.Recipients)) * 100 - submissionRate = (float64(resultStats.SubmittedData) / float64(resultStats.Recipients)) * 100 - reportRate = (float64(resultStats.Reported) / float64(resultStats.Recipients)) * 100 - } - // Determine campaign type campaignType := "scheduled" if campaign.SendStartAt == nil && campaign.SendEndAt == nil { @@ -3805,10 +3792,6 @@ func (c *Campaign) GenerateCampaignStats(ctx context.Context, session *model.Ses WebsiteVisits: int(resultStats.WebsiteLoaded), DataSubmissions: int(resultStats.SubmittedData), Reported: int(resultStats.Reported), - OpenRate: openRate, - ClickRate: clickRate, - SubmissionRate: submissionRate, - ReportRate: reportRate, TemplateName: templateName, CampaignType: campaignType, @@ -3905,14 +3888,6 @@ func (c *Campaign) CreateManualCampaignStats(ctx context.Context, session *model req.UpdatedAt = &statsDate req.CampaignID = nil // No campaign reference for manual stats - // Calculate rates - if req.TotalRecipients > 0 { - req.OpenRate = float64(req.TrackingPixelLoaded) / float64(req.TotalRecipients) * 100 - req.ClickRate = float64(req.WebsiteVisits) / float64(req.TotalRecipients) * 100 - req.SubmissionRate = float64(req.DataSubmissions) / float64(req.TotalRecipients) * 100 - req.ReportRate = float64(req.Reported) / float64(req.TotalRecipients) * 100 - } - // Calculate total events req.TotalEvents = req.EmailsSent + req.TrackingPixelLoaded + req.WebsiteVisits + req.DataSubmissions + req.Reported @@ -3988,14 +3963,6 @@ func (c *Campaign) UpdateManualCampaignStats(ctx context.Context, session *model req.UpdatedAt = &now req.CampaignID = nil // Ensure it remains manual - // Calculate rates - if req.TotalRecipients > 0 { - req.OpenRate = float64(req.TrackingPixelLoaded) / float64(req.TotalRecipients) * 100 - req.ClickRate = float64(req.WebsiteVisits) / float64(req.TotalRecipients) * 100 - req.SubmissionRate = float64(req.DataSubmissions) / float64(req.TotalRecipients) * 100 - req.ReportRate = float64(req.Reported) / float64(req.TotalRecipients) * 100 - } - // Calculate total events req.TotalEvents = req.EmailsSent + req.TrackingPixelLoaded + req.WebsiteVisits + req.DataSubmissions + req.Reported @@ -4014,10 +3981,6 @@ func (c *Campaign) UpdateManualCampaignStats(ctx context.Context, session *model "website_visits": req.WebsiteVisits, "data_submissions": req.DataSubmissions, "reported": req.Reported, - "open_rate": req.OpenRate, - "click_rate": req.ClickRate, - "submission_rate": req.SubmissionRate, - "report_rate": req.ReportRate, "template_name": req.TemplateName, "campaign_type": req.CampaignType, } diff --git a/frontend/src/lib/components/CampaignTrendChart.svelte b/frontend/src/lib/components/CampaignTrendChart.svelte index db7ef16..029dcfd 100644 --- a/frontend/src/lib/components/CampaignTrendChart.svelte +++ b/frontend/src/lib/components/CampaignTrendChart.svelte @@ -219,17 +219,19 @@ (a, b) => new Date(a.campaignClosedAt).getTime() - new Date(b.campaignClosedAt).getTime() ); + // calculate percentage with one decimal place + function pct(n, d) { + return d > 0 ? Math.round((n / d) * 1000) / 10 : 0; + } + return sortedStats.map((stat, index) => ({ index: index + 1, date: stat.campaignClosedAt ? new Date(stat.campaignClosedAt) : null, name: stat.campaignName || `Campaign ${index + 1}`, - // If your backend provides fractions (e.g., 0.425 for 42.5%), multiply by 100 below. - // If it produces percentages (e.g., 42.5 for 42.5%), leave as is. - openRate: Math.round((stat.openRate || 0) * (stat.openRate > 1 ? 1 : 100) * 10) / 10, - clickRate: Math.round((stat.clickRate || 0) * (stat.clickRate > 1 ? 1 : 100) * 10) / 10, - submissionRate: - Math.round((stat.submissionRate || 0) * (stat.submissionRate > 1 ? 1 : 100) * 10) / 10, - reportRate: Math.round((stat.reportRate || 0) * (stat.reportRate > 1 ? 1 : 100) * 10) / 10, + openRate: pct(stat.trackingPixelLoaded, stat.totalRecipients), + clickRate: pct(stat.websiteVisits, stat.totalRecipients), + submissionRate: pct(stat.dataSubmissions, stat.totalRecipients), + reportRate: pct(stat.reported, stat.totalRecipients), totalRecipients: stat.totalRecipients })); } diff --git a/frontend/src/lib/components/table/TableCell.svelte b/frontend/src/lib/components/table/TableCell.svelte index 62d91a6..19e82e0 100644 --- a/frontend/src/lib/components/table/TableCell.svelte +++ b/frontend/src/lib/components/table/TableCell.svelte @@ -7,12 +7,16 @@ export let value = ''; export let isDate = false; export let hideHours = false; - export let isRelative = false; // new prop to toggle relative time + export let isRelative = false; // relative time + export let alignText = ''; {#if value} {#if isDate} diff --git a/frontend/src/routes/company/[id]/stats/+page.svelte b/frontend/src/routes/company/[id]/stats/+page.svelte index 4030708..e51a9b0 100644 --- a/frontend/src/routes/company/[id]/stats/+page.svelte +++ b/frontend/src/routes/company/[id]/stats/+page.svelte @@ -25,6 +25,41 @@ import FormColumns from '$lib/components/FormColumns.svelte'; import FormColumn from '$lib/components/FormColumn.svelte'; + function getStatPercentages(stats) { + const totalRecipients = stats.totalRecipients || 0; + const emailsSent = stats.emailsSent || 0; + const read = stats.trackingPixelLoaded || 0; + const clicked = stats.websiteVisits || 0; + const reported = stats.reported || 0; + + function pct(n, d) { + return d > 0 ? Math.round((n / d) * 100) : 0; + } + + return { + sent: { + count: emailsSent, + absolute: pct(emailsSent, totalRecipients), + relative: pct(emailsSent, totalRecipients) + }, + read: { + count: read, + absolute: pct(read, totalRecipients), + relative: pct(read, emailsSent) + }, + clicked: { + count: clicked, + absolute: pct(clicked, totalRecipients), + relative: pct(clicked, read) + }, + reported: { + count: reported, + absolute: pct(reported, totalRecipients), + relative: pct(reported, emailsSent) + } + }; + } + // Get company ID from URL params $: companyId = $page.params.id; @@ -299,15 +334,52 @@ Add + + {#each customStats as stats} + {@const pct = getStatPercentages(stats)}