diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 540e828..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 88fb27f..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eslint.enable": true -} diff --git a/Procfile b/Procfile index 28fe750..0e339ba 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ web: npm run start +clock: node server/dist/cron/index.js diff --git a/client/.DS_Store b/client/.DS_Store deleted file mode 100644 index 01ae780..0000000 Binary files a/client/.DS_Store and /dev/null differ diff --git a/client/src/.DS_Store b/client/src/.DS_Store deleted file mode 100644 index c97186b..0000000 Binary files a/client/src/.DS_Store and /dev/null differ diff --git a/client/src/app/.DS_Store b/client/src/app/.DS_Store deleted file mode 100644 index 3f73546..0000000 Binary files a/client/src/app/.DS_Store and /dev/null differ diff --git a/client/src/app/actions/actiontype.js b/client/src/app/actions/actiontype.js index 9d34e20..8086955 100644 --- a/client/src/app/actions/actiontype.js +++ b/client/src/app/actions/actiontype.js @@ -5,7 +5,7 @@ export const USER_LOG_IN_FAILURE = 'USER_LOG_IN_FAILURE'; export const USER_LOGGED_OUT = 'USER_LOGGED_OUT'; -export const FETCHING_BOOKS= 'FETCHING_BOOKS'; +export const FETCHING_BOOKS = 'FETCHING_BOOKS'; export const SIGN_UP_USER_FAILURE = 'SIGN_UP_USER_FAILURE'; @@ -17,7 +17,11 @@ export const FETCH_ALL_BOOKS = 'FETCH_ALL_BOOKS '; export const FETCH_BOOKS_BY_USER_ID = 'FETCH_BOOKS_BY_USER_ID'; -export const FETCH_BOOKS_REJECTED= 'FETCH_BOOKS_REJECTED'; +export const FETCH_BOOKS_REJECTED = 'FETCH_BOOKS_REJECTED'; + +export const FETCH_ALL_OVERDUE_BOOKS = 'FETCH_ALL_OVERDUE_BOOKS'; + +export const FETCH_ALL_OVERDUE_REJECTED = 'FETCH_ALL_OVERDUE_REJECTED'; export const UPLOAD_TO_CLOUD_IMAGE_SUCCESS = 'UPLOAD_TO_CLOUD_IMAGE_SUCCESS'; diff --git a/client/src/app/actions/api.js b/client/src/app/actions/api.js index 08fa0a3..c315131 100644 --- a/client/src/app/actions/api.js +++ b/client/src/app/actions/api.js @@ -13,6 +13,9 @@ export default { fetchRecentBooks: (offset, limit) => axios .get(`api/v1/auth/books/recentbooks?offset=${offset}&limit=${limit}`) .then(res => res.data), + fetchOverdueBooks: (offset,limit) => axios + .get(`api/v1/users/getoverduebooks?offset=${offset}&limit=${limit}`) + .then(res=>res.data), fetchbooksbyUserId: (offset, limit) => axios .get(`api/v1/users/borrowedbooks?offset=${offset}&limit=${limit}&returned=false`) .then(res => res.data), diff --git a/client/src/app/actions/authenticate.js b/client/src/app/actions/authenticate.js index 9724d0b..e9f244a 100644 --- a/client/src/app/actions/authenticate.js +++ b/client/src/app/actions/authenticate.js @@ -22,7 +22,7 @@ export const userLoggedIn = data => data }); - export const userLogInFailure = error => +export const userLogInFailure = error => ({ type: USER_LOG_IN_FAILURE, error @@ -41,16 +41,16 @@ export const userLoggedOut = user => }); /** - * create action: userAuthFailure : user + * create action: sign : user * @function userAuthFailure * @param {object} response * @returns {object} action: type and response */ -export const signUpUserFailure = (user) => -({ - type: SIGNUP_USER_FAILURE, - user -}); +export const signUpUserFailure = user => + ({ + type: SIGNUP_USER_FAILURE, + user + }); /** * create action: signUpUserSuccess : user @@ -59,10 +59,10 @@ export const signUpUserFailure = (user) => * @returns {object} action: type and response */ export const signUpUserSuccess = user => -({ - type: SIGNUP_USER_SUCCESS, - user -}); + ({ + type: SIGNUP_USER_SUCCESS, + user + }); /** * async helper function: sign up user @@ -75,10 +75,10 @@ export const signup = data => dispatch => api .signup(data) .then((user) => { dispatch(signUpUserSuccess(user)); - dispatch(showSuccessNotification({user})); + dispatch(showSuccessNotification({ user })); return user; }) - .catch((error) =>{ + .catch((error) => { dispatch(showErrorNotification({ error })); dispatch(signUpUserFailure(error)); }); @@ -93,17 +93,17 @@ export const login = credentials => dispatch => api .user .login(credentials) .then((user) => { - const token = user.data.token; + const token = user.data.token; localStorage.setItem('token', token); - dispatch(showSuccessNotification({user})); + dispatch(showSuccessNotification({ user })); setAuthorizationToken(token); dispatch(userLoggedIn(user.data)); }) - .catch(error =>{ + .catch((error) => { dispatch(showErrorNotification({ error })); - dispatch(userLogInFailure(error)) - }); + dispatch(userLogInFailure(error)); + }); /** * async helper function: log out user diff --git a/client/src/app/actions/borrowbooks.js b/client/src/app/actions/borrowbooks.js index cbfd244..39731ed 100644 --- a/client/src/app/actions/borrowbooks.js +++ b/client/src/app/actions/borrowbooks.js @@ -13,18 +13,16 @@ export const LoanBooksRejected = error => ({ type: BORROW_BOOKS_FAIL, error }); * @function BorrowBooks * @returns {function} asynchronous action */ -export const borrowbooks = data => dispatch =>{ -return api +export const borrowbooks = data => dispatch => api .book .loanbook(data) - .then((response)=>{ - dispatch(LoanBooksSuccess(response)) - dispatch(showSuccessNotification(response)) - return (response) + .then((response) => { + dispatch(LoanBooksSuccess(response)); + dispatch(showSuccessNotification(response)); + return (response); }) - .catch((error)=>{ - dispatch(showErrorNotification({error})) - dispatch(LoanBooksRejected({error})) - return ({error}) - }) -} + .catch((error) => { + dispatch(showErrorNotification({ error })); + dispatch(LoanBooksRejected({ error })); + return ({ error }); + }); diff --git a/client/src/app/actions/fetchbooks.js b/client/src/app/actions/fetchbooks.js index bbde5c5..efd9966 100644 --- a/client/src/app/actions/fetchbooks.js +++ b/client/src/app/actions/fetchbooks.js @@ -4,20 +4,17 @@ import { FETCH_ALL_BOOKS, FETCH_BOOKS_REJECTED, FETCH_BOOKS_BY_USER_ID, - FETCHING_BOOKS + FETCHING_BOOKS, + FETCH_ALL_OVERDUE_BOOKS } from './actiontype'; import api from './api'; - export const fetchBooksRejected = error => ({ type: FETCH_BOOKS_REJECTED, error }); export const fetchRecentBooks = books => ({ type: FETCH_ALL_RECENT_BOOKS, books }); export const fetchBooks = books => ({ type: FETCH_ALL_BOOKS, books }); export const fetchBooksByUserId = books => ({ type: FETCH_BOOKS_BY_USER_ID, books }); - -export const fetchingBooks = state => ({ - type: FETCHING_BOOKS, - state, -}); +export const fetchingBooks = state => ({ type: FETCHING_BOOKS, state }); +export const fetchOverdueBooks = books => ({ type: FETCH_ALL_OVERDUE_BOOKS, books }); /** * async helper function: log in user @@ -35,10 +32,31 @@ export const fetchAllBooks = (offset, limit) => dispatch => api return response; }) .catch((error) => { - dispatch(fetchBooksRejected ({ error })); + dispatch(showErrorNotification({ error })) + dispatch(fetchBooksRejected({ error })); dispatch(fetchingBooks(false)); }); + /** + * async helper function: log in user + * @function fetchOverdueBooks + * @param {integer} offset + * @param {integer} limit + * @returns {function} asynchronous action + */ +export const fetchOverdueBookstoDashboard = (offset, limit) => dispatch => api +.book +.fetchOverdueBooks(offset, limit) +.then((response) => { + dispatch(fetchOverdueBooks(response)); + return response; +}) +.catch((error) => { + dispatch(showErrorNotification({ error })) + dispatch(fetchBooksRejected({ error })); +}); + + /** * async helper function: fetch books to go on the dashboard * @function fetchBooksforDashboard @@ -46,8 +64,7 @@ export const fetchAllBooks = (offset, limit) => dispatch => api * @param {integer} limit * @returns {function} asynchronous action */ -export const fetchBooksforDashboard = (offset, limit) => dispatch => -api +export const fetchBooksforDashboard = (offset, limit) => dispatch => api .book .fetchRecentBooks(offset, limit) .then((response) => { @@ -56,7 +73,8 @@ api return response; }) .catch((error) => { - dispatch(fetchBooksRejected ({ error })); + dispatch(showErrorNotification({ error })) + dispatch(fetchBooksRejected({ error })); dispatch(fetchingBooks(false)); }); @@ -74,5 +92,6 @@ export const fetchAllBooksbyId = (offset, limit) => dispatch => api dispatch(fetchBooksByUserId(response)); }) .catch((error) => { + dispatch(showErrorNotification({ error })) dispatch(fetchBooksRejected({ error })); }); diff --git a/client/src/app/actions/loanhistory.js b/client/src/app/actions/loanhistory.js index 8391045..ce5eeab 100644 --- a/client/src/app/actions/loanhistory.js +++ b/client/src/app/actions/loanhistory.js @@ -1,12 +1,12 @@ import { showErrorNotification } from './notifications'; -import { - LOAN_HISTORY_FAILURE, - LOAN_HISTORY_SUCCESS -} from './actiontype'; +import { LOAN_HISTORY_FAILURE, LOAN_HISTORY_SUCCESS } from './actiontype'; import api from './api'; -export const loanhistorySuccess = bookOperations => ({ type: LOAN_HISTORY_SUCCESS, bookOperations }); -export const loanhistoryFailure = error => ({ type: LOAN_HISTORY_SUCCESS, error }); +export const loanhistorySuccess = bookOperations => ({ + type: LOAN_HISTORY_SUCCESS, + bookOperations +}); +export const loanhistoryFailure = error => ({ type: LOAN_HISTORY_FAILURE, error }); /** * async helper function: loan history @@ -15,14 +15,14 @@ export const loanhistoryFailure = error => ({ type: LOAN_HISTORY_SUCCESS, error * @param {integer} limit * @returns {function} asynchronous action */ -export const loanhistory = (offset, limit) => dispatch => - api +export const loanhistory = (offset, limit) => dispatch => api .book .loanhistory(offset, limit) .then((response) => { - dispatch(loanhistorySuccess(response)) + dispatch(loanhistorySuccess(response)); return response; }) .catch((error) => { + dispatch(showErrorNotification({ error })) dispatch(loanhistoryFailure({ error })); }); diff --git a/client/src/app/actions/notifications.js b/client/src/app/actions/notifications.js index 02d628f..3bb0b96 100644 --- a/client/src/app/actions/notifications.js +++ b/client/src/app/actions/notifications.js @@ -1,10 +1,8 @@ -import { reducer as notifReducer, actions as notifActions, Notifs } from 'redux-notifications'; +import { actions as notifActions } from 'redux-notifications'; +import setAuthorizationToken from '../utils/setAuthorizationToken'; const { notifSend } = notifActions; -import { Redirect, browserHistory } from 'react-router'; -import { React } from 'react'; -import setAuthorizationToken from '../utils/setAuthorizationToken'; -import logout from '../actions/authenticate'; + /** * @description async notifications: show error notification @@ -40,7 +38,7 @@ export const showErrorNotification = ({ message, error }) => (dispatch) => { */ export const showSuccessNotification = ({ message, user }) => (dispatch) => { dispatch(notifSend({ - message: message || user.data.message || data.message, + message: message || user.data.message, kind: 'success', dismissAfter: 2500 })); diff --git a/client/src/app/actions/returnbooks.js b/client/src/app/actions/returnbooks.js index 482b289..38e2be4 100644 --- a/client/src/app/actions/returnbooks.js +++ b/client/src/app/actions/returnbooks.js @@ -1,8 +1,5 @@ import { showErrorNotification, showSuccessNotification } from './notifications'; -import { - RETURN_BOOKS_FAIL, - RETURN_BOOKS_SUCCESS -} from './actiontype'; +import { RETURN_BOOKS_FAIL, RETURN_BOOKS_SUCCESS } from './actiontype'; import api from './api'; export const ReturnBookSuccess = returnedBook => ({ type: RETURN_BOOKS_SUCCESS, returnedBook }); @@ -14,18 +11,13 @@ export const ReturnBookRejected = error => ({ type: RETURN_BOOKS_FAIL, error }); * @returns {function} asynchronous action */ export const returnbook = data => dispatch => api -.book -.returnbook(data) -.then((response)=>{ - dispatch(ReturnBookSuccess(response.returnedBook)) - dispatch(showSuccessNotification(response)) - -}) -.catch((error)=>{ - dispatch(showErrorNotification({error})) - dispatch(ReturnBookRejected(error)) -}) - - - - + .book + .returnbook(data) + .then((response) => { + dispatch(ReturnBookSuccess(response.returnedBook)); + dispatch(showSuccessNotification(response)); + }) + .catch((error) => { + dispatch(showErrorNotification({ error })); + dispatch(ReturnBookRejected(error)); + }); diff --git a/client/src/app/actions/uploadImage.js b/client/src/app/actions/uploadImage.js index e47df09..b06b4f8 100644 --- a/client/src/app/actions/uploadImage.js +++ b/client/src/app/actions/uploadImage.js @@ -1,8 +1,8 @@ +import { showErrorNotification, showSuccessNotification } from './notifications'; import request from 'superagent'; -import { showErrorNotification } from './notifications'; -import { UPLOAD_TO_CLOUD_IMAGE_SUCCESS, - UPLOAD_TO_CLOUD_IMAGE_FAILURE, +import { UPLOAD_TO_CLOUD_IMAGE_SUCCESS, + UPLOAD_TO_CLOUD_IMAGE_FAILURE, CLOUDINARY_UPLOAD_PRESET, CLOUDINARY_UPLOAD_URL } from './actiontype'; @@ -12,14 +12,15 @@ export const UploadImageToCloudFailure = error => ({ type: UPLOAD_TO_CLOUD_IMAGE export const imageUploadToCloud = (username, imageData) => (dispatch) => { return request .post(CLOUDINARY_UPLOAD_URL) - .field({'upload_preset': CLOUDINARY_UPLOAD_PRESET}) + .field({ upload_preset: CLOUDINARY_UPLOAD_PRESET }) .field('file', imageData) .field('public_id', `${username}`) .then((response) => { - dispatch(UploadImageToCloud(response.body)) - return (response.body) + dispatch(UploadImageToCloud(response.body)); + return (response.body); }) - .catch(error => { - UploadImageToCloudFailure(error) + .catch((error) => { + dispatch(showErrorNotification({ error })) + UploadImageToCloudFailure(error); }); -} +}; diff --git a/client/src/app/components/container/Dashboard.jsx b/client/src/app/components/container/Dashboard.jsx index f164198..ca02113 100644 --- a/client/src/app/components/container/Dashboard.jsx +++ b/client/src/app/components/container/Dashboard.jsx @@ -16,8 +16,8 @@ Dashboard.defaultProps = { }; -const mapStateToProps = state=> ({ - username: state.userReducer.user.username, +const mapStateToProps = state => ({ + username: state.userReducer.user.username, firstname: state.userReducer.user.firstname, email: state.userReducer.user.email }); diff --git a/client/src/app/components/container/booklist/DisplayAllBooks.jsx b/client/src/app/components/container/booklist/DisplayAllBooks.jsx index 1d71971..2b1875d 100644 --- a/client/src/app/components/container/booklist/DisplayAllBooks.jsx +++ b/client/src/app/components/container/booklist/DisplayAllBooks.jsx @@ -1,10 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; -import Book from '../../presentation/common/book/DisplayBook.jsx'; -import { fetchAllBooks } from '../../../actions/fetchbooks'; import { PropTypes } from 'prop-types'; -import { Row, Preloader, Pagination,Col } from 'react-materialize'; -import PaginationWrapper from '../common/Pagination.jsx' +import { Row, Preloader, Col } from 'react-materialize'; +import Book from '../../presentation/common/book/DisplayBook.jsx'; +import { fetchAllBooks } from '../../../actions/fetchbooks'; +import PaginationWrapper from '../common/Pagination.jsx'; /** * @description Component for Display Books on the Landing page for all users @@ -12,85 +12,82 @@ import PaginationWrapper from '../common/Pagination.jsx' * @extends {Component} */ class DisplayAllBooks extends React.Component { - constructor(props) { - super(props); - this.state = { - limit: 8, - offset: 0 - }; - } + constructor(props) { + super(props); + this.state = { + limit: 8, + offset: 0 + }; + } - /** + /** * @description dispatch actions that help populate the dashboard with books * fetch books for the dashboard * @method componentDidMount * @memberof DisplayLandingBooks * @returns {void} */ - componentDidMount() { - this.props.fetchAllBooks(this.state.offset, this.state.limit); - } - /** + componentDidMount() { + this.props.fetchAllBooks(this.state.offset, this.state.limit); + } + /** * render Display All Books page component * @method render * @member Display All Books * @returns {object} component */ - render() { - if (!this.props.allBooksList) { - return ; - } - - const getAllBooks = - this.props.allBooksList.books.map((book) => { - return ( - - ); - }); - const { pagination } = this.props.allBooksList; - const config = { items: pagination.pageCount, - activePage: pagination.page - }; + render() { + if (!this.props.allBooksList) { + return ; + } + const getAllBooks = +this.props.allBooksList.books.map(book => ( + +)); + const { pagination } = this.props.allBooksList; + const config = { + items: pagination.pageCount, + activePage: pagination.page + }; - return ( -
- - - { + return ( +
+ + + { [...getAllBooks]} - - - + + + -
- ); - } +
+ ); + } } -DisplayAllBooks.propTypes = { - allBooksList: PropTypes.object -}; +// DisplayAllBooks.propTypes = { +// allBooksList: PropTypes. +// }; DisplayAllBooks.defaultProps = { - allBooksList: null + allBooksList: null }; -const mapStateToProps = (state) => { - return { - allBooksList: state.bookReducer.allBooksList, - }; -}; +const mapStateToProps = state => ({ + allBooksList: state.bookReducer.allBooksList, +}); export default connect(mapStateToProps, { fetchAllBooks })(DisplayAllBooks); diff --git a/client/src/app/components/container/booklist/DisplayAllBorrowedBooks.jsx b/client/src/app/components/container/booklist/DisplayBorrowedBooks.jsx similarity index 58% rename from client/src/app/components/container/booklist/DisplayAllBorrowedBooks.jsx rename to client/src/app/components/container/booklist/DisplayBorrowedBooks.jsx index 97340a9..be45476 100644 --- a/client/src/app/components/container/booklist/DisplayAllBorrowedBooks.jsx +++ b/client/src/app/components/container/booklist/DisplayBorrowedBooks.jsx @@ -1,9 +1,9 @@ import React from 'react'; import { connect } from 'react-redux'; import { PropTypes } from 'prop-types'; -import { Preloader,Pagination, Row, Col } from 'react-materialize'; -import PaginationWrapper from '../common/Pagination.jsx' -import Book from '../../presentation/common/book/DisplayBook.jsx'; +import { Preloader, Row, Col } from 'react-materialize'; +import PaginationWrapper from '../common/Pagination.jsx'; +import Book from '../../presentation/common/book/DisplayBook.jsx'; import { fetchAllBooksbyId } from '../../../actions/fetchbooks'; import MessageforNoBooks from '../../presentation/messages/dashboardMessages/MessageforNoBooks.jsx'; @@ -19,7 +19,6 @@ class DisplayAllBorrowedBooks extends React.Component { limit: 8, offset: 0 }; - } /** @@ -31,8 +30,8 @@ class DisplayAllBorrowedBooks extends React.Component { */ componentDidMount() {
- {this.state.isLoading && } -
+ {this.state.isLoading && } + ; this.props.fetchAllBooksbyId(this.state.offset, this.state.limit); } @@ -43,43 +42,43 @@ class DisplayAllBorrowedBooks extends React.Component { * @returns {object} component */ render() { - if (!this.props.borrowedBooks || this.props.borrowedBooks.books.length === 0) { return ; } - const getAllBooks = this.props.borrowedBooks.books.map((book) => { - return ( - - ); - }); + const getAllBooks = this.props.borrowedBooks.books.map(book => ( + + )); const { pagination } = this.props.borrowedBooks; - const config = { items: pagination.pageCount, - activePage: pagination.page - }; + const config = { + items: pagination.pageCount, + activePage: pagination.page + }; - return
+ return (
- -
- {[...getAllBooks]} -
- + +
+ {[...getAllBooks]} +
+
- - -
; + + +
); } } DisplayAllBorrowedBooks.PropTypes = { @@ -92,11 +91,9 @@ DisplayAllBorrowedBooks.defaultProps = { }; - -const mapStateToProps = ({bookReducer}) => ({ +const mapStateToProps = ({ bookReducer }) => ({ borrowedBooks: bookReducer.borrowedBooksList }); export default connect(mapStateToProps, { fetchAllBooksbyId })(DisplayAllBorrowedBooks); - diff --git a/client/src/app/components/container/booklist/DisplayLandingBooks.jsx b/client/src/app/components/container/booklist/DisplayLandingBooks.jsx index 5a7a653..508da55 100644 --- a/client/src/app/components/container/booklist/DisplayLandingBooks.jsx +++ b/client/src/app/components/container/booklist/DisplayLandingBooks.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { PropTypes } from 'prop-types'; -import { Preloader,Row } from 'react-materialize'; +import { Preloader, Row } from 'react-materialize'; import Book from '../../presentation/common/book/DisplayBook.jsx'; import { fetchBooksforDashboard } from '../../../actions/fetchbooks'; @@ -34,9 +34,9 @@ class DisplayLandingBooks extends React.Component { * @returns {void} */ componentDidMount() { - this.setState({ isFetching: true }) + this.setState({ isFetching: true }); if (this.props.books) { - return + return; } this .props @@ -50,28 +50,29 @@ class DisplayLandingBooks extends React.Component { */ render() { const fetchingState = this.props.isFetching ? - : null; - + : null; const getAllBooks = (this .props - .books)?this - .props - .books - .map(book => ()) : []; + .recentBooks) ? this + .props + .recentBooks + .map(book => ()) : []; return ( -
+
- {[...getAllBooks]} +
+ {[...getAllBooks]} +
); @@ -86,8 +87,8 @@ DisplayLandingBooks.defaultProps = { books: null, }; -const mapStateToProps = ({bookReducer })=> ({ - books: bookReducer.recentBooksList, +const mapStateToProps = ({ bookReducer }) => ({ + recentBooks: bookReducer.recentBooksList, isFetching: bookReducer.fetchingBooks }); diff --git a/client/src/app/components/container/booklist/DisplayOverdueBooks.jsx b/client/src/app/components/container/booklist/DisplayOverdueBooks.jsx new file mode 100644 index 0000000..762fe49 --- /dev/null +++ b/client/src/app/components/container/booklist/DisplayOverdueBooks.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { PropTypes } from 'prop-types'; +import { Preloader, Row, Col } from 'react-materialize'; +import PaginationWrapper from '../common/Pagination.jsx'; +import Book from '../../presentation/common/book/DisplayBook.jsx'; +import { fetchOverdueBookstoDashboard } from '../../../actions/fetchbooks'; +import MessageforNoOverdueBooks from '../../presentation/messages/dashboardMessages/MessageforNoOverdueBooks.jsx'; + +/** + * @description Component for Display Books on the Landing page for all users + * @class DisplayLandingBooks + * @extends {Component} + */ +class DisplayOverdueBooks extends React.Component { + constructor(props) { + super(props); + this.state = { + limit: 8, + offset: 0 + }; + } + + /** + * @description dispatch actions that help populate the dashboard with books + * fetch books for the current user + * @method componentDidMount + * @memberof LandingPage + * @returns {void} + */ + componentDidMount() { +
+ {this.state.isLoading && } +
; + this.props.fetchOverdueBookstoDashboard (this.state.offset, this.state.limit); + } + /** + * render Landing page component + * @method render + * @member LandingPage + * @returns {object} component + */ + render() { + if (!this.props.overdueBooks || this.props.overdueBooks.books.length === 0) { + return ; + } + const getAllBooks = this.props.overdueBooks.books.map(book => ( + + )); + const { pagination } = this.props.overdueBooks; + + const config = { + items: pagination.pageCount, + activePage: pagination.page + }; + + return (
+ + +
+ {[...getAllBooks]} +
+ +
+ +
); + } +} +// DisplayOverdueBooks.PropTypes = { +// overdueBooks: PropTypes.array +// }; + +DisplayOverdueBooks.defaultProps = { + overdueBooks: null, + +}; + + +const mapStateToProps = ({ bookReducer }) => ({ + overdueBooks: bookReducer.overdueBooksList +}); + +export default connect(mapStateToProps, { fetchOverdueBookstoDashboard})(DisplayOverdueBooks); diff --git a/client/src/app/components/container/common/Pagination.jsx b/client/src/app/components/container/common/Pagination.jsx index 616a795..f0d9909 100644 --- a/client/src/app/components/container/common/Pagination.jsx +++ b/client/src/app/components/container/common/Pagination.jsx @@ -5,19 +5,23 @@ import { Pagination, Row } from 'react-materialize'; class PaginationWrapper extends React.Component { pageLimit = (pagenumber, numberOfRecords) => { let pageOffset; - pageOffset = (pagenumber === 1) ? 0 : pagenumber - 1; - return pageOffset * numberOfRecords; + pageOffset = (pagenumber === 1) + ? 0 + : pagenumber - 1; + return pageOffset * numberOfRecords; } onSelect = (number) => { - const { numberOfRecords } = this.props; - this.props.fetch(this.pageLimit(number, numberOfRecords), numberOfRecords); + const {numberOfRecords} = this.props; + this + .props + .fetch(this.pageLimit(number, numberOfRecords), numberOfRecords); } - render (){ - return( + render() { + return ( - + ) } @@ -26,13 +30,12 @@ class PaginationWrapper extends React.Component { export default PaginationWrapper; PaginationWrapper.defaultProps = { - items: 0, - activePage: 1, - maxButtons: 5 + items: 0, + activePage: 1 } PaginationWrapper.proptypes = { - items: PropTypes.number.isRequired, + items: PropTypes.number.isRequired, activePage: PropTypes.number.isRequired, - maxButtons: PropTypes.number.isRequired -} + maxButtons: PropTypes.number.isRequired +} diff --git a/client/src/app/components/container/loanhistory/LoanHistory.jsx b/client/src/app/components/container/loanhistory/LoanHistory.jsx index 794de1e..c83a39d 100644 --- a/client/src/app/components/container/loanhistory/LoanHistory.jsx +++ b/client/src/app/components/container/loanhistory/LoanHistory.jsx @@ -1,9 +1,9 @@ import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Preloader } from 'react-materialize'; import PropTypes from 'prop-types'; import { loanhistory } from '../../../actions/loanhistory'; -import { connect } from 'react-redux'; -import { Pagination, Row ,Preloader } from 'react-materialize'; -import PaginationWrapper from '../common/Pagination.jsx' +import PaginationWrapper from '../common/Pagination.jsx'; import LoanHistoryTable from '../../presentation/loanhistory/LoanHistoryTable.jsx'; /** @@ -15,51 +15,53 @@ class LoanHistory extends React.Component { constructor(props) { super(props); this.state = { - limit: 3, + limit: 5, offset: 0, isLoading: false }; - } - componentDidMount(){ - this.props - .loanhistory(this.state.offset, this.state.limit) + componentDidMount() { + $("body").css("background-color","#ffff") + this.props + .loanhistory(this.state.offset, this.state.limit); } - - render(){ + + render() { if (!this.props.bookOperations) { - return ; - } + return ; + } const { pagination } = this.props.bookOperations; - const config = { items: pagination.pageCount, - activePage: pagination.page - }; - return( + const config = { + items: pagination.pageCount, + activePage: pagination.page + }; + return (
- - -
+ + +
); } - } -LoanHistory.PropTypes = { - bookOperations: PropTypes.array - -}; +// LoanHistory.propTypes = { +// bookOperations: PropTypes.array + +// }; LoanHistory.defaultProps = { - bookOperations: null + bookOperations: null }; -const mapStateToProps = state => ({ - bookOperations : state.bookReducer.bookOperations +const mapStateToProps = state => ({ + bookOperations: state.bookReducer.bookOperations }); export default connect(mapStateToProps, { loanhistory })(LoanHistory); diff --git a/client/src/app/components/presentation/Dashboard.jsx b/client/src/app/components/presentation/Dashboard.jsx index 0263ccd..b6f2bc6 100644 --- a/client/src/app/components/presentation/Dashboard.jsx +++ b/client/src/app/components/presentation/Dashboard.jsx @@ -2,10 +2,11 @@ import React from 'react'; import { Row } from 'react-materialize'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import SideNav from '../presentation/common/SideNav/index.jsx'; -import DisplayAllBorrowedBooks from '../container/booklist/DisplayAllBorrowedBooks.jsx'; +import DisplayAllBorrowedBooks from '../container/booklist/DisplayBorrowedBooks.jsx'; import DisplayAllBooks from '../container/booklist/DisplayAllBooks.jsx'; import LoanHistoryTable from '../container/loanhistory/LoanHistory.jsx'; import MessageforNoOverdueBooks from '../presentation/messages/dashboardMessages/MessageforNoOverdueBooks.jsx'; +import DisplayOverdueBooks from '../container/booklist/DisplayOverdueBooks.jsx'; /** * @description Show User Dashboard @@ -38,7 +39,7 @@ const Dashoard = props => - + diff --git a/client/src/app/components/presentation/common/book/DisplayBook.jsx b/client/src/app/components/presentation/common/book/DisplayBook.jsx index 308be4d..e339e82 100644 --- a/client/src/app/components/presentation/common/book/DisplayBook.jsx +++ b/client/src/app/components/presentation/common/book/DisplayBook.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ReactTooltip from 'react-tooltip'; import { Modal } from 'react-materialize'; -import DisplayModal from './DisplayBookModal.jsx'; +import DisplayBookModal from './DisplayBookModal.jsx'; /** * @description Book component taking book props @@ -29,12 +29,10 @@ const Book = books => ( src={books.image || 'http://res.cloudinary.com/digpnxufx/image/upload/c_scale,h_499,w_325/v1507822148' + '/bookplaceholder_kdbixx.png'} alt={books.title}/> - - - { + if (!loanStatus) + return ( +
+ +
+ ) + else + return ( +
+ +
+ ) +} + +showDatePicker =(loanStatus) =>{ + if(!loanStatus) + return( +
+
User's Loan Status:

Available to You

+
+
Specify Return Date: + +
+
+ ) +else + return( +
User's Loan Status:

Loaned

+
+ + ) + +} + + + handleBorrowClick = (event) => { event.preventDefault(); const dateString = this.state.returndate.format("YYYY-MM-DD") @@ -54,67 +100,30 @@ class DisplayBookModal extends React.Component { $(`#modal-${this.props.id}`).modal({opacity: 0}) }) } + render(){ + if(!this.props.isAuthenticated){ + return( + + ) + } - -render(){ +else{ const isBorrowed = (this.props.borrowedBooksList.books) ? this.props.borrowedBooksList.books.map(book => { return (book.bookid) }) : []; - const loanstatus = isBorrowed.includes(this.state.bookId) - -if(!this.props.isAuthenticated){ -return( - - -
- -
- {this.props.title}/ -
- - -
Title: {this.props.title}
-
-
Author: {this.props.author}
-
Category: {this.props.category}
-
Description: {this.props.description}
- -
-
-
-) + const loanStatus = isBorrowed.includes(this.state.bookId) + + const bookModalActions = this.bookActions(loanStatus) + const chooseReturnDate = this.showDatePicker(loanStatus) + -} -else{ return ( - {!loanstatus ? - : - }}> - -
- -
- {this.props.title}/ -
- - -
Title: {this.props.title}
-
-
Author: {this.props.author}
-
Description: {this.props.description}
-
Loan Status: {!loanstatus?

Available

:

On Loan

}
-
Specify Return Date: - -
- -
-
-
+ + { + chooseReturnDate + } + ) } } @@ -130,3 +139,38 @@ const mapStateToProps = state => ({ export default connect(mapStateToProps,{returnbook,borrowbooks})(DisplayBookModal); + + +class BookModal extends React.Component{ + constructor(props) { + super(props); + } + render(){ + const { id,image,author,category,description,title,header,actions } = this.props; + return( + + +
+ +
+ {title}/ +
+ + +
Title: {title}
+
+
Author: {author}
+
Category: {category}
+
Description: {description}
+ +
+ {this.props.children} +
+
+ ) + } + +} + + + diff --git a/client/src/app/components/presentation/loanhistory/LoanHistoryTable.jsx b/client/src/app/components/presentation/loanhistory/LoanHistoryTable.jsx index fd8b804..f5748a5 100644 --- a/client/src/app/components/presentation/loanhistory/LoanHistoryTable.jsx +++ b/client/src/app/components/presentation/loanhistory/LoanHistoryTable.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; -import { Row }from 'react-materialize'; +import { Row } from 'react-materialize'; import MessageforNoBooksHistory from '../messages/dashboardMessages/MessageforNoBooksHistory.jsx'; /** @@ -10,32 +10,33 @@ import MessageforNoBooksHistory from '../messages/dashboardMessages/MessageforNo * @returns {JSX} JSX representation of Books table */ const BorrowHistoryTable = (props) => { - const rows = props.books && props.books.length ? props.books.map((book,index) => { - return ( - - {book.book.title || 'N/A'} - - {book.book.author || 'N/A'} - {moment(book.createdAt).format('LLLL') || 'N/A'} - {moment(book.returndate).format('LLLL') || 'N/A'} - {book.userReturndate ? moment(book.userReturndate).format('LLLL') || 'N/A': "-"} - {book.returnstatus ? 'Returned' : 'Still Out on Loan'} - - ); - }) : null; + const rows = props.books && props.books.length ? props.books.map((book, index) => ( + + {/* {book.book.title || 'N/A'} */} + {book.book.title} + {/* {book.book.author || 'N/A'} */} + {moment(book.createdAt).format('LL') || 'N/A'} + {moment(book.returndate).format('LL') || 'N/A'} + {book.userReturndate ? moment(book.userReturndate).format('LL') || 'N/A' : '-'} + {book.returnstatus ? 'Returned' : 'Still Out on Loan'} + {/* {moment(book.returndate) > moment() ?
Overdue
: '-'} */} + {(moment(book.returndate) < moment() && book.returnstatus == false )?
Overdue
: '-'} + + )) : null; return (rows ?
- - - - + + {/* */} + + {/* */} + @@ -44,7 +45,7 @@ const BorrowHistoryTable = (props) => {
TitleCoverAuthors
TitleBookAuthorsDate Borrowed Date To Be Returned User Return Date StatusOverdue
: - + ); }; diff --git a/client/src/app/css/style.scss b/client/src/app/css/style.scss index 6e93486..1343b49 100644 --- a/client/src/app/css/style.scss +++ b/client/src/app/css/style.scss @@ -3,7 +3,7 @@ // Variables $font-stack: 'Coiny', sans-serif; $font-stack2: 'Purple Purse', sans-serif; -$base-orange: #f2751c; +$base-orange: #fd6737; $base-white: #ffffff; $base-background: rgb(204, 204, 204); $secondary-color: #0089ec; @@ -31,15 +31,14 @@ nav ul a { .side-nav { z-index: 9 !important; - height: 100% !important + height: 100% !important; } a { font-family: $font-stack2; - color: #ec3a00; } .nav-wrapper { - background-color: $base-orange; + background-color: #fd6737; } body { font-family: $font-stack2; @@ -56,15 +55,17 @@ p { font-family: $font-stack2; } - /* * Landing Page */ -.recent-books{ +.recent-books { margin-left: 90px; padding-top: 20px; } +.landing-page-image .card { + max-width: 85% !important; +} form p { color: $secondary-color; @@ -110,20 +111,26 @@ form p { .book-modal { padding: 10px; } - +.loan-status-text{ + color: $secondary-color; +} +.modal-image{ + max-width : 90%; +} +.loan-status{ + padding: 10px; +} .modal-title { font-size: 1.8em; } .modal-image { padding-left: 15px; - max-width: 100% - + max-width: 100%; } .loan-book { padding-top: 10px; - } .main-wrapper { @@ -158,21 +165,22 @@ form p { font-family: $font-stack2; color: $base-white; text-align: center; - margin-left: 4rem; - margin-right: 4rem; + margin-left: 4.5rem; + margin-right: 4.5rem; border-radius: 4px; font-size: 4.5em; - padding: 40px; + padding: 30px; background-color: rgba(0, 0, 0, 0.42); } .overlay-main { - margin-top: -59vh; + margin-top: -56vh; padding: 50px; - width: calc(100% - 20px); + width: calc(100% - 10px); max-width: 1600px; background-color: $base-white; clear: both; + border-radius: 8px; } .overlay { clear: both; @@ -196,8 +204,8 @@ form p { * Footer css */ .footer-copyright { - background-color: $base-orange !important; min-height: 60px !important; + background-color: $base-orange !important; } footer { height: 3rem; @@ -366,6 +374,9 @@ footer { margin-left: auto; } +.overdue{ + color: $base-orange; +} .nobooks-message { color: #aaa; } @@ -374,7 +385,8 @@ footer { * * Modal * */ -.return-date, .loan-status p { +.return-date, +.loan-status p { display: inline-flex; } @@ -382,6 +394,25 @@ footer { margin-top: -15px; } +.react-datepicker__header { + background-color: $base-light-orange; +} + +@media screen and (min-width: 776px) { + .overlay-main { + margin-top: -61vh; + } +} +@media screen and (max-width: 776px) { + .landing-page-image .card { + max-width: 73% !important; + margin-left: 10px; + } + .overlay-main { + margin-top: -51vh; + } +} + @media screen and (max-width: 541px) { .signup-wrapper { width: auto; @@ -400,6 +431,11 @@ footer { } } +@media only screen and (min-width: 1078px) { + .overlay-main { + margin-top: -60vh; + } +} @media only screen and (min-width: 1600px) { .overlay-main { max-width: 100%; diff --git a/client/src/app/img/.DS_Store b/client/src/app/img/.DS_Store deleted file mode 100644 index 421a87d..0000000 Binary files a/client/src/app/img/.DS_Store and /dev/null differ diff --git a/client/src/app/mainRoot.jsx b/client/src/app/mainRoot.jsx index 61c20f2..616093f 100644 --- a/client/src/app/mainRoot.jsx +++ b/client/src/app/mainRoot.jsx @@ -9,6 +9,7 @@ import Dashboard from './components/container/Dashboard.jsx'; import Logout from './components/container/authentication/Logout.jsx'; import UserRoutes from './components/hoc/UserRoutes.jsx'; import '../app/css/style.scss'; + /** * diff --git a/client/src/app/reducers/bookReducers.js b/client/src/app/reducers/bookReducers.js index 223d499..fede32c 100644 --- a/client/src/app/reducers/bookReducers.js +++ b/client/src/app/reducers/bookReducers.js @@ -9,18 +9,16 @@ import { RETURN_BOOKS_SUCCESS, LOAN_HISTORY_FAILURE, LOAN_HISTORY_SUCCESS, - FETCHING_BOOKS + FETCHING_BOOKS, + FETCH_ALL_OVERDUE_BOOKS } from '../actions/actiontype'; /** * * * * @export - * @param {boolean} [state={ - * books: [], - * fetching: false, - * fetched: false, - * error: null + * @param {object} [state={ + * * }] * @param {object} action * @returns {object} state @@ -33,6 +31,13 @@ export default function bookReducer(state = { ...state, fetchingBooks: action.state }; + case FETCH_ALL_OVERDUE_BOOKS: + { + return { + ...state, + overdueBooksList: action.books + }; + } case FETCH_BOOKS_BY_USER_ID: { return { @@ -86,7 +91,15 @@ export default function bookReducer(state = { .borrowedBooksList .books .filter((book) => book.bookid !== action.returnedBook.id) + }, + overdueBooksList:{ + ...state.overdueBooksList, + books: state + .overdueBooksList + .books + .filter((book) => book.bookid !== action.returnedBook.id) } + } } case RETURN_BOOKS_FAIL: diff --git a/package.json b/package.json index 39dcebe..fb629ad 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,21 @@ "pretest": "rimraf server/dist && mkdir server/dist && npm run build && sequelize db:migrate:undo:all --env=test && sequelize db:migrate --env=test && sequelize db:seed:all --env=test ", "test": "export NODE_ENV=test && mocha server/dist/test/index.spec.js --timeout 10000 && export NODE_ENV=development", "client:test": "jest client/ --coverage", - "coverage": "export NODE_ENV=test && npm run pretest && nyc --reporter=lcov --reporter=text --reporter=lcovonly mocha --compilers js:babel-register server/src/test/* --exclude 'server/dist/' ", + "coverage": "export NODE_ENV=test && npm run pretest && nyc --reporter=lcov --reporter=text --reporter=lcovonly mocha --compilers js:babel-register server/src/test/*", "start:migrate": "sequelize db:migrate", "undo:migrate": "sequelize db:migrate:undo:all", "start:dev": "babel-node server/dist/bin/www.js", "start:webdev": "webpack -d && webpack-dev-server && --content-base client/src/ --inline --hot", - "start-prod": "babel server/src --out-dir server/dist --presets es2015 && npm run heroku-postbuild", + "start-prod": "babel server/src --out-dir server/dist --presets es2015 && npm run heroku-postbuild", "heroku-postbuild": "rimraf client/dist && mkdir client/dist && npm run build && export NODE_ENV=production && webpack" - }, + }, + "nyc": { + "exclude": [ + "server/dist/", + "server/src/cron/", + "server/src/mailer/" + ] + }, "engines": { "npm": "5.3.0", "node": "7.4.0" @@ -45,6 +52,7 @@ "cheerio": "^1.0.0-rc.2", "colors": "^1.1.2", "concurrently": "^3.5.0", + "cron": "^1.3.0", "cross-env": "^5.0.5", "css-loader": "^0.28.7", "debug": "^3.0.0", @@ -69,12 +77,15 @@ "jsonwebtoken": "^7.4.1", "jwt-decode": "^2.2.0", "lodash": "^4.17.4", + "maildev": "^1.0.0-rc3", "materialize-css": "^0.100.2", "mocha": "^3.5.0", "moment": "^2.19.2", "morgan": "^1.8.2", "muicss": "^0.9.25", + "node-cron": "^1.2.1", "node-sass": "^4.5.3", + "nodemailer-mailgun-transport": "^1.3.5", "nodemon": "^1.11.0", "pg": "^7.0.2", "pg-hstore": "^2.3.2", @@ -127,7 +138,7 @@ "eslint-plugin-import": "^2.8.0", "git-flow": "^0.2.0", "mocha-lcov-reporter": "^1.3.0", - "nodemailer": "^4.1.0", + "nodemailer": "^4.4.0", "nyc": "^11.1.0", "path": "^0.12.7", "prop-types": "^15.5.10", diff --git a/server/.DS_Store b/server/.DS_Store deleted file mode 100644 index 4a06079..0000000 Binary files a/server/.DS_Store and /dev/null differ diff --git a/server/src/.DS_Store b/server/src/.DS_Store deleted file mode 100644 index bd718c5..0000000 Binary files a/server/src/.DS_Store and /dev/null differ diff --git a/server/src/app.js b/server/src/app.js index 05e34fd..0ae372b 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -5,6 +5,9 @@ import path from 'path'; import dotenv from 'dotenv'; import routes from './routes'; import authenticate from './controllers/middleware/authenticate'; +import { sendSurchargeJob } from './cron/index'; + + dotenv.config(); const app = express(); @@ -27,7 +30,10 @@ app.use((req, res, next) => { next(); }); +sendSurchargeJob(); + + // We got a new email! app.use('/api/v1', authenticateRoutes, routes); app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../../client/dist/app/index.html'))); diff --git a/server/src/controllers/user.js b/server/src/controllers/user.js index a904a7f..1cc8e02 100644 --- a/server/src/controllers/user.js +++ b/server/src/controllers/user.js @@ -70,7 +70,7 @@ export default { * @returns {void|Response} status, send * */ - signin(req, res) { + signIn(req, res) { return User.findOne({ where: { username: req.body.username diff --git a/server/src/controllers/userbooks.js b/server/src/controllers/userbooks.js index 1d98653..721c36c 100644 --- a/server/src/controllers/userbooks.js +++ b/server/src/controllers/userbooks.js @@ -15,82 +15,102 @@ export default { * @returns {any} book * @memmberOf UserBooks Controller */ - loanbook(req, res) { + loanBook(req, res) { req.params.userId = req.user.id.id || req.user.id; if (!req.body.returndate) { - return res.status(404).send({ message: 'Please specify a valid return date' }); + return res + .status(404) + .send({message: 'Please specify a valid return date'}); } const returndate = req.body.returndate; - if (toDate(returndate) < Date.now() || !toDate(returndate)) { - return res.status(422).send({ message: 'Please provide a valid return date' }); + if (toDate(returndate) <= Date.now() || !toDate(returndate) ) { + + return res + .status(422) + .send({message: 'Please provide a valid return date'}); } if (req.params.userId === '') { - return res.send(404).send({ success: false, message: 'User does not exist' }); + return res + .send(404) + .send({success: false, message: 'User does not exist'}); } - User.findById(req.params.userId) + User + .findById(req.params.userId) .then((user) => { if (!user) { - return res.status(404).send({ message: 'User does not exist' }); + return res + .status(404) + .send({message: 'User does not exist'}); } UserBooks.findOne({ - where: { userid: req.params.userId, bookid: req.body.bookId, returnstatus: false }, - include: [{ model: Books, as: 'book', required: true }] - }).then((bookfound) => { - if (bookfound) { - return res.status(409).send({ success: false, message: 'You have already borrowed this book' }); - } - UserBooks.create({ + where: { userid: req.params.userId, bookid: req.body.bookId, - returndate - }) + returnstatus: false + }, + include: [ + { + model: Books, + as: 'book', + required: true + } + ] + }).then((bookFound) => { + if (bookFound) { + return res + .status(409) + .send({success: false, message: 'You have already borrowed this book'}); + } + UserBooks + .create({userid: req.params.userId, bookid: req.body.bookId, returndate}) .then(() => { - Books.findOne({ where: { id: req.body.bookId } }) - .then((booktoborrow) => { - if (!booktoborrow || booktoborrow.quantity === 0) { - return res.status(404).send({ - success: false, - message: "Sorry we can't find this book or all copies of this book are on loan" - }); + Books + .findOne({ + where: { + id: req.body.bookId + } + }) + .then((bookToBorrow) => { + if (!bookToBorrow || bookToBorrow.quantity === 0) { + return res + .status(404) + .send({success: false, message: "Sorry we can't find this book or all copies of this book are on loan"}); } - booktoborrow + bookToBorrow .update({ - quantity: (booktoborrow.quantity -= 1) - }) - .then((borrowedbook) => { + quantity: (bookToBorrow.quantity -= 1) + }) + .then((borrowedBook) => { res .status(201) - .send({ - success: true, - message: `${borrowedbook.title} succesfully loaned` - }); + .send({success: true, message: `${borrowedBook.title} succesfully loaned`}); }) .catch(() => { res .status(500) - .send({ success: false, message: 'Error from the client end' }); + .send({success: false, message: 'Error from the client end'}); }); }) .catch(() => { - res.status(500).send({ success: false, message: 'Error from the client end' }); + res + .status(500) + .send({success: false, message: 'Error from the client end'}); }); }) .catch(() => { res .status(404) - .send({ - success: false, - message:'There is a problem with this user or book, Please contact the administrator' - }); + .send({success: false, message: 'There is a problem with this user or book, Please contact the administrator'}); }); }); }) .catch((error) => { - res.status(400).send({ success: false, message: ` ${error.message}` }); + res + .status(400) + .send({success: false, message: ` ${error.message}`}); }); }, - /** * Route: GET: /users/getborrowedBooklist * @description Get list of borrowed books @@ -99,17 +119,22 @@ export default { * @returns {any} book list * @memmberOf UserBooks Controller */ - getborrowedBooklist(req, res) { + getBorrowedBookList(req, res) { const offset = req.query.offset || 0; const limit = req.query.limit || 3; req.params.userId = req.user.id.id || req.user.id; if (!req.query.returned) { - return res.status(404).send({ message: 'Please specify a value for returned books' }); + return res + .status(404) + .send({message: 'Please specify a value for returned books'}); } return UserBooks.findAndCountAll({ where: { userid: req.params.userId, - returnstatus: req.query.returned.trim() + returnstatus: req + .query + .returned + .trim() }, include: [ { @@ -120,17 +145,19 @@ export default { ], limit, offset - }) - .then((book) => { - if (book.length === 0) { - return res.status(404).send({ success: false, message: 'You have no books on your loan list' }); - } - res.status(200).send({ + }).then((book) => { + if (book.length === 0) { + return res + .status(404) + .send({success: false, message: 'You have no books on your loan list'}); + } + res + .status(200) + .send({ books: book.rows, pagination: paginationfunc(offset, limit, book) }); - }) - .catch(error => res.status(400).send(error.message)); + }).catch(error => res.status(400).send(error.message)); }, /** @@ -141,7 +168,7 @@ export default { * @returns {any} book * @memmberOf UserBooks Controller */ - returnbook(req, res) { + returnBook(req, res) { req.params.userId = req.user.id.id || req.user.id; UserBooks.findOne({ where: { @@ -156,65 +183,109 @@ export default { required: true } ] - }) - .then((book) => { - if (!book) { - return res.status(409).send({ success: false, message: 'You did not borrow this book' }); + }).then((book) => { + if (!book) { + return res + .status(409) + .send({success: false, message: 'You did not borrow this book'}); + } + UserBooks.update({ + returnstatus: true, + userReturndate: Date.now() + }, { + where: { + userid: req.params.userId, + bookid: req.body.bookId } - UserBooks.update( - { - returnstatus: true, - userReturndate: Date.now() - }, - { - where: { - userid: req.params.userId, - bookid: req.body.bookId - } + }).then(() => { + Books + .findOne({ + where: { + id: req.body.bookId } - ).then(() => { - Books.findOne({ - where: { - id: req.body.bookId - } - }).then((bookToreturn) => { + }) + .then((bookToreturn) => { if (!bookToreturn) { - return res.status(404).send({ message: 'The book is not in our library' }); + return res + .status(404) + .send({message: 'The book is not in our library'}); } bookToreturn .update({ - quantity: bookToreturn.quantity + 1 - }) + quantity: bookToreturn.quantity + 1 + }) .then((returnedBook) => { if (returnedBook.userReturndate > returnedBook.returndate) { - res.status(201).send({ - success: true, - message: `You have just returned ${returnedBook.title} late, A fine will be sent to you`, - returnedBook - }); + res + .status(201) + .send({success: true, message: `You have just returned ${returnedBook.title} late, A fine will be sent to you`, returnedBook}); } else { - res.status(201).send({ - success: true, - message: `You have just returned ${returnedBook.title}`, - returnedBook - }); + res + .status(201) + .send({success: true, message: `You have just returned ${returnedBook.title}`, returnedBook}); } }); }); + }); + }).catch(error => res.status(500).send(error.message)); + }, + + /** + * Route: GET: /users/overduebooks + * @description Get user overdue books + * @param {any} req + * @param {any} res + * @returns {any} book + * @memmberOf UserBooks Controller + */ + getOverdueBooks(req, res) { + const offset = req.query.offset || 0; + const limit = req.query.limit || 3; + req.params.userId = req.user.id.id || req.user.id; + return UserBooks.findAndCountAll({ + where: { + userid: req.params.userId, + returnstatus: false, + returndate: { + $lt: Date.now() - 24*60*60*1000 + } + }, + include: [ + { + model: Books, + as: 'book', + required: true + } + ], + limit, + offset + }). + then((book) => { + if (book.length === 0) { + return res + .status(404) + .send({ message: 'You have no overdue books' }); + } + res + .status(200) + .send({ + books: book.rows, + pagination: paginationfunc(offset, limit, book) }); - }) - .catch(error => res.status(500).send(error.message)); + }).catch(error => res.status(400).send(error.message)); }, -/** - * Route: PUT: /users/userhistory + + + /** + * Route: GET: /users/userhistory * @description Get user loan history * @param {any} req * @param {any} res * @returns {any} book * @memmberOf UserBooks Controller */ - getHistory(req, res){ + getLoanHistory(req, res) { const offset = req.query.offset || 0; const limit = req.query.limit || 3; req.params.userId = req.user.id.id || req.user.id; @@ -230,18 +301,23 @@ export default { } ], limit, - offset - }) - .then((book) => { - if (book.length === 0) { - return res.status(404).send({ success: false, message: 'You have no books on your loan list' }); - } - res.status(200).send({ + offset, + order: [ + ['createdAt', 'DESC'] + ] + }).then((book) => { + if (book.length === 0) { + return res + .status(404) + .send({ message: 'You have no books on your loan list' }); + } + res + .status(200) + .send({ books: book.rows, pagination: paginationfunc(offset, limit, book) }); - }) - .catch(error => res.status(400).send(error.message)); + }).catch(error => res.status(400).send(error.message)); } }; diff --git a/server/src/cron/index.js b/server/src/cron/index.js new file mode 100644 index 0000000..8b3614f --- /dev/null +++ b/server/src/cron/index.js @@ -0,0 +1,20 @@ +import sendSurcharge from './sendSurcharge'; +import { CronJob } from 'cron'; + + +export const setCron = props => new CronJob(props); + +export const sendSurchargeJob = () => + setCron({ + cronTime: '50 15 * * 1-7', + onTick: sendSurcharge, + timeZone: 'Africa/Lagos', + start: true + }); + +if (require.main === module) { + sendSurchargeJob(); +} +export default { + +}; diff --git a/server/src/cron/sendSurcharge.js b/server/src/cron/sendSurcharge.js new file mode 100644 index 0000000..4eddddf --- /dev/null +++ b/server/src/cron/sendSurcharge.js @@ -0,0 +1,63 @@ +import moment from 'moment'; +import model from '../models'; +import { transporter, mailOptions } from '../mailer/mailer'; + +const { UserBooks, User, Books } = model; + +const sendSurcharge = () => (UserBooks.findAll({ + where: { + returnstatus: false, + returndate: { + $lt: Date.now() - 24 * 1000 + } + }, + include: [ + { + model: Books, + as: 'book', + required: true + }, { + model: User + + } + + ] +}).then((overdueBooks) => { + const bookIds = []; + const usernames = []; + const emails = []; + const bookTitles = []; + overdueBooks.forEach((book) => { + bookIds.push(book.book.id); + usernames.push(book.User.firstname); + emails.push(book.User.email); + bookTitles.push(book.book.title); + }); + + emails.forEach((email,index) => { + const to = email; + const bcc = null; + const subject = "Default on Returning Book"; + const html = ` +

Hello ${usernames[index]},

+

This is to notify you that you have exceeded the borrowing duration

+

for one of our books you will be be sent a daily fine till you return the book

+

Please return the book ${bookTitles[index]} +

Thank you for the understanding


+

Kind regards,

`; + transporter.sendMail(mailOptions(to, bcc, subject, html), function (error, info) { + if (error) { + console.log(error); + } else { + console.log('Email sent: ' + info.response); + } + }) + }); +}) +.catch((error) => { + process.stdout.write(error.stack); + process.exit(0); +}) +); + +export default sendSurcharge; diff --git a/server/src/mailer/mailer.js b/server/src/mailer/mailer.js index e69de29..b7ab5b6 100644 --- a/server/src/mailer/mailer.js +++ b/server/src/mailer/mailer.js @@ -0,0 +1,14 @@ +import nodemailer from 'nodemailer'; +require('dotenv').config(); + +export const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_ADDRESS, + pass: process.env.EMAIL_PASSWORD + } +}); + +export const mailOptions = (to, bcc, subject, html) => ({ + from: 'Mailer from library.com', to, bcc, subject, html +}); diff --git a/server/src/routes/index.js b/server/src/routes/index.js index 0a287ae..3b95a93 100644 --- a/server/src/routes/index.js +++ b/server/src/routes/index.js @@ -16,7 +16,7 @@ Router.get('/auth/books/recentbooks', BooksController.getAllBooks); Router.post('/auth/users/signup', fieldValidationMiddleware, nullvalidationMiddleware, UserController.create); -Router.post('/auth/users/signin', nullvalidationMiddleware, UserController.signin); +Router.post('/auth/users/signin', nullvalidationMiddleware, UserController.signIn); Router.post('/books', nullvalidationMiddleware, BooksController.create); @@ -24,12 +24,14 @@ Router.put('/books/:bookId', nullvalidationMiddleware, BooksController.update); Router.get('/books/', BooksController.getAllBooks); -Router.post('/users/loanbook', authdecodeToken, UserBooksController.loanbook); +Router.post('/users/loanbook', authdecodeToken, UserBooksController.loanBook); -Router.put('/users/returnbook', authdecodeToken, UserBooksController.returnbook); +Router.put('/users/returnbook', authdecodeToken, UserBooksController.returnBook); -Router.get('/users/getloanhistory', authdecodeToken, UserBooksController.getHistory); +Router.get('/users/getloanhistory', authdecodeToken, UserBooksController.getLoanHistory); -Router.get('/users/borrowedbooks', authdecodeToken, UserBooksController.getborrowedBooklist); +Router.get('/users/getoverduebooks', authdecodeToken, UserBooksController.getOverdueBooks); + +Router.get('/users/borrowedbooks', authdecodeToken, UserBooksController.getBorrowedBookList); export default Router;