Merge pull request #66 from Pear-Trading/Release-v0.9.4

Release v0.9.4
This commit is contained in:
Tom Bloor 2017-10-02 14:41:34 +01:00 committed by GitHub
commit 8f699c25c7
21 changed files with 696 additions and 185 deletions

View file

@ -2,6 +2,13 @@
# Next Release # 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 # v0.9.3
* **Feature:** lat/long locations on customers and organisations * **Feature:** lat/long locations on customers and organisations

View file

@ -21,12 +21,15 @@ has schema => sub {
sub startup { sub startup {
my $self = shift; my $self = shift;
my $version = `git describe --tags`;
$self->plugin('Config', { $self->plugin('Config', {
default => { default => {
storage_path => tempdir, storage_path => tempdir,
sessionTimeSeconds => 60 * 60 * 24 * 7, sessionTimeSeconds => 60 * 60 * 24 * 7,
sessionTokenJsonName => 'session_key', sessionTokenJsonName => 'session_key',
sessionExpiresJsonName => 'sessionExpires', sessionExpiresJsonName => 'sessionExpires',
version => $version,
}, },
}); });
my $config = $self->config; my $config = $self->config;
@ -36,6 +39,7 @@ sub startup {
$self->plugin('Pear::LocalLoop::Plugin::BootstrapPagination', { bootstrap4 => 1 } ); $self->plugin('Pear::LocalLoop::Plugin::BootstrapPagination', { bootstrap4 => 1 } );
$self->plugin('Pear::LocalLoop::Plugin::Validators'); $self->plugin('Pear::LocalLoop::Plugin::Validators');
$self->plugin('Pear::LocalLoop::Plugin::Datetime'); $self->plugin('Pear::LocalLoop::Plugin::Datetime');
$self->plugin('Pear::LocalLoop::Plugin::TemplateHelpers');
$self->plugin('Authentication' => { $self->plugin('Authentication' => {
'load_user' => sub { 'load_user' => sub {
@ -192,6 +196,8 @@ sub startup {
$admin_routes->get('/transactions/:id/image')->to('admin-transactions#image'); $admin_routes->get('/transactions/:id/image')->to('admin-transactions#image');
$admin_routes->post('/transactions/:id/delete')->to('admin-transactions#delete'); $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'); # my $user_routes = $r->under('/')->to('root#under');
# $user_routes->get('/home')->to('root#home'); # $user_routes->get('/home')->to('root#home');

View file

@ -7,7 +7,7 @@ sub under {
if ( $c->is_user_authenticated ) { if ( $c->is_user_authenticated ) {
return 1 if $c->current_user->is_admin; return 1 if $c->current_user->is_admin;
} }
$c->redirect_to('/'); $c->redirect_to('/admin');
return 0; return 0;
} }

View file

@ -9,8 +9,15 @@ has result_set => sub {
sub index { sub index {
my $c = shift; my $c = shift;
my $feedback_rs = $c->result_set; my $feedback_rs = $c->result_set->search(
$c->stash( feedbacks => [ $feedback_rs->all ] ); undef,
{
page => $c->param('page') || 1,
rows => 12,
order_by => { -desc => 'submitted_at' },
},
);
$c->stash( feedback_rs => $feedback_rs );
} }
sub read { sub read {

View 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;

View file

@ -6,6 +6,17 @@ use DateTime::Format::Strptime;
sub register { sub register {
my ( $plugin, $app, $conf ) = @_; 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 { $app->helper( iso_datetime_parser => sub {
return DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S.%3N%z' ); return DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S.%3N%z' );
}); });

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -1,6 +1,7 @@
body { body {
background: whitesmoke; background: whitesmoke;
padding-top: 70px; padding-top: 70px;
padding-bottom: 70px;
} }
.panel { .panel {

View 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;

View file

@ -11,12 +11,30 @@
<strong>Success!</strong> <%= $success %> <strong>Success!</strong> <%= $success %>
</div> </div>
% } % }
<div class="list-group"> <div class="row">
% for my $feedback (@$feedbacks) { % for my $feedback ( $feedback_rs->all ) {
<a href="<%= url_for . '/' . $feedback->id %>" class="list-group-item list-group-item-action"> <div class="col col-md-4 mb-3">
<div> <div class="card">
<%= $feedback->user->email %> <%= $feedback->submitted_at %> <div class="card-header">
%= $feedback->user->email;
</div> </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> </a>
</div>
</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>

View file

@ -11,33 +11,59 @@
<strong>Success!</strong> <%= $success %> <strong>Success!</strong> <%= $success %>
</div> </div>
% } % }
<form action="<%= url_for %>" method="post"> <div class="row">
<div class="form-group"> <div class="col col-md-6 mb-3">
<label for="email">Email Address</label> <div class="card">
<input id="email" type="text" class="form-control" placeholder="Email" name="email" value="<%= $feedback->user->email %>" disabled> <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>
<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>
<div class="form-group"> <div class="col col-md-6 mb-3">
<label for="feedback">Feedback</label> <div class="card">
<input id="feedback" type="text" class="form-control" placeholder="Feedback" name="feedback" value="<%= $feedback->feedbacktext %>" disabled> <h4 class="card-header">
Feedback Message
</h4>
<div class="card-body">
<div class="card-text">
<pre><%= $feedback->feedbacktext %></pre>
</div> </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>
<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>
<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>
<div class="form-group"> <div class="col col-md-6 mb-3">
<label for="version_number">Version Number</label> <div class="card">
<input id="version_number" type="text" class="form-control" placeholder="Version Number" name="feedback" value="<%= $feedback->version_number %>" disabled> <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>
</form> </div>
</div>

View file

@ -3,41 +3,41 @@
% content_for javascript => begin % content_for javascript => begin
% end % end
<div class="card-deck"> <div class="card-deck">
<div class="card text-center"> <div class="card text-center text-white bg-primary">
<div class="card-header card-inverse card-primary"> <div class="card-header card-inverse">
User Count User Count
</div> </div>
<div class="card-block"> <div class="card-body">
<h2 class="card-title"> <h2 class="card-title">
%= $user_count %= $user_count
</h2> </h2>
</div> </div>
</div> </div>
<div class="card text-center"> <div class="card text-center text-white bg-success">
<div class="card-header card-inverse card-success"> <div class="card-header card-inverse">
Unused Tokens Unused Tokens
</div> </div>
<div class="card-block"> <div class="card-body">
<h2 class="card-title"> <h2 class="card-title">
<%= $tokens->{unused} %> / <%= $tokens->{total} %> <%= $tokens->{unused} %> / <%= $tokens->{total} %>
</h2> </h2>
</div> </div>
</div> </div>
<div class="card text-center"> <div class="card text-center text-white bg-danger">
<div class="card-header card-inverse card-danger"> <div class="card-header card-inverse">
Pending Organisations Pending Organisations
</div> </div>
<div class="card-block"> <div class="card-body">
<h2 class="card-title"> <h2 class="card-title">
%= $pending_orgs %= $pending_orgs
</h2> </h2>
</div> </div>
</div> </div>
<div class="card text-center"> <div class="card text-center text-white bg-danger">
<div class="card-header card-inverse card-danger"> <div class="card-header card-inverse">
Pending Transactions Pending Transactions
</div> </div>
<div class="card-block"> <div class="card-body">
<h2 class="card-title"> <h2 class="card-title">
%= $pending_trans %= $pending_trans
</h2> </h2>

View file

@ -36,7 +36,7 @@ function initMap() {
<h3 class="card-header"> <h3 class="card-header">
%= $valid_org->name %= $valid_org->name
</h3> </h3>
<div class="card-block"> <div class="card-body">
<form action="<%= url_for %>" method="post"> <form action="<%= url_for %>" method="post">
<div class="form-group row"> <div class="form-group row">
<label for="name" class="col-md-4 col-form-label">Organisation Name</label> <label for="name" class="col-md-4 col-form-label">Organisation Name</label>
@ -93,23 +93,17 @@ function initMap() {
</h3> </h3>
</div> </div>
<div id="mapBody" role="tabpanel"> <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 id="map"></div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="col-12"> <div class="col-12">
<div class="card mb-3"> <div class="card mb-3">
<h3 class="card-header"> <h3 class="card-header">
Transactions Transactions
</h3> </h3>
<ul class="list-group list-group-flush"> <div class="list-group list-group-flush">
% for my $transaction ( $transactions->all ) { % 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"> <a href="<%= url_for '/admin/transactions/' . $transaction->id %>" class="list-group-item list-group-item-action">
<div class="row text-center"> <div class="row text-center">
<div class="col">From: <%= $transaction->buyer->name %></div> <div class="col">From: <%= $transaction->buyer->name %></div>
@ -119,15 +113,11 @@ function initMap() {
<div class="col">Purchase Time: <%= $transaction->purchase_time %></div> <div class="col">Purchase Time: <%= $transaction->purchase_time %></div>
</div> </div>
</a> </a>
</div>
</li>
% } % }
<li class="list-group-item"> <div class="list-group-item">
<div class="container">
%= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } ); %= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } );
</div> </div>
</li> </div>
</ul>
</div> </div>
</div> </div>
</div> </div>

View 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>

View file

@ -23,13 +23,9 @@
</form> </form>
<div class="list-group"> <div class="list-group">
% for my $token (@$tokens) { % for my $token (@$tokens) {
<a href="<%= url_for . '/' . $token->{id} %>" class="list-group-item list-group-item-action"> <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' %>">
<div> <span><%= $token->{name} %></span>
%= $token->{name} <span><%= $token->{used} == 1 ? 'Used' : 'Available' %></span>
</div>
<div class="ml-auto">
<%= $token->{used} == 1 ? 'Used' : 'Available' %>
</div>
</a> </a>
% } % }
</div> </div>

View file

@ -11,10 +11,8 @@
<strong>Success!</strong> <%= $success %> <strong>Success!</strong> <%= $success %>
</div> </div>
% } % }
<div class="list-group list-group-flush"> <div class="list-group">
% for my $transaction ( $transactions->all ) { % 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"> <a href="<%= url_for . '/' . $transaction->id %>" class="list-group-item list-group-item-action">
<div class="row text-center"> <div class="row text-center">
<div class="col">From: <%= $transaction->buyer->name %></div> <div class="col">From: <%= $transaction->buyer->name %></div>
@ -24,12 +22,8 @@
<div class="col">Purchase Time: <%= $transaction->purchase_time %></div> <div class="col">Purchase Time: <%= $transaction->purchase_time %></div>
</div> </div>
</a> </a>
</div>
</li>
% } % }
<li class="list-group-item"> <div class="list-group-item">
<div class="container">
%= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } ); %= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } );
</div> </div>
</li>
</div> </div>

View file

@ -12,15 +12,15 @@
</div> </div>
% } % }
<div class="card mb-3"> <div class="card mb-3">
<h3 class="card-header"> <h3 class="card-header d-flex justify-content-between">
Transaction Details Transaction Details
<form action="<%= url_for . '/delete' %>" method="post"> <form class="form-inline" action="<%= url_for . '/delete' %>" method="post">
<div class="form-group"> <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> </div>
</form> </form>
</h3> </h3>
<div class="card-block"> <div class="card-body">
<form> <form>
<div class="form-group"> <div class="form-group">
<label for="email">Buyer</label> <label for="email">Buyer</label>
@ -42,8 +42,8 @@
<label for="email">Purchase Time</label> <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> <input id="purchase_time" type="text" class="form-control" placeholder="Purchase Time" name="purchase_time" value="<%= $transaction->purchase_time %>" disabled>
</div> </div>
<div class="form-group"> <div class="form-group d-flex justify-content-center">
<img src="<%= url_for . '/image' %>"/> <img class="mw-100" src="<%= url_for . '/image' %>"/>
</div> </div>
</form> </form>
</div> </div>

View file

@ -12,9 +12,11 @@
</div> </div>
% } % }
<form action="<%= url_for %>" method="post" autocomplete="off"> <form action="<%= url_for %>" method="post" autocomplete="off">
<div class="card mb-3">
<h3 class="card-header"> <h3 class="card-header">
User Details User Details
</h3> </h3>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="email">Email Address</label> <label for="email">Email Address</label>
<input id="email" type="text" autocomplete="off" class="form-control" placeholder="Email" name="email" value="<%= $user->email %>"> <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"> <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> <p class="help-block">Leave blank unless you want to change their password</p>
</div> </div>
</div>
</div>
<div class="card mb-3">
% if ( my $customer_rs = $user->entity->customer ) { % if ( my $customer_rs = $user->entity->customer ) {
<h3 class="card-header"> <h3 class="card-header">
Customer Details Customer Details
</h3> </h3>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="postcode">Customer Postcode</label> <label for="postcode">Customer Postcode</label>
<input id="postcode" type="text" class="form-control" placeholder="Postcode" name="postcode" value="<%= $customer_rs->postcode %>"> <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> <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> <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>
</div>
% } elsif ( my $org_rs = $user->entity->organisation ) { % } elsif ( my $org_rs = $user->entity->organisation ) {
<h3 class="card-header"> <h3 class="card-header">
Organisation Details Organisation Details
</h3> </h3>
<div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="postcode">Organisation Postcode</label> <label for="postcode">Organisation Postcode</label>
<input id="postcode" type="text" class="form-control" placeholder="Postcode" name="postcode" value="<%= $org_rs->postcode %>"> <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> <label for="town">Town</label>
<input id="town" type="sector" class="form-control" placeholder="Sector Area Code" name="sector" value="<%= $org_rs->sector %>"> <input id="town" type="sector" class="form-control" placeholder="Sector Area Code" name="sector" value="<%= $org_rs->sector %>">
</div> </div>
</div>
% } else { % } else {
<h3 class="card-header"> <h3 class="card-header text-white bg-danger">
User is not a customer or an organisation Warning!
</h3> </h3>
<div class="card-body">
User is not a customer or an organisation
</div>
% } % }
</div>
<div class="form-group"> <div class="form-group">
<button class="btn btn-primary form-control" type="submit">Edit Account</button> <button class="btn btn-primary form-control" type="submit">Edit Account</button>
</div> </div>
@ -90,10 +102,8 @@
<h3 class="card-header"> <h3 class="card-header">
Transactions Transactions
</h3> </h3>
<ul class="list-group list-group-flush"> <div class="list-group list-group-flush">
% for my $transaction ( $transactions->all ) { % 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"> <a href="<%= url_for '/admin/transactions/' . $transaction->id %>" class="list-group-item list-group-item-action">
<div class="row text-center"> <div class="row text-center">
<div class="col">From: <%= $transaction->buyer->name %></div> <div class="col">From: <%= $transaction->buyer->name %></div>
@ -103,13 +113,9 @@
<div class="col">Purchase Time: <%= $transaction->purchase_time %></div> <div class="col">Purchase Time: <%= $transaction->purchase_time %></div>
</div> </div>
</a> </a>
</div>
</li>
% } % }
<li class="list-group-item"> <div class="list-group-item">
<div class="container">
%= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } ); %= bootstrap_pagination( $c->param('page') || 1, $transactions->pager->last_page, { class => 'justify-content-center' } );
</div> </div>
</li> </div>
</ul>
</div> </div>

View file

@ -2,19 +2,18 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LocalLoop Admin - <%= title %></title> <title>LocalLoop Admin - <%= title %></title>
<!-- Bootstrap and jQuery js --> <!-- 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'; %= stylesheet '/static/admin/css/main.css';
</head> </head>
<body> <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" <button class="navbar-toggler navbar-toggler-right"
type="button" type="button"
data-toggle="collapse" data-toggle="collapse"
@ -25,21 +24,50 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<a class="navbar-brand" href="<%= url_for '/admin/home' %>">LocalLoop Admin</a>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<div class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<a class="nav-item nav-link<%= title eq 'Feedback' ? ' active' : '' %>" href="<%= url_for '/admin/feedback' %>">Feedback</a> <li class="nav-item dropdown">
<a class="nav-item nav-link<%= title eq 'Tokens' ? ' active' : '' %>" href="<%= url_for '/admin/tokens' %>">Tokens</a> <a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#">
<a class="nav-item nav-link<%= title eq 'Transactions' ? ' active' : '' %>" href="<%= url_for '/admin/transactions' %>">Transactions</a> Reports
<a class="nav-item nav-link<%= title eq 'Users' ? ' active' : '' %>" href="<%= url_for '/admin/users' %>">Users</a> </a>
<a class="nav-item nav-link<%= title eq 'Organisations' ? ' active' : '' %>" href="<%= url_for '/admin/organisations' %>">Organisations</a> <div class="dropdown-menu">
<a class="nav-item nav-link" href="<%= url_for '/admin/logout' %>">Logout</a> <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>
</div> </div>
</nav> </nav>
<div class="container"> <div class="container">
<%= content %> <%= content %>
</div> </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'; %= content_for 'javascript';
</body> </body>
</html> </html>