Merge branch 'add-support-for-multicurrency' into 'master'

Add support for multicurrency

See merge request saburly/gangsta/roraccounting!5
This commit was merged in pull request #5.
This commit is contained in:
Senad Uka
2020-09-04 03:32:06 +00:00
17 changed files with 618 additions and 26 deletions

View File

@@ -0,0 +1,40 @@
class ChipValuesController < ApplicationController
def create
if ChipValue.create_full_chip_value(chip_value_params)
json_response all_chips_response
else
error_response :bad_request
end
rescue StandardError
error_response :bad_request
end
def update
if ChipValue.update_full_chip_value(chip_value_params)
json_response all_chips_response
end
rescue StandardError
error_response :bad_request
end
def destroy
if ChipValue.destroy_full_chip_value(params[:id])
json_response all_chips_response
else
error_response :bad_request
end
rescue StandardError
error_response :bad_request
end
private
def chip_value_params
params.require(:chip_value).permit(:id, :base_chip_id, :secondary_chip_id, :value)
end
def all_chips_response
Chip.all.order(:name).to_json(include: :base_chip_values)
end
end

View File

@@ -0,0 +1,29 @@
class ChipsController < ApplicationController
def index
json_response Chip.all.order(:name).to_json(include: :base_chip_values)
end
def create
chip = Chip.new(chip_params)
if chip.save
json_response chip
else
error_response :bad_request
end
end
def destroy
chip_id = params[:id]
if chip_id.present? && Chip.destroy(chip_id)
index
else
error_response :bad_request
end
end
private
def chip_params
params.require(:chip).permit(:id, :name, :symbol, :enabled)
end
end

23
app/models/chip.rb Normal file
View File

@@ -0,0 +1,23 @@
class Chip < ApplicationRecord
has_many :base_chip_values, class_name: "ChipValue", foreign_key: "base_chip_id", dependent: :delete_all
has_many :secondary_chip_values, class_name: "ChipValue", foreign_key: "secondary_chip_id", dependent: :delete_all
validates :name, uniqueness: true
validates :name, :symbol, presence: true
after_create :add_chip_values
private
# When new chip(currency) is added, add new record to the chip_values table for every existing chip(currency)
def add_chip_values
Chip.all.each do |chip|
next if chip.name == name
transaction do
ChipValue.new(base_chip: self, secondary_chip: chip, value: 0).save
ChipValue.new(base_chip: chip, secondary_chip: self, value: 0).save
end
end
end
end

53
app/models/chip_value.rb Normal file
View File

@@ -0,0 +1,53 @@
class ChipValue < ApplicationRecord
belongs_to :base_chip, class_name: 'Chip'
belongs_to :secondary_chip, class_name: 'Chip'
validates :base_chip_id, uniqueness: { scope: :secondary_chip_id }
def self.create_full_chip_value(params)
base_chip_id = params[:base_chip_id]
secondary_chip_id = params[:secondary_chip_id]
value = params[:value].to_f
chips = Chip.where(id: [base_chip_id, secondary_chip_id])
base_chip = chips.first
secondary_chip = chips.second
mirrored_value = value.zero? ? 0 : (1 / value)
base_chip_value = ChipValue.new(base_chip: base_chip, secondary_chip: secondary_chip, value: value)
mirrored_chip_value = ChipValue.new(base_chip: secondary_chip, secondary_chip: base_chip, value: mirrored_value)
transaction do
base_chip_value.save
mirrored_chip_value.save
end
end
def self.update_full_chip_value(params)
id = params[:id]
new_value = params[:value].to_f
mirrored_value = new_value.zero? ? 0 : (1 / new_value)
chip_value = ChipValue.find(id)
mirrored_chip_value = ChipValue.where(base_chip_id: chip_value.secondary_chip_id, secondary_chip_id: chip_value.base_chip_id).first
chip_value.value = new_value
mirrored_chip_value.value = mirrored_value
transaction do
chip_value.save
mirrored_chip_value.save
end
end
def self.destroy_full_chip_value(id)
chip_value = ChipValue.find(id)
mirrored_chip_value = ChipValue.where(base_chip_id: chip_value.secondary_chip_id, secondary_chip_id: chip_value.base_chip_id).first
transaction do
chip_value.destroy
mirrored_chip_value.destroy
end
end
end

View File

@@ -41,3 +41,7 @@
z-index: 996;
}
.mr-10 {
margin-right: 100px;
}

View File

@@ -1,39 +1,43 @@
import React from 'react';
import './App.css';
import { Tabs, Tab, Navbar } from 'react-materialize';
import { Navbar } from 'react-materialize';
import MakeMoneyMove from './cash/MakeMoneyMove';
import Cash from './cash/Cash';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import Chips from './chips/Chips';
import { BrowserRouter as Router, Route } from "react-router-dom";
import RoutableNavItem from './common/RoutableNavItem';
import {
CRIB,
CRIB,
CHIPS,
MAKE_MONEY_MOVE
} from './RouteNames';
function App() {
return (
<Router>
<div className="navbar-fixed">
<Navbar brand={<div>GKS</div>} alignLinks="right">
<RoutableNavItem href={CRIB}>
Crib
</RoutableNavItem>
<Router>
<div className="navbar-fixed">
<Navbar brand={<div>GKS</div>} alignLinks="right">
<RoutableNavItem href={CRIB}>
Crib
</RoutableNavItem>
<RoutableNavItem>
Homies
</RoutableNavItem>
<RoutableNavItem href={CHIPS}>
Chips
</RoutableNavItem>
<RoutableNavItem href={MAKE_MONEY_MOVE}>
Make Money Move
</RoutableNavItem>
<RoutableNavItem>
Homies
</RoutableNavItem>
</Navbar>
<RoutableNavItem href={MAKE_MONEY_MOVE}>
Make Money Move
</RoutableNavItem>
</Navbar>
</div>
</div>
<div className="autoscrolling">
<div className="autoscrolling">
<Route exact path={CRIB} component={Cash} />
<Route path={CHIPS} component={Chips} />
<Route path={MAKE_MONEY_MOVE} component={MakeMoneyMove} />
</div>
</Router>

View File

@@ -1,2 +1,3 @@
export const CRIB = '/';
export const CHIPS = '/chips';
export const MAKE_MONEY_MOVE = '/make-money-move';

View File

@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
import { Button, Table } from 'react-materialize';
import './Cash.css';
import axios from 'axios';
import { Link } from 'react-router-dom';
import { MAKE_MONEY_MOVE } from '../RouteNames';
import { withRouter } from 'react-router-dom';

View File

@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import { TextInput, Button } from "react-materialize";
import axios from "axios";
import M from "materialize-css";
const AddChip = (props) => {
const [chipName, setChipName] = useState('');
const [chipSymbol, setChipSymbol] = useState('');
const [submitInProgress, setSubmitInProgress] = useState(false);
const handleInputChange = (e) => {
const newValue = e.target.value;
switch (e.target.id){
case 'chipName':
setChipName(newValue);
break;
case 'chipSymbol':
setChipSymbol(newValue);
break;
default:
break;
}
}
const disableSubmit = () => {
return submitInProgress || chipName.length === 0 || chipSymbol.length === 0;
}
const clearForm = () => {
setChipName('');
setChipSymbol('');
}
const errorToast = () => M.toast({ html: "Yo! It ain't workin'" });
const handleSubmit = async () => {
setSubmitInProgress(true);
const chipRequest = {
chip: {
name: chipName,
symbol: chipSymbol,
}
}
try{
const submitResponse = await axios.post('/chips', chipRequest);
if (submitResponse && submitResponse.status === 200 && submitResponse.data) {
M.toast({ html: "Chipped In" });
clearForm();
} else {
errorToast();
}
}catch (e) {
errorToast();
}
setSubmitInProgress(false);
}
return (
<div className='section'>
<h5>Add New Chip</h5>
<TextInput
id='chipName'
label="Chip Name"
value={chipName}
onChange={handleInputChange}
/>
<TextInput
id='chipSymbol'
label="Chip Symbol"
value={chipSymbol}
onChange={handleInputChange}
/>
<Button disabled={disableSubmit()} waves="light" onClick={handleSubmit}>
Do It
</Button>
</div>
);
}
export default AddChip;

14
client/src/chips/Chips.js Normal file
View File

@@ -0,0 +1,14 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import AddChip from "./AddChip";
import ListChips from "./ListChips";
const Chips = (props) => (
<div className='container'>
<AddChip />
<ListChips />
</div>
);
export default withRouter(Chips);

View File

@@ -0,0 +1,248 @@
import React, { useState, useEffect } from 'react';
import axios from "axios";
import { Table, Collapsible, CollapsibleItem, Button, TextInput, Row, Col, Select } from "react-materialize";
import M from "materialize-css";
import YesNoModal from "../common/YesNoModal";
const ListChips = (props) => {
const [chipsList, setChipsList] = useState([]);
const [chipValuePairs, setChipValuePairs] = useState([]);
const [chipValueActiveIndex, setChipValueActiveIndex] = useState(undefined);
const [editingChipValue, setEditingChipValue] = useState("");
const [newChipValueSecondaryChipId, setNewChipValueSecondaryChipId] = useState("");
const reloadChipsListEffect = () => {
(async() => {
try {
const chipsResponse = await axios.get(`/chips`);
if (chipsResponse && chipsResponse.status === 200 && Array.isArray(chipsResponse.data)) {
setChipsList(chipsResponse.data);
}
} catch (e) {
errorToast();
}
})();
}
const updateChipValuePairsEffect = () => {
const result = [];
chipsList.forEach(chip => {
const chipValues = chip['base_chip_values'];
chipValues.forEach(chipValue => {
result.push({
baseChipId: chip.id,
secondaryChipId: chipValue['secondary_chip_id']
});
});
});
setChipValuePairs(result);
}
useEffect(reloadChipsListEffect, []);
useEffect(updateChipValuePairsEffect, [chipsList]);
const deleteChip = async (chipId) => {
try {
const chipsResponse = await axios.delete(`/chips/${chipId}`);
if (chipsResponse && chipsResponse.status === 200 && Array.isArray(chipsResponse.data)){
setChipsList(chipsResponse.data);
M.toast({ html: 'Chip destroyed!' });
}
} catch (e) {
errorToast();
}
}
const addNewChipValue = async (baseChipId) => {
try{
const newChipValueObject = {
'chip_value': {
base_chip_id: baseChipId,
secondary_chip_id: newChipValueSecondaryChipId,
value: editingChipValue
}
}
const chipsResponse = await axios.post(`/chip_values`, newChipValueObject);
if (chipsResponse && chipsResponse.status === 200){
setChipsList(chipsResponse.data);
setNewChipValueSecondaryChipId("");
setChipValueActiveIndex(undefined);
setEditingChipValue("");
M.toast({ html: 'I smell money $$$' });
}
} catch (e) {
errorToast();
}
}
const updateChipValue = async (chipValueId) => {
try{
const updatedChipValue = {
'chip_value': {
id: chipValueId,
value: editingChipValue
}
}
const chipsResponse = await axios.put(`/chip_values/${chipValueId}`, updatedChipValue);
if (chipsResponse && chipsResponse.status === 200){
setChipsList(chipsResponse.data);
setChipValueActiveIndex(undefined);
setEditingChipValue("");
}
} catch (e) {
errorToast();
}
}
const deleteChipValue = async (chipValueId) => {
try {
const chipsResponse = await axios.delete(`/chip_values/${chipValueId}`);
if (chipsResponse && chipsResponse.status === 200){
setChipsList(chipsResponse.data);
M.toast({ html: 'Destroyed!' });
}
} catch (e) {
errorToast();
}
}
const errorToast = () => M.toast({ html: "Yo! It ain't workin'" });
const getChipData = (chipId) => chipsList.find(chip => chip.id === chipId);
const checkIfPairExists = (baseChipId, secondaryChipId) => {
return chipValuePairs.find(chipValuePair =>
chipValuePair.baseChipId === baseChipId &&
chipValuePair.secondaryChipId === secondaryChipId);
}
const secondaryChipOptions = (baseChipId) => {
const options = chipsList.map((chip, index) => {
if (chip.id !== baseChipId && !checkIfPairExists(baseChipId, chip.id)) {
return <option key={`chip-option-${index}`} value={chip.id}>{chip.name}</option>
}else {
return null;
}
});
return options.filter(option => option !== null);
};
const chipActions = (id) => (
<Row>
<Col>
<Button flat node="button" icon="add" onClick={() => setChipValueActiveIndex(id)} />
</Col>
<Col>
<YesNoModal
body={"Maan, y'a sure about this?"}
yesAction={() => deleteChip(id)}
triggerNode={<Button flat node="button" icon="delete" />} />
</Col>
</Row>
)
const newChipValueForm = (id) => {
if (chipValueActiveIndex !== id){
return null;
}
return (
<Row>
<Col>
<Select value={newChipValueSecondaryChipId} name="secondary_chip" onChange={(e) => setNewChipValueSecondaryChipId(e.target.value)}>
<option disabled value="">Secondary chip</option>
{secondaryChipOptions(id)}
</Select>
</Col>
<Col>
<TextInput label="Value" type="number" value={editingChipValue} onChange={(e) => setEditingChipValue(e.target.value)} />,
</Col>
<Col>
<Button flat icon="check" onClick={() => addNewChipValue(id)} />
</Col>
<Col>
<Button flat icon="cancel" onClick={() => setChipValueActiveIndex(undefined)} />
</Col>
</Row>
);
}
const singleChipValueInfo = (chipValueData, index) => {
const secondaryChipId = chipValueData && chipValueData.secondary_chip_id ? chipValueData.secondary_chip_id : undefined;
const { name:secondaryChipName, symbol:secondaryChipSymbol } = getChipData(secondaryChipId) || {};
return(
<tr key={`secondary-chip-${index}`}>
<td>{ `${secondaryChipName} [${secondaryChipSymbol}]` }</td>
{
chipValueActiveIndex === chipValueData.id &&
[
<td key={`chip-value-input-${index}`}><TextInput defaultValue={chipValueData.value} onChange={(e) => setEditingChipValue(e.target.value)} /></td>,
<td key={`chip-value-save-${index}`}><Button flat node="button" icon="check" onClick={() => updateChipValue(chipValueData.id)} /></td>,
<td key={`chip-value-cancel-${index}`}><Button flat node="button" icon="cancel" onClick={() => setChipValueActiveIndex(undefined)} /></td>
]
}
{
chipValueActiveIndex !== chipValueData.id &&
[
<td key={`chip-value-${index}`}>{ chipValueData.value }</td>,
<td key={`chip-value-edit-${index}`}><Button flat node="button" icon="edit" onClick={() => setChipValueActiveIndex(chipValueData.id)} /></td>
]
}
<td>
<YesNoModal
body={"Maan, y'a sure about this?"}
yesAction={() => deleteChipValue(chipValueData.id)}
triggerNode={<Button flat node="button" icon="delete" />} />
</td>
</tr>
)
}
const chipValuesTable = (chip) => (
<Table>
<thead>
<tr>
<th>Secondary chip</th>
<th>Value</th>
<th colSpan={3}>Actions</th>
</tr>
</thead>
<tbody>
{ chip.base_chip_values.map((chipValueData, index) => singleChipValueInfo(chipValueData, index)) }
</tbody>
</Table>
)
const singleChipInfo = (chip, index) => (
<CollapsibleItem key={`base-chip-${index}`} expanded={false} header={`${chip.name} [${chip.symbol}]`} node="div">
{ chipActions(chip.id) }
{ newChipValueForm(chip.id) }
{ chipValuesTable(chip) }
</CollapsibleItem>
)
return (
<div className='section'>
<h5>Chips In Tha Hood</h5>
<Collapsible>
{
chipsList.map((chip, index) => singleChipInfo(chip, index))
}
</Collapsible>
</div>
);
}
export default ListChips;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Navbar, NavItem } from 'react-materialize';
import { NavItem } from 'react-materialize';
import { withRouter } from 'react-router-dom';
const RoutableNavItem = (props) => {

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Modal, Button, Row, Col } from 'react-materialize';
const YesNoModal = (props) => {
const { title, body, yesAction, noAction, triggerNode } = props;
return(
<Modal
actions={[
<Row>
<Col s={3}>
<Button modal="confirm" node="button" waves="green" onClick={yesAction}>Yes</Button>
</Col>
<Col s={3}>
<Button modal="close" node="button" waves="red" onClick={noAction}>No</Button>
</Col>
</Row>
]}
bottomSheet
fixedFooter={false}
header={title}
id="Modal-0"
open={false}
options={{
dismissible: true,
endingTop: '10%',
inDuration: 250,
onCloseEnd: null,
onCloseStart: null,
onOpenEnd: null,
onOpenStart: null,
opacity: 0.5,
outDuration: 250,
preventScrolling: true,
startingTop: '4%'
}}
trigger={triggerNode}
>
<p>{body}</p>
</Modal>
)
}
export default YesNoModal;

View File

@@ -1,8 +1,10 @@
Rails.application.routes.draw do
constraints format: :json do
resources :money_moves
resources :homies do
collection do
resources :money_moves
resources :chips, only: %i[index create destroy]
resources :chip_values, only: %i[create update destroy]
resources :homies do
collection do
get 'cash'
end
end

View File

@@ -0,0 +1,12 @@
class CreateChips < ActiveRecord::Migration[5.2]
def change
create_table :chips do |t|
t.text :name, null: false
t.text :symbol, null: false
t.boolean :enabled, default: true
t.timestamps
end
add_index :chips, :name, unique: true
end
end

View File

@@ -0,0 +1,12 @@
class CreateChipValues < ActiveRecord::Migration[5.2]
def change
create_table :chip_values do |t|
t.references :base_chip, foreign_key: { to_table: :chips }
t.references :secondary_chip, foreign_key: { to_table: :chips }
t.decimal :value, precision: 12, scale: 3, null: false
t.timestamps
t.index [:base_chip_id, :secondary_chip_id], unique: true
end
end
end

View File

@@ -10,11 +10,31 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_06_20_200006) do
ActiveRecord::Schema.define(version: 2020_08_27_142428) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "chip_values", force: :cascade do |t|
t.bigint "base_chip_id"
t.bigint "secondary_chip_id"
t.decimal "value", precision: 12, scale: 3, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["base_chip_id", "secondary_chip_id"], name: "index_chip_values_on_base_chip_id_and_secondary_chip_id", unique: true
t.index ["base_chip_id"], name: "index_chip_values_on_base_chip_id"
t.index ["secondary_chip_id"], name: "index_chip_values_on_secondary_chip_id"
end
create_table "chips", force: :cascade do |t|
t.text "name", null: false
t.text "symbol", null: false
t.boolean "enabled", default: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_chips_on_name", unique: true
end
create_table "homies", force: :cascade do |t|
t.text "name", null: false
t.integer "importance", default: 5, null: false
@@ -31,4 +51,6 @@ ActiveRecord::Schema.define(version: 2019_06_20_200006) do
t.datetime "updated_at", null: false
end
add_foreign_key "chip_values", "chips", column: "base_chip_id"
add_foreign_key "chip_values", "chips", column: "secondary_chip_id"
end