Commit 082ffdbf authored by ZeHiro's avatar ZeHiro
Browse files

Remove operation types from database. Fix #447.

Integrated new operation types. Fixes #446
parent e9e638c8
Pipeline #8343 passed with stage
in 3 minutes and 50 seconds
......@@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { get } from '../../store';
import { debug as dbg, translate as $t } from '../../helpers';
import { debug as dbg, translate as $t, UNKNOWN_OPERATION_TYPE } from '../../helpers';
import Pair from './item';
function debug(text) {
......@@ -10,7 +10,7 @@ function debug(text) {
}
// Algorithm
function findRedundantPairs(operations, duplicateThreshold, unknownOperationTypeId) {
function findRedundantPairs(operations, duplicateThreshold) {
let before = Date.now();
debug('Running findRedundantPairs algorithm...');
debug(`Input: ${operations.length} operations`);
......@@ -34,9 +34,9 @@ function findRedundantPairs(operations, duplicateThreshold, unknownOperationType
let datediff = Math.abs(+op.date - +next.date);
if (datediff <= threshold && +op.dateImport !== +next.dateImport) {
// Two operations with the same known type can be considered as duplicates.
if (op.operationTypeID === unknownOperationTypeId ||
next.operationTypeID === unknownOperationTypeId ||
op.operationTypeID === next.operationTypeID) {
if (op.type === UNKNOWN_OPERATION_TYPE ||
next.type === UNKNOWN_OPERATION_TYPE ||
op.type === next.type) {
similar.push([op, next]);
}
}
......@@ -60,9 +60,8 @@ export default connect(state => {
let currentOperations = get.currentOperations(state);
let formatCurrency = get.currentAccount(state).formatCurrency;
let unknownOperationTypeId = get.unknownOperationType(state).id;
let pairs = findRedundantPairs(currentOperations, duplicateThreshold, unknownOperationTypeId);
let pairs = findRedundantPairs(currentOperations, duplicateThreshold);
return {
pairs,
formatCurrency
......
......@@ -8,14 +8,9 @@ export default connect((state, ownProps) => {
let categoryA = get.categoryById(state, ownProps.a.categoryId);
let categoryB = get.categoryById(state, ownProps.b.categoryId);
let operationTypeA = get.labelOfOperationType(state, ownProps.a.operationTypeID);
let operationTypeB = get.labelOfOperationType(state, ownProps.b.operationTypeID);
return {
categoryA,
categoryB,
operationTypeA,
operationTypeB
categoryB
};
}, dispatch => {
return {
......@@ -58,7 +53,7 @@ export default connect((state, ownProps) => {
<td>{ props.a.title }</td>
<td>{ props.formatCurrency(props.a.amount) }</td>
<td>{ props.categoryA.title }</td>
<td>{ props.operationTypeA }</td>
<td>{ $t(`client.${props.a.type}`) }</td>
<td>{ new Date(props.a.dateImport).toLocaleString() }</td>
<td rowSpan={ 2 }>
<button className="btn btn-primary" onClick={ handleMerge }>
......@@ -74,7 +69,7 @@ export default connect((state, ownProps) => {
<td>{ props.b.title }</td>
<td>{ props.formatCurrency(props.b.amount) }</td>
<td>{ props.categoryB.title }</td>
<td>{ props.operationTypeB }</td>
<td>{ $t(`client.${props.b.type}`) }</td>
<td>{ new Date(props.b.dateImport).toLocaleString() }</td>
</tr>
......
......@@ -254,8 +254,8 @@ function filter(operations, search) {
op.categoryId === search.categoryId
);
filtered = filterIf(search.typeId !== '', filtered, op =>
op.operationTypeID === search.typeId
filtered = filterIf(search.type !== '', filtered, op =>
op.type === search.type
);
filtered = filterIf(search.amountLow !== '', filtered, op =>
......
import React from 'react';
import { connect } from 'react-redux';
import { actions, get } from '../../store';
import { actions } from '../../store';
import { translate as $t } from '../../helpers';
import ButtonSelect from '../ui/button-select';
export default connect(state => {
return {
operationTypes: get.operationTypes(state),
getTypeLabel(id) {
return get.labelOfOperationType(state, id);
}
};
}, (dispatch, props) => {
import OperationTypes from '../../../shared/operation-types.json';
export default connect(null, (dispatch, props) => {
let ret = {};
// Only define handleSelectId if none was provided.
if (!props.onSelectId) {
ret.onSelectId = function(id) {
actions.setOperationType(dispatch, props.operation, id);
ret.onSelectId = function(type) {
actions.setOperationType(dispatch, props.operation, type);
};
}
return ret;
})(props => {
let getThisTypeId = () => props.operation.operationTypeID;
let getThisType = () => props.operation.type;
let idToLabel = type => $t(`client.${type}`);
let opTypes = [];
for (let type of OperationTypes) {
type.id = type.name;
opTypes.push(type);
}
return (
<ButtonSelect
key={ `operation-type-select-operation-${props.operation.id}` }
optionsArray={ props.operationTypes }
selectedId={ getThisTypeId }
idToLabel={ props.getTypeLabel }
optionsArray={ opTypes }
selectedId={ getThisType }
idToLabel={ idToLabel }
onSelectId={ props.onSelectId }
/>
);
......
......@@ -7,6 +7,8 @@ import { get, actions } from '../../store';
import DatePicker from '../ui/date-picker';
import OperationTypes from '../../../shared/operation-types.json';
class SearchComponent extends React.Component {
constructor(props) {
super(props);
......@@ -50,15 +52,15 @@ class SearchComponent extends React.Component {
</option>
].concat(
this.props.operationTypes.map(type =>
<option key={ type.id } value={ type.id }>
{ this.props.labelOfOperationType(type.id) }
<option key={ type.name } value={ type.name }>
{ $t(type.name) }
</option>
)
);
let handleKeyword = () => this.props.setKeywords(this.refs.keywords.value);
let hanldeCategory = () => this.props.setCategoryId(this.refs.cat.value);
let handleOperationType = () => this.props.setTypeId(this.refs.type.value);
let handleOperationType = () => this.props.setType(this.refs.type.value);
let handleAmountLow = () => this.props.setAmountLow(this.refs.amount_low.value);
let handleAmountHigh = () => this.props.setAmountHigh(this.refs.amount_high.value);
let handleDateLow = value => this.props.setDateLow(value);
......@@ -202,8 +204,7 @@ class SearchComponent extends React.Component {
const Export = connect(state => {
return {
categories: get.categories(state),
operationTypes: get.operationTypes(state),
labelOfOperationType: id => get.labelOfOperationType(state, id),
operationTypes: OperationTypes,
searchFields: get.searchFields(state),
};
}, dispatch => {
......@@ -221,8 +222,8 @@ const Export = connect(state => {
actions.setSearchField(dispatch, 'categoryId', categoryId);
},
setTypeId(typeId) {
actions.setSearchField(dispatch, 'typeId', typeId);
setType(type) {
actions.setSearchField(dispatch, 'type', type);
},
setAmountLow(amountLow) {
......
import React from 'react';
import { connect } from 'react-redux';
import { get, actions } from '../../../store';
import { assertHas, translate as $t, NONE_CATEGORY_ID } from '../../../helpers';
import { actions } from '../../../store';
import { assertHas,
translate as $t,
NONE_CATEGORY_ID,
UNKNOWN_OPERATION_TYPE } from '../../../helpers';
import CategorySelect from '../../operations/category-select';
import OperationTypeSelect from '../../operations/operation-type-select';
......@@ -24,7 +27,7 @@ class AddOperationModal extends React.Component {
this.returnDateValue = date => this.setState({ date });
this.returnTitleValue = title => this.setState({ title });
this.returnAmountValue = amount => this.setState({ amount });
this.handleOnSelectOperationType = id => this.setState({ operationTypeID: id });
this.handleOnSelectOperationType = type => this.setState({ type });
this.handleOnSelectCategory = id => this.setState({ categoryId: id });
}
......@@ -36,7 +39,7 @@ class AddOperationModal extends React.Component {
title: this.state.title,
amount: this.state.amount,
categoryId: this.state.categoryId,
operationTypeID: this.state.operationTypeID,
type: this.state.type,
bankAccount: this.props.account.accountNumber
};
......@@ -52,7 +55,7 @@ class AddOperationModal extends React.Component {
title: null,
amount: null,
categoryId: NONE_CATEGORY_ID,
operationTypeID: this.props.unknownOperationTypeId
type: UNKNOWN_OPERATION_TYPE
};
}
......@@ -158,11 +161,7 @@ class AddOperationModal extends React.Component {
}
}
let Export = connect(state => {
return {
unknownOperationTypeId: get.unknownOperationType(state).id
};
}, dispatch => {
let Export = connect(null, dispatch => {
return {
createOperation(operation) {
actions.createOperation(dispatch, operation);
......
......@@ -11,7 +11,8 @@ import { assert as assert_,
setupTranslator as setupTranslator_,
translate as translate_,
currency as currency_,
localeComparator as localeComparator_ } from '../shared/helpers.js';
localeComparator as localeComparator_,
UNKNOWN_OPERATION_TYPE as UNKNOWN_OPERATION_TYPE_ } from '../shared/helpers.js';
export let assert = assert_;
export let assertHas = assertHas_;
......@@ -21,6 +22,7 @@ export let setupTranslator = setupTranslator_;
export let translate = translate_;
export let localeComparator = localeComparator_;
export let currency = currency_;
export let UNKNOWN_OPERATION_TYPE = UNKNOWN_OPERATION_TYPE_;
const DEBUG = true;
......
......@@ -161,5 +161,5 @@ init().then(initialState => {
<Kresus />
</Provider>, document.querySelector('#main'));
}).catch(err => {
alert('Error when starting the app:', err);
alert(`Error when starting the app:\n${err}`);
});
......@@ -5,6 +5,7 @@ import {
maybeHas,
NONE_CATEGORY_ID,
stringToColor,
UNKNOWN_OPERATION_TYPE
} from './helpers';
export class Bank {
......@@ -53,8 +54,7 @@ export class Account {
}
export class Operation {
constructor(arg, unknownTypeId) {
assert(typeof unknownTypeId === 'string', 'unknown type id must be a string');
constructor(arg) {
this.bankAccount = assertHas(arg, 'bankAccount') && arg.bankAccount;
this.title = assertHas(arg, 'title') && arg.title;
this.date = assertHas(arg, 'date') && new Date(arg.date);
......@@ -65,8 +65,7 @@ export class Operation {
this.dateImport = (maybeHas(arg, 'dateImport') && new Date(arg.dateImport)) || 0;
this.id = assertHas(arg, 'id') && arg.id;
this.categoryId = arg.categoryId || NONE_CATEGORY_ID;
this.operationTypeID = (maybeHas(arg, 'operationTypeID') && arg.operationTypeID) ||
unknownTypeId;
this.type = arg.type || UNKNOWN_OPERATION_TYPE;
this.customLabel = (maybeHas(arg, 'customLabel') && arg.customLabel) || null;
}
}
......@@ -96,14 +95,6 @@ export class Setting {
}
}
export class OperationType {
constructor(arg) {
this.name = assertHas(arg, 'name') && arg.name;
this.id = assertHas(arg, 'id') && arg.id;
this.weboobvalue = assertHas(arg, 'weboobvalue') && arg.weboobvalue;
}
}
export class Alert {
constructor(arg) {
this.id = assertHas(arg, 'id') && arg.id;
......
......@@ -136,8 +136,8 @@ export function setCategoryForOperation(operationId, categoryId) {
return this.updateOperation(operationId, { categoryId });
}
export function setTypeForOperation(operationId, operationTypeID) {
return this.updateOperation(operationId, { operationTypeID });
export function setTypeForOperation(operationId, type) {
return this.updateOperation(operationId, { type });
}
export function setCustomLabel(operationId, customLabel) {
......
......@@ -65,12 +65,12 @@ const basic = {
};
},
setOperationType(operation, typeId, formerTypeId) {
setOperationType(operation, type, formerType) {
return {
type: SET_OPERATION_TYPE,
operation,
typeId,
formerTypeId
operationType: type,
formerType
};
},
......@@ -192,19 +192,19 @@ export function setCurrentAccountId(accountId) {
return basic.setAccountId(accountId);
}
export function setOperationType(operation, typeId) {
export function setOperationType(operation, type) {
assert(typeof operation.id === 'string', 'SetOperationType first arg must have an id');
assert(typeof typeId === 'string', 'SetOperationType second arg must be a String id');
assert(typeof type === 'string', 'SetOperationType second arg must be a String id');
let formerTypeId = operation.operationTypeID;
let formerType = operation.type;
return dispatch => {
dispatch(basic.setOperationType(operation, typeId, formerTypeId));
backend.setTypeForOperation(operation.id, typeId)
dispatch(basic.setOperationType(operation, type, formerType));
backend.setTypeForOperation(operation.id, type)
.then(() => {
dispatch(success.setOperationType(operation, typeId, formerTypeId));
dispatch(success.setOperationType(operation, type, formerType));
}).catch(err => {
dispatch(fail.setOperationType(err, operation, typeId, formerTypeId));
dispatch(fail.setOperationType(err, operation, type, formerType));
});
};
}
......@@ -525,18 +525,18 @@ function reduceSetOperationType(state, action) {
}
// Optimistic update.
let operationTypeID;
let type;
if (status === FAIL) {
debug('Error when setting type for an operation', action.error);
operationTypeID = action.formerTypeId;
type = action.formerType;
} else {
debug('Starting setting type for an operation...');
operationTypeID = action.typeId;
type = action.operationType;
}
return u.updateIn('operations',
updateMapIf('id', action.operation.id, { operationTypeID }),
updateMapIf('id', action.operation.id, { type }),
state);
}
......@@ -669,7 +669,7 @@ function reduceLoadOperations(state, action) {
debug('Successfully loaded operations.');
// Create the new operations.
operations = operations.map(o => new Operation(o, state.constants.unknownOperationTypeId));
operations = operations.map(o => new Operation(o));
// Remove former operations, add the result to the new ones:
let unrelated = state.operations.filter(o => o.bankAccount !== accountNumber);
......@@ -697,8 +697,7 @@ function reduceMergeOperations(state, action) {
let ret = u.updateIn('operations', u.reject(o => o.id === action.toRemove.id), state);
// Replace the kept one:
let unknownOperationTypeId = state.constants.unknownOperationTypeId;
let newKept = new Operation(action.toKeep, unknownOperationTypeId);
let newKept = new Operation(action.toKeep);
return u.updateIn('operations',
updateMapIf('id', action.toKeep.id, newKept),
ret);
......@@ -722,7 +721,7 @@ function reduceCreateOperation(state, action) {
let { operation } = action;
let operations = state.operations;
let newOp = [new Operation(operation, state.constants.unknownOperationTypeId)];
let newOp = [new Operation(operation)];
operations = newOp.concat(operations);
sortOperations(operations);
......@@ -1015,7 +1014,7 @@ function sortOperations(ops) {
export function initialState(external, allBanks, allAccounts, allOperations, allAlerts) {
// Retrieved from outside.
let { unknownOperationTypeId, defaultCurrency, defaultAccountId } = external;
let { defaultCurrency, defaultAccountId } = external;
// Build internal state.
let banks = allBanks.map(b => new Bank(b));
......@@ -1033,7 +1032,7 @@ export function initialState(external, allBanks, allAccounts, allOperations, all
}
let accesses = Array.from(accessMap.values());
let operations = allOperations.map(op => new Operation(op, unknownOperationTypeId));
let operations = allOperations.map(op => new Operation(op));
sortOperations(operations);
let alerts = allAlerts.map(al => new Alert(al));
......@@ -1069,8 +1068,7 @@ export function initialState(external, allBanks, allAccounts, allOperations, all
currentAccessId,
currentAccountId,
constants: {
defaultCurrency,
unknownOperationTypeId
defaultCurrency
}
}, {});
}
......
......@@ -9,7 +9,6 @@ import { createSelector } from 'reselect';
import * as Bank from './banks';
import * as Category from './categories';
import * as OperationType from './operation-types';
import * as StaticBank from './static-banks';
import * as Settings from './settings';
import * as Ui from './ui';
......@@ -45,8 +44,7 @@ const rootReducer = combineReducers({
settings: augmentReducer(Settings.reducer, 'settings'),
ui: augmentReducer(Ui.reducer, 'ui'),
// Static information
staticBanks: (state = {}) => state,
operationTypes: (state = {}) => state
staticBanks: (state = {}) => state
});
// Store
......@@ -151,12 +149,6 @@ export const get = {
return Settings.getDefaultAccountId(state.settings);
},
// String
unknownOperationType(state) {
assertDefined(state);
return OperationType.unknown(state.operationTypes);
},
// *** UI *****************************************************************
// { searchFields } (see ui.js)
searchFields(state) {
......@@ -193,19 +185,6 @@ export const get = {
return Category.fromId(state.categories, id);
},
// *** Operation types ****************************************************
// [OperationType]
operationTypes(state) {
assertDefined(state);
return OperationType.all(state.operationTypes);
},
// String
labelOfOperationType(state, id) {
assertDefined(state);
return OperationType.idToLabel(state.operationTypes, id);
},
// *** Settings ***********************************************************
// String
setting(state, key) {
......@@ -256,9 +235,9 @@ export const actions = {
dispatch(Bank.setOperationCategory(operation, catId));
},
setOperationType(dispatch, operation, typeId) {
setOperationType(dispatch, operation, type) {
assertDefined(dispatch);
dispatch(Bank.setOperationType(operation, typeId));
dispatch(Bank.setOperationType(operation, type));
},
setOperationCustomLabel(dispatch, operation, label) {
......@@ -412,12 +391,8 @@ export function init() {
assertHas(world, 'categories');
state.categories = Category.initialState(world.categories);
assertHas(world, 'operationtypes');
state.operationTypes = OperationType.initialState(world.operationtypes);
// Define external values for the Bank initialState:
let external = {
unknownOperationTypeId: get.unknownOperationType(state).id,
defaultCurrency: get.setting(state, 'defaultCurrency'),
defaultAccountId: get.defaultAccountId(state)
};
......
import u from 'updeep';
import { assertHas, assert, localeComparator, translate as $t } from '../helpers';
import { OperationType } from '../models';
export function initialState(operationtypes) {
let state = {};
state.items = operationtypes.map(type => new OperationType(type));
// Sort operation types by names
state.items.sort((a, b) => {
let al = $t(`client.${a.name}`);
let bl = $t(`client.${b.name}`);
return localeComparator(al, bl);
});
state.labels = {};
for (let c of operationtypes) {
assertHas(c, 'id');
assertHas(c, 'name');
state.labels[c.id] = $t(`client.${c.name}`);
}
// Cache unknown operation type id.
state.cachedUnknown = null;
for (let t of state.items) {
if (t.name === 'type.unknown') {
state.cachedUnknown = t;
}
}
assert(state.cachedUnknown, 'should have an "unknown" operation type');
return u({}, state);
}
// Getters
export function all(state) {
return state.items;
}
export function idToLabel(state, id) {
assert(typeof state.labels[id] !== 'undefined', `idTolabel lookup failed for id: ${id}`);
return state.labels[id];
}
export function unknown(state) {
return state.cachedUnknown;
}
......@@ -74,7 +74,7 @@ function initialSearch() {
return {
keywords: [],
categoryId: '',
typeId: '',
type: '',
amountLow: '',
amountHigh: '',
dateLow: null,
......@@ -99,7 +99,7 @@ export function hasSearchFields(state) {
let { search } = state;
return search.keywords.length ||
search.categoryId !== '' ||
search.typeId !== '' ||
search.type !== '' ||
search.amountLow !== '' ||
search.amountHigh !== '' ||
search.dateLow !== null ||
......
......@@ -6,13 +6,12 @@ import Account from '../models/account';
import Alert from '../models/alert';
import Category from '../models/category';
import Operation from '../models/operation';
import OperationType from '../models/operationtype';
import Config from '../models/config';
import Cozy from '../models/cozyinstance';
import { run as runMigrations } from '../models/migrations';
import { makeLogger, KError, asyncErr } from '../helpers';