Add subscriber status counts to the lists UI.
- Change `query-lists` query to aggregate the subscriber count by status (confirmed, unsubscribed etc.) and expose them under a new `subscriber_statuses: {}` field in the `GET /lists` API. - Display the statuses and counts in the lists table on the UI. Closes #616
This commit is contained in:
parent
182795ec10
commit
da30d4688e
4 changed files with 43 additions and 15 deletions
|
@ -93,6 +93,11 @@ func handleGetLists(c echo.Context) error {
|
|||
if v.Tags == nil {
|
||||
out.Results[i].Tags = make(pq.StringArray, 0)
|
||||
}
|
||||
|
||||
// Total counts.
|
||||
for _, c := range v.SubscriberCounts {
|
||||
out.Results[i].SubscriberCount += c
|
||||
}
|
||||
}
|
||||
|
||||
if single {
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="type" :label="$t('globals.fields.type')"
|
||||
header-class="cy-type" sortable>
|
||||
header-class="cy-type" sortable width="15%">
|
||||
<div class="tags">
|
||||
<b-tag :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||
{{ $t(`lists.types.${props.row.type}`) }}
|
||||
|
@ -94,6 +94,16 @@
|
|||
</router-link>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="subscriber_counts"
|
||||
header-class="cy-subscribers" width="10%">
|
||||
<div class="fields stats">
|
||||
<p v-for="(count, status) in props.row.subscriberStatuses" :key="status">
|
||||
<label>{{ $t(`subscribers.status.${status}`) }}</label>
|
||||
<span :class="status">{{ $utils.formatNumber(count) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
|
||||
header-class="cy-created_at" sortable>
|
||||
{{ $utils.niceDate(props.row.createdAt) }}
|
||||
|
|
|
@ -149,6 +149,9 @@ type subLists struct {
|
|||
// SubscriberAttribs is the map of key:value attributes of a subscriber.
|
||||
type SubscriberAttribs map[string]interface{}
|
||||
|
||||
// StringIntMap is used to define DB Scan()s.
|
||||
type StringIntMap map[string]int
|
||||
|
||||
// Subscribers represents a slice of Subscriber.
|
||||
type Subscribers []Subscriber
|
||||
|
||||
|
@ -167,13 +170,14 @@ type SubscriberExport struct {
|
|||
type List struct {
|
||||
Base
|
||||
|
||||
UUID string `db:"uuid" json:"uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Type string `db:"type" json:"type"`
|
||||
Optin string `db:"optin" json:"optin"`
|
||||
Tags pq.StringArray `db:"tags" json:"tags"`
|
||||
SubscriberCount int `db:"subscriber_count" json:"subscriber_count"`
|
||||
SubscriberID int `db:"subscriber_id" json:"-"`
|
||||
UUID string `db:"uuid" json:"uuid"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Type string `db:"type" json:"type"`
|
||||
Optin string `db:"optin" json:"optin"`
|
||||
Tags pq.StringArray `db:"tags" json:"tags"`
|
||||
SubscriberCount int `db:"-" json:"subscriber_count"`
|
||||
SubscriberCounts StringIntMap `db:"subscriber_statuses" json:"subscriber_statuses"`
|
||||
SubscriberID int `db:"subscriber_id" json:"-"`
|
||||
|
||||
// This is only relevant when querying the lists of a subscriber.
|
||||
SubscriptionStatus string `db:"subscription_status" json:"subscription_status,omitempty"`
|
||||
|
@ -319,12 +323,20 @@ func (s SubscriberAttribs) Value() (driver.Value, error) {
|
|||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
// Scan unmarshals JSON into SubscriberAttribs.
|
||||
// Scan unmarshals JSONB from the DB.
|
||||
func (s SubscriberAttribs) Scan(src interface{}) error {
|
||||
if data, ok := src.([]byte); ok {
|
||||
return json.Unmarshal(data, &s)
|
||||
}
|
||||
return fmt.Errorf("Could not not decode type %T -> %T", src, s)
|
||||
return fmt.Errorf("could not not decode type %T -> %T", src, s)
|
||||
}
|
||||
|
||||
// Scan unmarshals JSONB from the DB.
|
||||
func (s StringIntMap) Scan(src interface{}) error {
|
||||
if data, ok := src.([]byte); ok {
|
||||
return json.Unmarshal(data, &s)
|
||||
}
|
||||
return fmt.Errorf("could not not decode type %T -> %T", src, s)
|
||||
}
|
||||
|
||||
// GetIDs returns the list of campaign IDs.
|
||||
|
|
11
queries.sql
11
queries.sql
|
@ -351,12 +351,13 @@ WITH ls AS (
|
|||
OFFSET $3 LIMIT (CASE WHEN $4 = 0 THEN NULL ELSE $4 END)
|
||||
),
|
||||
counts AS (
|
||||
SELECT COUNT(*) as subscriber_count, list_id FROM subscriber_lists
|
||||
WHERE status != 'unsubscribed'
|
||||
AND ($1 = 0 OR list_id = $1)
|
||||
GROUP BY list_id
|
||||
SELECT list_id, JSON_OBJECT_AGG(status, subscriber_count) AS subscriber_statuses FROM (
|
||||
SELECT COUNT(*) as subscriber_count, list_id, status FROM subscriber_lists
|
||||
WHERE ($1 = 0 OR list_id = $1)
|
||||
GROUP BY list_id, status
|
||||
) row GROUP BY list_id
|
||||
)
|
||||
SELECT ls.*, COALESCE(subscriber_count, 0) AS subscriber_count FROM ls
|
||||
SELECT ls.*, subscriber_statuses FROM ls
|
||||
LEFT JOIN counts ON (counts.list_id = ls.id) ORDER BY %s %s;
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue