initial real commit

This commit is contained in:
Gwenhael Le Moine 2017-11-24 10:58:58 +01:00
parent 71c20889aa
commit 4cff98de69
No known key found for this signature in database
GPG key ID: FDFE3669426707A7
16 changed files with 1772 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
/lib
/public/vendor/node_modules
/credger
/public/js
/.shards

73
credger.cr Normal file
View file

@ -0,0 +1,73 @@
require "kemal"
require "./ledger"
CURRENCY = ""
SEPARATOR = ","
ledger = Ledger.new
# Matches GET "http://host:port/"
get "/" do |env|
env.response.content_type = "text/html"
send_file env, "./public/index.html"
end
get "/api/ledger/accounts" do |env|
env.response.content_type = "application/json"
ledger.accounts.to_json
end
# get "/api/ledger/accounts/depth/:depth/?" do |env|
# env.response.content_type = "application/json"
# ledger.accounts( ).to_json
# end
# get "/api/ledger/dates_salaries/?" do |env|
# env.response.content_type = "application/json"
# ledger.dates_salaries( "salaire" ).to_json
# end
# get "/api/ledger/register/?" do |env|
# env.response.content_type = "application/json"
# { key: params[ :categories ],
# values: ledger.register( params[ :period ], params[ :categories ] ) }
# .to_json
# end
get "/api/ledger/balance" do |env|
env.response.content_type = "application/json"
cleared = env.params.query.has_key?( "cleared" ) ? env.params.query[ "cleared" ] == "true" : false
ledger.balance( cleared,
env.params.query[ "depth" ].to_i,
env.params.query[ "period" ],
env.params.query[ "categories" ] )
.to_json
end
# get "/api/ledger/cleared/?" do |env|
# env.response.content_type = "application/json"
# ledger.cleared().to_json
# end
# get "/api/ledger/budget/?" do |env|
# env.response.content_type = "application/json"
# ledger.budget( params[ :period ],
# params[ :categories ] ).to_json
# end
get "/api/ledger/graph_values" do |env|
env.response.content_type = "application/json"
ledger.graph_values( env.params.query["period"], env.params.query["categories"].split(" ") ).to_json
#ledger.graph_values.to_json
end
get "/api/ledger/version" do |env|
env.response.content_type = "text"
ledger.version
end
Kemal.run 9292

73
ledger.cr Normal file
View file

@ -0,0 +1,73 @@
# encoding: utf-8
require "csv"
# Crystal wrapper module for calling ledger
class Ledger
def initialize( binary : String = "ledger", ledger_file : String = ENV[ "LEDGER_FILE" ] )
@binary = binary
@file = ledger_file
end
def run( options : String, command : String = "", command_parameters : String = "" ) : String
STDERR.puts "#{@binary} -f #{@file} #{options} #{command} #{command_parameters}"
`#{@binary} -f #{@file} #{options} #{command} #{command_parameters}`
end
def version : String
run "--version"
end
def accounts( depth : Number = 9999 ) : Array( Array( String ) )
accounts = run( "", "accounts" )
.split( "\n" )
.map do |a|
a.split( ":" )
.each_slice( depth )
.to_a.first
end.uniq
accounts.map{|a| a.size}.max.times do |i|
accounts += accounts.map { |acc| acc.first( i ) }
end
accounts
.uniq
.sort
.reject { |a| a.empty? || a.first.empty? }
.sort_by { |a| a.size }
end
def balance( cleared : Bool = false, depth : Int32 = 9999, period : String = nil, categories : String = "" ) : Array( NamedTuple( account: String, amount: Float64 ) )
period = period.nil? ? "" : "-p '#{period}'"
depth = depth.nil? ? "" : "--depth #{depth}"
operation = cleared ? "cleared" : "balance"
run( "--flat --no-total --exchange '#{CURRENCY}' #{period} #{depth}", operation, categories )
.split( "\n" )
.reject {|line| line.empty?}
.map do |line|
line_array = line.split( "#{CURRENCY}" )
{ account: line_array[ 1 ].strip,
amount: line_array[ 0 ].tr( SEPARATOR, "." ).to_f }
end
end
def graph_values( period : String = "", categories : Array(String) = ["Expenses"] ) : Hash( String, Array( NamedTuple( date: String, amount: String, currency: String ) ) )
period = period == "" ? "" : "-p '#{period}'"
result = {} of String => Array(NamedTuple(date: String, amount: String, currency: String))
categories.map do |category|
result[category] = CSV
.parse( run( "-MAn --exchange '#{CURRENCY}' #{period}", "csv --no-revalued", category ) )
.map do |row|
{ date: row[ 0 ],
amount: row[ 5 ],
currency: row[ 4 ] }
end
end
result
end
end

1
public/css/app.css Normal file
View file

@ -0,0 +1 @@
/* credger css stylesheet */

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

42
public/index.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html lang="en" ng-app="app" class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html lang="en" ng-app="app" class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html lang="en" ng-app="app" class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html lang="en" ng-app="app" class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>ledger le-moine.org</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link type="image/x-icon" rel="icon" href="/favicon.ico" />
<link type="text/css" rel="stylesheet" href="/vendor/node_modules/nvd3/build/nv.d3.min.css">
<link type="text/css" rel="stylesheet" href="/vendor/node_modules/angular-loading-bar/build/loading-bar.min.css">
<link type="text/css" rel="stylesheet" href="/css/app.css"/>
</head>
<body>
<div class="main" role="main" tabindex="-1" layout="column" ui-view></div>
<script src="/vendor/node_modules/underscore/underscore-min.js"></script>
<script src="/vendor/node_modules/moment/min/moment-with-locales.min.js"></script>
<script src="/vendor/node_modules/hammerjs/hammer.min.js"></script>
<script src="/vendor/node_modules/d3/d3.min.js"></script>
<script src="/vendor/node_modules/nvd3/build/nv.d3.min.js"></script>
<script src="/vendor/node_modules/angular/angular.min.js"></script>
<script src="/vendor/node_modules/angular-i18n/angular-locale_en-us.js"></script>
<script src="/vendor/node_modules/@uirouter/angularjs/release/angular-ui-router.min.js"></script>
<script src="/vendor/node_modules/angular-aria/angular-aria.min.js"></script>
<script src="/vendor/node_modules/angular-animate/angular-animate.min.js"></script>
<script src="/vendor/node_modules/angular-moment/angular-moment.min.js"></script>
<script src="/vendor/node_modules/angular-nvd3/dist/angular-nvd3.min.js"></script>
<script src="/vendor/node_modules/angular-loading-bar/build/loading-bar.min.js"></script>
<!-- APP -->
<script src="/js/app.min.js"></script>
</body>
</html>

15
public/ts/app.ts Normal file
View file

@ -0,0 +1,15 @@
var app = angular.module('app',
['ui.router',
'nvd3',
'angularMoment',
'chieffancypants.loadingBar',
])
.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: '',
component: 'dashboard'
});
}
]);

View file

@ -0,0 +1,98 @@
app.component('bucket',
{
bindings: {
categories: '<',
period: '<'
},
controller: ['$filter', 'API',
function($filter, API) {
let ctrl = this;
ctrl.depth = 99;
ctrl.graph_options = {
chart: {
type: 'multiBarHorizontalChart',
height: 600,
margin: {
top: 20,
right: 20,
bottom: 20,
left: 200
},
x: (d) => { return d.account; },
y: (d) => { return d.amount; },
valueFormat: (d) => { return `${d}`; },
showYAxis: false,
showValues: true,
showLegend: true,
showControls: false,
showTooltipPercent: true,
duration: 500,
labelThreshold: 0.01,
labelSunbeamLayout: true,
labelsOutside: true
}
};
ctrl.$onChanges = (changes) => {
if (changes.period && changes.period.currentValue != undefined) {
API.balance({
period: ctrl.period,
categories: ctrl.categories,
depth: ctrl.depth
})
.then((response) => {
ctrl.raw_data = _(response.data)
.sortBy((account) => { return account.amount; })
.reverse();
ctrl.raw_total = _(response.data).reduce((memo, account) => { return memo + account.amount; }, 0);
ctrl.total_detailed = _.chain(ctrl.raw_data)
.groupBy((account) => {
return account.account.split(':')[0];
})
.each((category) => {
category.total = _(category).reduce((memo, account) => {
return memo + account.amount;
}, 0);
})
.value();
ctrl.total_detailed = _.chain(ctrl.total_detailed)
.keys()
.map((key) => {
return {
account: key,
amount: ctrl.total_detailed[key].total
};
})
.value();
ctrl.graph_options.chart.height = 60 + (25 * ctrl.raw_data.length);
ctrl.data = ctrl.categories.split(' ').map((category) => {
return {
key: category,
values: _(ctrl.raw_data).select((line) => { return line.account.match(`^${category}:.*`); })
}
})
});
}
};
}
],
template: `
<div class="bucket">
<div class="tollbar">
<span ng:repeat="account in $ctrl.total_detailed">{{account.account}} = {{account.amount | number:2}} </span>
</div>
<div class="content">
<div class="graph">
<nvd3 data="$ctrl.data"
options="$ctrl.graph_options">
</nvd3>
</div>
</div>
</div>
`
});

View file

@ -0,0 +1,143 @@
app.component('dashboard',
{
controller: ['$filter', 'API',
function($filter, API) {
let ctrl = this;
ctrl.graphed_accounts = ['Expenses', 'Income'];
let retrieve_graph_values = (params) => {
API.graph_values(params)
.then((response) => {
ctrl.periods = [];
let largest_cat = _(response.data).reduce((memo, cat) => { return cat.length > memo.length ? cat : memo; }, []);
_.chain(largest_cat)
.pluck('date')
.each((date) => {
_(response.data).each((cat) => {
let value = _(cat).find({ date: date });
if (_(value).isUndefined()) {
cat.push({
date: date,
amount: 0,
currency: _(cat).first().currency
});
}
});
});
_(response.data).each((cat) => {
cat = _(cat).sortBy((month) => {
return month.date;
});
});
ctrl.graphiques = {
monthly_values: {
options: {
chart: {
type: 'multiBarChart',
height: 300,
showControls: false,
showLegend: true,
showLabels: true,
showValues: true,
showYAxis: false,
x: (d) => { return d.x; },
y: (d) => { return d.y; },
valueFormat: (d) => { return `${d}`; },
xAxis: {
tickFormat: (d) => {
return `${d}${d == ctrl.period ? '*' : ''}`;
}
},
stacked: false,
duration: 500,
reduceXTicks: false,
rotateLabels: -67,
labelSunbeamLayout: true,
useInteractiveGuideline: true,
multibar: {
dispatch: {
elementClick: (event) => {
console.log('change period')
console.log(ctrl.period)
ctrl.period = event.data.x;
console.log(ctrl.period)
}
}
}
}
},
data: _.chain(response.data)
.keys()
.reverse()
.map((key) => {
let multiplicator = (key == "Income") ? -1 : 1;
return {
key: key,
values: _.chain(response.data[key]).map((value) => {
let date = new Date(value.date);
let period = date.getFullYear() + '-' + (date.getMonth() < 9 ? '0' : '') + (date.getMonth() + 1);
ctrl.periods.push(period);
return {
key: key,
x: period,
y: parseInt(value.amount) * multiplicator
};
})
.sortBy((item) => { return item.x; })
.value()
};
})
.value()
}
};
ctrl.periods = _.chain(ctrl.periods).uniq().sort().reverse().value();
ctrl.period = _(ctrl.periods).first();
});
};
API.accounts()
.then((response) => {
ctrl.raw_accounts = response.data;
ctrl.accounts = ctrl.raw_accounts.map((account_ary) => { return account_ary.join(':'); });
});
retrieve_graph_values({
period: '',
categories: ctrl.graphed_accounts.join(' ')
});
}
],
template: `
<div class="dashboard">
<div class="global-graph" style="height: 300px;">
<div class="accounts" style="width: 20%; height: 100%; float: left;">
<select style="height: 100%;" multiple ng:model="$ctrl.graphed_accounts">
<option ng:repeat="account in $ctrl.accounts">{{account}}</option>
</select>
</div>
<div class="graph" style="width: 80%; float: left;">
<nvd3 data="$ctrl.graphiques.monthly_values.data"
options="$ctrl.graphiques.monthly_values.options">
</nvd3>
</div>
</div>
<h1 style="text-align: center;">
<select ng:options="p as p | amDateFormat:'MMMM YYYY' for p in $ctrl.periods" ng:model="$ctrl.period"></select>
</h1>
<bucket categories="'Expenses Income Equity Liabilities'" period="$ctrl.period"></bucket>
</div>
`
});

54
public/ts/services/API.ts Normal file
View file

@ -0,0 +1,54 @@
app.service('API',
['$http',
function($http) {
let API = this;
API.balance = function(params) {
return $http.get('/api/ledger/balance', {
params: {
period: params.period,
categories: params.categories,
depth: params.depth
}
});
};
API.register = function(params) {
return $http.get('/api/ledger/register', {
params: {
period: params.period,
categories: params.categories
}
});
};
API.graph_values = function(params) {
return $http.get('/api/ledger/graph_values', {
params: {
period: params.period,
categories: params.categories
}
});
};
API.budget = function(params) {
return $http.get('/api/ledger/budget', {
params: {
period: params.period,
categories: params.categories
}
});
};
API.dates_salaries = function() {
return $http.get('/ai/ledger/dates_salaries');
};
API.accounts = function() {
return $http.get('/api/ledger/accounts');
};
API.cleared = function() {
return $http.get('/api/ledger/cleared');
};
}]);

18
public/tsconfig.json Normal file
View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es5",
"module": "none",
"declaration": false,
"removeComments": true,
"sourceMap": false,
"outFile": "js/app.js",
"lib": [
"dom","es2015"
],
},
"include": [
"ts/app.ts",
"ts/services/*.ts",
"ts/components/*.ts",
],
}

23
public/tsfmt.json Normal file
View file

@ -0,0 +1,23 @@
{
"baseIndentSize": 0,
"indentSize": 2,
"tabSize": 2,
"indentStyle": 2,
"newLineCharacter": "\n",
"convertTabsToSpaces": true,
"insertSpaceAfterCommaDelimiter": true,
"insertSpaceAfterSemicolonInForStatements": true,
"insertSpaceBeforeAndAfterBinaryOperators": true,
"insertSpaceAfterConstructor": false,
"insertSpaceAfterKeywordsInControlFlowStatements": true,
"insertSpaceAfterFunctionKeywordForAnonymousFunctions": false,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": true,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": true,
"insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true,
"insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true,
"insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true,
"insertSpaceAfterTypeAssertion": true,
"insertSpaceBeforeFunctionParenthesis": false,
"placeOpenBraceOnNewLineForFunctions": false,
"placeOpenBraceOnNewLineForControlBlocks": false,
}

1165
public/vendor/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load diff

41
public/vendor/package.json vendored Normal file
View file

@ -0,0 +1,41 @@
{
"name": "ledgerrb",
"version": "1.0.0",
"description": "Ledger reporting for the Web",
"main": "./app/index.html",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cycojesus/ledgerrb.git"
},
"author": "Gwenhael Le Moine",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/cycojesus/ledgerrb/issues"
},
"homepage": "https://github.com/cycojesus/ledgerrb#readme",
"dependencies": {
"@uirouter/angularjs": "^1.0.11",
"angular": "^1.5.8",
"angular-animate": "^1.5.8",
"angular-aria": "^1.5.8",
"angular-i18n": "^1.5.8",
"angular-loader": "^1.5.8",
"angular-loading-bar": "^0.9.0",
"angular-messages": "^1.5.8",
"angular-mocks": "^1.5.8",
"angular-moment": "^1.0.0-beta.6",
"angular-nvd3": "^1.0.8",
"angularjs-nvd3-directives": "0.0.8",
"boilerplate": "^0.6.1",
"d3": "^3.5.17",
"google-closure-compiler-js": "^20170910.0.1",
"hammerjs": "^2.0.8",
"moment": "^2.19.2",
"nvd3": "^1.8.4",
"typescript": "^2.6.1",
"underscore": "^1.8.3"
}
}

14
shard.lock Normal file
View file

@ -0,0 +1,14 @@
version: 1.0
shards:
kemal:
github: kemalcr/kemal
commit: 8cb9770ec3c6cf5897e644229dad8d0b5c360941
kilt:
github: jeromegn/kilt
version: 0.4.0
radix:
github: luislavena/radix
version: 0.3.8

7
shard.yml Normal file
View file

@ -0,0 +1,7 @@
name: credger
version: 1.99
dependencies:
kemal:
github: kemalcr/kemal
branch: master