update custom stats table and db

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-10-27 19:27:22 +01:00
parent 495f3e0b0a
commit 77cd13812d
5 changed files with 105 additions and 63 deletions
-6
View File
@@ -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'
-37
View File
@@ -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,
}
@@ -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
}));
}
@@ -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 = '';
</script>
<td
class={`pl-4 font-regular text-slate-600 dark:text-gray-200 text-ellipsis whitespace-nowrap overflow-hidden pr-4 transition-colors duration-200`}
title={isDate ? '' : value}
class:text-left={alignText === 'left'}
class:text-center={alignText === 'center'}
class:text-right={alignText === 'right'}
>
{#if value}
{#if isDate}
@@ -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 @@
<BigButton on:click={openCreateModal}>Add</BigButton>
<script>
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)
}
};
}
</script>
<Table
columns={[
{ column: 'Campaign Name', size: 'large' },
{ column: 'Recipients', size: 'small' },
{ column: 'Open Rate', size: 'small' },
{ column: 'Click Rate', size: 'small' },
{ column: 'Submission Rate', size: 'small' },
{ column: 'Report Rate', size: 'small' },
{ column: 'Date', size: 'small' }
{ column: 'Recipients', size: 'small', alignText: 'center' },
{ column: 'Sent', size: 'small', alignText: 'center' },
{ column: 'Read', size: 'small', alignText: 'center' },
{ column: 'Clicked', size: 'small', alignText: 'center' },
{ column: 'Reported', size: 'small', alignText: 'center' },
{ column: 'Time ago', size: 'small', alignText: 'center' }
]}
sortable={[]}
hasData={!!customStats.length}
@@ -315,6 +387,7 @@
isGhost={isTableLoading}
>
{#each customStats as stats}
{@const pct = getStatPercentages(stats)}
<TableRow>
<TableCell>
<button
@@ -324,12 +397,18 @@
{stats.campaignName}
</button>
</TableCell>
<TableCell value={stats.totalRecipients} />
<TableCell value="{Math.round(stats.openRate)}%" />
<TableCell value="{Math.round(stats.clickRate)}%" />
<TableCell value="{Math.round(stats.submissionRate)}%" />
<TableCell value="{Math.round(stats.reportRate)}%" />
<TableCell value={stats.createdAt} isDate isRelative />
<TableCell alignText="center" value={stats.totalRecipients} />
<TableCell alignText="center" value={pct.sent.count} />
<TableCell alignText="center" value={`${pct.read.count} (${pct.read.absolute}%)`} />
<TableCell
alignText="center"
value={`${pct.clicked.count} (${pct.clicked.absolute}%, rel: ${pct.clicked.relative}%)`}
/>
<TableCell
alignText="center"
value={`${pct.reported.count} (${pct.reported.absolute}%, rel: ${pct.reported.relative}%)`}
/>
<TableCell alignText="center" value={stats.createdAt} isDate isRelative />
<TableCellEmpty />
<TableCellAction>
<TableDropDownEllipsis>