Commit 73db7369 authored by Tamás Danis's avatar Tamás Danis

save transaction with products

parent 54f598a1
<?php
namespace App\Http\Controllers;
use App\Product;
use App\Transaction;
use Exception;
use function foo\func;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TransactionController extends Controller {
public function save( Request $request ) {
DB::beginTransaction();
$payload = [
'created_at' => $request["date"],
'description' => $request["description"],
'user_id' => $request["userId"]
];
$transaction = new Transaction( $payload );
$transaction->save();
$transactionId = $transaction->getKey();
try {
foreach ( $request["products"] as $product ) {
$payload = [
'name' => $product["product"],
'price' => $product["price"],
'created_at' => $request["date"],
'transaction_id' => $transactionId
];
$product = new Product( $payload );
$product->save();
}
} catch ( Exception $e ) {
DB::rollBack();
return response()->json( [
"success" => false,
"data" => "Can't save the transaction"
] );
}
DB::commit();
return response()->json( [
"success" => true,
"data" => $transaction->where( 'id', '=', $transactionId )
->with( 'products' )
->get()->first()->toArray()
] );
}
public function get( Request $request ) {
$token = $request->input( 'token' );
$transactions = Transaction::whereHas( 'user', function ( $query ) use ( $token ) {
$query->where( 'auth_token', '=', $token );
} )->with( 'products' )->orderByDesc( 'id' )->get()->toArray();
return response()->json( [
"success" => true,
"data" => $transactions
] );
}
}
......@@ -8,11 +8,12 @@ class Product extends Model {
protected $fillable = [
'name',
'price',
'created_at'
'created_at',
'transaction_id'
];
public function transactions() {
return $this->belongsToMany( 'App\Transaction' );
return $this->belongsTo( 'App\Transaction' );
}
public function type() {
......
......@@ -5,11 +5,17 @@ namespace App;
use Illuminate\Database\Eloquent\Model;
class Transaction extends Model {
protected $fillable = [
'user_id',
'description',
'created_at'
];
public function user() {
return $this->belongsTo( 'App\User' );
}
public function products() {
return $this->belongsToMany( 'App\Product' );
return $this->hasMany( 'App\Product' );
}
}
......@@ -14,6 +14,7 @@ class CreateTransactionsTable extends Migration {
Schema::create( 'transactions', function ( Blueprint $table ) {
$table->bigIncrements( 'id' );
$table->bigInteger( 'user_id' )->unsigned();
$table->string( 'description' );
$table->timestamps();
} );
......
......@@ -16,10 +16,14 @@ class CreateProductsTable extends Migration {
$table->string( 'name' );
$table->float( 'price' );
$table->bigInteger( 'type_id' )->unsigned()->nullable();
$table->bigInteger( 'transaction_id' )->unsigned()->nullable();
$table->timestamps();
} );
Schema::table( 'products', function ( Blueprint $table ) {
$table->foreign( 'transaction_id' )
->references( 'id' )->on( 'transactions' )
->onDelete( 'cascade' );
$table->foreign( 'type_id' )
->references( 'id' )->on( 'types' )
->onDelete( 'cascade' );
......
......@@ -4,5 +4,8 @@ export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
export const REGISTER_ERROR = 'REGISTER_ERROR';
export const LOGOUT = 'LOGOUT';
export const SAVE_TRANSACTION = 'SAVE_TRANSACTION';
export const FETCH_TRANSACTIONS = 'FETCH_TRANSACTIONS';
export const FETCH_TRANSACTIONS_SUCCESS = 'FETCH_TRANSACTIONS_SUCCESS';
export const FETCH_TRANSACTIONS_ERROR = 'FETCH_TRANSACTIONS_ERROR';
export const SAVE_TRANSACTION_SUCCESS = 'SAVE_TRANSACTION_SUCCESS';
export const SAVE_TRANSACTION_ERROR = 'SAVE_TRANSACTION_ERROR';
import * as types from './actionTypes';
import API from '../api/API';
export function saveTransaction(data) {
export function fetchTransactions() {
return dispatch =>
API.saveTransaction(data)
API.fetchTransactions()
.then(data => {
dispatch({ type: types.FETCH_TRANSACTIONS, transactions: data.data });
if (data.success)
dispatch({ type: types.FETCH_TRANSACTIONS, transactions: data.data });
else throw new Error(data.data);
})
.catch(error => {
dispatch({ type: types.FETCH_TRANSACTIONS_ERROR, data: error });
});
}
export function fetchTransactions() {
export function saveTransaction(data) {
return dispatch =>
API.fetchTransactions()
API.saveTransaction(data)
.then(data => {
dispatch({ type: types.FETCH_TRANSACTIONS, transactions: data.data });
console.log(data)
if (data.success)
dispatch({ type: types.SAVE_TRANSACTION_SUCCESS, data: data.data });
else throw new Error(data.data);
})
.catch(error => {
dispatch({ type: types.SAVE_TRANSACTION_ERROR, data: error });
});
}
......@@ -2,8 +2,12 @@ import axios from 'axios';
const loginUrl = '/api/user/login';
const registerUrl = '/api/user/register';
const saveTransactionUrl = `/api/product`;
const fetchTransactionUrl = `/api/products`;
const saveTransactionUrl = `/api/transaction/save?token=`;
const fetchTransactionUrl = `/api/transactions?token=`;
const getToken = () => JSON.parse(localStorage.getItem('auth_user')).auth_token;
const getUserId = () => JSON.parse(localStorage.getItem('auth_user')).id;
class API {
static login(loginData) {
......@@ -33,11 +37,16 @@ class API {
}
static saveTransaction(data) {
const payload = JSON.stringify({
...data,
userId: getUserId()
});
const url = `${saveTransactionUrl}${getToken()}`;
// console.log(url, payload);
return new Promise(resolve => {
axios
.post(saveTransactionUrl, JSON.stringify(data))
axios.post(url, payload)
.then(response => {
resolve(response);
resolve(response.data);
})
.catch(error => {
resolve(error);
......@@ -46,11 +55,11 @@ class API {
}
static fetchTransactions() {
const url = `${fetchTransactionUrl}${getToken()}`;
return new Promise(resolve => {
axios
.get(fetchTransactionUrl)
axios.get(url)
.then(response => {
resolve(response);
resolve(response.data);
})
.catch(error => {
resolve(error);
......
import React from 'react';
import PropTypes from 'prop-types';
const ButtonField = ({ value, disabled }) => {
const ButtonField = ({ value, disabled, onClick, className }) => {
return (
<button
type="submit"
className="btn btn-primary"
className={`btn btn-primary ${className}`}
disabled={disabled}
onClick={onClick}
>
{value}
</button>);
......@@ -14,11 +15,15 @@ const ButtonField = ({ value, disabled }) => {
ButtonField.propTypes = {
value: PropTypes.string.isRequired,
disabled: PropTypes.bool
disabled: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
};
ButtonField.defaultProps = {
disabled: false
disabled: false,
onClick: undefined,
className: ''
};
export default ButtonField;
import React from 'react';
import PropTypes from 'prop-types';
const InputPriceField = props => {
const { input, label, type, meta: { touched, error, warning } } = props;
const isInvalid = touched && error ? 'is-invalid' : null;
return (
<div className="form-group">
<label htmlFor={input.name}>{label}</label>
<div className="input-group">
<input {...input} className={`form-control ${isInvalid}`} type={type}/>
<div className="input-group-append">
<span className="input-group-text">&euro;</span>
</div>
</div>
{touched &&
((error && <div className="invalid-feedback">{error}</div>) ||
(warning && <span>{warning}</span>))}
</div>
);
};
InputPriceField.propTypes = {
input: PropTypes.any.isRequired,
label: PropTypes.any.isRequired,
type: PropTypes.any.isRequired,
meta: PropTypes.any.isRequired
};
export default InputPriceField;
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Field, formValueSelector, reduxForm } from 'redux-form';
import { bindActionCreators } from 'redux';
import moment from 'moment/moment';
import { Field, FieldArray, formValueSelector, reduxForm } from 'redux-form';
import moment from 'moment';
import InputField from '../../../common/form/InputField';
import { required, numeric } from '../../../../validation/validation';
import { required } from '../../../../validation/validation';
import ButtonField from '../../../common/form/ButtonField';
import * as transactionActions from '../../../../actions/transactionActions';
import RenderProducts from './TransactionForm/RenderProducts';
const TransactionForm = ({ valid, actions, data }) => {
const handleSubmit = e => {
e.preventDefault();
actions.saveTransaction(data);
};
const TransactionForm = props => {
const { valid, handleSubmit } = props;
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-md-3">
<Field
label="Date"
type="date"
name="date"
component={InputField}
validate={[required]}
/>
</div>
<div className="col-md-5">
<Field
label="Description"
type="text"
name="description"
component={InputField}
validate={[required]}
/>
</div>
<div className="col-md-2">
<Field
label="Price"
type="text"
name="price"
component={InputField}
validate={[required, numeric]}
/>
</div>
<div className="card">
<div className="card-body">
<h5>Add new transaction</h5>
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-md-4">
<Field
label="Date"
type="date"
name="date"
component={InputField}
validate={[required]}
/>
</div>
<div className="col-md-2">
<ButtonField value="Save" disabled={!valid}/>
</div>
<div className="col-md-8">
<Field
label="Description"
type="text"
name="description"
component={InputField}
validate={[required]}
/>
</div>
</div>
<FieldArray name="products" component={RenderProducts}/>
<div className='row'>
<div className="col-md-12">
<ButtonField value="Save" disabled={!valid}/>
</div>
</div>
</form>
</div>
</form>
</div>
);
};
TransactionForm.propTypes = {
valid: PropTypes.bool.isRequired,
actions: PropTypes.object.isRequired,
data: PropTypes.object
};
TransactionForm.defaultProps = {
data: null
handleSubmit: PropTypes.func.isRequired
};
const selector = formValueSelector('transactionForm');
const mapStateToProps = state => ({
initialValues: {
date: moment().format('YYYY-MM-DD')
date: moment().format('YYYY-MM-DD'),
products:[{}]
},
data: {
date: selector(state, 'date'),
......@@ -80,11 +68,10 @@ const mapStateToProps = state => ({
}
});
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators(transactionActions, dispatch)
};
};
const mapDispatchToProps = dispatch => ({
onSubmit: values => dispatch(transactionActions.saveTransaction(values))
}
);
export default connect(mapStateToProps, mapDispatchToProps)(reduxForm({
form: 'transactionForm'
......
import React from 'react';
import PropTypes from 'prop-types';
import { Field } from 'redux-form';
import ButtonField from '../../../../common/form/ButtonField';
import InputField from '../../../../common/form/InputField';
import { numeric, required } from '../../../../../validation/validation';
import InputPriceField from '../../../../common/form/InputPriceField';
const RenderProducts = ({ fields }) => {
const handleAdd = () => fields.push({});
const handleRemove = index => fields.remove(index);
return (
<>
{fields.map((item, index) =>
<div className="card mb-3" key={index}>
<div className="card-body">
<div className="row">
<div className="col-md-7">
<Field
label="Product"
type="text"
name={`${item}.product`}
component={InputField}
validate={[required]}
/>
</div>
<div className="col-md-3">
<Field
label="Price"
type="number"
step="0.01"
name={`${item}.price`}
component={InputPriceField}
validate={[required, numeric]}
/>
</div>
<div className="col-md-2 d-flex flex-column mb-3">
<ButtonField className="mt-auto" value="Remove" onClick={() => handleRemove(index)}/>
</div>
</div>
</div>
</div>
)}
<div className="row">
<div className="col-md-12">
<ButtonField className="float-right" value="Add product" onClick={handleAdd}/>
</div>
</div>
</>
);
};
RenderProducts.propTypes = {
fields: PropTypes.object.isRequired
};
export default RenderProducts;
......@@ -2,10 +2,11 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import TransactionListItem from './TransactionListItem';
import TransactionListItem from './TransactionList/TransactionListItem';
import * as transactionActions from '../../../../actions/transactionActions';
const TransactionList = ({ actions, transactions }) => {
// console.log(transactions)
useEffect(() => {
actions.fetchTransactions();
......
import React from 'react';
import PropTypes from 'prop-types';
const ProductsArea = ({ products }) => (
<small>
{products.map(
product => `${product.name} ${product.price}€`
).join(', ')
}
</small>
);
ProductsArea.propTypes = {
products: PropTypes.array.isRequired
};
export default ProductsArea;
import React from 'react';
import PropTypes from 'prop-types';
import ProductsArea from './ProductsArea';
const TransactionListItem = ({ transaction }) => {
const countPrice = () => {
let res = 0;
transaction.products.forEach(product => {
res += product.price;
});
return res;
};
return (
<li className="list-group-item d-flex justify-content-between align-items-center">
{transaction.name}
<span className="badge badge-primary badge-pill">{transaction.price}&euro;</span>
{transaction.description}
<ProductsArea products={transaction.products}/>
<span className="badge badge-primary">{countPrice()}&euro;</span>
</li>
);
};
......
......@@ -6,6 +6,7 @@ const Dashboard = () => (
<>
<h1 className="pt-3">Dashboard</h1>
<TransactionForm/>
<h2>History</h2>
<TransactionList/>
</>
);
......
......@@ -13,7 +13,6 @@ const auth = (state = initialState.auth, action) => {
case types.LOGIN_ERROR:
return {
...state,
...action.user,
loggedIn: false,
hasError: true,
error: { message: 'User does not exists or password is wrong' }
......
const storedUser = JSON.parse(localStorage.getItem('auth_user'));
const authUser = storedUser || {};
export default {
auth: {
user: authUser,
user: storedUser || {},
loggedIn: Boolean(storedUser),
hasError: false,
error: {},
......
......@@ -5,6 +5,17 @@ const transaction = (state = initialState.transactions, action) => {
switch (action.type) {
case types.FETCH_TRANSACTIONS:
return action.transactions;
case types.FETCH_TRANSACTIONS_SUCCESS:
return {
...state
};
case types.FETCH_TRANSACTIONS_ERROR:
return action.data;
case types.SAVE_TRANSACTION_SUCCESS:
return [
action.data,
...state
];
default:
return state;
}
......
......@@ -27,6 +27,9 @@ Route::group( [ 'middleware' => [ 'jwt.auth', 'api-header' ] ], function () {
return response()->json( $response, 201 );
} );
Route::post( 'transaction/save', 'TransactionController@save' );
Route::get( 'transactions', 'TransactionController@get' );
} );
Route::group( [ 'middleware' => 'api-header' ], function () {
......
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