Commit de503827 by jhrabal

radegast

parent 6a7a102c
package com.jh.radegast.api;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -11,6 +10,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.jh.common.web.list.DefaultSorting;
......@@ -28,7 +28,6 @@ public class RatingApiController {
@Autowired
private RatingService service;
//paged endpoint
@RequestMapping(path = "ratings", method = RequestMethod.GET)
@DefaultSorting(field = "key", trend = SortTrend.DESCENDING)
public ResponseEntity<List<Rating>> filterRatings(/*Date from, Date to, */PagingInfo pagingInfo) {
......@@ -42,6 +41,9 @@ public class RatingApiController {
service.saveBulk(ratings);
}
//stats endpoint
@RequestMapping(path = "ratings/stats", method = RequestMethod.GET)
public @ResponseBody RatingsStats ratingsStats(/*Date from, Date to */) {
return service.calculateStats(new RatingsFilter());
}
}
package com.jh.radegast.api;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.LinkedHashMap;
public class RatingsStats {
private Long ratings;
private BigDecimal average;
private HashMap<Long, Long> values;
public RatingsStats() {
this.values = new LinkedHashMap<>();
ratings = 0L;
}
public RatingsStats(Long ratings, BigDecimal average, HashMap<Long, Long> values) {
super();
this.ratings = ratings;
this.values = values;
this.average = average;
}
public Long getRatings() {
return ratings;
}
public void setRatings(Long ratings) {
this.ratings = ratings;
}
public HashMap<Long, Long> getValues() {
return values;
}
public void setValues(HashMap<Long, Long> values) {
this.values = values;
}
public BigDecimal getAverage() {
return average;
}
public void setAverage(BigDecimal average) {
this.average = average;
}
}
package com.jh.radegast.model;
public class RatingStat {
private Long rating;
private Long count;
public RatingStat() {
}
public RatingStat(Long rating, Long count) {
super();
this.rating = rating;
this.count = count;
}
public Long getRating() {
return rating;
}
public void setRating(Long rating) {
this.rating = rating;
}
public Long getCount() {
return count;
}
public void setCount(Long count) {
this.count = count;
}
}
package com.jh.radegast.rating;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
......@@ -8,6 +9,7 @@ import java.util.Set;
import org.hibernate.Criteria;
import org.hibernate.criterion.Restrictions;
import org.hibernate.query.NativeQuery;
import org.springframework.stereotype.Repository;
import com.jh.common.jpa.AbstractHibernateRepository;
......@@ -15,6 +17,7 @@ import com.jh.common.web.list.Page;
import com.jh.common.web.list.PagingInfo;
import com.jh.radegast.api.RatingsFilter;
import com.jh.radegast.model.Rating;
import com.jh.radegast.model.RatingStat;
@Repository
......@@ -66,5 +69,39 @@ public class RatingRepository extends AbstractHibernateRepository {
return c;
}
public List<RatingStat> stats(RatingsFilter filter) {
StringBuilder sql = new StringBuilder("select rating, count(*) from rating where 1=1");
if (filter != null) {
if (filter.getFrom() != null) {
sql.append(" and key >= :from ");
}
if (filter.getTo() != null) {
sql.append(" and key <= :to ");
}
}
sql.append(" group by rating");
NativeQuery query = getSession().createNativeQuery(sql.toString());
if (filter != null) {
if (filter.getFrom() != null) {
query.setParameter("from", String.valueOf(filter.getFrom().getTime()));
}
if (filter.getTo() != null) {
query.setParameter("to", String.valueOf(filter.getTo().getTime()));
}
}
List<RatingStat> stats = new ArrayList<>();
List<Object[]> rows = query.list();
if (rows != null) {
for (Object[] row : rows) {
stats.add(new RatingStat(((Number) row[0]).longValue(), ((Number) row[1]).longValue()));
}
}
return stats;
}
}
package com.jh.radegast.rating;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
......@@ -12,10 +15,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.jh.common.utils.Utils;
import com.jh.common.web.list.Page;
import com.jh.common.web.list.PagingInfo;
import com.jh.radegast.api.RatingsFilter;
import com.jh.radegast.api.RatingsStats;
import com.jh.radegast.model.Rating;
import com.jh.radegast.model.RatingStat;
@Service
public class RatingService {
......@@ -69,4 +75,32 @@ public class RatingService {
return repo.filter(ratingsFilter);
}
@Transactional
public RatingsStats calculateStats(RatingsFilter filter) {
Long total = 0L;
Long value = 0L;
BigDecimal average = BigDecimal.ZERO;
HashMap<Long, Long> ratings = new LinkedHashMap<>();
for (int i = 10; i > 0; i--) {
ratings.put(Long.valueOf(i), 0L);
}
List<RatingStat> list = repo.stats(filter);
if (list != null) {
for (RatingStat rs : list) {
ratings.put(rs.getRating(), rs.getCount());
total += rs.getCount();
value += rs.getRating() * rs.getCount();
}
}
if (total > 0) {
average = Utils.divide(new BigDecimal(value), new BigDecimal(total), 2);
}
return new RatingsStats(total, average, ratings);
}
}
......@@ -9,11 +9,8 @@ class UserInfo extends Component {
render() {
let { user, name } = this.props;
if (!user) {
user = ( window.cfg && window.cfg.user ) || {};
user = ( window.cfg && window.cfg.principal ) || {};
}
if (!name) {
name = window.cfg && window.cfg.unit && window.cfg.unit.name;
}
let img = user.avatar || require("./user-placeholder.png");
......@@ -21,11 +18,7 @@ class UserInfo extends Component {
<div className="user-placeholder">
<img src={ img } />
<br/>
<span className="user-name">{ name || (user.firstName + " " + user.lastName) }</span>
<br/>
<span>{ user.email || "" }</span>
<br/>
<span>{ user.position || "" }</span>
<span className="user-name">{ user && (user.firstName + " " + user.lastName) }</span>
</div>
);
......
......@@ -14,7 +14,6 @@ export default () => new Promise((resolve) => {
global._transitions = true; //FIXME
//set colors
let chartColors = ["4ec3e0", "1287ed", "929ba3"]; //, "678096", "5beeff"];
//let pal = palette(chartColors, 17);
......
......@@ -22,8 +22,8 @@ export default injectIntl((props) => {
let menuItems = [
{
id: 'home',
route: '/home',
id: 'stats',
route: '/stats',
title: intl.formatMessage(messages.homeMenu),
icon: icons.reports
}, {
......
......@@ -127,6 +127,10 @@
color: #009A00;
}
.list-row-data .main-value.neutral-amount {
color: #f5ab00;
}
.list-row-data .main-value.minus-amount {
color: #FF0000;
}
......
......@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { fetchSummary, fetchCashflow, fetchStatuses, fetchDashboardRecordsAction, selector } from './redux.js';
import { fetchStatsAction, fetchRatingsAction, selector } from './redux.js';
import { PageTitle, ActivityIndicator, Info, MessagePanel, SummaryPanel, ToolbarButton, Toolbar, ToolbarSection, ToolbarButtons, ToolbarRow, PageSwitcher } from 'lib/components';
......@@ -14,32 +14,24 @@ import FetchedContent, { ErrorInfo } from 'components/FetchedContent';
//localization
import { injectIntl } from 'react-intl';
import { formatMoney, formatDate } from 'lib/i18n';
import { formatMoney, formatDate, formatNumber } from 'lib/i18n';
import { settingsSelector } from 'utils/settings';
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
import palette from 'utils/palette';
import icons from 'constants/icons';
import messages from './messages.js';
import moment from 'moment';
require("./Home.scss");
const renderButtons = (router, intl) => {
return [
<ToolbarButton key="invoice" label={ intl.formatMessage(messages.addInvoiceButton) } onClick={ () => handleBillableClick("invoice", "new", router) }>
<span className="icon">{ icons.invoice }</span>
</ToolbarButton>
];
}
//TODO
class DashboardList extends Component {
render() {
let { data, fresh, paging, status, i18n, unitId, sorting, from, to, dateType, className, error, onRefresh, intl } = this.props;
let content = null;
let { router } = this.context;
return (
<FetchedContent
......@@ -49,40 +41,36 @@ class DashboardList extends Component {
fresh={ fresh }
empty={ !data || !data.length }
size="big"
buttons={ renderButtons(router, intl) }
onRefresh={ onRefresh }>
{ Array.isArray(data) ? data.map((row, key) => {
let amount = row.type == 'income' || row.type == 'invoice' ? row.amount : row.amount * -1;
let amountClassName = "right main-value p5 " + (amount < 0 ? "minus-amount" : "plus-amount");
let rating = row.rating;
let amountClassName = "right main-value p5 ";
if (rating > 6) {
amountClassName += "plus-amount";
} else if (rating > 3) {
amountClassName += "neutral-amount";
} else {
amountClassName += "minus-amount";
}
let date = moment(row.created).format('DD.MM.YYYY hh:mm:ss');
return (
<div key={ key } className="flex-list-row">
<div className="content">
<div className="list-row-data">
<div className="record-type">
<div className="icon">
{ icons[row.type] || icons.empty }
<div className={`status-indicator ${row.status || "pending"}`}/>
</div>
<div className="date">{ dateType == "BILLED_DATE" ? formatDate(row.billedDate) : formatDate(row.dueDate) }</div>
</div>
<div className="main">
<div className="main-value">
{ row.name }
</div>
<div className="additional-info">
{ row.description || " " }
{ date }
</div>
</div>
<div className={ amountClassName }>
{ formatMoney(amount, row.currency, i18n) }
{ rating }
</div>
</div>
</div>
<div className="actions">
<button className="icon" onClick={ () => handleBillableClick(row.type, row.objectId, router) }>{icons.leftMenu}</button>
</div>
</div>
);
}) : null }
......@@ -90,7 +78,7 @@ class DashboardList extends Component {
<PageSwitcher
paging={ paging }
displayNoPages
onPageChanged={ (paging) => this.props.fetchDashboardRecordsAction(unitId, { from, to }, paging, sorting) }
onPageChanged={ (paging) => this.props.fetchRatingsAction({ from, to }, paging, sorting) }
status={ status }
disabled={ status == "pending" }
/>
......@@ -106,7 +94,7 @@ DashboardList.contextTypes = {
DashboardList = connect((state, props) => state.getIn(["home", "list"]).toJS(), (dispatch, props) => {
return {
dispatch,
fetchDashboardRecordsAction: (unitId, filter, paging, sorting) => dispatch(fetchDashboardRecordsAction(filter, paging, sorting, { unitId }))
fetchRatingsAction: (filter, paging, sorting) => dispatch(fetchRatingsAction(filter, paging, sorting))
};
})(DashboardList);
DashboardList = injectIntl(DashboardList);
......@@ -138,89 +126,16 @@ class DashboardSummary extends Component {
return (
<div className="row-flex box-margin-5">
{ this.renderSummaryPanel(status, messages.incomeTitle, "", 0, intl, "income") }
{ this.renderSummaryPanel(status, messages.outcomeTitle, "", 0, intl, "expenses") }
{ this.renderSummaryPanel(status, messages.incomeTitle, "", data.ratings || 0, intl, "income") }
{ this.renderSummaryPanel(status, messages.outcomeTitle, "", formatNumber(data.average || 0, i18n, 2), intl, "expenses") }
</div>
); }
}
DashboardSummary = connect(settingsSelector((state, props) => state.getIn(["home", "summary"]).toJS()), (dispatch) => ({}))(DashboardSummary);
DashboardSummary = connect(settingsSelector((state, props) => state.getIn(["home", "stats"]).toJS()), (dispatch) => ({}))(DashboardSummary);
DashboardSummary = injectIntl(DashboardSummary);
class DashboardCashflowChart extends Component {
render() {
let { currency, currencySymbol, data, labels, status, i18n, intl, unit, error } = this.props;
return (
<div className="pv20">
<div className="p10 hover-highlight-light">
<h2 className="mb20">{ intl.formatMessage(messages.cashflowTitle) }</h2>
<div className="big-chart">
{ error ? <ErrorInfo size="normal"/> : null }
</div>
<ActivityIndicator show={ status === "pending" } type="overlay" size="big" />
</div>
</div>
);
}
}
DashboardCashflowChart = connect((state, props) => state.getIn(["home", "cashflow"]).toJS(), () => ({}) )(DashboardCashflowChart);
DashboardCashflowChart = injectIntl(DashboardCashflowChart);
class DashboardStatusChart extends Component {
render() {
let { data, status, i18n, intl, error, onRefresh } = this.props;
let
draftLabel = intl.formatMessage(messages.draft),
activeLabel = intl.formatMessage(messages.active),
finishedLabel = intl.formatMessage(messages.finished),
overdueLabel = intl.formatMessage(messages.overdue),
currency = this.props.currencySymbol || this.props.currency;
return (
<div className="pv20">
<div className="status-charts">
<div className="relative">
<h3>{ intl.formatMessage(messages.statusIncome) }</h3>
{ error ? <ErrorInfo size="normal" onRefresh={ onRefresh } /> : <StatusChart
data={ data && data.income }
currency={ currency }
i18n={ i18n }
draftLabel={ draftLabel }
activeLabel={ activeLabel }
finishedLabel={ finishedLabel }
overdueLabel={ overdueLabel }
/> }
<ActivityIndicator show={ status === "pending" } type="overlay" size="big" />
</div>
<div className="relative">
<h3>{ intl.formatMessage(messages.statusOutcome) }</h3>
{ error ? <ErrorInfo size="normal" onRefresh={ onRefresh } /> : <StatusChart
data={ data && data.outcome }
currency={ currency }
i18n={ i18n }
draftLabel={ draftLabel }
activeLabel={ activeLabel }
finishedLabel={ finishedLabel }
overdueLabel={ overdueLabel }
/> }
<ActivityIndicator show={ status === "pending" } type="overlay" size="big" />
</div>
</div>
</div>
);
}
}
DashboardStatusChart = connect(settingsSelector((state, props) => state.getIn(["home", "statuses"]).toJS()), () => ({}) )(DashboardStatusChart);
DashboardStatusChart = injectIntl(DashboardStatusChart);
class Home extends Component {
......@@ -257,11 +172,8 @@ class Home extends Component {
let unitId = this.props.unitId;
let { from, to, dateType } = periodParams;
this.props.fetchSummary(unitId, from, to, dateType);
this.props.fetchCashflow(unitId, from, to, dateType);
this.props.fetchStatuses(unitId, from, to, dateType);
this.props.fetchDashboardRecordsAction(unitId, { from, to }, paging, sorting);
this.props.fetchStatsAction(from, to, dateType);
this.props.fetchRatingsAction({ from, to }, paging, sorting);
}
......@@ -280,24 +192,18 @@ class Home extends Component {
<PageTitle title={ intl.formatMessage(messages.pageTitle) } titleIcon={ icons.home } />
</ToolbarSection>
<ToolbarButtons>
{ renderButtons(router, intl) }
</ToolbarButtons>
<ToolbarRow>
{/*<PeriodTitle changePeriod={ this.changePeriod } />*/}
</ToolbarRow>
</Toolbar>
</div>
</div>
<div className="dashboard-row">
<div className="col-md-6">
<div className="col-md-4 col-sm-6">
<div className="p10 min-height-120">
{/*<DashboardList unitId={ unitId } from={ from } to={ to } dateType={ dateType } i18n={ i18n } onRefresh={ this.init } />*/}
<DashboardList from={ from } to={ to } i18n={ i18n } onRefresh={ this.init } />
</div>
</div>
<div className="col-md-6 p10">
<div className="col-md-8 col-sm-6 p10">
<DashboardSummary i18n={ i18n } onRefresh={ this.init }/>
<DashboardCashflowChart i18n={ i18n } onRefresh={ this.init }/>
<DashboardStatusChart i18n={ i18n } onRefresh={ this.init }/>
</div>
</div>
......@@ -316,10 +222,8 @@ Home.contextTypes = {
function mapDispatchToProps(dispatch, props) {
return {
fetchSummary: (unitId, from, to, dateType) => dispatch(fetchSummary(unitId, from, to, dateType)),
fetchCashflow: (unitId, from, to, dateType) => dispatch(fetchCashflow(unitId, from, to, dateType)),
fetchStatuses: (unitId, from, to, dateType) => dispatch(fetchStatuses(unitId, from, to, dateType)),
fetchDashboardRecordsAction: (unitId, filter, paging, sorting) => dispatch(fetchDashboardRecordsAction(filter, {...paging, pageSize: 7}, sorting))
fetchStatsAction: (from, to, dateType) => dispatch(fetchStatsAction(from, to, dateType)),
fetchRatingsAction: (filter, paging, sorting) => dispatch(fetchRatingsAction(filter, {...paging, pageSize: 7}, sorting))
};
}
......
......@@ -18,31 +18,19 @@ let initialState = {
status: 'pending',
fresh: true,
paging: {
pageSize: 7
pageSize: 15
},
data: []
},
summary: {
stats: {
status: 'pending',
},
cashflow: {
status: 'pending',
fresh: true,
data: {
empty: true
total: 0,
values: {
}
}
},
statuses: {
status: 'pending',
fresh: true,
data: {
income: {},
outcome: {},
data: {},
empty: true
}
}
};
let reducer = createReducer(initialState);
......@@ -55,97 +43,41 @@ let api = reduxList("home/dashboard", {
});
//export actions
export const fetchDashboardRecordsAction = api.actions.fetchList;
export const fetchRatingsAction = api.actions.fetchList;
//TODO
export const selector = settingsSelector((state, props) => {
let home = safe(state.get("home"));
let periodParams = safe(state.getIn(["params", "period"]));
let list = api.selector(state, props);
return { ...home, periodParams };
return home;
});
export function fetchSummary(unitId, from, to, dateType) {
export function fetchStatsAction(unitId, from, to, dateType) {
return {
type: 'home/FETCH_SUMMARY',
payload: backendRequest('/api/' + unitId + '/home/dashboard', 'GET', { params: { from, to } })
type: 'home/FETCH_STATS',
payload: backendRequest('/api/ratings/stats', 'GET', { params: { from, to } })
};
};
reducer.handleAction('home/FETCH_SUMMARY_PENDING', (state, action) =>
state.setIn(["summary", "status"], "pending")
reducer.handleAction('home/FETCH_STATS_PENDING', (state, action) =>
state.setIn(["stats", "status"], "pending")
);
reducer.handleAction('home/FETCH_SUMMARY_FULFILLED', (state, action) =>
reducer.handleAction('home/FETCH_STATS_FULFILLED', (state, action) =>
state
.setIn(["summary", "status"], "success")
.setIn(["summary", "data"], action.payload.body)
.deleteIn(["summary", "error"])
.setIn(["stats", "status"], "success")
.setIn(["stats", "data"], action.payload.body)
.deleteIn(["stats", "error"])
);
reducer.handleAction('home/FETCH_SUMMARY_REJECTED', (state, action) =>
state.setIn(["summary", "status"], "failed").deleteIn(["summary", "data"]).setIn(["summary", "error"], true)
reducer.handleAction('home/FETCH_STATS_REJECTED', (state, action) =>
state.setIn(["stats", "status"], "failed").deleteIn(["stats", "data"]).setIn(["stats", "error"], true)
);
export function fetchCashflow(unitId, from, to, dateType) {
return {
type: 'home/FETCH_CASHFLOW',
payload: backendRequest('/api/' + unitId + '/home/cashflow', 'GET', { params: { from, to, dateType } })
};
};
reducer.handleAction('home/FETCH_CASHFLOW_PENDING', (state, action) =>
state.setIn(["cashflow", "status"], "pending")
);
reducer.handleAction('home/FETCH_CASHFLOW_FULFILLED', (state, action) =>
state
.withMutations(s => {
let b = action.payload.body;
s = s.setIn(["cashflow", "data"], b.data);
s = s.setIn(["cashflow", "labels"], b.labels);
s = s.setIn(["cashflow", "params"], b.params);
s = s.setIn(["cashflow", "currency"], b.currency);
s = s.setIn(["cashflow", "currencySymbol"], b.currencySymbol);
s = s.deleteIn(["cashflow", "error"]);
return s;
})
.setIn(["cashflow", "status"], "success")
);
reducer.handleAction('home/FETCH_CASHFLOW_REJECTED', (state, action) =>
state.setIn(["cashflow", "status"], "failed").setIn(["cashflow", "error"], true)
);
export function fetchStatuses(unitId, from, to, dateType) {
return {
type: 'home/FETCH_STATUSES',
payload: backendRequest('/api/' + unitId + '/home/statuses', 'GET', { params: { from, to, dateType } })
};
};
reducer.handleAction('home/FETCH_STATUSES_PENDING', (state, action) =>
state.setIn(["statuses", "status"], "pending")
);
reducer.handleAction('home/FETCH_STATUSES_FULFILLED', (state, action) =>
state
.withMutations(s => {
s = s.set("statuses", fromJS(action.payload.body));
s = s.setIn(["statuses", "status"], "success");
s = s.deleteIn(["statuses", "error"]);
return s;
})
);
reducer.handleAction('home/FETCH_STATUSES_REJECTED', (state, action) =>
state.setIn(["statuses", "status"], "failed").setIn(["statuses", "error"], true)
);
//export reducer
export default reducer.export();
//TODO transform summary response
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment