commit
8f699c25c7
21 changed files with 696 additions and 185 deletions
|
@ -2,6 +2,13 @@
|
|||
|
||||
# Next Release
|
||||
|
||||
# v0.9.4
|
||||
|
||||
* **Admin Feature:** Report of transaction data graphs
|
||||
* **Fix:** Mobile view meta tag for admin
|
||||
* Upgrade all CSS to Bootstrap 4 beta
|
||||
* **Admin Feature:** Added version number to admin console
|
||||
|
||||
# v0.9.3
|
||||
|
||||
* **Feature:** lat/long locations on customers and organisations
|
||||
|
|
|
@ -21,12 +21,15 @@ has schema => sub {
|
|||
sub startup {
|
||||
my $self = shift;
|
||||
|
||||
my $version = `git describe --tags`;
|
||||
|
||||
$self->plugin('Config', {
|
||||
default => {
|
||||
storage_path => tempdir,
|
||||
sessionTimeSeconds => 60 * 60 * 24 * 7,
|
||||
sessionTokenJsonName => 'session_key',
|
||||
sessionExpiresJsonName => 'sessionExpires',
|
||||
version => $version,
|
||||
},
|
||||
});
|
||||
my $config = $self->config;
|
||||
|
@ -36,6 +39,7 @@ sub startup {
|
|||
$self->plugin('Pear::LocalLoop::Plugin::BootstrapPagination', { bootstrap4 => 1 } );
|
||||
$self->plugin('Pear::LocalLoop::Plugin::Validators');
|
||||
$self->plugin('Pear::LocalLoop::Plugin::Datetime');
|
||||
$self->plugin('Pear::LocalLoop::Plugin::TemplateHelpers');
|
||||
|
||||
$self->plugin('Authentication' => {
|
||||
'load_user' => sub {
|
||||
|
@ -192,6 +196,8 @@ sub startup {
|
|||
$admin_routes->get('/transactions/:id/image')->to('admin-transactions#image');
|
||||
$admin_routes->post('/transactions/:id/delete')->to('admin-transactions#delete');
|
||||
|
||||
$admin_routes->get('/reports/transactions')->to('admin-reports#transaction_data');
|
||||
|
||||
# my $user_routes = $r->under('/')->to('root#under');
|
||||
|
||||
# $user_routes->get('/home')->to('root#home');
|
||||
|
|
|
@ -7,7 +7,7 @@ sub under {
|
|||
if ( $c->is_user_authenticated ) {
|
||||
return 1 if $c->current_user->is_admin;
|
||||
}
|
||||
$c->redirect_to('/');
|
||||
$c->redirect_to('/admin');
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,15 @@ has result_set => sub {
|
|||
sub index {
|
||||
my $c = shift;
|
||||
|
||||
my $feedback_rs = $c->result_set;
|
||||
$c->stash( feedbacks => [ $feedback_rs->all ] );
|
||||
my $feedback_rs = $c->result_set->search(
|
||||
undef,
|
||||
{
|
||||
page => $c->param('page') || 1,
|
||||
rows => 12,
|
||||
order_by => { -desc => 'submitted_at' },
|
||||
},
|
||||
);
|
||||
$c->stash( feedback_rs => $feedback_rs );
|
||||
}
|
||||
|
||||
sub read {
|
||||
|
|
80
lib/Pear/LocalLoop/Controller/Admin/Reports.pm
Normal file
80
lib/Pear/LocalLoop/Controller/Admin/Reports.pm
Normal file
|
@ -0,0 +1,80 @@
|
|||
package Pear::LocalLoop::Controller::Admin::Reports;
|
||||
use Mojo::Base 'Mojolicious::Controller';
|
||||
|
||||
use Mojo::JSON qw/ encode_json /;
|
||||
|
||||
sub transaction_data {
|
||||
my $c = shift;
|
||||
|
||||
my $quantised_column = 'quantised_hours';
|
||||
if ( defined $c->param('scale') && $c->param('scale') eq 'days' ) {
|
||||
$quantised_column = 'quantised_days';
|
||||
}
|
||||
|
||||
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
|
||||
my $transaction_rs = $c->schema->resultset('ViewQuantisedTransaction' . $driver)->search(
|
||||
{},
|
||||
{
|
||||
columns => [
|
||||
{
|
||||
quantised => $quantised_column,
|
||||
count => \"COUNT(*)",
|
||||
sum_distance => $c->pg_or_sqlite(
|
||||
'SUM("me"."distance")',
|
||||
'SUM("me"."distance")',
|
||||
),
|
||||
average_distance => $c->pg_or_sqlite(
|
||||
'AVG("me"."distance")',
|
||||
'AVG("me"."distance")',
|
||||
),
|
||||
sum_value => $c->pg_or_sqlite(
|
||||
'SUM("me"."value")',
|
||||
'SUM("me"."value")',
|
||||
),
|
||||
average_value => $c->pg_or_sqlite(
|
||||
'AVG("me"."value")',
|
||||
'AVG("me"."value")',
|
||||
),
|
||||
}
|
||||
],
|
||||
group_by => $quantised_column,
|
||||
order_by => { '-asc' => $quantised_column },
|
||||
}
|
||||
);
|
||||
|
||||
my $transaction_data = [
|
||||
map{
|
||||
my $quantised = $c->db_datetime_parser->parse_datetime($_->get_column('quantised'));
|
||||
{
|
||||
sum_value => ($_->get_column('sum_value') || 0) * 1,
|
||||
sum_distance => ($_->get_column('sum_distance') || 0) * 1,
|
||||
average_value => ($_->get_column('average_value') || 0) * 1,
|
||||
average_distance => ($_->get_column('average_distance') || 0) * 1,
|
||||
count => $_->get_column('count'),
|
||||
quantised => $c->format_iso_datetime($quantised),
|
||||
}
|
||||
} $transaction_rs->all
|
||||
];
|
||||
|
||||
$c->respond_to(
|
||||
json => { json => { data => $transaction_data } },
|
||||
html => { transaction_rs => encode_json( $transaction_data ) },
|
||||
);
|
||||
}
|
||||
|
||||
sub pg_or_sqlite {
|
||||
my ( $c, $pg_sql, $sqlite_sql ) = @_;
|
||||
|
||||
my $driver = $c->schema->storage->dbh->{Driver}->{Name};
|
||||
|
||||
if ( $driver eq 'Pg' ) {
|
||||
return \$pg_sql;
|
||||
} elsif ( $driver eq 'SQLite' ) {
|
||||
return \$sqlite_sql;
|
||||
} else {
|
||||
$c->app->log->warn('Unknown Driver Used');
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
|
@ -6,6 +6,17 @@ use DateTime::Format::Strptime;
|
|||
sub register {
|
||||
my ( $plugin, $app, $conf ) = @_;
|
||||
|
||||
$app->helper( human_datetime_parser => sub {
|
||||
return DateTime::Format::Strptime->new( pattern => '%x %X' );
|
||||
});
|
||||
|
||||
$app->helper( format_human_datetime => sub {
|
||||
my ( $c, $datetime_obj ) = @_;
|
||||
return $c->human_datetime_parser->format_datetime(
|
||||
$datetime_obj,
|
||||
);
|
||||
});
|
||||
|
||||
$app->helper( iso_datetime_parser => sub {
|
||||
return DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S.%3N%z' );
|
||||
});
|
||||
|
|
18
lib/Pear/LocalLoop/Plugin/TemplateHelpers.pm
Normal file
18
lib/Pear/LocalLoop/Plugin/TemplateHelpers.pm
Normal file
|
@ -0,0 +1,18 @@
|
|||
package Pear::LocalLoop::Plugin::TemplateHelpers;
|
||||
use Mojo::Base 'Mojolicious::Plugin';
|
||||
|
||||
sub register {
|
||||
my ( $plugin, $app, $conf ) = @_;
|
||||
|
||||
$app->helper( truncate_text => sub {
|
||||
my ( $c, $string, $length ) = @_;
|
||||
if ( length $string < $length ) {
|
||||
return $string;
|
||||
} else {
|
||||
return substr( $string, 0, $length - 3 ) . '...';
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
1;
|
|
@ -0,0 +1,21 @@
|
|||
package Pear::LocalLoop::Schema::Result::ViewQuantisedTransactionPg;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
|
||||
__PACKAGE__->table('view_quantised_transactions');
|
||||
__PACKAGE__->result_source_instance->is_virtual(1);
|
||||
|
||||
__PACKAGE__->result_source_instance->view_definition( qq/
|
||||
SELECT "value",
|
||||
"distance",
|
||||
"purchase_time",
|
||||
DATE_TRUNC('hour', "purchase_time") AS "quantised_hours",
|
||||
DATE_TRUNC('day', "purchase_time") AS "quantised_days"
|
||||
FROM "transactions"
|
||||
/);
|
||||
|
||||
1;
|
|
@ -0,0 +1,21 @@
|
|||
package Pear::LocalLoop::Schema::Result::ViewQuantisedTransactionSQLite;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
|
||||
__PACKAGE__->table('view_quantised_transactions');
|
||||
__PACKAGE__->result_source_instance->is_virtual(1);
|
||||
|
||||
__PACKAGE__->result_source_instance->view_definition( qq/
|
||||
SELECT "value",
|
||||
"distance",
|
||||
"purchase_time",
|
||||
DATETIME(STRFTIME('%Y-%m-%d %H:00:00',"purchase_time")) AS "quantised_hours",
|
||||
DATETIME(STRFTIME('%Y-%m-%d 00:00:00',"purchase_time")) AS "quantised_days"
|
||||
FROM "transactions"
|
||||
/);
|
||||
|
||||
1;
|
|
@ -1,6 +1,7 @@
|
|||
body {
|
||||
background: whitesmoke;
|
||||
padding-top: 70px;
|
||||
padding-bottom: 70px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
|
|
141
t/admin/reports/transactions.t
Normal file
141
t/admin/reports/transactions.t
Normal file
|
@ -0,0 +1,141 @@
|
|||
use Mojo::Base -strict;
|
||||
|
||||
use FindBin qw/ $Bin /;
|
||||
|
||||
use Test::More;
|
||||
use Test::Pear::LocalLoop;
|
||||
use DateTime;
|
||||
|
||||
my $framework = Test::Pear::LocalLoop->new(
|
||||
etc_dir => "$Bin/../../etc",
|
||||
);
|
||||
$framework->install_fixtures('full');
|
||||
my $t = $framework->framework;
|
||||
my $schema = $t->app->schema;
|
||||
|
||||
my $dt_today = DateTime->today;
|
||||
my $dt_start = $dt_today->clone->subtract( 'minutes' => 30 );
|
||||
|
||||
use Devel::Dwarn;
|
||||
|
||||
my $session_key = $framework->login({
|
||||
email => 'test1@example.com',
|
||||
password => 'abc123',
|
||||
});
|
||||
|
||||
sub create_transaction {
|
||||
my ( $value, $time ) = @_;
|
||||
$t->ua->post('/api/upload' => json => {
|
||||
transaction_value => $value,
|
||||
transaction_type => 1,
|
||||
purchase_time => $time,
|
||||
organisation_id => 1,
|
||||
session_key => $session_key,
|
||||
});
|
||||
}
|
||||
|
||||
my $expected_days = {};
|
||||
my $expected_hours = {};
|
||||
|
||||
sub increment_day {
|
||||
my ( $value, $day, $distance ) = @_;
|
||||
$value *= 100000;
|
||||
$distance //= 0;
|
||||
$expected_days->{$day} = {
|
||||
quantised => $day,
|
||||
sum_value => ($expected_days->{$day}->{sum_value} || 0) + $value,
|
||||
sum_distance => ($expected_days->{$day}->{sum_distance} || 0) + $distance,
|
||||
count => ++$expected_days->{$day}->{count},
|
||||
};
|
||||
}
|
||||
|
||||
sub increment_hour {
|
||||
my ( $value, $day, $distance ) = @_;
|
||||
$value *= 100000;
|
||||
$distance //= 0;
|
||||
$expected_hours->{$day} = {
|
||||
quantised => $day,
|
||||
sum_value => ($expected_hours->{$day}->{sum_value} || 0) + $value,
|
||||
sum_distance => ($expected_hours->{$day}->{sum_distance} || 0) + $distance,
|
||||
count => ++$expected_hours->{$day}->{count},
|
||||
};
|
||||
}
|
||||
|
||||
for my $i ( 0 .. 48 ) {
|
||||
my $dt = $dt_start->clone->subtract( 'minutes' => 60 * $i );
|
||||
my $purchase_time = $t->app->format_iso_datetime($dt);
|
||||
my $quantised_day = $t->app->format_iso_datetime($dt->clone->truncate(to => 'day'));
|
||||
my $quantised_hour = $t->app->format_iso_datetime($dt->clone->truncate(to => 'hour'));
|
||||
create_transaction(10, $purchase_time);
|
||||
increment_day(10, $quantised_day);
|
||||
increment_hour(10, $quantised_hour);
|
||||
if ( $i % 2 == 0 ) {
|
||||
create_transaction(20, $purchase_time);
|
||||
increment_day(20, $quantised_day);
|
||||
increment_hour(20, $quantised_hour);
|
||||
}
|
||||
if ( $i % 3 == 0 ) {
|
||||
create_transaction(30, $purchase_time);
|
||||
increment_day(30, $quantised_day);
|
||||
increment_hour(30, $quantised_hour);
|
||||
}
|
||||
if ( $i % 5 == 0 ) {
|
||||
create_transaction(50, $purchase_time);
|
||||
increment_day(50, $quantised_day);
|
||||
increment_hour(50, $quantised_hour);
|
||||
}
|
||||
if ( $i % 7 == 0 ) {
|
||||
create_transaction(70, $purchase_time);
|
||||
increment_day(70, $quantised_day);
|
||||
increment_hour(70, $quantised_hour);
|
||||
}
|
||||
}
|
||||
|
||||
my $expected_days_array = [ map {
|
||||
my $data = $expected_days->{$_};
|
||||
{
|
||||
quantised => $data->{quantised},
|
||||
count => $data->{count},
|
||||
sum_value => $data->{sum_value},
|
||||
sum_distance => $data->{sum_distance},
|
||||
average_value => $data->{sum_value} / $data->{count},
|
||||
average_distance => $data->{sum_distance} / $data->{count},
|
||||
}
|
||||
} sort keys %$expected_days ];
|
||||
|
||||
my $expected_hours_array = [ map {
|
||||
my $data = $expected_hours->{$_};
|
||||
{
|
||||
quantised => $data->{quantised},
|
||||
count => $data->{count},
|
||||
sum_value => $data->{sum_value},
|
||||
sum_distance => $data->{sum_distance},
|
||||
average_value => $data->{sum_value} / $data->{count},
|
||||
average_distance => $data->{sum_distance} / $data->{count},
|
||||
}
|
||||
} sort keys %$expected_hours ];
|
||||
|
||||
is $t->app->schema->resultset('Transaction')->count, 108, 'Transactions created';
|
||||
|
||||
#login to admin
|
||||
$t->post_ok('/admin', form => {
|
||||
email => 'admin@example.com',
|
||||
password => 'abc123',
|
||||
})->status_is(302);
|
||||
|
||||
$t->get_ok(
|
||||
'/admin/reports/transactions',
|
||||
{ Accept => 'application/json' }
|
||||
)
|
||||
->status_is(200)
|
||||
->json_is('/data', $expected_hours_array)->or($framework->dump_error);
|
||||
|
||||
$t->get_ok(
|
||||
'/admin/reports/transactions',
|
||||
{ Accept => 'application/json' },
|
||||
form => { scale => 'days' }
|
||||
)
|
||||
->status_is(200)
|
||||
->json_is('/data', $expected_days_array)->or($framework->dump_error);
|
||||
|
||||
done_testing;
|
|
@ -11,12 +11,30 @@
|
|||
<strong>Success!</strong> <%= $success %>
|
||||
</div>
|
||||
% }
|
||||
<div class="list-group">
|
||||
% for my $feedback (@$feedbacks) {
|
||||
<a href="<%= url_for . '/' . $feedback->id %>" class="list-group-item list-group-item-action">
|
||||
<div>
|
||||
<%= $feedback->user->email %> <%= $feedback->submitted_at %>
|
||||
<div class="row">
|
||||
% for my $feedback ( $feedback_rs->all ) {
|
||||
<div class="col col-md-4 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
%= $feedback->user->email;
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">
|
||||
%= format_human_datetime $feedback->submitted_at;
|
||||
</h6>
|
||||
<pre class="card-text"><%= truncate_text $feedback->feedbacktext => 50; %></pre>
|
||||
</div>
|
||||
<div class="card-footer text-right">
|
||||
<a href="<%= url_for . '/' . $feedback->id %>" class="card-link">
|
||||
More info
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% }
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
%= bootstrap_pagination( $c->param('page') || 1, $feedback_rs->pager->last_page, { class => 'justify-content-center' } );
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,33 +11,59 @@
|
|||
<strong>Success!</strong> <%= $success %>
|
||||
</div>
|
||||
% }
|
||||
<form action="<%= url_for %>" method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input id="email" type="text" class="form-control" placeholder="Email" name="email" value="<%= $feedback->user->email %>" disabled>
|
||||
<div class="row">
|
||||
<div class="col col-md-6 mb-3">
|
||||
<div class="card">
|
||||
<h4 class="card-header">
|
||||
User Info
|
||||
</h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item justify-content-between">
|
||||
<span>Email Address:</span>
|
||||
<span><%= $feedback->user->email %></span>
|
||||
</li>
|
||||
<li class="list-group-item justify-content-between">
|
||||
<span>Submitted At:</span>
|
||||
<span><%= format_human_datetime $feedback->submitted_at %></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="joindate">Submitted At</label>
|
||||
<input id="submitted_at" type="datetime" class="form-control" placeholder="Date" name="submitted_at" value="<%= $feedback->submitted_at %>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="feedback">Feedback</label>
|
||||
<input id="feedback" type="text" class="form-control" placeholder="Feedback" name="feedback" value="<%= $feedback->feedbacktext %>" disabled>
|
||||
<div class="col col-md-6 mb-3">
|
||||
<div class="card">
|
||||
<h4 class="card-header">
|
||||
Feedback Message
|
||||
</h4>
|
||||
<div class="card-body">
|
||||
<div class="card-text">
|
||||
<pre><%= $feedback->feedbacktext %></pre>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="app_name">App Name</label>
|
||||
<input id="app_name" type="text" class="form-control" placeholder="App Name" name="app_name" value="<%= $feedback->app_name %>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="package_name">Package Name</label>
|
||||
<input id="package_name" type="text" class="form-control" placeholder="Package Name" name="package_name" value="<%= $feedback->package_name %>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version_code">Version Code</label>
|
||||
<input id="version_code" type="text" class="form-control" placeholder="Version Code" name="version_code" value="<%= $feedback->version_code %>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="version_number">Version Number</label>
|
||||
<input id="version_number" type="text" class="form-control" placeholder="Version Number" name="feedback" value="<%= $feedback->version_number %>" disabled>
|
||||
<div class="col col-md-6 mb-3">
|
||||
<div class="card">
|
||||
<h4 class="card-header">
|
||||
Debug Info
|
||||
</h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item justify-content-between">
|
||||
<span>App Name:</span>
|
||||
<span><%= $feedback->app_name %></span>
|
||||
</li>
|
||||
<li class="list-group-item justify-content-between">
|
||||
<span>Package Name:</span>
|
||||
<span><%= $feedback->package_name %></span>
|
||||
</li>
|
||||
<li class="list-group-item justify-content-between">
|
||||
<span>Version Code:</span>
|
||||
<span><%= $feedback->version_code %></span>
|
||||
</li>
|
||||
<li class="list-group-item justify-content-between">
|
||||
<span>Version Number:</span>
|
||||
<span><%= $feedback->version_number %></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -3,41 +3,41 @@
|
|||
% content_for javascript => begin
|
||||
% end
|
||||
<div class="card-deck">
|
||||
<div class="card text-center">
|
||||
<div class="card-header card-inverse card-primary">
|
||||
<div class="card text-center text-white bg-primary">
|
||||
<div class="card-header card-inverse">
|
||||
User Count
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
%= $user_count
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<div class="card-header card-inverse card-success">
|
||||
<div class="card text-center text-white bg-success">
|
||||
<div class="card-header card-inverse">
|
||||
Unused Tokens
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<%= $tokens->{unused} %> / <%= $tokens->{total} %>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<div class="card-header card-inverse card-danger">
|
||||
<div class="card text-center text-white bg-danger">
|
||||
<div class="card-header card-inverse">
|
||||
Pending Organisations
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
%= $pending_orgs
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<div class="card-header card-inverse card-danger">
|
||||
<div class="card text-center text-white bg-danger">
|
||||
<div class="card-header card-inverse">
|
||||
Pending Transactions
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
%= $pending_trans
|
||||
</h2>
|
||||
|
|
|
@ -36,7 +36,7 @@ function initMap() {
|
|||
<h3 class="card-header">
|
||||
%= $valid_org->name
|
||||
</h3>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<form action="<%= url_for %>" method="post">
|
||||
<div class="form-group row">
|
||||
<label for="name" class="col-md-4 col-form-label">Organisation Name</label>
|
||||
|
@ -93,23 +93,17 @@ function initMap() {
|
|||
</h3>
|
||||
</div>
|
||||
<div id="mapBody" role="tabpanel">
|
||||
<div class="card-block">
|
||||
<!-- Yes this is nasty. no i dont care. --!>
|
||||
<style> #map { width: 100%; height: 400px; background-color: grey; } </style>
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<h3 class="card-header">
|
||||
Transactions
|
||||
</h3>
|
||||
<ul class="list-group list-group-flush">
|
||||
<div class="list-group list-group-flush">
|
||||
% for my $transaction ( $transactions->all ) {
|
||||
<li class="list-group-item">
|
||||
<div class="container">
|
||||
<a href="<%= url_for '/admin/transactions/' . $transaction->id %>" class="list-group-item list-group-item-action">
|
||||
<div class="row text-center">
|
||||
<div class="col">From: <%= $transaction->buyer->name %></div>
|
||||
|
@ -119,15 +113,11 @@ function initMap() {
|
|||
<div class="col">Purchase Time: <%= $transaction->purchase_time %></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
% }
|
||||
<li class="list-group-item">
|
||||
<div class="container">
|
||||
<div class="list-group-item">
|
||||
%= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } );
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
140
templates/admin/reports/transaction_data.html.ep
Normal file
140
templates/admin/reports/transaction_data.html.ep
Normal file
|
@ -0,0 +1,140 @@
|
|||
% layout 'admin';
|
||||
% title 'Transaction Report';
|
||||
% content_for javascript => begin
|
||||
<script src="//www.gstatic.com/charts/loader.js"></script>
|
||||
<script>
|
||||
var raw_data = <%== $transaction_rs %>;
|
||||
var mapped_data = $.map(raw_data, function( val, i ) {
|
||||
return [
|
||||
[
|
||||
new Date(val.quantised),
|
||||
val.count,
|
||||
val.average_value / 100000,
|
||||
val.sum_value / 100000,
|
||||
val.average_distance / 1000,
|
||||
val.sum_distance / 1000,
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
google.charts.load('current', {packages: ['corechart']});
|
||||
google.charts.setOnLoadCallback(loadData);
|
||||
|
||||
function loadData() {
|
||||
var data = new google.visualization.DataTable();
|
||||
data.addColumn('datetime', 'Hours');
|
||||
data.addColumn('number', 'Count');
|
||||
data.addColumn('number', 'Average Value');
|
||||
data.addColumn('number', 'Sum Value');
|
||||
data.addColumn('number', 'Average Distance');
|
||||
data.addColumn('number', 'Sum Distance');
|
||||
|
||||
data.addRows(mapped_data);
|
||||
|
||||
drawCountChart(data);
|
||||
drawDistanceChart(data);
|
||||
drawValueChart(data);
|
||||
}
|
||||
|
||||
function drawCountChart(data) {
|
||||
var options = {
|
||||
title: 'Transaction Count',
|
||||
height: 500,
|
||||
series: {
|
||||
0: {targetAxisIndex: 0},
|
||||
},
|
||||
vAxes: {
|
||||
0: { title: 'Count' },
|
||||
},
|
||||
explorer: { axis: 'horizontal' }
|
||||
};
|
||||
|
||||
var chart_data = new google.visualization.DataView(data);
|
||||
chart_data.setColumns([0, 1]);
|
||||
|
||||
var chart = new google.visualization.LineChart(document.getElementById('count_chart_div'));
|
||||
chart.draw(chart_data, options);
|
||||
}
|
||||
|
||||
function drawDistanceChart(data) {
|
||||
var options = {
|
||||
title: 'Transaction Distance',
|
||||
height: 500,
|
||||
series: {
|
||||
0: {targetAxisIndex: 0},
|
||||
1: {targetAxisIndex: 0},
|
||||
},
|
||||
vAxes: {
|
||||
0: { title: 'Distance (km)' },
|
||||
},
|
||||
explorer: { axis: 'horizontal' }
|
||||
};
|
||||
|
||||
var chart_data = new google.visualization.DataView(data);
|
||||
chart_data.setColumns([0, 4, 5]);
|
||||
|
||||
var chart = new google.visualization.LineChart(document.getElementById('distance_chart_div'));
|
||||
chart.draw(chart_data, options);
|
||||
}
|
||||
|
||||
function drawValueChart(data) {
|
||||
var options = {
|
||||
title: 'Transaction Value',
|
||||
height: 500,
|
||||
series: {
|
||||
0: {targetAxisIndex: 0},
|
||||
1: {targetAxisIndex: 0},
|
||||
},
|
||||
vAxes: {
|
||||
0: { title: 'Value (GBP)' },
|
||||
},
|
||||
explorer: { axis: 'horizontal' }
|
||||
};
|
||||
|
||||
var chart_data = new google.visualization.DataView(data);
|
||||
chart_data.setColumns([0, 2, 3]);
|
||||
|
||||
var chart = new google.visualization.LineChart(document.getElementById('value_chart_div'));
|
||||
chart.draw(chart_data, options);
|
||||
}
|
||||
</script>
|
||||
% end
|
||||
% if ( my $error = flash 'error' ) {
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>Error!</strong> <%= $error %>
|
||||
</div>
|
||||
% } elsif ( my $success = flash 'success' ) {
|
||||
<div class="alert alert-success" role="alert">
|
||||
<strong>Success!</strong> <%= $success %>
|
||||
</div>
|
||||
% }
|
||||
<div class="card mb-3 text-center">
|
||||
<div class="card-header">
|
||||
Transaction Count
|
||||
</div>
|
||||
<div id="count_chart_div" class="card-body">
|
||||
<div class="card-text">
|
||||
<em>Loading...</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3 text-center">
|
||||
<div class="card-header">
|
||||
Transaction Distance
|
||||
</div>
|
||||
<div id="distance_chart_div" class="card-body">
|
||||
<div class="card-text">
|
||||
<em>Loading...</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3 text-center">
|
||||
<div class="card-header">
|
||||
Transaction Value
|
||||
</div>
|
||||
<div id="value_chart_div" class="card-body">
|
||||
<div class="card-text">
|
||||
<em>Loading...</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -23,13 +23,9 @@
|
|||
</form>
|
||||
<div class="list-group">
|
||||
% for my $token (@$tokens) {
|
||||
<a href="<%= url_for . '/' . $token->{id} %>" class="list-group-item list-group-item-action">
|
||||
<div>
|
||||
%= $token->{name}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<%= $token->{used} == 1 ? 'Used' : 'Available' %>
|
||||
</div>
|
||||
<a href="<%= url_for . '/' . $token->{id} %>" class="list-group-item list-group-item-action d-flex justify-content-between <%= $token->{used} ? 'disabled' : 'list-group-item-success' %>">
|
||||
<span><%= $token->{name} %></span>
|
||||
<span><%= $token->{used} == 1 ? 'Used' : 'Available' %></span>
|
||||
</a>
|
||||
% }
|
||||
</div>
|
||||
|
|
|
@ -11,10 +11,8 @@
|
|||
<strong>Success!</strong> <%= $success %>
|
||||
</div>
|
||||
% }
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group">
|
||||
% for my $transaction ( $transactions->all ) {
|
||||
<li class="list-group-item">
|
||||
<div class="container">
|
||||
<a href="<%= url_for . '/' . $transaction->id %>" class="list-group-item list-group-item-action">
|
||||
<div class="row text-center">
|
||||
<div class="col">From: <%= $transaction->buyer->name %></div>
|
||||
|
@ -24,12 +22,8 @@
|
|||
<div class="col">Purchase Time: <%= $transaction->purchase_time %></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
% }
|
||||
<li class="list-group-item">
|
||||
<div class="container">
|
||||
<div class="list-group-item">
|
||||
%= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } );
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
|
|
@ -12,15 +12,15 @@
|
|||
</div>
|
||||
% }
|
||||
<div class="card mb-3">
|
||||
<h3 class="card-header">
|
||||
<h3 class="card-header d-flex justify-content-between">
|
||||
Transaction Details
|
||||
<form action="<%= url_for . '/delete' %>" method="post">
|
||||
<form class="form-inline" action="<%= url_for . '/delete' %>" method="post">
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger" type="submit" style="float: left">Delete Transaction</button>
|
||||
<button class="btn btn-danger" type="submit">Delete Transaction</button>
|
||||
</div>
|
||||
</form>
|
||||
</h3>
|
||||
<div class="card-block">
|
||||
<div class="card-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="email">Buyer</label>
|
||||
|
@ -42,8 +42,8 @@
|
|||
<label for="email">Purchase Time</label>
|
||||
<input id="purchase_time" type="text" class="form-control" placeholder="Purchase Time" name="purchase_time" value="<%= $transaction->purchase_time %>" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<img src="<%= url_for . '/image' %>"/>
|
||||
<div class="form-group d-flex justify-content-center">
|
||||
<img class="mw-100" src="<%= url_for . '/image' %>"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -12,9 +12,11 @@
|
|||
</div>
|
||||
% }
|
||||
<form action="<%= url_for %>" method="post" autocomplete="off">
|
||||
<div class="card mb-3">
|
||||
<h3 class="card-header">
|
||||
User Details
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input id="email" type="text" autocomplete="off" class="form-control" placeholder="Email" name="email" value="<%= $user->email %>">
|
||||
|
@ -32,10 +34,14 @@
|
|||
<input id="new_password" type="password" autocomplete="off" class="form-control" placeholder="New Password" name="new_password">
|
||||
<p class="help-block">Leave blank unless you want to change their password</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3">
|
||||
% if ( my $customer_rs = $user->entity->customer ) {
|
||||
<h3 class="card-header">
|
||||
Customer Details
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="postcode">Customer Postcode</label>
|
||||
<input id="postcode" type="text" class="form-control" placeholder="Postcode" name="postcode" value="<%= $customer_rs->postcode %>">
|
||||
|
@ -52,10 +58,12 @@
|
|||
<label for="year_of_birth">Year of Birth</label>
|
||||
<input id="year_of_birth" type="number" class="form-control" placeholder="Year of Birth" name="year_of_birth" value="<%= $customer_rs->year_of_birth %>" disabled>
|
||||
</div>
|
||||
</div>
|
||||
% } elsif ( my $org_rs = $user->entity->organisation ) {
|
||||
<h3 class="card-header">
|
||||
Organisation Details
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="postcode">Organisation Postcode</label>
|
||||
<input id="postcode" type="text" class="form-control" placeholder="Postcode" name="postcode" value="<%= $org_rs->postcode %>">
|
||||
|
@ -76,12 +84,16 @@
|
|||
<label for="town">Town</label>
|
||||
<input id="town" type="sector" class="form-control" placeholder="Sector Area Code" name="sector" value="<%= $org_rs->sector %>">
|
||||
</div>
|
||||
</div>
|
||||
% } else {
|
||||
<h3 class="card-header">
|
||||
User is not a customer or an organisation
|
||||
<h3 class="card-header text-white bg-danger">
|
||||
Warning!
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
User is not a customer or an organisation
|
||||
</div>
|
||||
% }
|
||||
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary form-control" type="submit">Edit Account</button>
|
||||
</div>
|
||||
|
@ -90,10 +102,8 @@
|
|||
<h3 class="card-header">
|
||||
Transactions
|
||||
</h3>
|
||||
<ul class="list-group list-group-flush">
|
||||
<div class="list-group list-group-flush">
|
||||
% for my $transaction ( $transactions->all ) {
|
||||
<li class="list-group-item">
|
||||
<div class="container">
|
||||
<a href="<%= url_for '/admin/transactions/' . $transaction->id %>" class="list-group-item list-group-item-action">
|
||||
<div class="row text-center">
|
||||
<div class="col">From: <%= $transaction->buyer->name %></div>
|
||||
|
@ -103,13 +113,9 @@
|
|||
<div class="col">Purchase Time: <%= $transaction->purchase_time %></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
% }
|
||||
<li class="list-group-item">
|
||||
<div class="container">
|
||||
<div class="list-group-item">
|
||||
%= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } );
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -2,19 +2,18 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LocalLoop Admin - <%= title %></title>
|
||||
|
||||
<!-- Bootstrap and jQuery js -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
|
||||
|
||||
%= stylesheet '/static/admin/css/main.css';
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-toggleable-md fixed-top navbar-inverse bg-danger">
|
||||
<nav class="navbar navbar-expand-md fixed-top navbar-dark bg-danger">
|
||||
<a class="navbar-brand" href="<%= url_for '/admin/home' %>">LocalLoop Admin</a>
|
||||
<button class="navbar-toggler navbar-toggler-right"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
|
@ -25,21 +24,50 @@
|
|||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="<%= url_for '/admin/home' %>">LocalLoop Admin</a>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<div class="navbar-nav ml-auto">
|
||||
<a class="nav-item nav-link<%= title eq 'Feedback' ? ' active' : '' %>" href="<%= url_for '/admin/feedback' %>">Feedback</a>
|
||||
<a class="nav-item nav-link<%= title eq 'Tokens' ? ' active' : '' %>" href="<%= url_for '/admin/tokens' %>">Tokens</a>
|
||||
<a class="nav-item nav-link<%= title eq 'Transactions' ? ' active' : '' %>" href="<%= url_for '/admin/transactions' %>">Transactions</a>
|
||||
<a class="nav-item nav-link<%= title eq 'Users' ? ' active' : '' %>" href="<%= url_for '/admin/users' %>">Users</a>
|
||||
<a class="nav-item nav-link<%= title eq 'Organisations' ? ' active' : '' %>" href="<%= url_for '/admin/organisations' %>">Organisations</a>
|
||||
<a class="nav-item nav-link" href="<%= url_for '/admin/logout' %>">Logout</a>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
Reports
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="<%= url_for '/admin/reports/transactions' %>">Transactions (Hourly)</a>
|
||||
<a class="dropdown-item" href="<%= url_for('/admin/reports/transactions')->query(scale =>'days') %>">Transactions (Daily)</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link<%= title eq 'Feedback' ? ' active' : '' %>" href="<%= url_for '/admin/feedback' %>">Feedback</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link<%= title eq 'Tokens' ? ' active' : '' %>" href="<%= url_for '/admin/tokens' %>">Tokens</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link<%= title eq 'Transactions' ? ' active' : '' %>" href="<%= url_for '/admin/transactions' %>">Transactions</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link<%= title eq 'Users' ? ' active' : '' %>" href="<%= url_for '/admin/users' %>">Users</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link<%= title eq 'Organisations' ? ' active' : '' %>" href="<%= url_for '/admin/organisations' %>">Organisations</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<%= url_for '/admin/logout' %>">Logout</a>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<%= content %>
|
||||
</div>
|
||||
<div class="navbar bg-dark fixed-bottom">
|
||||
<span class="navbar-text ml-auto text-muted">
|
||||
Version: <%= $c->config->{version} %>
|
||||
</span>
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.5/umd/popper.min.js" integrity="sha256-jpW4gXAhFvqGDD5B7366rIPD7PDbAmqq4CO0ZnHbdM4=" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
|
||||
|
||||
%= content_for 'javascript';
|
||||
</body>
|
||||
</html>
|
||||
|
|
Reference in a new issue