diff --git a/frontend/src/views/Stats.vue b/frontend/src/views/Stats.vue index f5a3191..523786d 100644 --- a/frontend/src/views/Stats.vue +++ b/frontend/src/views/Stats.vue @@ -20,12 +20,24 @@ <table class="table" v-if="stats.length"> <thead> <tr> - <th class="key" v-for="attr in Object.keys(stats[0].key)" :key="attr"> - {{ displayName(attr) }} + <th class="key" + @click="onColumnHeadClick(attr)" + v-for="attr in Object.keys(stats[0].key)" + :key="attr"> + <font-awesome-icon v-if="orderBy === attr" + :icon="['fas', order === 'asc' ? 'sort-up' : 'sort-down']" /> + <font-awesome-icon icon="fas fa-sort" v-else /> + {{ displayName(attr) }} + </th> + <th :class="column.className" + @click="onColumnHeadClick(columnName)" + v-for="column, columnName in columns" + :key="columnName"> + <font-awesome-icon v-if="orderBy === columnName" + :icon="['fas', order === 'asc' ? 'sort-up' : 'sort-down']" /> + <font-awesome-icon icon="fas fa-sort" v-else /> + {{ column.displayName }} </th> - <th class="count"># of records</th> - <th class="date">First record</th> - <th class="date">Last record</th> </tr> </thead> <tbody> @@ -104,6 +116,20 @@ export default { data() { return { + columns: { + count: { + displayName: '# of records', + className: 'count', + }, + startDate: { + displayName: 'First record', + className: 'date', + }, + endDate: { + displayName: 'Last record', + className: 'date', + }, + }, loading: false, metrics: { country: true, @@ -112,6 +138,8 @@ export default { postalCode: false, description: false, }, + order: 'desc', + orderBy: 'count', showSelectForm: false, stats: [] as LocationStats[], } @@ -122,11 +150,34 @@ export default { return new StatsRequest({ // @ts-ignore userId: this.$root.user.id, + order: this.order, + orderBy: this.orderBy, groupBy: Object.entries(this.metrics) .filter(([_, enabled]) => enabled) .map(([metric]) => metric), }) }, + + urlQuery() { + return Object.entries(this.query) + .map(([key, value]) => { + if (key === 'userId') { + return '' + } + + if (key === 'groupBy') { + return `${key}=${(value as string[]).sort().join(',')}`; + } + + if (key === 'order') { + value = (value as string)?.toLowerCase(); + } + + return `${key}=${encodeURIComponent(value as any)}` + }) + .filter((param: string) => param.length) + .join('&'); + }, }, methods: { @@ -149,16 +200,14 @@ export default { return `/#${key}&order=${opts.ascending ? 'asc' : 'desc'}` }, - setURLQuery() { - const enabledMetrics = Object.entries(this.metrics) - .filter(([_, enabled]) => enabled) - .map(([metric]) => metric); - + async setURLQuery() { window.history.replaceState( window.history.state, '', - `${window.location.pathname}#groupBy=${enabledMetrics.sort().join(',')}`, + `${window.location.pathname}#${this.urlQuery}`, ); + + await this.refresh(); }, setState() { @@ -183,6 +232,9 @@ export default { } } + this.orderBy = params.get('orderBy') || 'count'; + this.order = params.get('order') || 'desc'; + this.metrics = metrics as { country: boolean, locality: boolean, @@ -208,6 +260,15 @@ export default { this.closeForm(); }, + onColumnHeadClick(attr: string) { + if (this.orderBy === attr) { + this.order = this.order === 'asc' ? 'desc' : 'asc'; + } else { + this.orderBy = attr; + this.order = attr === 'count' ? 'desc' : 'asc'; + } + }, + displayValue(key: string, value: any) { if (key === 'country') { return this.displayCountry(value); @@ -242,16 +303,26 @@ export default { query: { handler() { this.setURLQuery(); - this.refresh(); }, deep: true, }, + + orderBy: { + handler() { + this.setURLQuery(); + }, + }, + + order: { + handler() { + this.setURLQuery(); + }, + }, }, async created() { this.setState(); - this.setURLQuery(); - await this.refresh(); + await this.setURLQuery(); }, } </script> @@ -303,6 +374,18 @@ export default { } table { + thead { + th { + margin: 0 auto; + cursor: pointer; + + &:hover { + background-color: var(--color-accent); + color: var(--color-background); + } + } + } + tbody { tr { td { diff --git a/src/repos/Stats.ts b/src/repos/Stats.ts index 81f1b90..1b67baf 100644 --- a/src/repos/Stats.ts +++ b/src/repos/Stats.ts @@ -24,7 +24,7 @@ class Stats { .map((d) => d.dataValues.id) }, group: groupBy, - order: [[Sequelize.fn('COUNT', Sequelize.col($db.locationTableColumns.id)), req.order]], + order: [[req.orderBy, req.order]], }) ).map(({dataValues: data}: any) => new LocationStats({ diff --git a/src/requests/StatsRequest.ts b/src/requests/StatsRequest.ts index ca577de..2f7ee7f 100644 --- a/src/requests/StatsRequest.ts +++ b/src/requests/StatsRequest.ts @@ -4,11 +4,13 @@ type GroupBy = 'device' | 'country' | 'locality' | 'postalCode' | 'description'; class StatsRequest { userId: number; groupBy: GroupBy[]; + orderBy: string = 'count'; order: Order = 'DESC'; constructor(req: { userId: number; groupBy: string[] | string; + orderBy?: string; order?: string; }) { this.userId = req.userId; @@ -18,6 +20,7 @@ class StatsRequest { req.groupBy ) as GroupBy[]; + this.orderBy = req.orderBy || this.orderBy; this.order = (req.order || this.order).toUpperCase() as Order; } }