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 {
|
if v.Tags == nil {
|
||||||
out.Results[i].Tags = make(pq.StringArray, 0)
|
out.Results[i].Tags = make(pq.StringArray, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Total counts.
|
||||||
|
for _, c := range v.SubscriberCounts {
|
||||||
|
out.Results[i].SubscriberCount += c
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if single {
|
if single {
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column v-slot="props" field="type" :label="$t('globals.fields.type')"
|
<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">
|
<div class="tags">
|
||||||
<b-tag :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
<b-tag :class="props.row.type" :data-cy="`type-${props.row.type}`">
|
||||||
{{ $t(`lists.types.${props.row.type}`) }}
|
{{ $t(`lists.types.${props.row.type}`) }}
|
||||||
|
@ -94,6 +94,16 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
</b-table-column>
|
</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')"
|
<b-table-column v-slot="props" field="created_at" :label="$t('globals.fields.createdAt')"
|
||||||
header-class="cy-created_at" sortable>
|
header-class="cy-created_at" sortable>
|
||||||
{{ $utils.niceDate(props.row.createdAt) }}
|
{{ $utils.niceDate(props.row.createdAt) }}
|
||||||
|
|
|
@ -149,6 +149,9 @@ type subLists struct {
|
||||||
// SubscriberAttribs is the map of key:value attributes of a subscriber.
|
// SubscriberAttribs is the map of key:value attributes of a subscriber.
|
||||||
type SubscriberAttribs map[string]interface{}
|
type SubscriberAttribs map[string]interface{}
|
||||||
|
|
||||||
|
// StringIntMap is used to define DB Scan()s.
|
||||||
|
type StringIntMap map[string]int
|
||||||
|
|
||||||
// Subscribers represents a slice of Subscriber.
|
// Subscribers represents a slice of Subscriber.
|
||||||
type Subscribers []Subscriber
|
type Subscribers []Subscriber
|
||||||
|
|
||||||
|
@ -167,13 +170,14 @@ type SubscriberExport struct {
|
||||||
type List struct {
|
type List struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
UUID string `db:"uuid" json:"uuid"`
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Type string `db:"type" json:"type"`
|
Type string `db:"type" json:"type"`
|
||||||
Optin string `db:"optin" json:"optin"`
|
Optin string `db:"optin" json:"optin"`
|
||||||
Tags pq.StringArray `db:"tags" json:"tags"`
|
Tags pq.StringArray `db:"tags" json:"tags"`
|
||||||
SubscriberCount int `db:"subscriber_count" json:"subscriber_count"`
|
SubscriberCount int `db:"-" json:"subscriber_count"`
|
||||||
SubscriberID int `db:"subscriber_id" json:"-"`
|
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.
|
// This is only relevant when querying the lists of a subscriber.
|
||||||
SubscriptionStatus string `db:"subscription_status" json:"subscription_status,omitempty"`
|
SubscriptionStatus string `db:"subscription_status" json:"subscription_status,omitempty"`
|
||||||
|
@ -319,12 +323,20 @@ func (s SubscriberAttribs) Value() (driver.Value, error) {
|
||||||
return json.Marshal(s)
|
return json.Marshal(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan unmarshals JSON into SubscriberAttribs.
|
// Scan unmarshals JSONB from the DB.
|
||||||
func (s SubscriberAttribs) Scan(src interface{}) error {
|
func (s SubscriberAttribs) Scan(src interface{}) error {
|
||||||
if data, ok := src.([]byte); ok {
|
if data, ok := src.([]byte); ok {
|
||||||
return json.Unmarshal(data, &s)
|
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.
|
// 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)
|
OFFSET $3 LIMIT (CASE WHEN $4 = 0 THEN NULL ELSE $4 END)
|
||||||
),
|
),
|
||||||
counts AS (
|
counts AS (
|
||||||
SELECT COUNT(*) as subscriber_count, list_id FROM subscriber_lists
|
SELECT list_id, JSON_OBJECT_AGG(status, subscriber_count) AS subscriber_statuses FROM (
|
||||||
WHERE status != 'unsubscribed'
|
SELECT COUNT(*) as subscriber_count, list_id, status FROM subscriber_lists
|
||||||
AND ($1 = 0 OR list_id = $1)
|
WHERE ($1 = 0 OR list_id = $1)
|
||||||
GROUP BY list_id
|
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;
|
LEFT JOIN counts ON (counts.list_id = ls.id) ORDER BY %s %s;
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue