diff --git a/app/components/report/-icon.js b/app/components/report/-icon.js new file mode 100644 index 00000000..47986526 --- /dev/null +++ b/app/components/report/-icon.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/app/components/report/date-selector.js b/app/components/report/date-selector.js new file mode 100644 index 00000000..8995ef63 --- /dev/null +++ b/app/components/report/date-selector.js @@ -0,0 +1,47 @@ +import Component from '@ember/component'; +import InitValueMixin from 'fortnight/mixins/init-value'; +import ActionMixin from 'fortnight/mixins/action-mixin'; +import moment from 'moment'; +import { observer } from '@ember/object'; + +export default Component.extend(ActionMixin, InitValueMixin, { + center: null, + format: 'MMM Do, YYYY', + + disabled: false, + + init() { + this._super(...arguments); + const endDate = this.get('range.end'); + this.initValue('center', endDate ? moment(endDate) : moment()); + this.setDropdownLabel(); + }, + + hasRangeChanged: observer('range.{start,end}', function() { + const { start, end } = this.get('range'); + if (start && end) this.setDropdownLabel(); + }), + + setDropdownLabel() { + const { start, end } = this.get('range'); + const format = this.get('format'); + if (!start || !end) return 'Select Date Range'; + const label = `${moment(start).format(format)} - ${moment(end).format(format)}`; + this.set('dropdownLabel', label); + }, + + actions: { + setRange(dropdown, value) { + if (!this.get('disabled')) { + const range = value.moment; + this.set('range', range); + const { start, end } = range; + if (start && end) { + this.setDropdownLabel(); + dropdown.actions.close(); + this.sendEventAction('onchange', range); + } + } + } + }, +}); diff --git a/app/components/report/model-filter.js b/app/components/report/model-filter.js new file mode 100644 index 00000000..d3ebda28 --- /dev/null +++ b/app/components/report/model-filter.js @@ -0,0 +1,17 @@ +import Component from '@ember/component'; +import { inject } from '@ember/service'; +import { computed } from '@ember/object'; + +export default Component.extend({ + autocomplete: inject(), + classNames: ['form-group', 'col-sm-6', 'col-md-4'], + id: computed('modelName', function() { + return `model-filter-${this.get('modelName')}`; + }), + label: null, + actions: { + search(phrase) { + return this.get('autocomplete').query(this.get('modelName'), phrase); + } + } +}); diff --git a/app/controllers/manage/reports.js b/app/controllers/manage/reports.js new file mode 100644 index 00000000..67c216d8 --- /dev/null +++ b/app/controllers/manage/reports.js @@ -0,0 +1,76 @@ +import Controller from '@ember/controller'; +import moment from 'moment'; +import { inject } from '@ember/service'; +import ActionMixin from 'fortnight/mixins/action-mixin'; +import query from 'fortnight/gql/queries/report/list'; +import { computed } from '@ember/object'; + +export default Controller.extend(ActionMixin, { + apollo: inject(), + start: null, + end: null, + + init() { + this._super(...arguments); + this.set('start', moment().startOf('week')); + this.set('end', moment().endOf('week')); + this.setProperties({ + publishers: [], + topics: [], + placements: [], + advertisers: [], + campaigns: [], + }); + }, + + downloadInput: computed('start', 'end', 'publishers', 'topics', 'placements', 'advertisers', 'campaigns', function() { + return JSON.stringify(this.getInput()); + }), + + getInput() { + return { + start: moment(this.get('start')).valueOf(), + end: moment(this.get('end')).valueOf(), + publisherIds: this.get('publishers').map(v => v.id), + topicIds: this.get('topics').map(v => v.id), + placementIds: this.get('placements').map(v => v.id), + advertiserIds: this.get('advertisers').map(v => v.id), + campaignIds: this.get('campaigns').map(v => v.id), + }; + }, + + async execute() { + this.startAction(); + const variables = { input: this.getInput() }; + + try { + const response = await this.get('apollo').watchQuery({ query, variables, fetchPolicy: 'network-only' }, 'report'); + this.set('model', response); + } catch (e) { + this.get('graphErrors').show(e); + } finally { + this.endAction(); + } + }, + + actions: { + setDates({ start, end }) { + this.setProperties({ start, end }); + }, + reset() { + this.setProperties({ + start: moment().startOf('week'), + end: moment().endOf('week'), + publishers: [], + topics: [], + placements: [], + advertisers: [], + campaigns: [], + }); + this.execute(); + }, + update() { + this.execute(); + }, + } +}); diff --git a/app/gql/queries/report/list.graphql b/app/gql/queries/report/list.graphql new file mode 100644 index 00000000..6fd313b5 --- /dev/null +++ b/app/gql/queries/report/list.graphql @@ -0,0 +1,33 @@ +query ReportingList($input: ReportQueryInput!) { + report(input: $input) { + rows { + publisher { + id + name + } + topic { + id + name + } + placement { + id + name + } + advertiser { + id + name + } + campaign { + id + name + } + creative { + id + title + } + impressions + clicks + ctr + } + } +} diff --git a/app/router.js b/app/router.js index 29e69e11..bf538959 100644 --- a/app/router.js +++ b/app/router.js @@ -9,6 +9,7 @@ const Router = EmberRouter.extend({ Router.map(function() { this.route('manage', { path: '' }, function() { this.route('account'); + this.route('reports'); this.route('advertiser', function() { this.route('create'); diff --git a/app/routes/manage/reports.js b/app/routes/manage/reports.js new file mode 100644 index 00000000..9288abe1 --- /dev/null +++ b/app/routes/manage/reports.js @@ -0,0 +1,25 @@ +import Route from '@ember/routing/route'; +import moment from 'moment'; +import RouteQueryManager from 'ember-apollo-client/mixins/route-query-manager'; +import query from 'fortnight/gql/queries/report/list'; + +export default Route.extend(RouteQueryManager, { + /** + * + * @param {object} params + */ + async model() { + const start = moment().startOf('week').valueOf(); + const end = moment().endOf('week').valueOf(); + + const input = { start, end }; + const variables = { input }; + + try { + const response = await this.get('apollo').watchQuery({ query, variables, fetchPolicy: 'network-only' }, 'report'); + return response; + } catch (e) { + this.get('graphErrors').show(e); + } + }, +}); diff --git a/app/templates/components/report/-icon.hbs b/app/templates/components/report/-icon.hbs new file mode 100644 index 00000000..14f641b2 --- /dev/null +++ b/app/templates/components/report/-icon.hbs @@ -0,0 +1 @@ +{{entypo-icon "bar-graph"}} diff --git a/app/templates/components/report/date-selector.hbs b/app/templates/components/report/date-selector.hbs new file mode 100644 index 00000000..e3b50d80 --- /dev/null +++ b/app/templates/components/report/date-selector.hbs @@ -0,0 +1,30 @@ +{{#basic-dropdown as |dropdown|}} + + + {{#dropdown.content}} +
+
+ {{#power-calendar-range + center=center + selected=range + disabled=disabled + minRange=0 + onCenterChange=(action (mut center) value="moment") + onSelect=(action "setRange" dropdown) + as |calendar| + }} + {{calendar.nav}} + {{calendar.days}} + {{/power-calendar-range}} +
+
+ {{/dropdown.content}} +{{/basic-dropdown}} diff --git a/app/templates/components/report/model-filter.hbs b/app/templates/components/report/model-filter.hbs new file mode 100644 index 00000000..c594f631 --- /dev/null +++ b/app/templates/components/report/model-filter.hbs @@ -0,0 +1,17 @@ + +{{#form-elements/typeahead + id=id + dropdownClass=dropdownClass + autocomplete="off" + name=modelName + label=label + multiple=true + closeOnSelect=false + value=value + disabled=disabled + on-change=(action (mut value)) + on-search=(action "search") +as |result| +}} + {{if result.fullName result.fullName result.name}} +{{/form-elements/typeahead}} diff --git a/app/templates/manage/navigation.hbs b/app/templates/manage/navigation.hbs index 23b84074..0e761f38 100644 --- a/app/templates/manage/navigation.hbs +++ b/app/templates/manage/navigation.hbs @@ -10,6 +10,12 @@ {{/link-to}} {{/link-to}} + {{#link-to "manage.reports" tagName="li" class="nav-item"}} + {{#link-to "manage.reports" class="nav-link" title="Reports"}} + {{report/-icon}} Reports + {{/link-to}} + {{/link-to}} + {{#link-to "manage.campaign" tagName="li" class="nav-item"}} {{#link-to "manage.campaign" class="nav-link" title="Manage Campaigns"}} {{entypo-icon "calendar"}} Campaigns @@ -44,5 +50,3 @@ {{loading-progress class="bg-dark"}} - - diff --git a/app/templates/manage/reports.hbs b/app/templates/manage/reports.hbs new file mode 100644 index 00000000..d5d68d06 --- /dev/null +++ b/app/templates/manage/reports.hbs @@ -0,0 +1,153 @@ +{{#nav-breadcrumbs as |crumbs|}} + {{#crumbs.item active=true}}{{report/-icon}} Reports{{/crumbs.item}} +{{/nav-breadcrumbs}} + +
+
+
+ +
+ {{report/date-selector + disabled=isActionRunning + range=(hash start=start end=end) + onchange=(action "setDates") + }} +
+ +
+
+ {{report/model-filter + disabled=isActionRunning + modelName="publishers" + label="Publishers" + value=publishers + }} + {{report/model-filter + disabled=isActionRunning + modelName="topics" + label="Topics" + value=topics + }} + {{report/model-filter + disabled=isActionRunning + modelName="placements" + label="Placements" + value=placements + }} + {{report/model-filter + disabled=isActionRunning + modelName="advertisers" + label="Advertisers" + value=advertisers + }} + {{report/model-filter + disabled=isActionRunning + modelName="campaigns" + label="Campaigns" + value=campaigns + }} +
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + {{#each model.rows as |item|}} + + + + + + + + + + + + {{else}} + + {{/each}} + +
PublisherTopicPlacementAdvertiserCampaignCreativeImpressionsClicksCTR
+ {{#link-to "manage.publishers.edit" item.publisher.id}} + {{item.publisher.name}} + {{/link-to}} + + {{#link-to "manage.topics.edit" item.topic.id}} + {{item.topic.name}} + {{/link-to}} + + {{#link-to "manage.placements.edit" item.placement.id}} + {{item.placement.name}} + {{/link-to}} + + {{#link-to "manage.advertisers.edit" item.advertiser.id}} + {{item.advertiser.name}} + {{/link-to}} + + {{#link-to "manage.campaigns.edit" item.campaign.id}} + {{item.campaign.name}} + {{/link-to}} + + {{#link-to + "manage.campaigns.edit.creatives.edit.index" + item.campaign.id + item.creative.id + }} + {{item.creative.title}} + {{/link-to}} + {{number-format item.impressions "0a"}}{{number-format item.clicks "0a"}}{{number-format item.ctr "0.00%"}}
No results
+
+
+ + + +
+
+