commit
e35fe64b9e
31 changed files with 660 additions and 86 deletions
1
Gemfile
1
Gemfile
|
@ -95,3 +95,4 @@ gem 'high_voltage', '~> 2.4.0'
|
|||
gem 'airbrake'
|
||||
|
||||
gem 'bootstrap-sass', '~> 3.3.5'
|
||||
gem 'react-rails'
|
||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -46,6 +46,10 @@ GEM
|
|||
autoprefixer-rails (6.0.2)
|
||||
execjs
|
||||
json
|
||||
babel-source (5.8.35)
|
||||
babel-transpiler (0.7.0)
|
||||
babel-source (>= 4.0, < 6)
|
||||
execjs (~> 2.0)
|
||||
bcrypt (3.1.10)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
|
@ -79,6 +83,7 @@ GEM
|
|||
execjs
|
||||
coffee-script-source (1.9.1.1)
|
||||
colorize (0.7.7)
|
||||
connection_pool (2.2.1)
|
||||
coveralls (0.8.2)
|
||||
json (~> 1.8)
|
||||
rest-client (>= 1.6.8, < 2)
|
||||
|
@ -205,6 +210,13 @@ GEM
|
|||
thor (>= 0.18.1, < 2.0)
|
||||
rake (10.4.2)
|
||||
rdoc (4.2.0)
|
||||
react-rails (1.10.0)
|
||||
babel-transpiler (>= 0.7.0)
|
||||
coffee-script-source (~> 1.8)
|
||||
connection_pool
|
||||
execjs
|
||||
railties (>= 3.2)
|
||||
tilt
|
||||
responders (2.1.0)
|
||||
railties (>= 4.2.0, < 5)
|
||||
rest-client (1.8.0)
|
||||
|
@ -313,6 +325,7 @@ DEPENDENCIES
|
|||
omniauth-oauth2
|
||||
purecss-rails
|
||||
rails (= 4.2.4)
|
||||
react-rails
|
||||
rspec-rails
|
||||
sass-rails (~> 5.0)
|
||||
sdoc (~> 0.4.0)
|
||||
|
@ -324,4 +337,4 @@ DEPENDENCIES
|
|||
web-console (~> 2.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.10.6
|
||||
1.13.7
|
||||
|
|
|
@ -12,12 +12,16 @@
|
|||
//
|
||||
//= require jquery
|
||||
//= require jquery_ujs
|
||||
//= require bootstrap-sprockets
|
||||
//= require dataTables/jquery.dataTables
|
||||
//= require dataTables/extras/dataTables.responsive
|
||||
//= require dataTables/jquery.dataTables
|
||||
//= require select2
|
||||
//= require jquery-dateFormat
|
||||
//= require turbolinks
|
||||
//= require react
|
||||
//= require react_ujs
|
||||
//= require components
|
||||
//= require_tree .
|
||||
|
||||
ready = function() {
|
||||
|
|
1
app/assets/javascripts/components.js
Normal file
1
app/assets/javascripts/components.js
Normal file
|
@ -0,0 +1 @@
|
|||
//= require_tree ./components
|
0
app/assets/javascripts/components/.gitkeep
Normal file
0
app/assets/javascripts/components/.gitkeep
Normal file
213
app/assets/javascripts/components/transaction_form.jsx.coffee
Normal file
213
app/assets/javascripts/components/transaction_form.jsx.coffee
Normal file
|
@ -0,0 +1,213 @@
|
|||
{ button, div, form, h3, input, option, select } = React.DOM
|
||||
|
||||
Action = React.createFactory React.createClass
|
||||
buttonClass: (b) ->
|
||||
{ giving } = @props
|
||||
c = ['btn', 'btn-default']
|
||||
c.push 'active' if b == giving
|
||||
c.join ' '
|
||||
onClick: (b) ->
|
||||
=>
|
||||
@props.setAction b
|
||||
render: ->
|
||||
{ giving } = @props
|
||||
div className: 'btn-group btn-group-lg',
|
||||
button type: 'button', className: @buttonClass(true), onClick: @onClick(true),
|
||||
'Give Money'
|
||||
button type: 'button', className: @buttonClass(false), onClick: @onClick(false),
|
||||
'Request Money'
|
||||
|
||||
Amount = React.createFactory React.createClass
|
||||
onChange: (ref) ->
|
||||
@props.setAmount ref.target.value
|
||||
format: (ref) ->
|
||||
t = ref.target
|
||||
t.value = parseFloat(t.value).toFixed(2) if t.value
|
||||
render: ->
|
||||
div className: 'row',
|
||||
div className: 'col-xs-4',
|
||||
div className: 'input-group',
|
||||
div className: 'input-group-addon', '€'
|
||||
input {
|
||||
className: 'form-control input-lg',
|
||||
name: 'transaction[euros]'
|
||||
onBlur: @format,
|
||||
onChange: @onChange,
|
||||
placeholder: '0.00',
|
||||
type: 'number',
|
||||
}
|
||||
|
||||
Peer = React.createFactory React.createClass
|
||||
onChange: (ref) ->
|
||||
@props.setPeer ref.target.value
|
||||
options: ->
|
||||
{ peer, peers } = @props
|
||||
if peer == '' or peers.includes(peer)
|
||||
[]
|
||||
else
|
||||
re = new RegExp peer
|
||||
peers.filter (s) ->
|
||||
s.match(re) != null
|
||||
inputClass: (n) ->
|
||||
c = ['form-control', 'input-lg']
|
||||
c.push 'active' if n > 0
|
||||
c.join ' '
|
||||
setPeer: (p) ->
|
||||
=>
|
||||
@props.setPeer p
|
||||
render: ->
|
||||
options = @options()
|
||||
div className: 'row',
|
||||
div className: 'col-xs-4',
|
||||
div className: 'suggestions-wrapper',
|
||||
input {
|
||||
className: @inputClass(options.length),
|
||||
onChange: @onChange,
|
||||
placeholder: 'Zeus member',
|
||||
type: 'text',
|
||||
value: (@props.peer || '')
|
||||
}
|
||||
if options.length != 0
|
||||
div className: 'suggestions',
|
||||
@options().map (s, i) =>
|
||||
div className: 'suggestion', key: i, onClick: @setPeer(s),
|
||||
s
|
||||
|
||||
Message = React.createFactory React.createClass
|
||||
onChange: (ref) ->
|
||||
@props.setMessage ref.target.value
|
||||
render: ->
|
||||
div className: 'row',
|
||||
div className: 'col-xs-8',
|
||||
input {
|
||||
className: 'form-control input-lg',
|
||||
name: 'transaction[message]',
|
||||
onChange: @onChange,
|
||||
placeholder: 'Message'
|
||||
type: 'text',
|
||||
}
|
||||
|
||||
Submit = React.createFactory React.createClass
|
||||
render: ->
|
||||
{ onClick } = @props
|
||||
div className: 'row',
|
||||
div className: 'col-xs-4 col-xs-offset-4',
|
||||
button {
|
||||
className: 'btn btn-default btn-lg btn-block',
|
||||
onClick: onClick,
|
||||
type: 'submit',
|
||||
}, 'Confirm'
|
||||
|
||||
Step = React.createFactory React.createClass
|
||||
render: ->
|
||||
{ error } = @props
|
||||
div className: 'form-step',
|
||||
div className: 'form-step-counter', @props.step
|
||||
div className: 'form-step-content',
|
||||
div className: 'form-step-title',
|
||||
@props.title,
|
||||
div className: 'form-step-error',
|
||||
error
|
||||
div className: 'clear-both'
|
||||
@props.children
|
||||
div className: 'clear-both'
|
||||
|
||||
@TransactionForm = React.createClass
|
||||
getInitialState: ->
|
||||
step: 1, giving: null, amount: null, peer: null, message: null
|
||||
setAction: (b) ->
|
||||
@setState giving: b
|
||||
@setState step: 2 unless @state.step > 1
|
||||
setAmount: (a) ->
|
||||
@setState amount: a
|
||||
@setState step: 3 unless @state.step > 2
|
||||
setPeer: (p) ->
|
||||
@setState peer: p
|
||||
@setState step: 4 unless @state.step > 3
|
||||
setMessage: (m) ->
|
||||
@setState message: m
|
||||
@setState step: 5 unless @state.step > 4
|
||||
submit: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
{ giving, peer } = @state
|
||||
{ user } = @props
|
||||
|
||||
errors = @errors()
|
||||
if Object.keys(errors).length != 0
|
||||
return
|
||||
|
||||
if giving
|
||||
debtor = user.name
|
||||
creditor = peer
|
||||
else
|
||||
debtor = peer
|
||||
creditor = user.name
|
||||
|
||||
$('<input />')
|
||||
.attr('name', 'transaction[debtor]')
|
||||
.attr('value', debtor)
|
||||
.attr('type', 'hidden')
|
||||
.appendTo(@refs.form)
|
||||
$('<input />')
|
||||
.attr('name', 'transaction[creditor]')
|
||||
.attr('value', creditor)
|
||||
.attr('type', 'hidden')
|
||||
.appendTo(@refs.form)
|
||||
|
||||
@refs.form.submit()
|
||||
errors: ->
|
||||
{ amount, giving, message, peer } = @state
|
||||
{ peers, user } = @props
|
||||
|
||||
errors = {}
|
||||
|
||||
errors['giving'] = 'Please select an action.' unless giving != null
|
||||
|
||||
unless amount
|
||||
errors['amount'] = 'Please fill in an amount.'
|
||||
else if parseFloat(amount) <= 0
|
||||
errors['amount'] = 'Please fill in a positive number.'
|
||||
|
||||
unless message && message != ""
|
||||
errors['message'] = 'Please fill in a message.'
|
||||
|
||||
unless peer && peers.includes(peer) && peer != user
|
||||
errors['peer'] = 'Please select a valid Zeus member.'
|
||||
|
||||
errors
|
||||
render: ->
|
||||
{ step, amount, giving, message, peer } = @state
|
||||
{ peers } = @props
|
||||
|
||||
errors = @errors()
|
||||
|
||||
div id: 'transaction-form',
|
||||
h3 null, 'Transfer some money'
|
||||
form ref: 'form', action: '/transactions', acceptCharset: 'UTF-8', method: 'post',
|
||||
Step step: 1, title: 'What do you want to do?',
|
||||
Action giving: giving, setAction: @setAction
|
||||
if step >= 2
|
||||
Step {
|
||||
step: 2,
|
||||
title: "How much do you want to #{if giving then 'give' else 'receive'}?",
|
||||
error: errors['amount'] if step > 2
|
||||
},
|
||||
Amount setAmount: @setAmount
|
||||
if step >= 3
|
||||
Step {
|
||||
step: 3,
|
||||
title: "Who do you want to #{if giving then 'give it to' else 'receive it from'}?",
|
||||
error: errors['peer'] if step > 3
|
||||
},
|
||||
Peer peer: peer, peers: peers, setPeer: @setPeer
|
||||
if step >= 4
|
||||
Step {
|
||||
step: 4,
|
||||
title: "Why do you want to #{if giving then 'give' else 'receive'} this?",
|
||||
error: errors['message'] if step > 4
|
||||
},
|
||||
Message setMessage: @setMessage
|
||||
if step >= 5
|
||||
Submit onClick: @submit
|
||||
div className: 'clear-both'
|
|
@ -21,6 +21,6 @@
|
|||
@import "bootstrap-sprockets";
|
||||
@import "bootstrap";
|
||||
|
||||
body {
|
||||
padding: 30px;
|
||||
.clear-both {
|
||||
clear: both;
|
||||
}
|
||||
|
|
17
app/assets/stylesheets/card.scss
Normal file
17
app/assets/stylesheets/card.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
.card-wrapper {
|
||||
padding: 0 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.card {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0, 0.12), 0 1px 2px rgba(0,0,0, 0.24);
|
||||
border-radius: 2px;
|
||||
|
||||
h1, .h1, h2, .h2, h3, .h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 10px;
|
||||
}
|
|
@ -42,3 +42,76 @@ a.login-button {
|
|||
.shame-percentage {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.transaction {
|
||||
border-bottom: 1px solid #c7d0d5;
|
||||
padding: 15px 10px;
|
||||
font-size: 16px;
|
||||
color: rgb(45, 54, 59);
|
||||
|
||||
.transaction-calendar {
|
||||
float: left;
|
||||
text-align: center;
|
||||
color: #adbac2;
|
||||
line-height: 1.1;
|
||||
margin-top: 0.125em;
|
||||
|
||||
.transaction-day {
|
||||
font-size: 1.125em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.transaction-month {
|
||||
font-size: 0.750em;
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-block {
|
||||
padding-left: 3.500em;
|
||||
|
||||
.transaction-block-l {
|
||||
width: 75%;
|
||||
float: left;
|
||||
|
||||
.transaction-message {
|
||||
margin: 0;
|
||||
color: #0072ae;
|
||||
font-size: 1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.transaction-issuer {
|
||||
display: inline-block;
|
||||
color: #90949c;
|
||||
font-size: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.transaction-block-r {
|
||||
width: 25%;
|
||||
float: left;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.request {
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: #0072ae;
|
||||
font-size: 1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.notification ,.request {
|
||||
border-top: 1px solid #c7d0d5;
|
||||
padding: 15px 10px;
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
|
7
app/assets/stylesheets/layout.scss
Normal file
7
app/assets/stylesheets/layout.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
#content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
color: rgb(144, 148, 156);
|
||||
}
|
72
app/assets/stylesheets/menu.scss
Normal file
72
app/assets/stylesheets/menu.scss
Normal file
|
@ -0,0 +1,72 @@
|
|||
$background-color: #f3f3f3;
|
||||
$border-color: #cfcfcf;
|
||||
$color: #777;
|
||||
|
||||
.menu {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background: $background-color;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.menu-item {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
color: $color;
|
||||
font-size: 1.875rem;
|
||||
line-height: 1.4;
|
||||
font-weight: 300;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-heading {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: inline-block;
|
||||
border-left: 1px solid $border-color;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&.menu-right {
|
||||
float: right;
|
||||
|
||||
.menu-item {
|
||||
&.menu-item-signout:hover {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
border-right: 1px dotted $border-color;
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
min-width: 10px;
|
||||
padding: 3px 7px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background-color: $color;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
app/assets/stylesheets/transaction_form.scss
Normal file
93
app/assets/stylesheets/transaction_form.scss
Normal file
|
@ -0,0 +1,93 @@
|
|||
$step-color: #b5b5b5;;
|
||||
$step-padding-top: 3px;
|
||||
$step-width: 26px;
|
||||
|
||||
#transaction-form {
|
||||
.form-step {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.form-step-counter {
|
||||
float: left;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
min-width: 10px;
|
||||
padding: $step-padding-top 7px;
|
||||
height: $step-width;
|
||||
width: $step-width;
|
||||
background: $step-color;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.form-step-content {
|
||||
margin-left: $step-width + 10px;
|
||||
width: 100%;
|
||||
|
||||
.form-step-title {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.form-step-error {
|
||||
color: red;
|
||||
font-size: 11px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.form-options {
|
||||
& > div {
|
||||
border: 1px solid #000;
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
margin-right: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions-wrapper {
|
||||
position: relative;
|
||||
|
||||
input.active {
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-top: 0;
|
||||
border-radius: 0 0 6px 6px;
|
||||
background: #fff;
|
||||
z-index: 2;
|
||||
|
||||
.suggestion {
|
||||
cursor: pointer;
|
||||
padding: 5px 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
&:focus, &:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
current_user
|
||||
root_path
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class TransactionsQuery
|
|||
@transactions = Arel::Table.new(:transactions)
|
||||
@perspectived = Arel::Table.new(:perspectived_transactions)
|
||||
@peers = Arel::Table.new(:users).alias('peers')
|
||||
@arel_table = Arel::Table.new(@user.name.concat('_transactions'))
|
||||
@arel_table = Arel::Table.new("#{@user.name}_transactions")
|
||||
end
|
||||
|
||||
def query
|
||||
|
|
|
@ -10,7 +10,7 @@ class NotificationsController < ApplicationController
|
|||
|
||||
def read
|
||||
@notification.read!
|
||||
redirect_to user_notifications_path(@notification.user)
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
class PagesController < ApplicationController
|
||||
|
||||
require 'statistics'
|
||||
|
||||
def landing
|
||||
query = TransactionsQuery.new(current_user)
|
||||
@transactions = ActiveRecord::Base.connection.exec_query(query.query.order(query.arel_table[:time].desc).take(10).project(Arel.star).to_sql)
|
||||
@requests = current_user.incoming_requests.open.includes(:creditor).take(10)
|
||||
@outgoing_requests = current_user.outgoing_requests.open.includes(:debtor).take(10)
|
||||
@notifications = current_user.notifications.unread
|
||||
end
|
||||
|
||||
def sign_in_page
|
||||
@statistics = Statistics.new
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,12 +10,12 @@ class RequestsController < ApplicationController
|
|||
|
||||
def confirm
|
||||
@request.confirm!
|
||||
redirect_to user_requests_path(@request.debtor)
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def decline
|
||||
@request.decline!
|
||||
redirect_to user_requests_path(@request.debtor)
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -10,21 +10,21 @@ class TransactionsController < ApplicationController
|
|||
@transaction = Transaction.new(transaction_params)
|
||||
@transaction.reverse if @transaction.amount < 0
|
||||
|
||||
if can? :create, @transaction
|
||||
if @transaction.save
|
||||
render json: @transaction, status: :created
|
||||
else
|
||||
render json: @transaction.errors.full_messages,
|
||||
status: :unprocessable_entity
|
||||
unless can? :create, @transaction
|
||||
@transaction = Request.new @transaction.info
|
||||
authorize!(:create, @transaction)
|
||||
end
|
||||
|
||||
if @transaction.save
|
||||
respond_to do |format|
|
||||
format.html { redirect_to root_path }
|
||||
format.json { render json: @transaction, status: :created }
|
||||
end
|
||||
else
|
||||
request = Request.new @transaction.info
|
||||
authorize!(:create, request)
|
||||
if request.save
|
||||
render json: request, status: :created
|
||||
else
|
||||
render json: request.errors.full_messages,
|
||||
status: :unprocessable_entity
|
||||
respond_to do |format|
|
||||
format.html { redirect_to root_path }
|
||||
format.json { render json: @transaction.errors.full_messages,
|
||||
status: :unprocessable_entity }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
module BaseTransaction
|
||||
extend ActiveSupport::Concern
|
||||
include ActionView::Helpers::NumberHelper
|
||||
include ApplicationHelper
|
||||
|
||||
included do
|
||||
belongs_to :debtor, class_name: 'User'
|
||||
belongs_to :creditor, class_name: 'User'
|
||||
belongs_to :issuer, polymorphic: true
|
||||
|
||||
validates :amount, numericality: { greater_than: 0 }
|
||||
validates :debtor, presence: true
|
||||
validates :creditor, presence: true
|
||||
validates :amount, numericality: { greater_than: 0 }
|
||||
validate :different_debtor_creditor
|
||||
end
|
||||
|
||||
|
@ -18,7 +21,7 @@ module BaseTransaction
|
|||
end
|
||||
|
||||
def amount_f
|
||||
number_to_currency amount/100.0, unit: '€'
|
||||
euro_from_cents amount
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
class Notification < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
|
||||
scope :unread, -> { where read: false }
|
||||
|
||||
def read!
|
||||
update_attributes read: true
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ class User < ActiveRecord::Base
|
|||
has_many :incoming_requests,
|
||||
class_name: 'Request', foreign_key: 'debtor_id'
|
||||
has_many :outgoing_requests,
|
||||
class_name: 'Request', foreign_key: 'debtor_id'
|
||||
class_name: 'Request', foreign_key: 'creditor_id'
|
||||
has_many :notifications
|
||||
|
||||
has_many :issued_transactions, as: :issuer, class_name: 'Transaction'
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
.pure-u-1
|
||||
.pure-menu.pure-menu-horizontal
|
||||
= link_to "Tab", root_path, class: "pure-menu-heading pure-menu-link"
|
||||
%ul.pure-menu-list
|
||||
%li.pure-menu-item
|
||||
= link_to current_user.name.capitalize, current_user, class: "pure-menu-link"
|
||||
- if current_user.penning
|
||||
%li.pure-menu-item
|
||||
=link_to "Zeus", User.zeus, class: "pure-menu-link"
|
||||
%li.pure-menu-item
|
||||
= link_to "Requests (#{User.zeus.incoming_requests.size})", user_requests_path(User.zeus), class: 'pure-menu-link'
|
||||
%li.pure-menu-item
|
||||
= link_to "Notifications (#{User.zeus.notifications.size})", user_notifications_path(User.zeus), class: 'pure-menu-link'
|
||||
%li.pure-menu-item
|
||||
= link_to "Requests (#{current_user.incoming_requests.size})", user_requests_path(current_user), class: 'pure-menu-link'
|
||||
%li.pure-menu-item
|
||||
= link_to "Notifications (#{current_user.notifications.size})", user_notifications_path(current_user), class: 'pure-menu-link'
|
||||
.menu
|
||||
= link_to 'Tab', root_path, class: 'menu-heading menu-item'
|
||||
.menu-list
|
||||
= link_to 'Transactions', current_user, class: 'menu-item'
|
||||
= link_to user_requests_path(current_user), class: 'menu-item' do
|
||||
Requests
|
||||
%span.badge= current_user.incoming_requests.open.size
|
||||
= link_to user_notifications_path(current_user), class: 'menu-item' do
|
||||
Notifications
|
||||
%span.badge= current_user.notifications.unread.size
|
||||
- if current_user.penning
|
||||
= link_to 'Zeus', User.zeus, class: 'menu-item'
|
||||
= link_to user_requests_path(User.zeus), class: 'menu-item' do
|
||||
Zeus Requests
|
||||
%span.badge= User.zeus.incoming_requests.size
|
||||
= link_to user_notifications_path(User.zeus), class: 'menu-item' do
|
||||
Zeus Notifications
|
||||
%span.badge= User.zeus.notifications.size
|
||||
.menu-list.menu-right
|
||||
%span.menu-item= euro_from_cents current_user.balance
|
||||
= link_to 'Sign out', sign_out_path, method: :delete, class: 'menu-item menu-item-signout'
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
%body
|
||||
.pure-g
|
||||
= render 'menu' if current_user
|
||||
.pure-u-1
|
||||
#content.pure-u-1
|
||||
= render 'flash'
|
||||
= yield
|
||||
|
|
16
app/views/pages/_notifications.html.haml
Normal file
16
app/views/pages/_notifications.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
.card-wrapper
|
||||
- if @notifications.any?
|
||||
.card
|
||||
.padded
|
||||
%h3 Notifications
|
||||
- @notifications.each do |n|
|
||||
.notification.pure-g
|
||||
.pure-u-11-12
|
||||
= n.message
|
||||
.pure-u-1-12.actions
|
||||
= link_to notification_read_path(n), method: :post do
|
||||
%span.glyphicon.glyphicon-eye-open
|
||||
- else
|
||||
.card.padded
|
||||
%span.info-message
|
||||
You have no unread notifications.
|
17
app/views/pages/_outgoing_requests.html.haml
Normal file
17
app/views/pages/_outgoing_requests.html.haml
Normal file
|
@ -0,0 +1,17 @@
|
|||
.card-wrapper
|
||||
- if @outgoing_requests.any?
|
||||
.card
|
||||
.padded
|
||||
%h3 Outgoing Requests
|
||||
- @outgoing_requests.each do |r|
|
||||
.request.pure-g
|
||||
.pure-u-2-3
|
||||
%h4= r.message
|
||||
= r.debtor.name
|
||||
.pure-u-1-3.actions
|
||||
= euro_from_cents r.amount
|
||||
.clear-both
|
||||
- else
|
||||
.card.padded
|
||||
%span.info-message
|
||||
You have no open outgoing requests at the moment.
|
23
app/views/pages/_requests.html.haml
Normal file
23
app/views/pages/_requests.html.haml
Normal file
|
@ -0,0 +1,23 @@
|
|||
.card-wrapper
|
||||
- if @requests.any?
|
||||
.card
|
||||
.padded
|
||||
%h3 Requests
|
||||
- @requests.each do |r|
|
||||
.request.pure-g
|
||||
.pure-u-1-3
|
||||
%h4= r.message
|
||||
= r.creditor.name
|
||||
.pure-u-1-3
|
||||
= euro_from_cents r.amount
|
||||
.pure-u-1-3.actions
|
||||
.btn-group
|
||||
= link_to request_confirm_path(r), method: :post, class: 'btn btn-default btn-success' do
|
||||
%span.glyphicon.glyphicon-ok
|
||||
= link_to request_decline_path(r), method: :post, class: 'btn btn-default btn-danger' do
|
||||
%span.glyphicon.glyphicon-remove
|
||||
.clear-both
|
||||
- else
|
||||
.card.padded
|
||||
%span.info-message
|
||||
You have no open requests at the moment.
|
3
app/views/pages/_transaction_form.html.haml
Normal file
3
app/views/pages/_transaction_form.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
.card-wrapper
|
||||
.card.padded
|
||||
= react_component 'TransactionForm', user: current_user, peers: User.all.order(:name).pluck(:name)
|
21
app/views/pages/_transactions.html.haml
Normal file
21
app/views/pages/_transactions.html.haml
Normal file
|
@ -0,0 +1,21 @@
|
|||
.card-wrapper
|
||||
.card
|
||||
- @transactions.each do |t|
|
||||
- t.symbolize_keys!
|
||||
- date = Date.parse t[:time]
|
||||
.transaction
|
||||
.transaction-calendar
|
||||
%span.transaction-day= date.strftime('%d')
|
||||
%span.transaction-month= Date::MONTHNAMES[date.month][0..2]
|
||||
.transaction-block
|
||||
.transaction-block-l
|
||||
%h4.transaction-message
|
||||
= t[:message]
|
||||
.transaction-peer
|
||||
= t[:peer]
|
||||
- if t[:peer] != t[:issuer]
|
||||
.transaction-issuer
|
||||
= "issued by #{t[:issuer]}"
|
||||
.transaction-block-r
|
||||
= euro_from_cents t[:amount]
|
||||
.clear-both
|
|
@ -1,43 +1,8 @@
|
|||
%h1.columns-title Tab
|
||||
= javascript_include_tag "//www.google.com/jsapi", "chartkick"
|
||||
- unless user_signed_in?
|
||||
.pure-g.landing_columns
|
||||
.pure-u-1.pure-u-md-1-2
|
||||
%h2 Authentication
|
||||
%p Log een keer in en betaal uw schulden!
|
||||
%p= link_to "Log in met Zeus WPI", user_omniauth_authorize_path(:zeuswpi), class: "pure-button pure-button-primary login-button"
|
||||
.pure-u-1.pure-u-md-1-2
|
||||
%h2 Pie of Shame
|
||||
= pie_chart @statistics.shamehash
|
||||
- else
|
||||
%h2.columns-title Cute Little Statistics
|
||||
.pure-g
|
||||
.pure-u-1.pure-u-md-1-2.landing-column
|
||||
%h3.columns-title Pie of Shame
|
||||
= pie_chart @statistics.shamehash
|
||||
%h3.columns-title Table of Shame
|
||||
%table.pure-table.full-table
|
||||
%thead
|
||||
%th Shame on
|
||||
%th Contribution to Zeus' lack of money
|
||||
%tbody
|
||||
- @statistics.shameful_users.each do |user|
|
||||
%tr
|
||||
%td.shameful-person= user.name
|
||||
// Won't divide by zero because there won't be any users with
|
||||
// a shameful debt if the total debt is zero.
|
||||
%td.shame-percentage= "#{-100 * user.balance / @statistics.total_debt}%"
|
||||
.pure-u-1.pure-u-md-1-2.landing-column
|
||||
%h3.columns-title Distribution of Debt Sources
|
||||
= pie_chart @statistics.by_issuer
|
||||
%h3.columns-title Top Debt Creators
|
||||
%table.pure-table.full-table
|
||||
%thead
|
||||
%th Issuer
|
||||
%th Number of Transactions issued
|
||||
%tbody
|
||||
- @statistics.creation_counts.each do |name, count|
|
||||
%tr
|
||||
%td.shameful-person= name
|
||||
%td.shame-percentage= count
|
||||
|
||||
.pure-g
|
||||
.pure-u-7-12
|
||||
= render 'transactions'
|
||||
.pure-u-5-12
|
||||
= render 'transaction_form'
|
||||
= render 'requests'
|
||||
= render 'notifications'
|
||||
= render 'outgoing_requests'
|
||||
|
|
10
app/views/pages/sign_in_page.html.haml
Normal file
10
app/views/pages/sign_in_page.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
%h1.columns-title Tab
|
||||
= javascript_include_tag "//www.google.com/jsapi", "chartkick"
|
||||
.pure-g.landing_columns
|
||||
.pure-u-1.pure-u-md-1-2
|
||||
%h2 Authentication
|
||||
%p Log een keer in en betaal uw schulden!
|
||||
%p= link_to "Log in met Zeus WPI", user_omniauth_authorize_path(:zeuswpi), class: "pure-button pure-button-primary login-button"
|
||||
.pure-u-1.pure-u-md-1-2
|
||||
%h2 Pie of Shame
|
||||
= pie_chart @statistics.shamehash
|
|
@ -3,7 +3,15 @@ Rails.application.routes.draw do
|
|||
omniauth_callbacks: 'callbacks'
|
||||
}
|
||||
|
||||
root to: 'pages#landing'
|
||||
devise_scope :user do
|
||||
delete '/sign_out', to: 'devise/sessions#destroy'
|
||||
end
|
||||
|
||||
authenticated :user do
|
||||
root 'pages#landing', as: :authenticated_root
|
||||
end
|
||||
|
||||
root to: 'pages#sign_in_page'
|
||||
|
||||
resources :transactions, only: [:index, :create]
|
||||
resources :users, only: [:index, :show] do
|
||||
|
|
Loading…
Reference in a new issue