- |
+ |
Hi Team,
@@ -83,19 +83,19 @@
A migration appears in both of the first two tables when it carries both tags; the third
table lists the migrations carrying neither.
- Migrations Customer Requested to Hold ()
+ Migrations Customer Requested to Hold ()
- | Case ID |
- WSO2 ID |
- Created Date |
- Last Customer Comment |
- Assignee |
- WSO2 Product |
- Integration CS Team |
- Sales Region |
- Account Manager / Technical Owner |
+ Case ID |
+ WSO2 ID |
+ Created Date |
+ Last Customer Comment |
+ Assignee |
+ WSO2 Product |
+ Integration CS Team |
+ Sales Region |
+ Account Manager / Technical Owner |
@@ -103,19 +103,19 @@
- Migrations Waiting on Accounts Team ()
+ Migrations Waiting on Accounts Team ()
- | Case ID |
- WSO2 ID |
- Created Date |
- Last Customer Comment |
- Assignee |
- WSO2 Product |
- Integration CS Team |
- Sales Region |
- Account Manager / Technical Owner |
+ Case ID |
+ WSO2 ID |
+ Created Date |
+ Last Customer Comment |
+ Assignee |
+ WSO2 Product |
+ Integration CS Team |
+ Sales Region |
+ Account Manager / Technical Owner |
@@ -123,19 +123,19 @@
- Migrations in Progress for Over Days That Don't Fall Under Either of the Above Categories ()
+ Migrations in Progress for Over Days That Don't Fall Under Either of the Above Categories ()
- | Case ID |
- WSO2 ID |
- Created Date |
- Last Customer Comment |
- Assignee |
- WSO2 Product |
- Integration CS Team |
- Sales Region |
- Account Manager / Technical Owner |
+ Case ID |
+ WSO2 ID |
+ Created Date |
+ Last Customer Comment |
+ Assignee |
+ WSO2 Product |
+ Integration CS Team |
+ Sales Region |
+ Account Manager / Technical Owner |
@@ -153,7 +153,7 @@
-
+
diff --git a/operations/csm-scheduled-tasks/internal/notify/templates/worklogs_report.html b/operations/csm-scheduled-tasks/internal/notify/templates/worklogs_report.html
index 1ba6c1ce3b..df0556bd2b 100644
--- a/operations/csm-scheduled-tasks/internal/notify/templates/worklogs_report.html
+++ b/operations/csm-scheduled-tasks/internal/notify/templates/worklogs_report.html
@@ -25,7 +25,7 @@
.footerContent a:hover { color: #ff7300 !important; }
@media only screen and (max-width: 650px) {
- .bodyContent { padding: 0 20px 30px !important; }
+ .bodyContent { padding: 0 16px 24px !important; }
}
@media (prefers-color-scheme: dark) {
@@ -37,6 +37,14 @@
.footerContent a { color: #bbbdc1 !important; }
.reportTable th { background-color: #33383d !important; color: #e2e2e2 !important; }
.reportTable td { border-color: #454b52 !important; }
+ /* The summary grid and the team strip set light backgrounds
+ inline. Without these the rule above paints their text
+ #e2e2e2 and leaves the background white -- light on light,
+ unreadable. The pills are safe: their colours sit on divs,
+ which that rule does not target. */
+ .statGrid { background-color: #454b52 !important; }
+ .statGrid td { background-color: #33383d !important; }
+ .teamStrip, .teamStrip td { background-color: #3b332c !important; border-color: #5a4a39 !important; }
}
@@ -47,17 +55,17 @@
-
+
-
-
+
+
- |
+ |
|
- |
+ |
CS Worklogs · Report
|
@@ -68,55 +76,15 @@
-
+
- |
+ |
- CS Worklogs — Report
-
-
-
-
-
-
- By team
-
-
-
- | Team |
- Members |
- Issues |
- Approved |
- Pending |
- Total |
- Avg/person |
-
-
-
-
-
-
-
- By type
-
-
-
- | Type |
- Issues |
- Approved |
- Pending |
- Total |
-
-
-
-
-
-
+ CS Worklogs — Report
+
+
+
@@ -128,9 +96,9 @@ By type
|
-
+
- |
+ |
| ` +
+ ` ` +
+ escapeHTML(c.label) + ` | `)
+ values.WriteString(`` +
+ ` ` +
+ escapeHTML(c.value) + ` | `)
+ }
+ return `` +
+ `` + labels.String() + ` ` + values.String() + ` `
+}
+
+// breakdownCard is one measure -- Members, Issues, Approved/Pending/Total
+// time -- with its headline figure and the per-team (and, where the
+// original had one, per-type) pills beside it.
+func breakdownCard(title, total string, rows ...string) string {
+ var body strings.Builder
+ for _, r := range rows {
+ body.WriteString(r)
+ }
+ return `` +
+ `| ` +
+ ` ` + escapeHTML(title) + ` ` +
+ `` + escapeHTML(total) + ` | ` +
+ `` + body.String() + ` | ` +
+ ` `
+}
+
// WorklogsReportSubject is the mail's subject line, in the same shape the
// delivered reports use: "[2026-10-18] CS Worklogs Daily Report" and
// "[2026-10-05 - 2026-10-11] CS Worklogs Weekly Report".
@@ -88,9 +141,8 @@ func RenderWorklogsReport(r worklogreport.Report, caseLinkBase string) string {
replacer := strings.NewReplacer(
"", cadenceLabel,
"", escapeHTML(rangeLabel),
- "", renderWorklogSummary(r, daily),
- "", renderWorklogTeamSummary(r),
- "", renderWorklogTypes(r),
+ "", renderWorklogSummary(r, daily),
+ "", renderWorklogBreakdowns(r),
"", renderWorklogTeamSections(r, daily, caseLinkBase),
"", strconv.Itoa(time.Now().Year()),
)
@@ -108,125 +160,167 @@ func days(f float64) string {
return strconv.FormatFloat(f, 'f', 1, 64)
}
-func summaryRow(label, value string) string {
- return ` | ` + escapeHTML(label) + ` | ` + escapeHTML(value) + ` | `
-}
-
+// renderWorklogSummary is the four-measure grid at the top, with the
+// components of each measure on the row beneath -- the shape the
+// ServiceNow report has always used. The third column differs by
+// cadence: the daily counts who was not on leave, the weekly the mean
+// number of people logging on a day.
func renderWorklogSummary(r worklogreport.Report, daily bool) string {
t := r.Totals
- var b strings.Builder
- b.WriteString(summaryRow("Total members", strconv.Itoa(t.Members)))
- b.WriteString(summaryRow("Total issues", strconv.Itoa(t.Issues)))
- b.WriteString(summaryRow("Total support time (hrs)", hrs(t.TotalHours)))
- b.WriteString(summaryRow("Average support time (per person)", hrs(t.AvgHoursPerPerson)))
+ thirdLabel, thirdValue := "Average People Per Day", hrs(t.AveragePeoplePerDay)
if daily {
- b.WriteString(summaryRow("Non-leave members", days(t.NonLeaveMembers)))
- b.WriteString(summaryRow("On leave", strconv.Itoa(t.OnLeaveMembers)))
- } else {
- b.WriteString(summaryRow("Average people per day", hrs(t.AveragePeoplePerDay)))
+ thirdLabel, thirdValue = "Non Leave Members", days(t.NonLeaveMembers)
}
- b.WriteString(summaryRow("Queries", strconv.Itoa(t.QueryIssues)))
- b.WriteString(summaryRow("Incidents", strconv.Itoa(t.IncidentIssues)))
- b.WriteString(summaryRow("Approved (hrs)", hrs(t.ApprovedHours)))
- b.WriteString(summaryRow("Pending (hrs)", hrs(t.PendingHours)))
+ var b strings.Builder
+ b.WriteString(``)
+
+ b.WriteString(``)
+ b.WriteString(`| Total Members | `)
+ b.WriteString(`Total Issues | `)
+ b.WriteString(`Total Support Time (Hrs) | `)
+ b.WriteString(`Average Support Time (per person) | `)
+ b.WriteString(` `)
+ b.WriteString(`| ` + strconv.Itoa(t.Members) + ` | `)
+ b.WriteString(`` + strconv.Itoa(t.Issues) + ` | `)
+ b.WriteString(`` + hrs(t.TotalHours) + ` | `)
+ b.WriteString(`` + hrs(t.AvgHoursPerPerson) + ` | `)
+ b.WriteString(` `)
+ b.WriteString(`| ` + escapeHTML(thirdLabel) + ` | `)
+ b.WriteString(`Queries | `)
+ b.WriteString(`Incidents | `)
+ b.WriteString(`Approved | `)
+ b.WriteString(`Pending | `)
+ b.WriteString(`Approved | `)
+ b.WriteString(`Pending | `)
+ b.WriteString(` `)
+ b.WriteString(`| ` + escapeHTML(thirdValue) + ` | `)
+ b.WriteString(`` + strconv.Itoa(t.QueryIssues) + ` | `)
+ b.WriteString(`` + strconv.Itoa(t.IncidentIssues) + ` | `)
+ b.WriteString(`` + hrs(t.ApprovedHours) + ` | `)
+ b.WriteString(`` + hrs(t.PendingHours) + ` | `)
+ b.WriteString(`` + hrs(t.AvgApprovedHoursPerPerson) + ` | `)
+ b.WriteString(`` + hrs(t.AvgPendingHoursPerPerson) + ` | `)
+ b.WriteString(` `)
return b.String()
}
-func renderWorklogTeamSummary(r worklogreport.Report) string {
- if len(r.Teams) == 0 {
- return `| No teams logged time in this period. | `
+// renderWorklogBreakdowns is the five cards between the grid and the team
+// sections. Members and Approved Time break down by team only; the other
+// three also carry the work-item types, exactly as the original does.
+func renderWorklogBreakdowns(r worklogreport.Report) string {
+ if len(r.Teams) == 0 && len(r.ByType) == 0 {
+ return `No time was logged in this period. `
}
- var b strings.Builder
- for _, t := range r.Teams {
- b.WriteString(``)
- b.WriteString(`| ` + escapeHTML(t.TeamName) + ` | `)
- b.WriteString(`` + strconv.Itoa(t.Members) + ` | `)
- b.WriteString(`` + strconv.Itoa(t.Issues) + ` | `)
- b.WriteString(`` + hrs(t.ApprovedHours) + ` | `)
- b.WriteString(`` + hrs(t.PendingHours) + ` | `)
- b.WriteString(`` + hrs(t.TotalHours) + ` | `)
- b.WriteString(`` + hrs(t.AvgTotalHoursPerPerson) + ` | `)
- b.WriteString(` `)
+ teamPills := func(value func(worklogreport.TeamReport) string) string {
+ cells := make([]pillCell, 0, len(r.Teams))
+ for _, t := range r.Teams {
+ cells = append(cells, pillCell{t.TeamName, value(t)})
+ }
+ return pillRow(cells)
}
- return b.String()
-}
-
-func renderWorklogTypes(r worklogreport.Report) string {
- if len(r.ByType) == 0 {
- return `| No time logged in this period. | `
+ typePills := func(value func(worklogreport.TypeBucket) string) string {
+ cells := make([]pillCell, 0, len(r.ByType))
+ for _, t := range r.ByType {
+ cells = append(cells, pillCell{t.Label, value(t)})
+ }
+ return pillRow(cells)
}
+
var b strings.Builder
- for _, t := range r.ByType {
- b.WriteString(``)
- b.WriteString(`| ` + escapeHTML(t.Label) + ` | `)
- b.WriteString(`` + strconv.Itoa(t.Issues) + ` | `)
- b.WriteString(`` + hrs(t.ApprovedHours) + ` | `)
- b.WriteString(`` + hrs(t.PendingHours) + ` | `)
- b.WriteString(`` + hrs(t.TotalHours) + ` | `)
- b.WriteString(` `)
- }
+ b.WriteString(breakdownCard("Members", strconv.Itoa(r.Totals.Members),
+ teamPills(func(t worklogreport.TeamReport) string { return strconv.Itoa(t.Members) })))
+ b.WriteString(breakdownCard("Issues", strconv.Itoa(r.Totals.Issues),
+ teamPills(func(t worklogreport.TeamReport) string { return strconv.Itoa(t.Issues) }),
+ typePills(func(t worklogreport.TypeBucket) string { return strconv.Itoa(t.Issues) })))
+ b.WriteString(breakdownCard("Approved Time", hrs(r.Totals.ApprovedHours),
+ teamPills(func(t worklogreport.TeamReport) string { return hrs(t.ApprovedHours) }),
+ typePills(func(t worklogreport.TypeBucket) string { return hrs(t.ApprovedHours) })))
+ b.WriteString(breakdownCard("Pending Time", hrs(r.Totals.PendingHours),
+ teamPills(func(t worklogreport.TeamReport) string { return hrs(t.PendingHours) }),
+ typePills(func(t worklogreport.TypeBucket) string { return hrs(t.PendingHours) })))
+ b.WriteString(breakdownCard("Total Time", hrs(r.Totals.TotalHours),
+ teamPills(func(t worklogreport.TeamReport) string { return hrs(t.TotalHours) }),
+ typePills(func(t worklogreport.TypeBucket) string { return hrs(t.TotalHours) })))
return b.String()
}
// renderWorklogTeamSections builds the per-team developer tables -- the
-// bulk of the mail. The weekly report carries worked days and an average
-// per day; the daily one carries leave and disapproved entries instead.
+// bulk of the mail. Each opens with the team's own figures on one strip,
+// then the member table, whose header groups the approved and pending
+// columns under a shared heading the way the ServiceNow report does.
+//
+// The weekly report carries worked days and an average per day where the
+// daily one carries leave and disapproved entries; everything else is
+// shared.
func renderWorklogTeamSections(r worklogreport.Report, daily bool, caseLinkBase string) string {
+ const (
+ stat = `style="text-align:center; padding:6px 8px;"`
+ statName = `style="padding:6px 10px; width:18%;"`
+ grpHead = `style="background-color:#f1f3f5; border-right:2px solid #ffffff; text-align:center; font-weight:700; padding:5px 8px; border-bottom:1px solid #dfe3e6;"`
+ subHead = `style="background-color:#f1f3f5; border-right:2px solid #ffffff; text-align:center; padding:4px 8px;"`
+ devCell = `style="padding:4px 8px; line-height:16px; border-bottom:1px solid #e8eaed;"`
+ devMid = `style="padding:4px 8px; line-height:16px; text-align:center; border-bottom:1px solid #e8eaed;"`
+ )
var b strings.Builder
for _, t := range r.Teams {
- b.WriteString(`` +
- escapeHTML(t.TeamName) + ``)
- b.WriteString(`` +
- strconv.Itoa(t.Members) + ` members · ` + strconv.Itoa(t.Issues) +
- ` issues · ` + hrs(t.ApprovedHours) + ` approved hrs · ` +
- hrs(t.PendingHours) + ` pending hrs · ` +
- hrs(t.AvgTotalHoursPerPerson) + ` avg total hrs/person `)
-
- b.WriteString(``)
- b.WriteString(`| Developer | `)
- if daily {
- b.WriteString(`Leave | `)
- } else {
- b.WriteString(`Worked days | `)
+ b.WriteString(``)
+ b.WriteString(`TEAM ` +
+ `` + escapeHTML(t.TeamName) + ` | `)
+ for _, c := range []struct{ v, l string }{
+ {strconv.Itoa(t.Members), "Members"},
+ {strconv.Itoa(t.Issues), "Issues"},
+ {hrs(t.ApprovedHours), "Approved Hrs."},
+ {hrs(t.PendingHours), "Pending Hrs."},
+ {hrs(t.AvgApprovedHoursPerPerson), "Avg. Approved Hrs/person"},
+ {hrs(t.AvgTotalHoursPerPerson), "Avg. Total Hrs/person"},
+ } {
+ b.WriteString(`` +
+ escapeHTML(c.v) + ` ` + escapeHTML(c.l) + ` | `)
}
- b.WriteString(`Approved # | `)
- b.WriteString(`Approved hrs | `)
- b.WriteString(`Pending # | `)
- b.WriteString(`Pending hrs | `)
+ b.WriteString(` `)
+
+ secondLabel, lastLabel := "Worked Days", "Avg. Hrs/Day"
if daily {
- b.WriteString(`Disapproved | `)
- } else {
- b.WriteString(`Avg hrs/day | `)
+ secondLabel, lastLabel = "Leave", "Disapproved Entries"
}
- b.WriteString(`Issues | `)
+ b.WriteString(``)
+ b.WriteString(``)
+ b.WriteString(`| Developer | `)
+ b.WriteString(`` + escapeHTML(secondLabel) + ` | `)
+ b.WriteString(`Approved Issues | `)
+ b.WriteString(`Pending Issues | `)
+ b.WriteString(`Issues | `)
+ b.WriteString(`` + escapeHTML(lastLabel) + ` | `)
+ b.WriteString(` `)
+ b.WriteString(`| Issues # | Time (Hours) | `)
+ b.WriteString(`Issues # | Time (Hours) | `)
b.WriteString(` `)
if len(t.Developers) == 0 {
- b.WriteString(`| No members. | `)
+ b.WriteString(`| No members. | `)
}
for _, d := range t.Developers {
b.WriteString(``)
- b.WriteString(`| ` + escapeHTML(d.Email) + ` | `)
+ b.WriteString(`` + escapeHTML(d.Email) + ` | `)
if daily {
- b.WriteString(`` + escapeHTML(dashIfEmpty(d.Leave)) + ` | `)
+ b.WriteString(`` + escapeHTML(dashIfEmpty(d.Leave)) + ` | `)
} else {
- b.WriteString(`` + days(d.WorkedDays) + ` | `)
- }
- b.WriteString(`` + strconv.Itoa(d.ApprovedIssues) + ` | `)
- b.WriteString(`` + hrs(d.ApprovedHours) + ` | `)
- b.WriteString(`` + strconv.Itoa(d.PendingIssues) + ` | `)
- b.WriteString(`` + hrs(d.PendingHours) + ` | `)
- if daily {
- b.WriteString(`` + strconv.Itoa(d.DisapprovedEntries) + ` | `)
- } else {
- b.WriteString(`` + hrs(d.AvgTimePerDay) + ` | `)
+ b.WriteString(`` + days(d.WorkedDays) + ` | `)
}
+ b.WriteString(`` + strconv.Itoa(d.ApprovedIssues) + ` | `)
+ b.WriteString(`` + hrs(d.ApprovedHours) + ` | `)
+ b.WriteString(`` + strconv.Itoa(d.PendingIssues) + ` | `)
+ b.WriteString(`` + hrs(d.PendingHours) + ` | `)
links := make([]string, 0, len(d.Issues))
for _, iss := range d.Issues {
links = append(links, caseLink(caseLinkBase, iss))
}
- b.WriteString(`` + strings.Join(links, " ") + ` | `)
+ b.WriteString(`` + strings.Join(links, " ") + ` | `)
+ if daily {
+ b.WriteString(`` + strconv.Itoa(d.DisapprovedEntries) + ` | `)
+ } else {
+ b.WriteString(`` + hrs(d.AvgTimePerDay) + ` | `)
+ }
b.WriteString(` `)
}
b.WriteString(` `)
| | | | | |