Browse Source

Add more reports

Aaron Lindsay 2 years ago
parent
commit
4e547fe4fd

+ 50 - 0
js/actions/ReportActions.js

@@ -0,0 +1,50 @@
1
+var ReportConstants = require('../constants/ReportConstants');
2
+
3
+var ErrorActions = require('./ErrorActions');
4
+
5
+var Report = models.Report;
6
+var Error = models.Error;
7
+
8
+function fetchReport(reportId) {
9
+	return {
10
+		type: ReportConstants.FETCH_REPORT,
11
+		reportId: reportId
12
+	}
13
+}
14
+
15
+function reportFetched(report) {
16
+	return {
17
+		type: ReportConstants.REPORT_FETCHED,
18
+		report: report
19
+	}
20
+}
21
+
22
+function fetch(reportId) {
23
+	return function (dispatch) {
24
+		dispatch(fetchReport(reportId));
25
+
26
+		$.ajax({
27
+			type: "GET",
28
+			dataType: "json",
29
+			url: "report/?id="+reportId,
30
+			success: function(data, status, jqXHR) {
31
+				var e = new Error();
32
+				e.fromJSON(data);
33
+				if (e.isError()) {
34
+					dispatch(ErrorActions.serverError(e));
35
+				} else {
36
+					var r = new Report();
37
+					r.fromJSON(data);
38
+					dispatch(reportFetched(r));
39
+				}
40
+			},
41
+			error: function(jqXHR, status, error) {
42
+				dispatch(ErrorActions.ajaxError(e));
43
+			}
44
+		});
45
+	};
46
+}
47
+
48
+module.exports = {
49
+	fetch: fetch
50
+};

+ 0 - 23
js/components/AttendeeFrequencyChart.js

@@ -1,23 +0,0 @@
1
-var React = require('react');
2
-
3
-var BarChart = require('../components/BarChart');
4
-
5
-module.exports = React.createClass({
6
-	displayName: "AttendeeFrequencyChart",
7
-	render: function() {
8
-		var data = [];
9
-		for (var i = 0; i < this.props.popularAttendees.length; i++) {
10
-			var attendee = this.props.popularAttendees[i];
11
-			data.push({
12
-				'label': attendee.Name,
13
-				'value': attendee.Popularity
14
-			});
15
-		}
16
-
17
-		data.sort(function(a, b){return b.value - a.value;});
18
-
19
-		return (
20
-			<BarChart title="Attendee Frequency" data={data}/>
21
-		);
22
-	}
23
-});

+ 8 - 8
js/components/BarChart.js

@@ -7,17 +7,17 @@ module.exports = React.createClass({
7 7
 	render: function() {
8 8
 		/* Expects 'this.props.data' to be in the form:
9 9
 		 * var data = [
10
-		 * 	{'label': 'foo', 'value': 1.4},
11
-		 * 	{'label': 'bar', 'value': 8}
10
+		 * 	{'Label': 'foo', 'Value': 1.4},
11
+		 * 	{'Label': 'bar', 'Value': 8}
12 12
 		 * ];
13 13
 		 */
14 14
 		if (this.props.data.length < 1)
15 15
 			return (<div />);
16 16
 
17
-		var max = parseFloat(this.props.data[0].value);
18
-		var min = parseFloat(this.props.data[0].value);
17
+		var max = parseFloat(this.props.data[0].Value);
18
+		var min = parseFloat(this.props.data[0].Value);
19 19
 		for (var i = 0; i < this.props.data.length; i++) {
20
-			var cur = parseFloat(this.props.data[i].value);
20
+			var cur = parseFloat(this.props.data[i].Value);
21 21
 			if (cur > max)
22 22
 				max = cur;
23 23
 			if (cur < min)
@@ -30,12 +30,12 @@ module.exports = React.createClass({
30 30
 			if ((max - min) == 0.0)
31 31
 				var percent = 100;
32 32
 			else if (min < 0)
33
-				var percent = 100*(parseFloat(rowData.value)-min)/(max-min);
33
+				var percent = 100*(parseFloat(rowData.Value)-min)/(max-min);
34 34
 			else
35
-				var percent = 100*parseFloat(rowData.value)/max;
35
+				var percent = 100*parseFloat(rowData.Value)/max;
36 36
 			rows.push((
37 37
 				<tr key={i}>
38
-					<td style={{'width': '20%'}}>{rowData.label + " (" + rowData.value + ")"}</td>
38
+					<td style={{'width': '20%'}}>{rowData.Label + " (" + rowData.Value + ")"}</td>
39 39
 					<td style={{'width': '80%'}}><div style={{'width': percent + "%", 'backgroundColor': 'steelblue'}}>&nbsp;</div></td>
40 40
 				</tr>
41 41
 			));

+ 3 - 3
js/components/LunchApp.js

@@ -9,7 +9,7 @@ var Modal = ReactBootstrap.Modal;
9 9
 var TopBarContainer = require('../containers/TopBarContainer');
10 10
 var RecordLunchContainer = require('../containers/RecordLunchContainer');
11 11
 var AccountSettingsModalContainer = require('../containers/AccountSettingsModalContainer');
12
-var LunchStats = require('../components/LunchStats');
12
+var LunchStatsContainer = require('../containers/LunchStatsContainer');
13 13
 var NewUserForm = require('./NewUserForm');
14 14
 
15 15
 module.exports = React.createClass({
@@ -67,8 +67,8 @@ module.exports = React.createClass({
67 67
 						<Tab title="Record Lunch" eventKey={1} >
68 68
 							<RecordLunchContainer />
69 69
 						</Tab>
70
-						<Tab title="Statistics" eventKey={2} >
71
-							<LunchStats />
70
+						<Tab title="Reports" eventKey={2} >
71
+							<LunchStatsContainer />
72 72
 						</Tab>
73 73
 					</Tabs>);
74 74
 			else

+ 39 - 4
js/components/LunchStats.js

@@ -1,15 +1,50 @@
1 1
 var React = require('react');
2 2
 
3
-var PopularSuggestionsContainer = require('../containers/PopularSuggestionsContainer');
4
-var AttendeeFrequencyContainer = require('../containers/AttendeeFrequencyContainer');
3
+var ReactBootstrap = require('react-bootstrap');
4
+var ButtonGroup = ReactBootstrap.ButtonGroup;
5
+var DropdownButton = ReactBootstrap.DropdownButton;
6
+var MenuItem = ReactBootstrap.MenuItem;
7
+
8
+var BarChart = require('../components/BarChart');
5 9
 
6 10
 module.exports = React.createClass({
7 11
 	displayName: "LunchStats",
12
+	getInitialState: function() {
13
+		return {
14
+			selectedReportId: null
15
+		};
16
+	},
17
+	selectReport: function(reportId) {
18
+		this.props.fetchReport(reportId);
19
+		this.setState({
20
+			selectedReportId: reportId
21
+		});
22
+	},
8 23
 	render: function() {
24
+		var chart = (<div>Please select a report from above</div>);
25
+		if (this.state.selectedReportId &&
26
+				this.props.reports.hasOwnProperty(this.state.selectedReportId)) {
27
+			var report = this.props.reports[this.state.selectedReportId];
28
+			var data = report.Data;
29
+			data.sort(function(a, b){return b.Value - a.Value;});
30
+			var chart=(<BarChart title={report.Title} data={data}/>);
31
+		}
32
+
9 33
 		return (
10 34
 			<div>
11
-				<AttendeeFrequencyContainer />
12
-				<PopularSuggestionsContainer />
35
+				<ButtonGroup bsClass="lunch-report-dropdown">
36
+				<DropdownButton
37
+						title="Select Report"
38
+						id="lunch-report-selection-dropdown"
39
+						onSelect={this.selectReport}>
40
+					<MenuItem eventKey="suggestions">Suggestion Frequency</MenuItem>
41
+					<MenuItem eventKey="non-vetoed-suggestions">Non-Vetoed Suggestions</MenuItem>
42
+					<MenuItem eventKey="vetoed-suggestions">Vetoed Suggestions</MenuItem>
43
+					<MenuItem eventKey="attendees">Attendee Frequency</MenuItem>
44
+				</DropdownButton>
45
+				</ButtonGroup>
46
+
47
+				{chart}
13 48
 			</div>
14 49
 		);
15 50
 	}

+ 0 - 23
js/components/PopularSuggestionsChart.js

@@ -1,23 +0,0 @@
1
-var React = require('react');
2
-
3
-var BarChart = require('../components/BarChart');
4
-
5
-module.exports = React.createClass({
6
-	displayName: "PopularSuggestionsChart",
7
-	render: function() {
8
-		var data = [];
9
-		for (var i = 0; i < this.props.popularSuggestions.length; i++) {
10
-			var suggestion = this.props.popularSuggestions[i];
11
-			data.push({
12
-				'label': suggestion.RestaurantName,
13
-				'value': suggestion.Popularity
14
-			});
15
-		}
16
-
17
-		data.sort(function(a, b){return b.value - a.value;});
18
-
19
-		return (
20
-			<BarChart title="Suggestion Frequency" data={data}/>
21
-		);
22
-	}
23
-});

+ 6 - 0
js/constants/ReportConstants.js

@@ -0,0 +1,6 @@
1
+var keyMirror = require('keymirror');
2
+
3
+module.exports = keyMirror({
4
+	FETCH_REPORT: null,
5
+	REPORT_FETCHED: null
6
+});

+ 0 - 18
js/containers/AttendeeFrequencyContainer.js

@@ -1,18 +0,0 @@
1
-var connect = require('react-redux').connect;
2
-
3
-var AttendeeFrequencyChart = require('../components/AttendeeFrequencyChart');
4
-
5
-function mapStateToProps(state) {
6
-	return {
7
-		popularAttendees: state.popularAttendees
8
-	}
9
-}
10
-
11
-function mapDispatchToProps(dispatch) {
12
-	return {}
13
-}
14
-
15
-module.exports = connect(
16
-	mapStateToProps,
17
-	mapDispatchToProps
18
-)(AttendeeFrequencyChart)

+ 22 - 0
js/containers/LunchStatsContainer.js

@@ -0,0 +1,22 @@
1
+var connect = require('react-redux').connect;
2
+
3
+var LunchStats = require('../components/LunchStats');
4
+
5
+var ReportActions = require('../actions/ReportActions');
6
+
7
+function mapStateToProps(state) {
8
+	return {
9
+		reports: state.reports
10
+	}
11
+}
12
+
13
+function mapDispatchToProps(dispatch) {
14
+	return {
15
+		fetchReport: function(reportId) {dispatch(ReportActions.fetch(reportId))},
16
+	}
17
+}
18
+
19
+module.exports = connect(
20
+	mapStateToProps,
21
+	mapDispatchToProps
22
+)(LunchStats)

+ 0 - 18
js/containers/PopularSuggestionsContainer.js

@@ -1,18 +0,0 @@
1
-var connect = require('react-redux').connect;
2
-
3
-var PopularSuggestionsChart = require('../components/PopularSuggestionsChart');
4
-
5
-function mapStateToProps(state) {
6
-	return {
7
-		popularSuggestions: state.popularSuggestions
8
-	}
9
-}
10
-
11
-function mapDispatchToProps(dispatch) {
12
-	return {}
13
-}
14
-
15
-module.exports = connect(
16
-	mapStateToProps,
17
-	mapDispatchToProps
18
-)(PopularSuggestionsChart)

+ 32 - 0
js/models.js

@@ -218,6 +218,37 @@ Error.prototype.isError = function() {
218 218
 		this.ErrorString != empty_error.ErrorString;
219 219
 }
220 220
 
221
+function Report() {
222
+	this.ReportId = "invalid";
223
+	this.Title = "";
224
+	this.Data = [];
225
+}
226
+
227
+Report.prototype.toJSON = function() {
228
+	var json_obj = {};
229
+	json_obj.ReportId = this.ReportId;
230
+	json_obj.Title = this.Title;
231
+	json_obj.Data = this.Data;
232
+	return JSON.stringify(json_obj);
233
+}
234
+
235
+Report.prototype.fromJSON = function(json_input) {
236
+	var json_obj = getJSONObj(json_input);
237
+
238
+	if (json_obj.hasOwnProperty("ReportId"))
239
+		this.ReportId = json_obj.ReportId;
240
+	if (json_obj.hasOwnProperty("Title"))
241
+		this.Title = json_obj.Title;
242
+	if (json_obj.hasOwnProperty("Data"))
243
+		this.Data = json_obj.Data;
244
+}
245
+
246
+Report.prototype.isReport = function() {
247
+	var empty_report = new Report();
248
+	return this.ReportId != empty_report.ReportId ||
249
+		this.Title != empty_report.Title;
250
+}
251
+
221 252
 module.exports = models = {
222 253
 
223 254
 	// Classes
@@ -228,6 +259,7 @@ module.exports = models = {
228 259
 	PopularAttendee: PopularAttendee,
229 260
 	Suggestion: Suggestion,
230 261
 	PopularSuggestion: PopularSuggestion,
262
+	Report: Report,
231 263
 
232 264
 	// Constants
233 265
 	BogusPassword: "password"

+ 2 - 0
js/reducers/LunchReducer.js

@@ -6,6 +6,7 @@ var AttendeeReducer = require('./AttendeeReducer');
6 6
 var PopularAttendeeReducer = require('./PopularAttendeeReducer');
7 7
 var SuggestionReducer = require('./SuggestionReducer');
8 8
 var PopularSuggestionReducer = require('./PopularSuggestionReducer');
9
+var ReportReducer = require('./ReportReducer');
9 10
 var ErrorReducer = require('./ErrorReducer');
10 11
 
11 12
 module.exports = Redux.combineReducers({
@@ -15,5 +16,6 @@ module.exports = Redux.combineReducers({
15 16
 	popularAttendees: PopularAttendeeReducer,
16 17
 	suggestions: SuggestionReducer,
17 18
 	popularSuggestions: PopularSuggestionReducer,
19
+	reports: ReportReducer,
18 20
 	error: ErrorReducer
19 21
 });

+ 19 - 0
js/reducers/ReportReducer.js

@@ -0,0 +1,19 @@
1
+var assign = require('object-assign');
2
+
3
+var ReportConstants = require('../constants/ReportConstants');
4
+var UserConstants = require('../constants/UserConstants');
5
+
6
+module.exports = function(state = {}, action) {
7
+	switch (action.type) {
8
+		case ReportConstants.REPORT_FETCHED:
9
+			var report = action.report;
10
+			var reports = assign({}, state, {
11
+				[report.ReportId]: report
12
+			});
13
+			return reports;
14
+		case UserConstants.USER_LOGGEDOUT:
15
+			return {};
16
+		default:
17
+			return state;
18
+	}
19
+};

+ 1 - 0
main.go

@@ -69,6 +69,7 @@ func main() {
69 69
 	servemux.HandleFunc("/popularattendees/", PopularAttendeeHandler)
70 70
 	servemux.HandleFunc("/suggestion/", SuggestionHandler)
71 71
 	servemux.HandleFunc("/popularsuggestions/", PopularSuggestionHandler)
72
+	servemux.HandleFunc("/report/", ReportHandler)
72 73
 
73 74
 	listener, err := net.Listen("tcp", ":"+strconv.Itoa(port))
74 75
 	if err != nil {

+ 171 - 0
reports.go

@@ -0,0 +1,171 @@
1
+package main
2
+
3
+import (
4
+	"encoding/json"
5
+	"log"
6
+	"net/http"
7
+	"net/url"
8
+)
9
+
10
+type ReportElement struct {
11
+	Label string
12
+	Value int64
13
+}
14
+
15
+type Report struct {
16
+	ReportId string
17
+	Title    string
18
+	Data     []*ReportElement
19
+}
20
+
21
+func (r *ReportElement) Write(w http.ResponseWriter) error {
22
+	enc := json.NewEncoder(w)
23
+	return enc.Encode(r)
24
+}
25
+
26
+func (r *Report) Write(w http.ResponseWriter) error {
27
+	enc := json.NewEncoder(w)
28
+	return enc.Encode(r)
29
+}
30
+
31
+func (r *Report) FromSummedAttendeeList(attendees *[]*Attendee) {
32
+	attendeeMap := make(map[string]int64)
33
+	r.Data = make([]*ReportElement, 0)
34
+
35
+	for i := range *attendees {
36
+		attendeeMap[(*attendees)[i].Name] += 1
37
+	}
38
+	for name, count := range attendeeMap {
39
+		var element ReportElement
40
+		element.Label = name
41
+		element.Value = count
42
+		r.Data = append(r.Data, &element)
43
+	}
44
+}
45
+
46
+func (r *Report) FromSummedSuggestionList(suggestions *[]*Suggestion) {
47
+	suggestionMap := make(map[string]int64)
48
+	r.Data = make([]*ReportElement, 0)
49
+
50
+	for i := range *suggestions {
51
+		suggestionMap[(*suggestions)[i].RestaurantName] += 1
52
+	}
53
+	for name, count := range suggestionMap {
54
+		var element ReportElement
55
+		element.Label = name
56
+		element.Value = count
57
+		r.Data = append(r.Data, &element)
58
+	}
59
+}
60
+
61
+func GetAllSuggestions(userid int64) (*[]*Suggestion, error) {
62
+	var suggestions []*Suggestion
63
+
64
+	_, err := DB.Select(&suggestions, "SELECT * from suggestions WHERE UserId=?", userid)
65
+	if err != nil {
66
+		return nil, err
67
+	}
68
+
69
+	return &suggestions, nil
70
+}
71
+
72
+func GetVetoedSuggestions(userid int64) (*[]*Suggestion, error) {
73
+	var suggestions []*Suggestion
74
+
75
+	_, err := DB.Select(&suggestions, "SELECT suggestions.* FROM suggestions INNER JOIN suggestions AS s2 WHERE suggestions.UserId=? AND s2.UserId=? AND suggestions.Date=s2.Date AND s2.VetoingId=suggestions.SuggestionId", userid, userid)
76
+	if err != nil {
77
+		return nil, err
78
+	}
79
+
80
+	return &suggestions, nil
81
+}
82
+
83
+func GetNonVetoedSuggestions(userid int64) (*[]*Suggestion, error) {
84
+	var suggestions []*Suggestion
85
+
86
+	_, err := DB.Select(&suggestions, "SELECT suggestions.* FROM suggestions LEFT OUTER JOIN suggestions AS s2 ON suggestions.SuggestionId=s2.VetoingId WHERE s2.SuggestionId IS NULL AND suggestions.UserId=?;", userid)
87
+	if err != nil {
88
+		return nil, err
89
+	}
90
+
91
+	return &suggestions, nil
92
+}
93
+
94
+func GetAllAttendees(userid int64) (*[]*Attendee, error) {
95
+	var attendees []*Attendee
96
+
97
+	_, err := DB.Select(&attendees, "SELECT * from attendees WHERE UserId=?", userid)
98
+	if err != nil {
99
+		return nil, err
100
+	}
101
+
102
+	return &attendees, nil
103
+}
104
+
105
+func ReportHandler(w http.ResponseWriter, r *http.Request) {
106
+	user, err := GetUserFromSession(r)
107
+	if err != nil {
108
+		WriteError(w, 1 /*Not Signed In*/)
109
+		return
110
+	}
111
+
112
+	if r.Method == "GET" {
113
+		var report Report
114
+
115
+		query, _ := url.ParseQuery(r.URL.RawQuery)
116
+		reportid := query.Get("id")
117
+		report.ReportId = reportid
118
+
119
+		if reportid == "non-vetoed-suggestions" {
120
+			report.Title = "Non-Vetoed Suggestions"
121
+			suggestions, err := GetNonVetoedSuggestions(user.UserId)
122
+			if err != nil {
123
+				WriteError(w, 999 /*Internal Error*/)
124
+				log.Print(err)
125
+				return
126
+			}
127
+			report.FromSummedSuggestionList(suggestions)
128
+		} else if reportid == "vetoed-suggestions" {
129
+			report.Title = "Vetoed Suggestions"
130
+			suggestions, err := GetVetoedSuggestions(user.UserId)
131
+			if err != nil {
132
+				WriteError(w, 999 /*Internal Error*/)
133
+				log.Print(err)
134
+				return
135
+			}
136
+			report.FromSummedSuggestionList(suggestions)
137
+		} else if reportid == "suggestions" {
138
+			report.Title = "Suggestion Frequency"
139
+			suggestions, err := GetAllSuggestions(user.UserId)
140
+			if err != nil {
141
+				WriteError(w, 999 /*Internal Error*/)
142
+				log.Print(err)
143
+				return
144
+			}
145
+			report.FromSummedSuggestionList(suggestions)
146
+		} else if reportid == "attendees" {
147
+			report.Title = "Attendee Frequency"
148
+			attendees, err := GetAllAttendees(user.UserId)
149
+			if err != nil {
150
+				WriteError(w, 999 /*Internal Error*/)
151
+				log.Print(err)
152
+				return
153
+			}
154
+			report.FromSummedAttendeeList(attendees)
155
+		} else {
156
+			WriteError(w, 3 /*Invalid Request*/)
157
+			return
158
+		}
159
+
160
+		err = (&report).Write(w)
161
+		if err != nil {
162
+			WriteError(w, 999 /*Internal Error*/)
163
+			log.Print(err)
164
+			return
165
+		}
166
+	} else {
167
+		/* No POST, PUT, or DELETE */
168
+		WriteError(w, 3 /*Invalid Request*/)
169
+		return
170
+	}
171
+}

+ 4 - 0
static/stylesheet.css

@@ -25,3 +25,7 @@ div.row div.panel div.panel-body {
25 25
 .addsuggestionbutton {
26 26
 	margin-top: 25px;
27 27
 }
28
+
29
+.lunch-report-dropdown {
30
+	margin-bottom: 25px;
31
+}