Merge branch 'master' into login_build_alpine

This commit is contained in:
einhornimmond 2021-05-12 13:04:20 +02:00
commit 9d0c1da9ad
118 changed files with 2280 additions and 838 deletions

View File

@ -6,4 +6,4 @@ title: 🐛 [Bug]
---
## 🐛 Bugreport
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.--
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
/node_modules/*
.vscode
messages.pot
.skeema
nbproject

View File

@ -0,0 +1,2 @@
INSERT INTO `migrations` (`id`, `db_version`) VALUES
(1, 2);

View File

@ -6,5 +6,5 @@ INSERT INTO `transaction_types` (`id`, `name`, `text`) VALUES
(5, 'group remove member', 'remove user from group, maybe he was moved elsewhere'),
(6, 'hedera topic create', 'create new topic on hedera'),
(7, 'hedera topic send message', 'send consensus message over hedera topic'),
(8, 'hedera account create', 'create new account on hedera for holding some founds with unencrypted keys');
(8, 'hedera account create', 'create new account on hedera for holding some founds with unencrypted keys'),
(9, 'decay start', 'signalize the starting point for decay calculation, allowed only once per chain');

View File

@ -0,0 +1,5 @@
CREATE TABLE `migrations` (
`id` int UNSIGNED NOT NULL AUTO_INCREMENT,
`db_version` int DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -1,6 +1,6 @@
CREATE TABLE `transaction_types` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(90) COLLATE utf8mb4_unicode_ci NOT NULL,
`text` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@ -156,8 +156,24 @@ class AppController extends Controller
}
}
protected function checkForMigration($html = true)
{
$migrationsTable = TableRegistry::getTableLocator()->get('Migrations');
$last_migration = $migrationsTable->find()->last();
$current_db_version = 1;
if($last_migration) {
$current_db_version = $last_migration->db_version;
}
$php_data_version = 2;
if($current_db_version < $php_data_version) {
$this->redirect(['controller' => 'Migrations', 'action' => 'migrate', 'html' => $html, 'db_version' => $current_db_version]);
}
}
protected function requestLogin($sessionId = 0, $redirect = true)
{
$this->checkForMigration($redirect);
$session = $this->getRequest()->getSession();
// check login
// disable encryption for cookies

View File

@ -22,6 +22,7 @@ class AppRequestsController extends AppController
{
parent::initialize();
$this->loadComponent('JsonRequestClient');
$this->loadComponent('GradidoNumber');
//$this->loadComponent('JsonRpcRequestClient');
//$this->Auth->allow(['add', 'edit']);
$this->Auth->allow(['index', 'sendCoins', 'createCoins', 'getBalance', 'listTransactions']);
@ -115,9 +116,10 @@ class AppRequestsController extends AppController
return $required_fields;
}
if(intval($param['amount']) <= 0) {
if(floatval($param['amount']) <= 0.0) {
return ['state' => 'error', 'msg' => 'amount is invalid', 'details' => $param['amount']];
}
$param['amount'] = $this->GradidoNumber->parseInputNumberToCentNumber($param['amount']);
if(isset($data->memo)) {
$param['memo'] = $data->memo;
@ -268,7 +270,7 @@ class AppRequestsController extends AppController
public function getBalance($session_id = 0)
{
$this->viewBuilder()->setLayout('ajax');
$login_result = $this->requestLogin($session_id, false);
if($login_result !== true) {
return $this->returnJson($login_result);
@ -284,16 +286,18 @@ class AppRequestsController extends AppController
return $this->returnJson(['state' => 'success', 'balance' => 0]);
}
$now = new FrozenTime();
return $this->returnJson([
$body = [
'state' => 'success',
'balance' => $state_balance->amount,
'decay' => $state_balance->partDecay($now),
'decay_date' => $now
]);
];
$this->set('body', $body);
}
public function listTransactions($page = 1, $count = 25, $orderDirection = 'ASC', $session_id = 0)
{
$this->viewBuilder()->setLayout('ajax');
$startTime = microtime(true);
$login_result = $this->requestLogin($session_id, false);
if($login_result !== true) {
@ -337,14 +341,29 @@ class AppRequestsController extends AppController
$transactions = array_reverse($transactions);
}
}
return $this->returnJson([
'state' => 'success',
'transactions' => $transactions,
'transactionExecutingCount' => $session->read('Transactions.executing'),
'count' => count($transactions),
'gdtSum' => $gdtSum,
'timeUsed' => microtime(true) - $startTime
]);
$state_balance = $stateBalancesTable->find()->where(['state_user_id' => $user['id']])->first();
$body = [
'state' => 'success',
'transactions' => $transactions,
'transactionExecutingCount' => $session->read('Transactions.executing'),
'count' => count($transactions),
'gdtSum' => $gdtSum,
'timeUsed' => microtime(true) - $startTime
];
$now = new FrozenTime();
$body['decay_date'] = $now;
if(!$state_balance) {
$body['balance'] = 0.0;
$body['decay'] = 0.0;
} else {
$body['balance'] = $state_balance->amount;
$body['decay'] = $state_balance->partDecay($now);
}
$this->set('body', $body);
}
private function acquireAccessToken($session_id)

View File

@ -0,0 +1,167 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\ORM\TableRegistry;
/**
* Migrations Controller
*
* @property \App\Model\Table\MigrationsTable $Migrations
*
* @method \App\Model\Entity\Migration[]|\Cake\Datasource\ResultSetInterface paginate($object = null, array $settings = [])
*/
class MigrationsController extends AppController
{
public function initialize()
{
parent::initialize();
$this->Auth->allow('migrate');
}
/**
* Index method
*
* @return \Cake\Http\Response|null
*/
public function index()
{
$migrations = $this->paginate($this->Migrations);
$this->set(compact('migrations'));
}
protected function callFunctions(array $callables)
{
foreach($callables as $callable) {
$result = call_user_func($callable);
if(!$result['success']) {
return $result;
}
}
return ['success' => true];
}
public function migrate()
{
$html = $this->request->getQuery('html');
$current_db_version = $this->request->getQuery('db_version');
$startTime = microtime(true);
$stateUserTransactionsTable = TableRegistry::getTableLocator()->get('StateUserTransactions');
$transactionsTable = TableRegistry::getTableLocator()->get('Transactions');
$transactionTypesTable = TableRegistry::getTableLocator()->get('TransactionTypes');
$stateBalancesTable = TableRegistry::getTableLocator()->get('StateBalances');
$blockchainTypesTable = TableRegistry::getTableLocator()->get('BlockchainTypes');
$new_db_version = 1;
$commands = [];
// migrate from version 1 to 2
if($current_db_version == 1) {
$stateUserTransactionsTable->truncate();
$commands = [
[$blockchainTypesTable, 'fillWithDefault'],
[$transactionTypesTable, 'fillWithDefault'],
[$transactionsTable, 'fillStateUserTransactions'],
[$stateBalancesTable, 'updateAllBalances']
];
$new_db_version = 2;
}
$migration_result = $this->callFunctions($commands);
if($migration_result['success']) {
$migration_entity = $this->Migrations->newEntity();
$migration_entity->db_version = $new_db_version;
$this->Migrations->save($migration_entity);
}
if(!$html) {
return $this->returnJson($migration_result);
} else {
$this->set('db_version', $current_db_version);
$this->set('result', $migration_result);
$this->set('timeUsed', microtime(true) - $startTime);
}
}
/**
* View method
*
* @param string|null $id Migration id.
* @return \Cake\Http\Response|null
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function view($id = null)
{
$migration = $this->Migrations->get($id, [
'contain' => [],
]);
$this->set('migration', $migration);
}
/**
* Add method
*
* @return \Cake\Http\Response|null Redirects on successful add, renders view otherwise.
*/
public function add()
{
$migration = $this->Migrations->newEntity();
if ($this->request->is('post')) {
$migration = $this->Migrations->patchEntity($migration, $this->request->getData());
if ($this->Migrations->save($migration)) {
$this->Flash->success(__('The migration has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The migration could not be saved. Please, try again.'));
}
$this->set(compact('migration'));
}
/**
* Edit method
*
* @param string|null $id Migration id.
* @return \Cake\Http\Response|null Redirects on successful edit, renders view otherwise.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function edit($id = null)
{
$migration = $this->Migrations->get($id, [
'contain' => [],
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$migration = $this->Migrations->patchEntity($migration, $this->request->getData());
if ($this->Migrations->save($migration)) {
$this->Flash->success(__('The migration has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The migration could not be saved. Please, try again.'));
}
$this->set(compact('migration'));
}
/**
* Delete method
*
* @param string|null $id Migration id.
* @return \Cake\Http\Response|null Redirects to index.
* @throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$migration = $this->Migrations->get($id);
if ($this->Migrations->delete($migration)) {
$this->Flash->success(__('The migration has been deleted.'));
} else {
$this->Flash->error(__('The migration could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
}

View File

@ -62,7 +62,7 @@ class StateBalancesController extends AppController
$user = $session->read('StateUser');
$update_balance_result = $this->StateBalances->updateBalances($user['id']);
if($update_balance_result !== true) {
if($update_balance_result['success'] !== true) {
$this->addAdminError('StateBalances', 'overview', $update_balance_result, $user['id']);
}
// sendRequestGDT

View File

@ -313,15 +313,20 @@ class TransactionsController extends AppController
if ($this->request->is('post')) {
$transaction = $this->Transactions->patchEntity($transaction, $this->request->getData());
if ($this->Transactions->save($transaction)) {
$this->Flash->success(__('The transaction has been saved.'));
return $this->redirect(['action' => 'index']);
$result = $this->Transactions->updateTxHash($transaction, 'start decay');
if($result === true) {
$this->Flash->success(__('The transaction has been saved.'));
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error(__('Error by saving: ' . json_encode($result)));
}
}
$this->Flash->error(__('The transaction could not be saved. Please, try again.'));
}
$stateGroups = $this->Transactions->StateGroups->find('list', ['limit' => 200]);
$transactionTypes = $this->Transactions->TransactionTypes->find('list', ['limit' => 200]);
$this->set(compact('transaction', 'stateGroups', 'transactionTypes'));
$blockchainTypes = $this->Transactions->BlockchainTypes->find('list');
$this->set(compact('transaction', 'stateGroups', 'transactionTypes', 'blockchainTypes'));
}
/**

View File

@ -0,0 +1,26 @@
<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
/**
* Migration Entity
*
* @property int $id
* @property int|null $db_version
*/
class Migration extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array
*/
protected $_accessible = [
'db_version' => true,
];
}

View File

@ -3,6 +3,7 @@ namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\I18n\Time;
use Cake\I18n\Number;
/**
* StateBalance Entity
@ -34,7 +35,7 @@ class StateBalance extends Entity
'state_user' => true
];
protected $_virtual = ['decay'];
protected $_virtual = ['decay','amount_float'];
private function convertToTimestamp($dateOrTime)
{
@ -43,9 +44,7 @@ class StateBalance extends Entity
} else if(method_exists($dateOrTime, 'i18nFormat')) {
return $dateOrTime->i18nFormat(Time::UNIX_TIMESTAMP_FORMAT);
} else {
var_dump($dateOrTime);
debug_print_backtrace(0, 6);
die("date or time unexpected object");
return 0;
}
}
@ -58,8 +57,12 @@ class StateBalance extends Entity
// SELECT TIMESTAMPDIFF(SECOND, modified, CURDATE()) AS age_in_seconds from state_balances
// decay_for_duration = decay_factor^seconds
// decay = gradido_cent * decay_for_duration
$decay_duration = intval(Time::now()->getTimestamp() - $this->convertToTimestamp($this->record_date));
$startDate = $this->convertToTimestamp($this->record_date);
if($startDate == 0) {
return $this->amount;
}
$decay_duration = intval(Time::now()->getTimestamp() - $startDate);
if($decay_duration === 0) {
return $this->amount;
}
@ -67,6 +70,7 @@ class StateBalance extends Entity
return intval($this->amount * pow(0.99999997802044727, $decay_duration));
}
public function partDecay($target_date)
{
$decay_duration = intval($this->convertToTimestamp($target_date) - $this->convertToTimestamp($this->record_date));

View File

@ -0,0 +1,40 @@
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
class AppTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
}
public function truncate()
{
$truncateCommands = $this->getSchema()->truncateSql($this->getConnection());
foreach ($truncateCommands as $truncateCommand) {
$this->getConnection()->query($truncateCommand);
}
$this->getConnection()->query('ALTER TABLE ' . $this->getSchema()->name() . ' AUTO_INCREMENT=1');
}
public function saveManyWithErrors($entities)
{
$save_results = $this->saveMany($entities);
// save all at once failed, no try one by one to get error message
if($save_results === false) {
foreach($entities as $entity) {
if(!$this->save($entity)) {
return ['success' => false, 'errors' => $entity->getErrors()];
}
}
} else {
return ['success' => true];
}
}
}

View File

@ -18,7 +18,7 @@ use Cake\Validation\Validator;
* @method \App\Model\Entity\BlockchainType[] patchEntities($entities, array $data, array $options = [])
* @method \App\Model\Entity\BlockchainType findOrCreate($search, callable $callback = null, $options = [])
*/
class BlockchainTypesTable extends Table
class BlockchainTypesTable extends AppTable
{
/**
* Initialize method
@ -65,4 +65,30 @@ class BlockchainTypesTable extends Table
return $validator;
}
public function fillWithDefault()
{
$entry_contents = [
[
'id' => 1,
'name' => 'mysql',
'text' => 'use mysql db as blockchain, work only with single community-server',
'symbol' => NULL
],
[
'id' => 2,
'name' => 'hedera',
'text' => 'use hedera for transactions',
'symbol' => 'HBAR'
]
];
$entities = $this->newEntities($entry_contents);
$this->truncate();
$save_results = $this->saveManyWithErrors($entities);
if(!$save_results['success']) {
$save_results['msg'] = 'error by saving default transaction types';
}
return $save_results;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Migrations Model
*
* @method \App\Model\Entity\Migration get($primaryKey, $options = [])
* @method \App\Model\Entity\Migration newEntity($data = null, array $options = [])
* @method \App\Model\Entity\Migration[] newEntities(array $data, array $options = [])
* @method \App\Model\Entity\Migration|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\Migration saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
* @method \App\Model\Entity\Migration patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* @method \App\Model\Entity\Migration[] patchEntities($entities, array $data, array $options = [])
* @method \App\Model\Entity\Migration findOrCreate($search, callable $callback = null, $options = [])
*/
class MigrationsTable extends Table
{
/**
* Initialize method
*
* @param array $config The configuration for the Table.
* @return void
*/
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('migrations');
$this->setDisplayField('id');
$this->setPrimaryKey('id');
}
/**
* Default validation rules.
*
* @param \Cake\Validation\Validator $validator Validator instance.
* @return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator)
{
$validator
->nonNegativeInteger('id')
->allowEmptyString('id', null, 'create');
$validator
->integer('db_version')
->allowEmptyString('db_version');
return $validator;
}
}

View File

@ -1,14 +1,12 @@
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\TableRegistry;
use Cake\I18n\Date;
use Cake\I18n\Time;
use Cake\I18n\FrozenTime;
/**
* StateBalances Model
@ -28,6 +26,7 @@ use Cake\I18n\Time;
*/
class StateBalancesTable extends Table
{
private static $startDecayDate = null;
/**
* Initialize method
*
@ -49,6 +48,15 @@ class StateBalancesTable extends Table
'joinType' => 'INNER'
]);
}
public static function getDecayStartDateCached()
{
if(self::$startDecayDate == null) {
$transactionsTable = TableRegistry::getTableLocator()->get('Transactions');
self::$startDecayDate = $transactionsTable->getDecayStartDate();
}
return self::$startDecayDate;
}
/**
* Default validation rules.
@ -83,20 +91,60 @@ class StateBalancesTable extends Table
return $rules;
}
public function sortTransactions($a, $b)
public function calculateDecay($startBalance, FrozenTime $startDate, FrozenTime $endDate, $withInterval = false)
{
if ($a['date'] == $b['date']) {
return 0;
$decayStartDate = self::getDecayStartDateCached();
// if no start decay block exist, we just return input
// if start date for decay is after enddate, we also just return input
if($decayStartDate === null || $decayStartDate >= $endDate) {
if($withInterval) {
return ['balance' => $startBalance, 'interval' => new \DateInterval('PT0S')];
} else {
return $startBalance;
}
}
return ($a['date'] > $b['date']) ? -1 : 1;
$state_balance = $this->newEntity();
$state_balance->amount = $startBalance;
$interval = null;
// if decay start date is before start date we calculate decay for full duration
if($decayStartDate < $startDate) {
$state_balance->record_date = $startDate;
$interval = $endDate->diff($startDate);
}
// if decay start in between start date and end date we caculcate decay from decay start time to end date
else {
$state_balance->record_date = $decayStartDate;
$interval = $endDate->diff($decayStartDate);
}
$decay = $state_balance->partDecay($endDate);
if($withInterval) {
return ['balance' => $decay, 'interval' => $interval];
} else {
return $decay;
}
}
public function updateAllBalances()
{
$stateUserTable = TableRegistry::getTableLocator()->get('StateUsers');
$state_users = $stateUserTable->find()->select(['id'])->contain([]);
foreach($state_users as $state_user) {
$result = $this->updateBalances($state_user->id);
if($result['success'] === false) {
$result['state_user_id'] = $state_user->id;
return $result;
}
}
return ['success' => true];
}
public function updateBalances($stateUserId)
{
$stateUserTransactionsTable = TableRegistry::getTableLocator()->get('StateUserTransactions');
$transactionsTable = TableRegistry::getTableLocator()->get('Transactions');
$now = new FrozenTime;
// info: cakephp use lazy loading, query will be executed later only if needed
$state_balances = $this->find('all')->where(['state_user_id' => $stateUserId]);
$state_user_transactions = $stateUserTransactionsTable
@ -107,7 +155,7 @@ class StateBalancesTable extends Table
;
if(!$state_user_transactions || !$state_user_transactions->count()) {
return true;
return ['success' => true];
}
// first: decide what todo
@ -128,18 +176,26 @@ class StateBalancesTable extends Table
if($state_user_transactions->count() == 0){
$clear_state_balance = true;
} else {
$first_state_balance = $state_balances->first();
$first_state_balance_decayed = self::calculateDecay(
$first_state_balance->amount,
$first_state_balance->record_date,
$now);
$last_state_user_transaction = $state_user_transactions->last();
$last_transaction = $this->newEntity();
$last_transaction->amount = $last_state_user_transaction->balance;
$last_transaction->record_date = $last_state_user_transaction->balance_date;
$last_state_user_transaction_decayed = self::calculateDecay(
$last_state_user_transaction->balance,
$last_state_user_transaction->balance_date,
$now);
// if entrys are nearly the same, we don't need doing anything
if(abs($last_transaction->decay - $state_balances->first()->decay) > 100) {
if(floor($last_state_user_transaction_decayed/100) !== floor($first_state_balance_decayed/100)) {
$recalculate_state_user_transactions_balance = true;
$update_state_balance = true;
}
}
}
if(!$recalculate_state_user_transactions_balance) {
$last_state_user_transaction = $state_user_transactions->last();
if($last_state_user_transaction && $last_state_user_transaction->balance <= 0) {
@ -193,37 +249,28 @@ class StateBalancesTable extends Table
$transaction = $transactions_indiced[$state_user_transaction->transaction_id];
if($transaction->transaction_type_id > 2) {
continue;
}
//echo "transaction id: ".$transaction->id . "<br>";
$amount_date = null;
}
$amount = 0;
if($transaction->transaction_type_id == 1) { // creation
$temp = $transaction->transaction_creation;
/*$balance_temp = $this->newEntity();
$balance_temp->amount = $temp->amount;
$balance_temp->record_date = $temp->target_date;
*/
$amount = intval($temp->amount);//$balance_temp->partDecay($transaction->received);
$amount_date = $temp->target_date;
//$amount_date =
$amount = intval($transaction->transaction_creation->amount);
} else if($transaction->transaction_type_id == 2) { // transfer
$temp = $transaction->transaction_send_coin;
$amount = intval($temp->amount);
// reverse if sender
if($stateUserId == $temp->state_user_id) {
$amount *= -1.0;
}
$amount_date = $transaction->received;
}
$amount_date = $transaction->received;
if($i == 0) {
$balance_cursor->amount = $amount;
} else {
$balance_cursor->amount = $balance_cursor->partDecay($amount_date) + $amount;
//$balance_cursor->amount = $balance_cursor->partDecay($amount_date) + $amount;
$balance_cursor->amount =
$this->calculateDecay($balance_cursor->amount, $balance_cursor->record_date, $amount_date)
+ $amount;
}
//echo "new balance: " . $balance_cursor->amount . "<br>";
@ -261,7 +308,7 @@ class StateBalancesTable extends Table
return ['success' => false, 'error' => 'error saving state balance', 'details' => $state_balance->getErrors()];
}
}
return true;
return ['success' => true];
}

View File

@ -22,7 +22,7 @@ use Cake\Validation\Validator;
* @method \App\Model\Entity\StateUserTransaction[] patchEntities($entities, array $data, array $options = [])
* @method \App\Model\Entity\StateUserTransaction findOrCreate($search, callable $callback = null, $options = [])
*/
class StateUserTransactionsTable extends Table
class StateUserTransactionsTable extends AppTable
{
/**
* Initialize method

View File

@ -122,9 +122,9 @@ class StateUsersTable extends Table
// exchange back
$involvedUserIds = array_flip($involvedUser_temp);
$involvedUser = $this->find('all', [
'contain' => false,
'contain' => [],
'where' => ['id IN' => $involvedUserIds],
'fields' => ['id', 'first_name', 'last_name', 'email']
'fields' => ['id', 'first_name', 'last_name', 'email'],
]);
//var_dump($involvedUser->toArray());
$involvedUserIndices = [];

View File

@ -20,7 +20,7 @@ use Cake\Validation\Validator;
* @method \App\Model\Entity\TransactionType[] patchEntities($entities, array $data, array $options = [])
* @method \App\Model\Entity\TransactionType findOrCreate($search, callable $callback = null, $options = [])
*/
class TransactionTypesTable extends Table
class TransactionTypesTable extends AppTable
{
/**
* Initialize method
@ -55,7 +55,7 @@ class TransactionTypesTable extends Table
$validator
->scalar('name')
->maxLength('name', 24)
->maxLength('name', 45)
->requirePresence('name', 'create')
->notEmptyString('name');
@ -66,4 +66,55 @@ class TransactionTypesTable extends Table
return $validator;
}
public function fillWithDefault()
{
$entry_contents = [
[
'id' => 1,
'name' => 'creation',
'text' => 'create new gradidos for member and also for group (in development)',
], [
'id' => 2,
'name' => 'transfer',
'text' => 'send gradidos from one member to another, also cross group transfer',
], [
'id' => 3,
'name' => 'group create',
'text' => 'create a new group, trigger creation of new hedera topic and new blockchain on node server'
], [
'id' => 4,
'name' => 'group add member',
'text' => 'add user to a group or move if he was already in a group'
], [
'id' => 5,
'name' => 'group remove member',
'text' => 'remove user from group, maybe he was moved elsewhere'
],[
'id' => 6,
'name' => 'hedera topic create',
'text' => 'create new topic on hedera'
],[
'id' => 7,
'name' => 'hedera topic send message',
'text' => 'send consensus message over hedera topic'
],[
'id' => 8,
'name' => 'hedera account create',
'text' => 'create new account on hedera for holding some founds with unencrypted keys'
],[
'id' => 9,
'name' => 'decay start',
'text' => 'signalize the starting point for decay calculation, allowed only once per chain'
]
];
$entities = $this->newEntities($entry_contents);
$this->truncate();
$save_results = $this->saveManyWithErrors($entities);
if(!$save_results['success']) {
$save_results['msg'] = 'error by saving default transaction types';
}
return $save_results;
}
}

View File

@ -1,12 +1,12 @@
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\TableRegistry;
use Cake\I18n\FrozenTime;
/**
* Transactions Model
*
@ -172,26 +172,31 @@ class TransactionsTable extends Table
//var_dump($su_transaction);
//die("step");
// add decay transactions
if($i > 0 && $decay == true)
{
$prev = null;
if($i > 0 ) {
$prev = $stateUserTransactions[$i-1];
}
if($prev && $decay == true)
{
if($prev->balance > 0) {
// var_dump($stateUserTransactions);
$current = $su_transaction;
//echo "decay between " . $prev->transaction_id . " and " . $current->transaction_id . "<br>";
$interval = $current->balance_date->diff($prev->balance_date);
$state_balance->amount = $prev->balance;
$state_balance->record_date = $prev->balance_date;
$diff_amount = $state_balance->partDecay($current->balance_date);
//echo $interval->format('%R%a days');
//echo "prev balance: " . $prev->balance . ", diff_amount: $diff_amount, summe: " . (-intval($prev->balance - $diff_amount)) . "<br>";
$final_transactions[] = [
'type' => 'decay',
'balance' => -intval($prev->balance - $diff_amount),
'decay_duration' => $interval->format('%a days, %H hours, %I minutes, %S seconds'),
'memo' => ''
];
$calculated_decay = $stateBalancesTable->calculateDecay($prev->balance, $prev->balance_date, $current->balance_date, true);
$balance = floatval($prev->balance - $calculated_decay['balance']);
// skip small decays (smaller than 0,00 GDD)
if(abs($balance) >= 100) {
//echo $interval->format('%R%a days');
//echo "prev balance: " . $prev->balance . ", diff_amount: $diff_amount, summe: " . (-intval($prev->balance - $diff_amount)) . "<br>";
$final_transactions[] = [
'type' => 'decay',
'balance' => $balance,
'decay_duration' => $calculated_decay['interval']->format('%a days, %H hours, %I minutes, %S seconds'),
'memo' => ''
];
}
}
}
@ -207,12 +212,16 @@ class TransactionsTable extends Table
echo "<br>";*/
if($su_transaction->transaction_type_id == 1) { // creation
$creation = $transaction->transaction_creation;
$balance = $stateBalancesTable->calculateDecay($creation->amount, $creation->target_date, $transaction->received);
$final_transactions[] = [
'name' => 'Gradido Akademie',
'type' => 'creation',
'transaction_id' => $transaction->id,
'date' => $creation->target_date,
'balance' => $creation->amount,
'date' => $transaction->received,// $creation->target_date,
'target_date' => $creation->target_date,
'creation_amount' => $creation->amount,
'balance' => $balance,
'memo' => $transaction->memo
];
} else if($su_transaction->transaction_type_id == 2) { // transfer or send coins
@ -252,18 +261,189 @@ class TransactionsTable extends Table
}
if($i == $stateUserTransactionsCount-1 && $decay == true) {
$state_balance->amount = $su_transaction->balance;
$state_balance->record_date = $su_transaction->balance_date;
$final_transactions[] = [
'type' => 'decay',
'balance' => -intval($su_transaction->balance - $state_balance->decay),
'decay_duration' => $su_transaction->balance_date->timeAgoInWords(),
'memo' => ''
];
$calculated_decay = $stateBalancesTable->calculateDecay(
$su_transaction->balance,
$su_transaction->balance_date, new FrozenTime(), true);
$decay_start_date = $stateBalancesTable->getDecayStartDateCached();
$duration = $su_transaction->balance_date->timeAgoInWords();
if($decay_start_date > $su_transaction->balance_date) {
$duration = $decay_start_date->timeAgoInWords();
}
$balance = floatval($su_transaction->balance - $calculated_decay['balance']);
if($balance > 100) {
$final_transactions[] = [
'type' => 'decay',
'balance' => $balance,
'decay_duration' => $duration,
'memo' => ''
];
}
}
}
return $final_transactions;
}
public function updateTxHash($transaction, $signatureMapString)
{
$transaction_id = $transaction->id;
$previousTxHash = null;
if($transaction_id > 1) {
try {
$previousTransaction = $this
->find('all', ['contain' => false])
->select(['tx_hash'])
->where(['id' => $transaction_id - 1])
->first();
/*$previousTransaction = $transactionsTable->get($this->mTransactionID - 1, [
'contain' => false,
'fields' => ['tx_hash']
]);*/
} catch(Cake\Datasource\Exception\RecordNotFoundException $ex) {
return ['state' => 'error', 'msg' => 'previous transaction not found', 'details' => $ex->getMessage()];
}
if(!$previousTransaction) {
// shouldn't occur
return ['state' => 'error', 'msg' => 'previous transaction not found'];
}
$previousTxHash = $previousTransaction->tx_hash;
}
try {
//$transactionEntity->received = $transactionsTable->get($transactionEntity->id, ['contain' => false, 'fields' => ['received']])->received;
$transaction->received = $this
->find('all', ['contain' => false])
->where(['id' => $transaction->id])
->select(['received'])->first()->received;
} catch(Cake\Datasource\Exception\RecordNotFoundException $ex) {
return ['state' => 'error', 'msg' => 'current transaction not found in db', 'details' => $ex->getMessage()];
}
// calculate tx hash
// previous tx hash + id + received + sigMap as string
// Sodium use for the generichash function BLAKE2b today (11.11.2019), mabye change in the future
$state = \Sodium\crypto_generichash_init();
//echo "prev hash: $previousTxHash\n";
if($previousTxHash != null) {
\Sodium\crypto_generichash_update($state, stream_get_contents($previousTxHash));
}
//echo "id: " . $transactionEntity->id . "\n";
\Sodium\crypto_generichash_update($state, strval($transaction->id));
//echo "received: " . $transactionEntity->received;
\Sodium\crypto_generichash_update($state, $transaction->received->i18nFormat('yyyy-MM-dd HH:mm:ss'));
\Sodium\crypto_generichash_update($state, $signatureMapString);
$transaction->tx_hash = \Sodium\crypto_generichash_final($state);
if ($this->save($transaction)) {
return true;
}
return ['state' => 'error', 'msg' => 'error by saving transaction', 'details' => $transaction->getErrors()];
}
/*!
* @return: false if no decay start block found
* @return: DateTime Object with start date if one start block found
* @return: ['state':'error'] if more than one found
*/
public function getDecayStartDate()
{
$transaction = $this->find()->where(['transaction_type_id' => 9])->select(['received'])->order(['received' => 'ASC']);
if($transaction->count() == 0) {
return null;
}
return $transaction->first()->received;
}
public function fillStateUserTransactions()
{
$missing_transaction_ids = [];
$transaction_ids = $this
->find('all')
->select(['id', 'transaction_type_id'])
->order(['id'])
->where(['transaction_type_id <' => 6])
->all()
;
$state_user_transaction_ids = $this->StateUserTransactions
->find('all')
->select(['transaction_id'])
->group(['transaction_id'])
->order(['transaction_id'])
->toArray()
;
$i2 = 0;
$count = count($state_user_transaction_ids);
foreach($transaction_ids as $tr_id) {
//echo "$i1: ";
if($i2 >= $count) {
$missing_transaction_ids[] = $tr_id;
//echo "adding to missing: $tr_id, continue <br>";
continue;
}
$stu_id = $state_user_transaction_ids[$i2];
if($tr_id->id == $stu_id->transaction_id) {
$i2++;
//echo "after i2++: $i2<br>";
} else if($tr_id->id < $stu_id->transaction_id) {
$missing_transaction_ids[] = $tr_id;
//echo "adding to missing: $tr_id<br>";
}
}
$tablesForType = [
1 => $this->TransactionCreations,
2 => $this->TransactionSendCoins,
3 => $this->TransactionGroupCreates,
4 => $this->TransactionGroupAddaddress,
5 => $this->TransactionGroupAddaddress
];
$idsForType = [];
foreach($missing_transaction_ids as $i => $transaction) {
if(!isset($idsForType[$transaction->transaction_type_id])) {
$idsForType[$transaction->transaction_type_id] = [];
}
$idsForType[$transaction->transaction_type_id][] = $transaction->id;
}
$entities = [];
$state_user_ids = [];
foreach($idsForType as $type_id => $transaction_ids) {
$specific_transactions = $tablesForType[$type_id]->find('all')->where(['transaction_id IN' => $transaction_ids])->toArray();
$keys = $tablesForType[$type_id]->getSchema()->columns();
//var_dump($keys);
foreach($specific_transactions as $specific) {
foreach($keys as $key) {
if(preg_match('/_user_id/', $key)) {
$entity = $this->StateUserTransactions->newEntity();
$entity->transaction_id = $specific['transaction_id'];
$entity->transaction_type_id = $type_id;
$entity->state_user_id = $specific[$key];
if(!in_array($entity->state_user_id, $state_user_ids)) {
array_push($state_user_ids, $entity->state_user_id);
}
$entities[] = $entity;
}
}
}
}
//var_dump($entities);
$stateUsersTable = TableRegistry::getTableLocator()->get('StateUsers');
$existingStateUsers = $stateUsersTable->find('all')->select(['id'])->where(['id IN' => $state_user_ids])->order(['id'])->all();
$existing_state_user_ids = [];
$finalEntities = [];
foreach($existingStateUsers as $stateUser) {
$existing_state_user_ids[] = $stateUser->id;
}
foreach($entities as $entity) {
if(in_array($entity->state_user_id, $existing_state_user_ids)) {
array_push($finalEntities, $entity);
}
}
$save_results = $this->StateUserTransactions->saveManyWithErrors($finalEntities);
if(!$save_results['success']) {
$save_results['msg'] = 'error by saving at least one state user transaction';
}
return $save_results;
}
}

View File

@ -72,9 +72,11 @@ class TransactionBase {
//debug($stateBalanceQuery);
if($stateBalanceQuery->count() > 0) {
$stateBalanceEntry = $stateBalanceQuery->first();
$stateBalanceEntry->amount = $stateBalanceEntry->partDecay($recordDate) + $addAmountCent;
$stateBalanceEntry->amount += $addAmountCent;
$stateBalanceEntry->amount =
$stateBalancesTable->calculateDecay($stateBalanceEntry->amount, $stateBalanceEntry->record_date, $recordDate)
+ $addAmountCent;
} else {
$stateBalanceEntry = $stateBalancesTable->newEntity();
$stateBalanceEntry->state_user_id = $stateUserId;
@ -93,11 +95,12 @@ class TransactionBase {
protected function addStateUserTransaction($stateUserId, $transactionId, $transactionTypeId, $balance, $balance_date) {
$stateUserTransactionTable = self::getTable('state_user_transactions');
$stateUserTransactions = $stateUserTransactionTable
->find('all')
->where(['state_user_id' => $stateUserId])
->order(['transaction_id DESC']);
$new_balance = $balance;
if($stateUserTransactions->count() > 0) {
$stateBalanceTable = self::getTable('state_balances');
$state_user_transaction = $stateUserTransactions->first();
@ -105,16 +108,17 @@ class TransactionBase {
$this->addError('TransactionBase::addStateUserTransaction', 'state_user_transaction is zero, no first entry exist?');
return false;
}
$balance_entity = $stateBalanceTable->newEntity();
$balance_entity->amount = $state_user_transaction->balance;
$balance_entity->record_date = $state_user_transaction->balance_date;
$balance = $balance_entity->decay + $balance;
$new_balance += $stateBalanceTable->calculateDecay(
$state_user_transaction->balance,
$state_user_transaction->balance_date,
$balance_date
);
}
$entity = $stateUserTransactionTable->newEntity();
$entity->state_user_id = $stateUserId;
$entity->transaction_id = $transactionId;
$entity->transaction_type_id = $transactionTypeId;
$entity->balance = $balance;
$entity->balance = $new_balance;
$entity->balance_date = $balance_date;
if(!$stateUserTransactionTable->save($entity)) {

View File

@ -138,8 +138,9 @@ class TransactionCreation extends TransactionBase {
return true;
}
public function save($transaction_id, $firstPublic)
public function save($transaction_id, $firstPublic, $received)
{
$stateBalancesTable = self::getTable('stateBalances');
$transactionCreationEntity = $this->transactionCreationsTable->newEntity();
$transactionCreationEntity->transaction_id = $transaction_id;
@ -151,23 +152,27 @@ class TransactionCreation extends TransactionBase {
$this->addError('TransactionCreation::save', 'couldn\'t get state user id');
return false;
}
$transactionCreationEntity->state_user_id = $receiverUserId;
$transactionCreationEntity->amount = $this->getAmount();
$transactionCreationEntity->target_date = $this->protoTransactionCreation->getTargetDate()->getSeconds();
$target_date = new FrozenTime($transactionCreationEntity->target_date);
$decayed_balance = $stateBalancesTable->calculateDecay($this->getAmount(), $target_date, $received);
if(!$this->transactionCreationsTable->save($transactionCreationEntity)) {
$this->addError('TransactionCreation::save', 'error saving transactionCreation with errors: ' . json_encode($transactionCreationEntity->getErrors()));
return false;
}
// update state balance
$final_balance = $this->updateStateBalance($receiverUserId, $this->getAmount(), $target_date);
$final_balance = $this->updateStateBalance($receiverUserId, $decayed_balance, $received);
if(false === $final_balance) {
return false;
}
// decay is a virtual field which is calculated from amount and now() - record_date
if(!$this->addStateUserTransaction($receiverUserId, $transaction_id, 1, $this->getAmount(), $target_date)) {
if(!$this->addStateUserTransaction($receiverUserId, $transaction_id, 1, $decayed_balance, $received)) {
return false;
}

View File

@ -99,12 +99,12 @@ class TransactionTransfer extends TransactionBase {
->where(['public_key' => $senderPublic])
->contain(['StateBalances' => ['fields' => ['amount', 'state_user_id']]])->first();
if(!$user) {
$this->addError($functionName, 'couldn\'t find sender ' . $i .' in db' );
$this->addError($functionName, 'couldn\'t find sender in db' );
return false;
}
//var_dump($user);
if(intval($user->state_balances[0]->amount) < intval($amount)) {
$this->addError($functionName, 'sender ' . $i . ' hasn\t enough GDD');
$this->addError($functionName, 'sender hasn\t enough GDD');
return false;
}
@ -116,7 +116,7 @@ class TransactionTransfer extends TransactionBase {
// check if receiver exist
$receiver_user = $stateUsersTable->find('all')->select(['id'])->where(['public_key' => $receiver_public_key])->first();
if(!$receiver_user) {
$this->addError($functionName, 'couldn\'t find receiver ' . $i .' in db' );
$this->addError($functionName, 'couldn\'t find receiver in db' );
return false;
}
if($amount < 0) {

View File

@ -0,0 +1,11 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
$body['balance'] = $this->element('centToFloat', ['cent' => $body['balance'], 'precision' => 4]);
$body['decay'] = $this->element('centToFloat', ['cent' => $body['decay'], 'precision' => 4]);
?><?= json_encode($body) ?>

View File

@ -0,0 +1,20 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
$body['balance'] = $this->element('centToFloat', ['cent' => $body['balance'], 'precision' => 4]);
$body['decay'] = $this->element('centToFloat', ['cent' => $body['decay'], 'precision' => 4]);
$body['gdtSum'] = $this->element('centToFloat', ['cent' => $body['gdtSum'], 'precision' => 2]);
foreach($body['transactions'] as $i => $transaction) {
$body['transactions'][$i]['balance'] = $this->element('centToFloat', ['cent' => $transaction['balance'], 'precision' => 4]);
if(isset($transaction['creation_amount'])) {
$body['transactions'][$i]['creation_amount'] = $this->element('centToFloat', ['cent' => $transaction['creation_amount'], 'precision' => 4]);
}
}
?><?= json_encode($body) ?>

View File

@ -0,0 +1,18 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
$cut_places = $precision - 2;
$transformAmount = $cent;
if($cut_places > 0) {
$transformAmount = floor($cent / pow(10, $cut_places));
}
if($cut_places < 0) {
$cut_places = 0;
}
echo $transformAmount / pow(10, $precision - $cut_places);

View File

@ -0,0 +1,23 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Migration $migration
*/
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
<ul class="side-nav">
<li class="heading"><?= __('Actions') ?></li>
<li><?= $this->Html->link(__('List Migrations'), ['action' => 'index']) ?></li>
</ul>
</nav>
<div class="migrations form large-9 medium-8 columns content">
<?= $this->Form->create($migration) ?>
<fieldset>
<legend><?= __('Add Migration') ?></legend>
<?php
echo $this->Form->control('db_version');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>

View File

@ -0,0 +1,29 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Migration $migration
*/
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
<ul class="side-nav">
<li class="heading"><?= __('Actions') ?></li>
<li><?= $this->Form->postLink(
__('Delete'),
['action' => 'delete', $migration->id],
['confirm' => __('Are you sure you want to delete # {0}?', $migration->id)]
)
?></li>
<li><?= $this->Html->link(__('List Migrations'), ['action' => 'index']) ?></li>
</ul>
</nav>
<div class="migrations form large-9 medium-8 columns content">
<?= $this->Form->create($migration) ?>
<fieldset>
<legend><?= __('Edit Migration') ?></legend>
<?php
echo $this->Form->control('db_version');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
</div>

View File

@ -0,0 +1,47 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Migration[]|\Cake\Collection\CollectionInterface $migrations
*/
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
<ul class="side-nav">
<li class="heading"><?= __('Actions') ?></li>
<li><?= $this->Html->link(__('New Migration'), ['action' => 'add']) ?></li>
</ul>
</nav>
<div class="migrations index large-9 medium-8 columns content">
<h3><?= __('Migrations') ?></h3>
<table cellpadding="0" cellspacing="0">
<thead>
<tr>
<th scope="col"><?= $this->Paginator->sort('id') ?></th>
<th scope="col"><?= $this->Paginator->sort('db_version') ?></th>
<th scope="col" class="actions"><?= __('Actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($migrations as $migration): ?>
<tr>
<td><?= $this->Number->format($migration->id) ?></td>
<td><?= $this->Number->format($migration->db_version) ?></td>
<td class="actions">
<?= $this->Html->link(__('View'), ['action' => 'view', $migration->id]) ?>
<?= $this->Html->link(__('Edit'), ['action' => 'edit', $migration->id]) ?>
<?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $migration->id], ['confirm' => __('Are you sure you want to delete # {0}?', $migration->id)]) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="paginator">
<ul class="pagination">
<?= $this->Paginator->first('<< ' . __('first')) ?>
<?= $this->Paginator->prev('< ' . __('previous')) ?>
<?= $this->Paginator->numbers() ?>
<?= $this->Paginator->next(__('next') . ' >') ?>
<?= $this->Paginator->last(__('last') . ' >>') ?>
</ul>
<p><?= $this->Paginator->counter(['format' => __('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')]) ?></p>
</div>
</div>

View File

@ -0,0 +1,18 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
?><h2>Migrate DB</h2>
<p>Migrate from Version <?= $db_version ?></p>
<?php if($result['success']) : ?>
<h3><success>Success</success></h3>
<?php else : ?>
<h3><error>Error</error></h3>
<p><?= json_encode($result) ?></p>
<?php endif; ?>
<p><?= $this->Html->link('Back to Dashboard', ['controller' => 'Dashboard', 'action' => 'index']) ?></p>

View File

@ -0,0 +1,28 @@
<?php
/**
* @var \App\View\AppView $this
* @var \App\Model\Entity\Migration $migration
*/
?>
<nav class="large-3 medium-4 columns" id="actions-sidebar">
<ul class="side-nav">
<li class="heading"><?= __('Actions') ?></li>
<li><?= $this->Html->link(__('Edit Migration'), ['action' => 'edit', $migration->id]) ?> </li>
<li><?= $this->Form->postLink(__('Delete Migration'), ['action' => 'delete', $migration->id], ['confirm' => __('Are you sure you want to delete # {0}?', $migration->id)]) ?> </li>
<li><?= $this->Html->link(__('List Migrations'), ['action' => 'index']) ?> </li>
<li><?= $this->Html->link(__('New Migration'), ['action' => 'add']) ?> </li>
</ul>
</nav>
<div class="migrations view large-9 medium-8 columns content">
<h3><?= h($migration->id) ?></h3>
<table class="vertical-table">
<tr>
<th scope="row"><?= __('Id') ?></th>
<td><?= $this->Number->format($migration->id) ?></td>
</tr>
<tr>
<th scope="row"><?= __('Db Version') ?></th>
<td><?= $this->Number->format($migration->db_version) ?></td>
</tr>
</table>
</div>

View File

@ -35,7 +35,8 @@
<?php
echo $this->Form->control('state_group_id', ['options' => $stateGroups]);
echo $this->Form->control('transaction_type_id', ['options' => $transactionTypes]);
echo $this->Form->control('received');
echo $this->Form->control('memo', ['type' => 'textarea']);
echo $this->Form->control('blockchain_type_id', ['options' => $blockchainTypes]);
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>

View File

@ -0,0 +1,44 @@
<?php
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* MigrationsFixture
*/
class MigrationsFixture extends TestFixture
{
/**
* Fields
*
* @var array
*/
// @codingStandardsIgnoreStart
public $fields = [
'id' => ['type' => 'integer', 'length' => 10, 'unsigned' => true, 'null' => false, 'default' => null, 'comment' => '', 'autoIncrement' => true, 'precision' => null],
'db_version' => ['type' => 'integer', 'length' => 11, 'unsigned' => false, 'null' => true, 'default' => '0', 'comment' => '', 'precision' => null, 'autoIncrement' => null],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id'], 'length' => []],
],
'_options' => [
'engine' => 'InnoDB',
'collation' => 'utf8mb4_unicode_ci'
],
];
// @codingStandardsIgnoreEnd
/**
* Init method
*
* @return void
*/
public function init()
{
$this->records = [
[
'id' => 1,
'db_version' => 1,
],
];
parent::init();
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace App\Test\TestCase\Controller;
use App\Controller\MigrationsController;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
/**
* App\Controller\MigrationsController Test Case
*
* @uses \App\Controller\MigrationsController
*/
class MigrationsControllerTest extends TestCase
{
use IntegrationTestTrait;
/**
* Fixtures
*
* @var array
*/
public $fixtures = [
'app.Migrations',
];
/**
* Test index method
*
* @return void
*/
public function testIndex()
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test view method
*
* @return void
*/
public function testView()
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test add method
*
* @return void
*/
public function testAdd()
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test edit method
*
* @return void
*/
public function testEdit()
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test delete method
*
* @return void
*/
public function testDelete()
{
$this->markTestIncomplete('Not implemented yet.');
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Test\TestCase\Model\Table;
use App\Model\Table\MigrationsTable;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;
/**
* App\Model\Table\MigrationsTable Test Case
*/
class MigrationsTableTest extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Table\MigrationsTable
*/
public $Migrations;
/**
* Fixtures
*
* @var array
*/
public $fixtures = [
'app.Migrations',
];
/**
* setUp method
*
* @return void
*/
public function setUp()
{
parent::setUp();
$config = TableRegistry::getTableLocator()->exists('Migrations') ? [] : ['className' => MigrationsTable::class];
$this->Migrations = TableRegistry::getTableLocator()->get('Migrations', $config);
}
/**
* tearDown method
*
* @return void
*/
public function tearDown()
{
unset($this->Migrations);
parent::tearDown();
}
/**
* Test initialize method
*
* @return void
*/
public function testInitialize()
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test validationDefault method
*
* @return void
*/
public function testValidationDefault()
{
$this->markTestIncomplete('Not implemented yet.');
}
}

View File

@ -23,13 +23,13 @@ Additional session can be provided as GET-Parameter
```json
{
"state":"success",
"balance":15906078,
"decay":15873851,
"balance":1590.60,
"decay":1587.38,
"decay_date":"2021-04-16T11:47:21+00:00"
}
```
- `balance` : balance describes gradido cents which are 4 digits behind the separator. A balance value of 174500 equals therefor 17,45 GDD
- `balance` : balance describes gradido as float with max two decimal places
- `decay` : balance with decay on it at the time in decay_date, so it is the precise balance of user at time of calling this function
- `decay_date`: date and time for decay amount, should be the time and date of function call
@ -59,16 +59,32 @@ Assuming: session is valid
{
"state":"success",
"transactions": [
{
"type": "decay",
"balance": "14.74",
"decay_duration": "4 days, 2 hours ago",
"memo": ""
},
{
"name": "Max Mustermann",
"email": "Maxim Mustermann",
"type": "send",
"transaction_id": 2,
"date": "2021-02-19T13:25:36+00:00",
"balance": 1920000,
"balance": 192.0,
"memo": "a piece of cake :)",
"pubkey": "038a6f93270dc57b91d76bf110ad3863fcb7d1b08e7692e793fcdb4467e5b6a7"
}
},
{
"name": "Gradido Akademie",
"type": "creation",
"transaction_id": 10,
"date": "2021-04-15T11:19:45+00:00",
"target_date": "2021-02-01T00:00:00+00:00",
"creation_amount": "1000.0",
"balance": "1000.0",
"memo": "AGE Februar 2021"
}
],
"transactionExecutingCount": 0,
"count": 1,
@ -95,8 +111,11 @@ Transaction:
- `receiver`: user has received gradidos from another user
- `transaction_id`: id of transaction in db, in stage2 also the hedera sequence number of transaction
- `date`: date of ordering transaction (booking date)
- `balance`: Gradido Cent, 4 Nachkommastellen (2 Reserve), 1920000 = 192,00 GDD
- `balance`: Gradido as float, max 2 Nachkommastellen, by creation balance after subtract decay amount
- `memo`: Details about transaction
- `decay_duration`: only for decay, time duration for decay calculation in english text
- `creation_amount`: only for creation transaction, created account before decay
- `target_date`: only by creation transaction, target date for creation, start time for decay calculation (if < as global decay start time)
## Creation transaction
Makes a creation transaction to create new Gradido
@ -117,7 +136,7 @@ with
{
"session_id" : -127182,
"email": "max.musterman@gmail.de",
"amount": 10000000,
"amount": 1000.0,
"target_date":"2021-02-19T13:25:36+00:00",
"memo":"AGE",
"auto_sign": true
@ -128,7 +147,7 @@ with
{
"session_id" : -127182,
"username": "Maxi_786",
"amount": 10000000,
"amount": 1000.0,
"target_date":"2021-02-19T13:25:36+00:00",
"memo":"AGE",
"auto_sign": true
@ -139,7 +158,7 @@ with
{
"session_id" : -127182,
"pubkey": "038a6f93270dc57b91d76bf110ad3863fcb7d1b08e7692e793fcdb4467e5b6a7",
"amount": 10000000,
"amount": 1000.0,
"target_date":"2021-02-19T13:25:36+00:00",
"memo":"AGE",
"auto_sign": true
@ -149,8 +168,7 @@ with
- `session_id`: optional, only used if cookie GRADIDO_LOGIN not exist and no sesion_id in php session
- `email` or `username` or `pubkey`: used to identify how gets the gradidos (email and username are only aliases for pubkey)
- `amount`: gdd amount to transfer in gradido cent (10000000 = 1000,00 GDD)
- `target_date`: target date for creation, can be max 3 months before current date, but not after current date, allowed formats do you find here: https://pocoproject.org/docs/Poco.DateTimeFormat.html
- `amount`: gdd amount to transfer in gradido as float
- `memo`: text for receiver, currently saved as clear text in blockchain
- `auto_sign`: if set to true, transaction will be directly signed on login-server and proceed if needed signs are there
if set to false, transaction must be signed after on `http://localhost/account/checkTransactions`
@ -186,7 +204,7 @@ with
{
"session_id" : -127182,
"email": "max.musterman@gmail.de",
"amount": 1000000,
"amount": 100.0,
"memo":"a gift",
"auto_sign": true
}
@ -196,7 +214,7 @@ with
{
"session_id" : -127182,
"username": "Maxi_786",
"amount": 1000000,
"amount": 100.0,
"memo":"a gift",
"auto_sign": true
}
@ -206,13 +224,13 @@ with
{
"session_id" : -127182,
"pubkey": "038a6f93270dc57b91d76bf110ad3863fcb7d1b08e7692e793fcdb4467e5b6a7",
"amount": 1000000,
"amount": 100.0,
"memo":"a gift",
"auto_sign": true
}
```
- `session_id`: optional, only used if cookie GRADIDO_LOGIN not exist and no sesion_id in php session
- `amount`: amount to transfer, 2000000 = 200,00 GDD
- `amount`: amount to transfer as float
- `email` or `username` or `pubkey`: used to identify how gets the gradidos (email and username are only aliases for pubkey)
- `memo`: text for receiver, currently saved as clear text in blockchain
- `auto_sign`: if set to true, transaction will be directly signed on login-server and proceed if needed signs are there
@ -246,3 +264,40 @@ Without auto-sign the transaction is pending on the login-server and waits for t
// TODO Should this not be handled client side?
# Klicktipp
## Subscribe
Subscribe current logged in user to gradido newsletter
### Request
`GET http://localhost/api/klicktipp_subscribe/[session_id]`
Parts symbolized by [] are optional
- session_id: session will be searched in php session and GRADIDO_LOGIN cookie and if not found use this
### Response
Assuming: session is valid
```json
{
"state": "success",
"redirect_url": "<redirect url from klicktipp>"
}
````
## Unsubscribe
Unsubscribe current logged in user from gradido newsletter
### Request
`GET http://localhost/api/klicktipp_unsubscribe/[session_id]`
Parts symbolized by [] are optional
- session_id: session will be searched in php session and GRADIDO_LOGIN cookie and if not found use this
### Response
Assuming: session is valid
```json
{
"state": "success"
}
````

View File

@ -0,0 +1,30 @@
# Das Locale Schöpfen
### MariaDB Insert Groups
wenn local geschöpft werden möchte kommt ein fehler das der user keiner gruppe zugeordnet ist.
folgende schritte musst du machen um eine gruppe anzulegen
hier findest du den Mysql befehl: configs/login_server/setup_db_tables/setup_docker_group.sql
in der Datei findest du folgenden Befehl
INSERT INTO `groups` (`id`, `alias`, `name`, `url`, `host`, `home`, `description`) VALUES
(1, 'docker', 'docker gradido group', 'localhost', 'nginx', '/', 'gradido test group for docker and stage2 with blockchain db');
# Ablauf
1. logge dich bei phpmyadmin ein http://localhost:8074/ (mariadb / root)
2. gehe auf tabelle "gradido_login"
3. gib folgenden Befehl in die console ein
INSERT INTO `groups` (`id`, `alias`, `name`, `url`, `host`, `home`, `description`) VALUES
(1, 'docker', 'docker gradido group', 'localhost', 'nginx', '/', 'gradido test group for docker and stage2 with blockchain db');
> es wird eine Gruppe mit id 1 angelgt. alle angelegten user sollten dieser gruppe zugeordnet sein.
das schöpfen sollte nun local funktionieren. :)
#ACHTUNG ! nach dem login kann noch zu fehlern kommen in der URL "localhostnginx/..." zu "localhost/..." ändern

View File

@ -3,28 +3,24 @@ module.exports = {
env: {
browser: true,
node: true,
jest: true
jest: true,
},
parserOptions: {
parser: 'babel-eslint'
parser: 'babel-eslint',
},
extends: [
'plugin:vue/essential',
'plugin:prettier/recommended'
],
extends: ['standard', 'plugin:vue/essential', 'plugin:prettier/recommended'],
// required to lint *.vue files
plugins: [
'vue',
'prettier',
'jest'
],
plugins: ['vue', 'prettier', 'jest'],
// add your custom rules here
rules: {
'no-console': ['error'],
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
'prettier/prettier': ['error', {
htmlWhitespaceSensitivity: 'ignore'
}],
}
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
},
}

View File

@ -176,7 +176,7 @@ Wenn alles okay:
"type": "creation|send|receive",
"transaction_id": <transaction_id>, // db id not id from blockchain
"date": "<date string>",
"balance": <GDD balance in GDD cent /10000>,
"balance": <GDD balance in GDD cent>,
"memo": "<Verwendungszweck>",
"pubkey": "<other_user.public_key in hex>"
@ -319,7 +319,7 @@ Wenn alles okay:
"type": "creation|send|receive",
"transaction_id": <transaction_id>, // db id not id from blockchain
"date": "<date string>",
"balance": <GDD balance in GDD cent /10000>,
"balance": <GDD balance in GDD cent>,
"memo": "<Verwendungszweck>",
"pubkey": "<other_user.public_key in hex>"

View File

@ -3,7 +3,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
moduleFileExtensions: [
'js',
//'jsx',
// 'jsx',
'json',
'vue',
],

View File

@ -31,7 +31,7 @@
"dropzone": "^5.5.1",
"element-ui": "2.4.11",
"es6-promise": "^4.1.1",
"eslint": "^5.16.0",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.1.0",
"eslint-config-standard": "^16.0.2",
"eslint-loader": "^4.0.2",
@ -40,7 +40,6 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.8.0",
"express": "^4.17.1",
"flatpickr": "^4.5.7",

View File

@ -7,6 +7,7 @@ const port = process.env.PORT || 3000
// Express Server
const app = express()
// eslint-disable-next-line node/no-path-concat
app.use(serveStatic(__dirname + '/../dist'))
app.listen(port)

View File

@ -36,11 +36,6 @@ export default {
DashboardLayout,
AuthLayoutGDD,
},
data() {
return {
language: 'en',
}
},
data() {
return {
config: {

View File

@ -32,15 +32,17 @@ const apiPost = async (url, payload) => {
}
const communityAPI = {
balance: async (session_id) => {
return apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + session_id)
balance: async (sessionId) => {
return apiGet(CONFIG.COMMUNITY_API_URL + 'getBalance/' + sessionId)
},
transactions: async (session_id) => {
return apiGet(CONFIG.COMMUNITY_API_URL + 'listTransactions/1/25/ASC/' + session_id)
transactions: async (sessionId, firstPage = 1, items = 1000, order = 'DESC') => {
return apiGet(
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${sessionId}`,
)
},
/*create: async (session_id, email, amount, memo, target_date = new Date() ) => {
/* create: async (sessionId, email, amount, memo, target_date = new Date() ) => {
const payload = {
session_id,
sessionId,
email,
amount,
target_date,
@ -48,14 +50,14 @@ const communityAPI = {
auto_sign: true,
}
return apiPost(CONFIG.COMMUNITY_API__URL + 'createCoins/', payload)
},*/
send: async (session_id, email, amount, memo, target_date) => {
}, */
send: async (sessionId, email, amount, memo, targetDate) => {
const payload = {
session_id,
session_id: sessionId,
email,
amount,
memo,
target_date,
target_date: targetDate,
auto_sign: true,
}
return apiPost(CONFIG.COMMUNITY_API_URL + 'sendCoins/', payload)

View File

@ -29,7 +29,7 @@ const apiPost = async (url, payload) => {
throw new Error('HTTP Status Error ' + result.status)
}
if (result.data.state === 'warning') {
return { success: true, result: error }
return { success: true, result: result.error }
}
if (result.data.state !== 'success') {
throw new Error(result.data.msg)
@ -48,15 +48,15 @@ const loginAPI = {
}
return apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', payload)
},
logout: async (session_id) => {
const payload = { session_id }
logout: async (sessionId) => {
const payload = { session_id: sessionId }
return apiPost(CONFIG.LOGIN_API_URL + 'logout', payload)
},
create: async (email, first_name, last_name, password) => {
create: async (email, firstName, lastName, password) => {
const payload = {
email,
first_name,
last_name,
first_name: firstName,
last_name: lastName,
password,
emailType: EMAIL_TYPE.DEFAULT,
login_after_register: true,
@ -76,9 +76,9 @@ const loginAPI = {
CONFIG.LOGIN_API_URL + 'loginViaEmailVerificationCode?emailVerificationCode=' + optin,
)
},
changePassword: async (session_id, email, password) => {
changePassword: async (sessionId, email, password) => {
const payload = {
session_id,
session_id: sessionId,
email,
update: {
'User.password': password,

View File

@ -42,7 +42,7 @@ export default {
FadeTransition,
},
created() {
//console.log('base-alert gesetzt in =>', this.$route.path)
// console.log('base-alert gesetzt in =>', this.$route.path)
},
props: {
type: {

View File

@ -62,7 +62,7 @@ export default {
})
const slider = this.$el.noUiSlider
slider.on('slide', () => {
let value = slider.get()
const value = slider.get()
if (value !== this.value) {
this.$emit('input', value)
}

View File

@ -2,7 +2,7 @@ import { parseOptions } from '@/components/Charts/optionHelpers'
import Chart from 'chart.js'
export const Charts = {
mode: 'light', //(themeMode) ? themeMode : 'light';
mode: 'light', // (themeMode) ? themeMode : 'light';
fonts: {
base: 'Open Sans',
},
@ -34,9 +34,9 @@ export const Charts = {
}
function chartOptions() {
let { colors, mode, fonts } = Charts
const { colors, mode, fonts } = Charts
// Options
let options = {
const options = {
defaults: {
global: {
responsive: true,
@ -59,21 +59,21 @@ function chartOptions() {
elements: {
point: {
radius: 0,
backgroundColor: colors.theme['primary'],
backgroundColor: colors.theme.primary,
},
line: {
tension: 0.4,
borderWidth: 4,
borderColor: colors.theme['primary'],
borderColor: colors.theme.primary,
backgroundColor: colors.transparent,
borderCapStyle: 'rounded',
},
rectangle: {
backgroundColor: colors.theme['warning'],
backgroundColor: colors.theme.warning,
},
arc: {
backgroundColor: colors.theme['primary'],
borderColor: mode == 'dark' ? colors.gray[800] : colors.white,
backgroundColor: colors.theme.primary,
borderColor: mode === 'dark' ? colors.gray[800] : colors.white,
borderWidth: 4,
},
},
@ -94,11 +94,11 @@ function chartOptions() {
},
cutoutPercentage: 83,
legendCallback: function (chart) {
let data = chart.data
const data = chart.data
let content = ''
data.labels.forEach(function (label, index) {
let bgColor = data.datasets[0].backgroundColor[index]
const bgColor = data.datasets[0].backgroundColor[index]
content += '<span class="chart-legend-item">'
content +=
@ -172,7 +172,7 @@ export const basicOptions = {
},
responsive: true,
}
export let blueChartOptions = {
export const blueChartOptions = {
scales: {
yAxes: [
{
@ -185,7 +185,7 @@ export let blueChartOptions = {
},
}
export let lineChartOptionsBlue = {
export const lineChartOptionsBlue = {
...basicOptions,
tooltips: {
backgroundColor: '#f5f5f5',

View File

@ -1,6 +1,6 @@
// Parse global options
export function parseOptions(parent, options) {
for (let item in options) {
for (const item in options) {
if (typeof options[item] !== 'object') {
parent[item] = options[item]
} else {

View File

@ -4,13 +4,13 @@
//
import Chart from 'chart.js'
Chart.elements.Rectangle.prototype.draw = function () {
let ctx = this._chart.ctx
let vm = this._view
let left, right, top, bottom, signX, signY, borderSkipped, radius
const ctx = this._chart.ctx
const vm = this._view
let left, right, top, bottom, signX, signY, borderSkipped
let borderWidth = vm.borderWidth
// Set Radius Here
// If radius is large enough to cause drawing errors a max radius is imposed
let cornerRadius = 6
const cornerRadius = 6
if (!vm.horizontal) {
// bar
@ -36,14 +36,14 @@ Chart.elements.Rectangle.prototype.draw = function () {
// adjust the sizes to fit if we're setting a stroke on the line
if (borderWidth) {
// borderWidth shold be less than bar width and bar height.
let barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom))
const barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom))
borderWidth = borderWidth > barSize ? barSize : borderWidth
let halfStroke = borderWidth / 2
const halfStroke = borderWidth / 2
// Adjust borderWidth when bar top position is near vm.base(zero).
let borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0)
let borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0)
let borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0)
let borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0)
const borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0)
const borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0)
const borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0)
const borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0)
// not become a vertical line?
if (borderLeft !== borderRight) {
top = borderTop
@ -64,7 +64,7 @@ Chart.elements.Rectangle.prototype.draw = function () {
// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
let corners = [
const corners = [
[left, bottom],
[left, top],
[right, top],
@ -72,7 +72,7 @@ Chart.elements.Rectangle.prototype.draw = function () {
]
// Find first (starting) corner with fallback to 'bottom'
let borders = ['bottom', 'left', 'top', 'right']
const borders = ['bottom', 'left', 'top', 'right']
let startCorner = borders.indexOf(borderSkipped, 0)
if (startCorner === -1) {
startCorner = 0
@ -89,16 +89,14 @@ Chart.elements.Rectangle.prototype.draw = function () {
for (let i = 1; i < 4; i++) {
corner = cornerAt(i)
let nextCornerId = i + 1
if (nextCornerId == 4) {
if (nextCornerId === 4) {
nextCornerId = 0
}
let nextCorner = cornerAt(nextCornerId)
let width = corners[2][0] - corners[1][0]
let height = corners[0][1] - corners[1][1]
let x = corners[1][0]
let y = corners[1][1]
const width = corners[2][0] - corners[1][0]
const height = corners[0][1] - corners[1][1]
const x = corners[1][0]
const y = corners[1][1]
let radius = cornerRadius

View File

@ -6,7 +6,7 @@ const localVue = global.localVue
describe('CloseButton', () => {
let wrapper
let propsData = {
const propsData = {
target: 'Target',
expanded: false,
}

View File

@ -70,7 +70,7 @@ export default {
},
methods: {
activate() {
let wasActive = this.active
const wasActive = this.active
if (!this.multipleActive) {
this.deactivateAll()
}

View File

@ -170,7 +170,7 @@ export default {
},
methods: {
updateValue(evt) {
let value = evt.target.value
const value = evt.target.value
this.$emit('input', value)
},
onFocus(evt) {

View File

@ -60,7 +60,7 @@ export default {
type: String,
default: '',
validator(value) {
let acceptedValues = ['', 'notice', 'mini']
const acceptedValues = ['', 'notice', 'mini']
return acceptedValues.indexOf(value) !== -1
},
description: 'Modal type (notice|mini|"") ',
@ -73,7 +73,7 @@ export default {
type: String,
description: 'Modal size',
validator(value) {
let acceptedValues = ['', 'sm', 'lg']
const acceptedValues = ['', 'sm', 'lg']
return acceptedValues.indexOf(value) !== -1
},
},

View File

@ -39,7 +39,7 @@ export default {
submittedNames: [],
}
},
/*Modal*/
/* Modal */
checkFormValidity() {
const valid = this.$refs.form.checkValidity()
this.nameState = valid

View File

@ -90,8 +90,8 @@ export default {
},
computed: {
classes() {
let color = `bg-${this.type}`
let classes = [
const color = `bg-${this.type}`
const classes = [
{ 'navbar-transparent': this.transparent },
{ [`navbar-expand-${this.expand}`]: this.expand },
]

View File

@ -59,7 +59,7 @@ export default {
type: String,
default: 'top',
validator: (value) => {
let acceptedValues = ['top', 'bottom']
const acceptedValues = ['top', 'bottom']
return acceptedValues.indexOf(value) !== -1
},
description: 'Vertical alignment of notification (top|bottom)',
@ -68,7 +68,7 @@ export default {
type: String,
default: 'right',
validator: (value) => {
let acceptedValues = ['left', 'center', 'right']
const acceptedValues = ['left', 'center', 'right']
return acceptedValues.indexOf(value) !== -1
},
description: 'Horizontal alignment of notification (left|center|right)',
@ -77,7 +77,7 @@ export default {
type: String,
default: 'info',
validator: (value) => {
let acceptedValues = ['default', 'info', 'primary', 'danger', 'warning', 'success']
const acceptedValues = ['default', 'info', 'primary', 'danger', 'warning', 'success']
return acceptedValues.indexOf(value) !== -1
},
description:
@ -129,8 +129,8 @@ export default {
return `alert-${this.type}`
},
customPosition() {
let initialMargin = 20
let alertHeight = this.elmHeight + 10
const initialMargin = 20
const alertHeight = this.elmHeight + 10
let sameAlertsCount = this.$notifications.state.filter((alert) => {
return (
alert.horizontalAlign === this.horizontalAlign &&
@ -141,8 +141,8 @@ export default {
if (this.$notifications.settings.overlap) {
sameAlertsCount = 1
}
let pixels = (sameAlertsCount - 1) * alertHeight + initialMargin
let styles = {}
const pixels = (sameAlertsCount - 1) * alertHeight + initialMargin
const styles = {}
if (this.verticalAlign === 'top') {
styles.top = `${pixels}px`
} else {

View File

@ -44,7 +44,7 @@ const NotificationStore = {
const NotificationsPlugin = {
install(Vue, options) {
let app = new Vue({
const app = new Vue({
data: {
notificationStore: NotificationStore,
},

View File

@ -5,7 +5,7 @@
import VueBootstrapTypeahead from 'vue-bootstrap-typeahead'
// Global registration
//Vue.component('vue-bootstrap-typeahead', VueBootstrapTypeahead)
// Vue.component('vue-bootstrap-typeahead', VueBootstrapTypeahead)
// OR

View File

@ -50,6 +50,7 @@
<a
:href="`https://elopage.com/s/gradido/sign_in?locale=${$i18n.locale}`"
class="nav-link text-lg"
target="_blank"
>
{{ $t('members_area') }}
</a>

View File

@ -110,7 +110,7 @@ export default {
},
linkPrefix() {
if (this.link.name) {
let words = this.link.name.split(' ')
const words = this.link.name.split(' ')
return words.map((word) => word.substring(0, 1)).join('')
}
return ''
@ -120,7 +120,7 @@ export default {
},
isActive() {
if (this.$route && this.$route.path) {
let matchingRoute = this.children.find((c) => this.$route.path.startsWith(c.link.path))
const matchingRoute = this.children.find((c) => this.$route.path.startsWith(c.link.path))
if (matchingRoute !== undefined) {
return true
}

View File

@ -29,7 +29,7 @@ const SidebarPlugin = {
if (options && options.sidebarLinks) {
SidebarStore.sidebarLinks = options.sidebarLinks
}
let app = new Vue({
const app = new Vue({
data: {
sidebarStore: SidebarStore,
},

View File

@ -67,7 +67,7 @@ export default {
type: String,
default: 'primary',
validator: (value) => {
let acceptedValues = ['primary', 'info', 'success', 'warning', 'danger']
const acceptedValues = ['primary', 'info', 'success', 'warning', 'danger']
return acceptedValues.indexOf(value) !== -1
},
},
@ -102,7 +102,7 @@ export default {
},
methods: {
findAndActivateTab(title) {
let tabToActivate = this.tabs.find((t) => t.title === title)
const tabToActivate = this.tabs.find((t) => t.title === title)
if (tabToActivate) {
this.activateTab(tabToActivate)
}

View File

@ -2,7 +2,7 @@ export default {
bind: function (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
// here I check that click was outside the el and his childrens
if (!(el == event.target || el.contains(event.target))) {
if (!(el === event.target || el.contains(event.target))) {
// and if it did, call method provided in attribute value
vnode.context[binding.expression](event)
}

View File

@ -3,7 +3,7 @@
"welcome":"Willkommen!",
"community": "Gemeinschaft",
"logout":"Abmelden",
"login":"Login",
"login":"Anmeldung",
"signup": "Registrieren",
"reset": "Passwort zurücksetzen",
"imprint":"Impressum",
@ -13,7 +13,7 @@
"back":"Zurück",
"send":"Senden",
"transactions":"Transaktionen",
"language":"Language",
"language":"Sprache",
"languages":{
"de": "Deutsch",
"en": "English"
@ -21,6 +21,7 @@
"form": {
"attention": "<strong>Achtung!</strong> Bitte überprüfe alle deine Eingaben sehr genau. Du bist alleine Verantwortlich für deine Entscheidungen. Versendete Gradidos können nicht wieder zurück geholt werden.",
"cancel":"Abbrechen",
"reset": "Zurücksetzen",
"close":"schließen",
"receiver":"Empfänger",
"sender":"Absender",
@ -40,14 +41,23 @@
"time":"Zeit",
"send_now":"Jetzt versenden",
"scann_code":"<strong>QR Code Scanner</strong> - Scanne den QR Code deines Partners",
"max_gdd_info":"maximale anzahl GDD zum versenden erreicht!",
"max_gdd_info":"Maximale anzahl GDD zum versenden erreicht!",
"send_check":"Bestätige deine Zahlung. Prüfe bitte nochmal alle Daten!",
"thx":"Danke!",
"send_success":"Deine Zahlung wurde erfolgreich versendet."
"thx":"Danke",
"sorry":"Entschuldigung",
"send_transaction_success":"Deine Transaktion wurde erfolgreich ausgeführt",
"send_transaction_error":"Leider konnte die Transaktion nicht ausgeführt werden!",
"validation": {
"double": "Das Feld {field} muss eine Dezimalzahl mit zwei Nachkommastellen sein",
"is-not": "Du kannst Dir selbst keine Gradidos überweisen"
}
},
"error": {
"error":"Fehler"
},
"transaction":{
"show_part": "Die letzten <strong>{count}</strong> Transaktionen",
"show_all":"Alle <strong>{count}</strong> Transaktionen ansehen",
"nullTransactions":"Du hast noch keine Transaktionen auf deinem Konto.",
"more": "mehr"
},
"site": {
@ -113,7 +123,7 @@
}
},
"reset-password": {
"title": "Passwort Zurücksetzen",
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünfitg in der GRADIDO App anmelden kannst."
"title": "Passwort zurücksetzen",
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst."
}
}

View File

@ -13,14 +13,15 @@
"back":"Back",
"send":"Send",
"transactions":"Transactions",
"language":"Sprache",
"language":"Language",
"languages":{
"de": "Deutsch",
"en": "English"
},
"form": {
"attention": "Achtung! Bitte überprüfe alle deine Eingaben sehr genau. Du bist alleine Verantwortlich für deine Entscheidungen. Versendete Gradidos können nicht wieder zurück geholt werden.",
"attention": "Attention! Please check all your entries very carefully. You are solely responsible for your decisions. Sent Gradidos cannot be retrieved.",
"cancel":"Cancel",
"reset": "Reset",
"close":"Close",
"receiver":"Receiver",
"sender":"Sender",
@ -39,15 +40,25 @@
"at":"at",
"time":"Time",
"send_now":"Send now",
"scann_code":"<strong>QR Code Scanner</strong> - Scanne den QR Code deines Partners",
"max_gdd_info":"maximale anzahl GDD zum versenden erreicht!",
"send_check":"Bestätige deine Zahlung. Prüfe bitte nochmal alle Daten!",
"thx":"THX",
"send_success":"Deine Zahlung wurde erfolgreich versendet."
"scann_code":"<strong>QR Code Scanner</strong> - Scan the QR Code of your partner",
"max_gdd_info":"Maximum number of GDDs to be sent has been reached!",
"send_check":"Confirm your payment. Please check all data again!",
"thx":"Thank you",
"sorry":"Sorry",
"send_transaction_success":"Your transaction was successfully completed",
"send_transaction_error":"Unfortunately, the transaction could not be executed!",
"validation": {
"double": "The {field} field must be a decimal with two digits",
"is-not": "You cannot send Gradidos to yourself"
}
},
"error": {
"error":"Error"
},
"transaction":{
"show_part": "The last <strong>{count}</strong> transactions",
"show_all":"View all <strong>{count}</strong> transactions",
"show_all":"View all <strong>{count}</strong> transactions.",
"show_part": "The last <strong>{count}</strong> transactions.",
"nullTransactions":"You don't have any transactions on your account yet.",
"more": "more"
},
"site": {
@ -114,6 +125,6 @@
},
"reset-password": {
"title": "Reset Password",
"text": "Now you can save a new password to login to the GRADIDO App in the future."
"text": "Now you can save a new password to login to the Gradido-App in the future."
}
}

View File

@ -2,7 +2,9 @@ import Vue from 'vue'
import DashboardPlugin from './plugins/dashboard-plugin'
import App from './App.vue'
import i18n from './i18n.js'
import VeeValidate from './vee-validate.js'
import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, between, double, is_not } from 'vee-validate/dist/rules'
// store
import { store } from './store/store'
@ -15,13 +17,52 @@ Vue.use(DashboardPlugin)
Vue.config.productionTip = false
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.session_id) {
if (to.meta.requiresAuth && !store.state.sessionId) {
next({ path: '/login' })
} else {
next()
}
})
configure({
defaultMessage: (field, values) => {
values._field_ = i18n.t(`fields.${field}`)
return i18n.t(`validations.messages.${values._rule_}`, values)
},
})
extend('email', {
...email,
message: (_, values) => i18n.t('validations.messages.email', values),
})
extend('required', {
...required,
message: (_, values) => i18n.t('validations.messages.required', values),
})
extend('min', {
...min,
message: (_, values) => i18n.t('validations.messages.min', values),
})
extend('double', {
...double,
message: (_, values) => i18n.t('form.validation.double', values),
})
extend('between', {
...between,
message: (_, values) => i18n.t('validations.messages.between', values),
})
// eslint-disable-next-line camelcase
extend('is_not', {
// eslint-disable-next-line camelcase
...is_not,
message: (_, values) => i18n.t('form.validation.is-not', values),
})
/* eslint-disable no-new */
new Vue({
el: '#app',

View File

@ -3,7 +3,7 @@ import '@/polyfills'
// Notifications plugin. Used on Notifications page
import Notifications from '@/components/NotificationPlugin'
// Validation plugin used to validate forms
import { configure } from 'vee-validate'
import { configure, extend } from 'vee-validate'
// A plugin file where you could register global components used across the app
import GlobalComponents from './globalComponents'
// A plugin file where you could register global directives
@ -14,7 +14,6 @@ import SideBar from '@/components/SidebarPlugin'
// element ui language configuration
import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'
locale.use(lang)
// vue-bootstrap
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
@ -22,7 +21,6 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
// asset imports
import '@/assets/scss/argon.scss'
import '@/assets/vendor/nucleo/css/nucleo.css'
import { extend } from 'vee-validate'
import * as rules from 'vee-validate/dist/rules'
import { messages } from 'vee-validate/dist/locale/en.json'
@ -40,6 +38,7 @@ import VueMoment from 'vue-moment'
import Loading from 'vue-loading-overlay'
// import the styles
import 'vue-loading-overlay/dist/vue-loading.css'
locale.use(lang)
Object.keys(rules).forEach((rule) => {
extend(rule, {

View File

@ -9,7 +9,7 @@ const routes = [
},
{
path: '/overview',
component: () => import('../views/Pages/KontoOverview.vue'),
component: () => import('../views/Pages/AccountOverview.vue'),
meta: {
requiresAuth: true,
},
@ -21,20 +21,20 @@ const routes = [
requiresAuth: true,
},
},
//{
// {
// path: '/profileedit',
// component: () => import('../views/Pages/UserProfileEdit.vue'),
// meta: {
// requiresAuth: true,
// },
//},
//{
// },
// {
// path: '/activity',
// component: () => import('../views/Pages/UserProfileActivity.vue'),
// meta: {
// requiresAuth: true,
// },
//},
// },
{
path: '/transactions',
component: () => import('../views/Pages/UserProfileTransactionList.vue'),

View File

@ -1,7 +1,7 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
export const mutations = {
language: (state, language) => {
@ -10,18 +10,18 @@ export const mutations = {
email: (state, email) => {
state.email = email
},
session_id: (state, session_id) => {
state.session_id = session_id
sessionId: (state, sessionId) => {
state.sessionId = sessionId
},
}
export const actions = {
login: ({ dispatch, commit }, data) => {
commit('session_id', data.session_id)
commit('sessionId', data.sessionId)
commit('email', data.email)
},
logout: ({ commit, state }) => {
commit('session_id', null)
commit('sessionId', null)
commit('email', null)
sessionStorage.clear()
},
@ -34,7 +34,7 @@ export const store = new Vuex.Store({
}),
],
state: {
session_id: null,
sessionId: null,
email: '',
language: 'en',
modals: false,

View File

@ -1,6 +1,6 @@
import { mutations, actions } from './store'
const { language, email, session_id } = mutations
const { language, email, sessionId } = mutations
const { login, logout } = actions
describe('Vuex store', () => {
@ -21,11 +21,11 @@ describe('Vuex store', () => {
})
})
describe('session_id', () => {
it('sets the state of session_id', () => {
const state = { session_id: null }
session_id(state, '1234')
expect(state.session_id).toEqual('1234')
describe('sessionId', () => {
it('sets the state of sessionId', () => {
const state = { sessionId: null }
sessionId(state, '1234')
expect(state.sessionId).toEqual('1234')
})
})
})
@ -36,17 +36,17 @@ describe('Vuex store', () => {
const state = {}
it('calls two commits', () => {
login({ commit, state }, { session_id: 1234, email: 'someone@there.is' })
login({ commit, state }, { sessionId: 1234, email: 'someone@there.is' })
expect(commit).toHaveBeenCalledTimes(2)
})
it('commits session_id', () => {
login({ commit, state }, { session_id: 1234, email: 'someone@there.is' })
expect(commit).toHaveBeenNthCalledWith(1, 'session_id', 1234)
it('commits sessionId', () => {
login({ commit, state }, { sessionId: 1234, email: 'someone@there.is' })
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', 1234)
})
it('commits email', () => {
login({ commit, state }, { session_id: 1234, email: 'someone@there.is' })
login({ commit, state }, { sessionId: 1234, email: 'someone@there.is' })
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'someone@there.is')
})
})
@ -60,9 +60,9 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenCalledTimes(2)
})
it('commits session_id', () => {
it('commits sessionId', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(1, 'session_id', null)
expect(commit).toHaveBeenNthCalledWith(1, 'sessionId', null)
})
it('commits email', () => {

View File

@ -1,25 +0,0 @@
import { configure, extend } from 'vee-validate'
import { required, email, min } from 'vee-validate/dist/rules'
import i18n from './i18n'
configure({
defaultMessage: (field, values) => {
values._field_ = i18n.t(`fields.${field}`)
return i18n.t(`validations.messages.${values._rule_}`, values)
},
})
extend('email', {
...email,
message: (_, values) => i18n.t('validations.messages.email', values),
})
extend('required', {
...required,
message: (_, values) => i18n.t('validations.messages.required', values),
})
extend('min', {
...min,
message: (_, values) => i18n.t('validations.messages.min', values),
})

View File

@ -1,11 +1,9 @@
<template>
<div class="main-content">
<template>
<div class="main-content">
<router-view></router-view>
<content-footer v-if="!$route.meta.hideFooter"></content-footer>
</div>
</template>
<div class="wrapper">
<div class="main-content">
<router-view></router-view>
<content-footer v-if="!$route.meta.hideFooter"></content-footer>
</div>
</div>
</template>
<script>

View File

@ -7,7 +7,7 @@ const localVue = global.localVue
describe('ContentFooter', () => {
let wrapper
let mocks = {
const mocks = {
$i18n: {
locale: 'en',
},
@ -98,6 +98,12 @@ describe('ContentFooter', () => {
)
})
it('links to the support', () => {
expect(wrapper.findAll('a.nav-link').at(4).attributes('href')).toEqual(
'https://gradido.net/en/contact/',
)
})
describe('links are localized', () => {
beforeEach(() => {
mocks.$i18n.locale = 'de'
@ -132,6 +138,12 @@ describe('ContentFooter', () => {
'https://docs.google.com/document/d/1jZp-DiiMPI9ZPNXmjsvOQ1BtnfDFfx8BX7CDmA8KKjY/edit?usp=sharing',
)
})
it('links to the German support-page when locale is de', () => {
expect(wrapper.findAll('a.nav-link').at(4).attributes('href')).toEqual(
'https://gradido.net/de/contact/',
)
})
})
})
})

View File

@ -4,7 +4,11 @@
<b-col>
<div class="copyright text-center text-lg-center text-muted">
© {{ year }}
<a :href="`https://gradido.net/${$i18n.locale}`" class="font-weight-bold ml-1">
<a
:href="`https://gradido.net/${$i18n.locale}`"
class="font-weight-bold ml-1"
target="_blank"
>
Gradido-Akademie
</a>
|
@ -39,6 +43,9 @@
>
{{ $t('whitepaper') }}
</b-nav-item>
<b-nav-item :href="`https://gradido.net/${$i18n.locale}/contact/`" target="_blank">
{{ $t('site.navbar.support') }}
</b-nav-item>
</b-nav>
</b-col>
</b-row>

View File

@ -20,7 +20,7 @@ const transitionStub = () => ({
describe('DashboardLayoutGdd', () => {
let wrapper
let mocks = {
const mocks = {
$i18n: {
locale: 'en',
},
@ -28,7 +28,7 @@ describe('DashboardLayoutGdd', () => {
$n: jest.fn(),
}
let state = {
const state = {
user: {
name: 'Peter Lustig',
balance: 2546,
@ -37,12 +37,12 @@ describe('DashboardLayoutGdd', () => {
email: 'peter.lustig@example.org',
}
let stubs = {
const stubs = {
RouterLink: RouterLinkStub,
FadeTransition: transitionStub(),
}
let store = new Vuex.Store({
const store = new Vuex.Store({
state,
})
@ -79,7 +79,7 @@ describe('DashboardLayoutGdd', () => {
})
it('has five items in the navbar', () => {
expect(navbar.findAll('ul > li')).toHaveLength(3)
expect(navbar.findAll('ul > li')).toHaveLength(2)
})
it('has first item "send" in navbar', () => {
@ -104,41 +104,41 @@ describe('DashboardLayoutGdd', () => {
expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/transactions')
})
it('has third item "My profile" in navbar', () => {
expect(navbar.findAll('ul > li').at(2).text()).toEqual('site.navbar.my-profil')
})
it.skip('has third item "My profile" linked to profile in navbar', async () => {
navbar.findAll('ul > li > a').at(2).trigger('click')
await flushPromises()
await jest.runAllTimers()
await flushPromises()
expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profile')
})
//it('has fourth item "Settigs" in navbar', () => {
// expect(navbar.findAll('ul > li').at(3).text()).toEqual('site.navbar.settings')
//})
// it('has third item "My profile" in navbar', () => {
// expect(navbar.findAll('ul > li').at(2).text()).toEqual('site.navbar.my-profil')
// })
//
//it.skip('has fourth item "Settings" linked to profileedit in navbar', async () => {
// it.skip('has third item "My profile" linked to profile in navbar', async () => {
// navbar.findAll('ul > li > a').at(2).trigger('click')
// await flushPromises()
// await jest.runAllTimers()
// await flushPromises()
// expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profile')
// })
// it('has fourth item "Settigs" in navbar', () => {
// expect(navbar.findAll('ul > li').at(3).text()).toEqual('site.navbar.settings')
// })
//
// it.skip('has fourth item "Settings" linked to profileedit in navbar', async () => {
// navbar.findAll('ul > li > a').at(3).trigger('click')
// await flushPromises()
// await jest.runAllTimers()
// await flushPromises()
// expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/profileedit')
//})
// })
//it('has fifth item "Activity" in navbar', () => {
// it('has fifth item "Activity" in navbar', () => {
// expect(navbar.findAll('ul > li').at(4).text()).toEqual('site.navbar.activity')
//})
// })
//
//it.skip('has fourth item "Activity" linked to activity in navbar', async () => {
// it.skip('has fourth item "Activity" linked to activity in navbar', async () => {
// navbar.findAll('ul > li > a').at(4).trigger('click')
// await flushPromises()
// await jest.runAllTimers()
// await flushPromises()
// expect(wrapper.findComponent(RouterLinkStub).props().to).toBe('/activity')
//})
// })
})
})
})

View File

@ -9,10 +9,10 @@
<b-nav-item href="#!" to="/transactions">
<b-nav-text class="p-0 text-lg text-muted">{{ $t('transactions') }}</b-nav-text>
</b-nav-item>
<!--
<b-nav-item href="#!" to="/profile">
<b-nav-text class="p-0 text-lg text-muted">{{ $t('site.navbar.my-profil') }}</b-nav-text>
</b-nav-item>
<!--
</b-nav-item>
<b-nav-item href="#!" to="/profileedit">
<b-nav-text class="p-0 text-lg text-muted">{{ $t('site.navbar.settings') }}</b-nav-text>
</b-nav-item>
@ -31,7 +31,10 @@
<router-view
:balance="balance"
:gdt-balance="GdtBalance"
:transactions="transactions"
:transactionCount="transactionCount"
@update-balance="updateBalance"
@update-transactions="updateTransactions"
></router-view>
</fade-transition>
</div>
@ -44,12 +47,19 @@ import PerfectScrollbar from 'perfect-scrollbar'
import 'perfect-scrollbar/css/perfect-scrollbar.css'
import loginAPI from '../../apis/loginAPI'
import DashboardNavbar from './DashboardNavbar.vue'
import ContentFooter from './ContentFooter.vue'
// import DashboardContent from './Content.vue';
import { FadeTransition } from 'vue2-transitions'
import communityAPI from '../../apis/communityAPI'
function hasElement(className) {
return document.getElementsByClassName(className).length > 0
}
function initScrollbar(className) {
if (hasElement(className)) {
// eslint-disable-next-line no-new
new PerfectScrollbar(`.${className}`)
} else {
// try to init it later in case this component is loaded async
@ -59,12 +69,6 @@ function initScrollbar(className) {
}
}
import DashboardNavbar from './DashboardNavbar.vue'
import ContentFooter from './ContentFooter.vue'
// import DashboardContent from './Content.vue';
import { FadeTransition } from 'vue2-transitions'
import communityAPI from '../../apis/communityAPI'
export default {
components: {
DashboardNavbar,
@ -76,33 +80,32 @@ export default {
return {
balance: 0,
GdtBalance: 0,
transactions: [],
bookedBalance: 0,
transactionCount: 0,
}
},
methods: {
initScrollbar() {
let isWindows = navigator.platform.startsWith('Win')
const isWindows = navigator.platform.startsWith('Win')
if (isWindows) {
initScrollbar('sidenav')
}
},
async logout() {
const result = await loginAPI.logout(this.$store.state.session_id)
await loginAPI.logout(this.$store.state.sessionId)
// do we have to check success?
this.$store.dispatch('logout')
this.$router.push('/login')
},
async loadBalance() {
const result = await communityAPI.balance(this.$store.state.session_id)
async updateTransactions() {
const result = await communityAPI.transactions(this.$store.state.sessionId)
if (result.success) {
this.balance = result.result.data.balance / 10000
} else {
// what to do when loading balance fails?
}
},
async loadGDTBalance() {
const result = await communityAPI.transactions(this.$store.state.session_id)
if (result.success) {
this.GdtBalance = result.result.data.gdtSum / 10000
this.GdtBalance = Number(result.result.data.gdtSum)
this.transactions = result.result.data.transactions
this.balance = Number(result.result.data.decay)
this.bookedBalance = Number(result.result.data.balance)
this.transactionCount = result.result.data.count
} else {
// what to do when loading balance fails?
}
@ -115,8 +118,7 @@ export default {
this.initScrollbar()
},
created() {
this.loadBalance()
this.loadGDTBalance()
this.updateTransactions()
},
}
</script>

View File

@ -2,10 +2,9 @@
<div>
<!-- Header -->
<div class="header py-1 py-lg-1 pt-lg-3">
<b-container>xx</b-container>
<b-container>
<div class="header-body text-center mb-3">
<a href="/login" to="/login">
<a href="login" to="login">
<div class="container">
<div class="row">
<div class="col-sm-12 col-md-12 mt-5 mb-5">

View File

@ -1,17 +1,17 @@
import { shallowMount } from '@vue/test-utils'
import KontoOverview from './KontoOverview'
import AccountOverview from './AccountOverview'
const localVue = global.localVue
describe('KontoOverview', () => {
describe('AccountOverview', () => {
let wrapper
let mocks = {
const mocks = {
$t: jest.fn((t) => t),
}
const Wrapper = () => {
return shallowMount(KontoOverview, { localVue, mocks })
return shallowMount(AccountOverview, { localVue, mocks })
}
describe('shallow Mount', () => {
@ -35,12 +35,6 @@ describe('KontoOverview', () => {
expect(wrapper.find('gdd-table-stub').exists()).toBeTruthy()
})
it('updates transctions data when change-transactions is emitted', async () => {
wrapper.find('gdd-table-stub').vm.$emit('change-transactions', [0, 1])
await wrapper.vm.$nextTick()
expect(wrapper.vm.transactions).toEqual(expect.arrayContaining([0, 1]))
})
describe('updateBalance method', () => {
beforeEach(async () => {
wrapper.find('gdd-send-stub').vm.$emit('update-balance', {

View File

@ -1,12 +1,8 @@
<template>
<div>
<base-header class="pb-6 pb-8 pt-5 pt-md-8 bg-transparent"></base-header>
<b-container fluid class="mt--7">
<gdd-status
:balance="balance"
:gdt-balance="GdtBalance"
:show-transaction-list="showTransactionList"
/>
<base-header class="pb-4 pt-2 bg-transparent"></base-header>
<b-container fluid class="p-2 mt-5">
<gdd-status v-if="showTransactionList" :balance="balance" :gdt-balance="GdtBalance" />
<br />
<gdd-send
:balance="balance"
@ -16,34 +12,44 @@
/>
<hr />
<gdd-table
:show-transaction-list="showTransactionList"
v-if="showTransactionList"
:transactions="transactions"
@change-transactions="setTransactions"
:max="5"
:timestamp="timestamp"
:transactionCount="transactionCount"
@update-transactions="updateTransactions"
/>
<gdd-table-footer :count="transactionCount" />
</b-container>
</div>
</template>
<script>
import GddStatus from '../KontoOverview/GddStatus.vue'
import GddSend from '../KontoOverview/GddSend.vue'
import GddTable from '../KontoOverview/GddTable.vue'
import GddStatus from './AccountOverview/GddStatus.vue'
import GddSend from './AccountOverview/GddSend.vue'
import GddTable from './AccountOverview/GddTable.vue'
import GddTableFooter from './AccountOverview/GddTableFooter.vue'
export default {
name: 'Overview',
components: {
GddStatus,
GddSend,
GddTable,
GddTableFooter,
},
data() {
return {
transactions: [],
showTransactionList: true,
timestamp: Date.now(),
}
},
props: {
balance: { type: Number, default: 0 },
GdtBalance: { type: Number, default: 0 },
},
components: {
GddStatus,
GddSend,
GddTable,
transactions: {
default: () => [],
},
transactionCount: { type: Number, default: 0 },
},
methods: {
toggleShowList(bool) {
@ -52,8 +58,8 @@ export default {
updateBalance(data) {
this.$emit('update-balance', data.ammount)
},
setTransactions(transactions) {
this.transactions = transactions
updateTransactions() {
this.$emit('update-transactions')
},
},
}

View File

@ -102,7 +102,7 @@ export default {
created() {},
watch: {
$form: function () {
stunden(this.form)
this.stunden(this.form)
},
},
mounted() {},
@ -133,16 +133,16 @@ export default {
},
deleteNewMessage: function (event) {
this.form.splice(event, null)
this.messages.splice(index, 1)
this.messages.splice(this.index, 1)
this.index--
},
submitForm: function (e) {
//console.log('submitForm')
// console.log('submitForm')
this.messages = [{ DaysNumber: '', TextDecoded: '' }]
this.submitted = true
},
textFocus() {
//console.log('textFocus TODO')
// console.log('textFocus TODO')
},
newWorkForm() {
this.formular = `
@ -174,7 +174,7 @@ export default {
></textarea>
</base-input>
</b-col>
`
`
// console.log('newWorkForm TODO')
const myElement = this.$refs.mydiv

View File

@ -0,0 +1,129 @@
import { mount } from '@vue/test-utils'
import GddSend from './GddSend'
import Vuex from 'vuex'
const localVue = global.localVue
describe('GddSend', () => {
let wrapper
const state = {
user: {
balance: 1234,
balance_gdt: 9876,
},
}
const store = new Vuex.Store({
state,
})
const mocks = {
// $n: jest.fn((n) => n),
$t: jest.fn((t) => t),
$moment: jest.fn((m) => ({
format: () => m,
})),
$i18n: {
locale: jest.fn(() => 'en'),
},
$n: jest.fn((n) => String(n)),
}
const Wrapper = () => {
return mount(GddSend, { localVue, store, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.gdd-send').exists()).toBeTruthy()
})
describe('warning messages', () => {
it('has a warning message', () => {
expect(wrapper.find('div.alert-default').find('span').text()).toBe('form.attention')
})
})
describe('transaction form', () => {
describe('email field', () => {
it('has an input field of type email', () => {
expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email')
})
it('has an envelope icon', () => {
expect(wrapper.find('#input-group-1').find('svg').attributes('aria-label')).toBe(
'envelope',
)
})
it('has a label form.receiver', () => {
expect(wrapper.findAll('div.text-left').at(0).text()).toBe('form.receiver')
})
it('has a placeholder "E-Mail"', () => {
expect(wrapper.find('#input-group-1').find('input').attributes('placeholder')).toBe(
'E-Mail',
)
})
})
describe('ammount field', () => {
it('has an input field of type number', () => {
expect(wrapper.find('#input-group-2').find('input').attributes('type')).toBe('number')
})
it('has an GDD text icon', () => {
expect(wrapper.find('#input-group-2').find('div.h3').text()).toBe('GDD')
})
it('has a label form.amount', () => {
expect(wrapper.findAll('div.text-left').at(1).text()).toBe('form.amount')
})
it('has a placeholder "0.01"', () => {
expect(wrapper.find('#input-group-2').find('input').attributes('placeholder')).toBe(
'0.01',
)
})
})
describe('message text box', () => {
it('has an textarea field', () => {
expect(wrapper.find('#input-group-3').find('textarea').exists()).toBeTruthy()
})
it('has an chat-right-text icon', () => {
expect(wrapper.find('#input-group-3').find('svg').attributes('aria-label')).toBe(
'chat right text',
)
})
it('has a label form.memo', () => {
expect(wrapper.findAll('div.text-left').at(2).text()).toBe('form.memo')
})
})
describe('cancel button', () => {
it('has a cancel button', () => {
expect(wrapper.find('button[type="reset"]').exists()).toBeTruthy()
})
it('has the text "form.cancel"', () => {
expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset')
})
it.skip('clears the email field on click', async () => {
wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
wrapper.find('button[type="reset"]').trigger('click')
await wrapper.vm.$nextTick()
expect(wrapper.vm.form.email).toBeNull()
})
})
})
})
})

View File

@ -1,11 +1,12 @@
<template>
<div>
<div class="gdd-send">
<b-row v-show="showTransactionList">
<b-col xl="12" md="12">
<b-alert show dismissible variant="warning" class="text-center">
<span class="alert-text" v-html="$t('form.attention')"></span>
<b-alert show dismissible variant="default" class="text-center">
<span class="alert-text h3 text-light" v-html="$t('form.attention')"></span>
</b-alert>
<b-card class="p-0 p-md-3" style="background-color: #ebebeba3 !important">
<!--
<b-alert show variant="secondary">
<span class="alert-text" v-html="$t('form.scann_code')"></span>
<b-col v-show="!scan" lg="12" class="text-right">
@ -15,9 +16,9 @@
</b-col>
<div v-if="scan">
<!-- <b-row>
<b-row>
<qrcode-capture @detect="onDetect" capture="user" ></qrcode-capture>
</b-row> -->
</b-row>
<qrcode-stream class="mt-3" @decode="onDecode" @detect="onDetect"></qrcode-stream>
@ -39,6 +40,7 @@
</b-alert>
</div>
</b-alert>
-->
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form
@ -47,83 +49,105 @@
@reset="onReset"
v-if="show"
>
<br />
<div>
<!-- <div>
<qrcode-drop-zone id="input-0" v-model="form.img"></qrcode-drop-zone>
</div>
<br />
-->
<div>
<b-col class="text-left p-3 p-sm-1">{{ $t('form.receiver') }}</b-col>
<b-input-group
id="input-group-1"
label="Empfänger:"
label-for="input-1"
description="We'll never share your email with anyone else."
size="lg"
class="mb-3"
<validation-provider
name="Email"
:rules="{
required: true,
email: true,
is_not: $store.state.email,
}"
v-slot="{ errors }"
>
<b-input-group-prepend class="p-3 d-none d-md-block">
<b-icon icon="envelope" class="display-3"></b-icon>
</b-input-group-prepend>
<b-form-input
id="input-1"
v-model="form.email"
type="email"
placeholder="E-Mail"
:rules="{ required: true, email: true }"
required
style="font-size: xx-large; padding-left: 20px"
></b-form-input>
</b-input-group>
<b-row>
<b-col class="text-left p-3 p-sm-1">{{ $t('form.receiver') }}</b-col>
<b-col v-if="errors" class="text-right p-3 p-sm-1">
<span v-for="error in errors" :key="error" class="errors">{{ error }}</span>
</b-col>
</b-row>
<b-input-group
id="input-group-1"
label="Empfänger:"
label-for="input-1"
description="We'll never share your email with anyone else."
size="lg"
class="mb-3"
>
<b-input-group-prepend class="p-3 d-none d-md-block">
<b-icon icon="envelope" class="display-3"></b-icon>
</b-input-group-prepend>
<b-form-input
id="input-1"
v-model="form.email"
type="email"
placeholder="E-Mail"
style="font-size: xx-large; padding-left: 20px"
></b-form-input>
</b-input-group>
</validation-provider>
</div>
<br />
<div>
<b-col class="text-left p-3 p-sm-1">{{ $t('form.amount') }}</b-col>
<b-col v-if="balance == form.amount" class="text-right">
<b-badge variant="primary">{{ $t('form.max_gdd_info') }}</b-badge>
</b-col>
<b-input-group
id="input-group-2"
label="Betrag:"
label-for="input-2"
size="lg"
class="mb-3"
<validation-provider
:name="$t('form.amount')"
:rules="{
required: true,
double: [2, $i18n.locale === 'de' ? ',' : '.'],
between: [0.01, balance],
}"
v-slot="{ errors }"
>
<b-input-group-prepend class="p-2 d-none d-md-block">
<div class="h3 pt-3 pr-3">GDD</div>
</b-input-group-prepend>
<b-form-input
id="input-2"
v-model="form.amount"
type="number"
placeholder="0.01"
step="0.01"
min="0.01"
:max="balance"
style="font-size: xx-large; padding-left: 20px"
></b-form-input>
</b-input-group>
<b-col class="text-left p-3 p-sm-1">{{ $t('form.memo') }}</b-col>
<b-input-group>
<b-input-group-prepend class="p-3 d-none d-md-block">
<b-icon icon="chat-right-text" class="display-3"></b-icon>
</b-input-group-prepend>
<b-form-textarea
rows="3"
v-model="form.memo"
class="pl-3"
style="font-size: x-large"
></b-form-textarea>
</b-input-group>
<b-row>
<b-col class="text-left p-3 p-sm-1">{{ $t('form.amount') }}</b-col>
<b-col v-if="errors" class="text-right p-3 p-sm-1">
<span v-for="error in errors" class="errors" :key="error">{{ error }}</span>
</b-col>
</b-row>
<b-input-group
id="input-group-2"
label="Betrag:"
label-for="input-2"
size="lg"
class="mb-3"
>
<b-input-group-prepend class="p-2 d-none d-md-block">
<div class="h3 pt-3 pr-3">GDD</div>
</b-input-group-prepend>
<b-form-input
id="input-2"
v-model="form.amount"
type="number"
:lang="$i18n.locale"
:placeholder="$n(0.01)"
step="0.01"
style="font-size: xx-large; padding-left: 20px"
></b-form-input>
</b-input-group>
<b-col class="text-left p-3 p-sm-1">{{ $t('form.memo') }}</b-col>
<b-input-group id="input-group-3">
<b-input-group-prepend class="p-3 d-none d-md-block">
<b-icon icon="chat-right-text" class="display-3"></b-icon>
</b-input-group-prepend>
<b-form-textarea
rows="3"
v-model="form.memo"
class="pl-3"
style="font-size: x-large"
></b-form-textarea>
</b-input-group>
</validation-provider>
</div>
<br />
<b-row>
<b-col>
<b-button type="reset" variant="secondary">
{{ $t('form.cancel') }}
<b-button type="reset" variant="secondary" @click="onReset">
{{ $t('form.reset') }}
</b-button>
</b-col>
<b-col class="text-right">
@ -155,7 +179,7 @@
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
{{ ajaxCreateData.memo }}
{{ ajaxCreateData.memo ? ajaxCreateData.memo : '-' }}
<b-badge variant="primary" pill>{{ $t('form.message') }}</b-badge>
</b-list-group-item>
<b-list-group-item class="d-flex justify-content-between align-items-center">
@ -178,29 +202,46 @@
</b-row>
<b-row v-show="row_thx">
<b-col>
<div class="display-1 p-4">
{{ $t('form.thx') }}
<hr />
{{ $t('form.send_success') }}
</div>
<b-card class="p-0 p-md-3" style="background-color: #ebebeba3 !important">
<div class="display-2 p-4">
{{ $t('form.thx') }}
<hr />
{{ $t('form.send_transaction_success') }}
</div>
<b-button variant="success" @click="onReset">{{ $t('form.close') }}</b-button>
<hr />
<p class="text-center">
<b-button variant="success" @click="onReset">{{ $t('form.close') }}</b-button>
</p>
</b-card>
</b-col>
</b-row>
<b-row v-show="row_error">
<b-col>
<b-card class="p-0 p-md-3" style="background-color: #ebebeba3 !important">
<div class="display-2 p-4">
{{ $t('form.sorry') }}
<hr />
{{ $t('form.send_transaction_error') }}
</div>
<p class="text-center">
<b-button variant="success" @click="onReset">{{ $t('form.close') }}</b-button>
</p>
</b-card>
</b-col>
</b-row>
</div>
</template>
<script>
import { QrcodeStream, QrcodeDropZone } from 'vue-qrcode-reader'
// import { QrcodeStream, QrcodeDropZone } from 'vue-qrcode-reader'
import { BIcon } from 'bootstrap-vue'
import communityAPI from '../../apis/communityAPI.js'
import communityAPI from '../../../apis/communityAPI.js'
export default {
name: 'GddSent',
components: {
QrcodeStream,
QrcodeDropZone,
// QrcodeStream,
// QrcodeDropZone,
BIcon,
},
props: {
@ -209,7 +250,7 @@ export default {
},
data() {
return {
scan: false,
// scan: false,
show: true,
form: {
img: '',
@ -227,21 +268,22 @@ export default {
send: false,
row_check: false,
row_thx: false,
row_error: false,
}
},
computed: {},
methods: {
toggle() {
this.scan = !this.scan
},
async onDecode(decodedString) {
const arr = JSON.parse(decodedString)
this.form.email = arr[0].email
this.form.amount = arr[0].amount
this.scan = false
},
// toggle() {
// this.scan = !this.scan
// },
// async onDecode(decodedString) {
// const arr = JSON.parse(decodedString)
// this.form.email = arr[0].email
// this.form.amount = arr[0].amount
// this.scan = false
// },
async onSubmit() {
//event.preventDefault()
// event.preventDefault()
this.ajaxCreateData.email = this.form.email
this.ajaxCreateData.amount = this.form.amount
const now = new Date(Date.now()).toISOString()
@ -250,12 +292,13 @@ export default {
this.$emit('toggle-show-list', false)
this.row_check = true
this.row_thx = false
this.row_error = false
},
async sendTransaction() {
const result = await communityAPI.send(
this.$store.state.session_id,
this.$store.state.sessionId,
this.ajaxCreateData.email,
this.ajaxCreateData.amount * 10000,
this.ajaxCreateData.amount,
this.ajaxCreateData.memo,
this.ajaxCreateData.target_date,
)
@ -263,22 +306,25 @@ export default {
this.$emit('toggle-show-list', false)
this.row_check = false
this.row_thx = true
this.row_error = false
this.$emit('update-balance', { ammount: this.ajaxCreateData.amount })
} else {
alert('error')
this.$emit('toggle-show-list', true)
this.row_check = false
this.row_thx = false
this.row_error = true
}
},
onReset(event) {
event.preventDefault()
this.form.email = ''
this.form.amount = ''
this.form.memo = ''
this.show = false
this.$emit('toggle-show-list', true)
this.row_check = false
this.row_thx = false
this.row_error = false
this.$nextTick(() => {
this.show = true
})
@ -294,4 +340,7 @@ video {
max-height: 665px;
max-width: 665px;
}
span.errors {
color: red;
}
</style>

View File

@ -6,11 +6,11 @@ const localVue = global.localVue
describe('GddStatus', () => {
let wrapper
let mocks = {
const mocks = {
$n: jest.fn((n) => n),
}
let propsData = {
const propsData = {
balance: 1234,
GdtBalance: 9876,
}

View File

@ -1,7 +1,7 @@
<template>
<div>
<b-row v-show="showTransactionList">
<b-col xl="6" md="6">
<b-row>
<b-col>
<stats-card
type="gradient-red"
sub-title="balance_gdd"
@ -11,7 +11,7 @@
{{ $n(balance) }} GDD
</stats-card>
</b-col>
<b-col xl="6" md="6">
<b-col>
<stats-card
type="gradient-orange"
sub-title="balance_gdt"
@ -29,7 +29,6 @@
export default {
name: 'GddStatus',
props: {
showTransactionList: { type: Boolean, default: true },
balance: { type: Number, default: 0 },
GdtBalance: { type: Number, default: 0 },
},

View File

@ -1,28 +1,44 @@
<template>
<div>
<b-list-group v-show="showTransactionList">
<b-list-group>
<b-list-group-item
v-for="item in filteredItems"
v-for="item in transactions.slice(0, max)"
:key="item.id"
style="background-color: #ebebeba3 !important"
>
<div class="d-flex w-100 justify-content-between">
<b-icon
v-if="item.type === 'send'"
icon="box-arrow-left"
class="m-1"
icon="arrow-left-circle"
class="m-1 text-danger"
font-scale="2"
style="color: red"
></b-icon>
<b-icon
v-else
icon="box-arrow-right"
v-else-if="item.type === 'receive'"
icon="arrow-right-circle"
class="m-1"
font-scale="2"
style="color: green"
></b-icon>
<h1 class="mb-1">
{{ $n(item.balance / 10000) }}
<b-icon
v-else-if="item.type === 'creation'"
icon="gift"
class="m-1"
font-scale="2"
style="color: orange"
></b-icon>
<b-icon
v-else
icon="droplet-half"
class="m-1"
font-scale="2"
style="color: orange"
></b-icon>
<h1 class="">
<span v-if="item.type === 'receive' || item.type === 'creation'">+</span>
<span v-else>-</span>
{{ $n(item.balance) }}
<small>GDD</small>
</h1>
<h2 class="text-muted">{{ item.name }}</h2>
@ -72,68 +88,40 @@
</b-card>
</b-collapse>
</b-list-group-item>
<b-list-group-item v-show="this.$route.path == '/overview'">
<b-alert v-if="count < 5" show variant="secondary">
<span class="alert-text" v-html="$t('transaction.show_part', { count: count })"></span>
<b-list-group-item>
<b-alert v-if="transactions.length === 0" show variant="secondary">
<span class="alert-text">{{ $t('transaction.nullTransactions') }}</span>
</b-alert>
<router-link
v-else
to="/transactions"
v-html="$t('transaction.show_all', { count: count })"
></router-link>
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import axios from 'axios'
import communityAPI from '../../apis/communityAPI'
export default {
name: 'GddTable',
props: {
showTransactionList: { type: Boolean, default: true },
transactions: { default: [] },
max: { type: Number, default: 25 },
timestamp: { type: Number, default: 0 },
transactionCount: { type: Number, default: 0 },
},
data() {
return {
form: [],
fields: ['balance', 'date', 'memo', 'name', 'transaction_id', 'type', 'details'],
items: [],
count: 0,
}
},
async created() {
const result = await communityAPI.transactions(this.$store.state.session_id)
if (result.success) {
this.items = result.result.data.transactions
this.count = result.result.data.count
} else {
//console.log('error', result)
}
},
computed: {
filteredItems() {
return this.ojectToArray(this.items).reverse()
watch: {
timestamp: {
immediate: true,
handler: 'updateTransactions',
},
},
methods: {
ojectToArray(obj) {
let result = new Array(Object.keys(obj).length)
Object.entries(obj).forEach((entry) => {
const [key, value] = entry
result[key] = value
})
return result
},
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.type === 'receive') return 'table-success'
if (item.type === 'send') return 'table-warning'
if (item.type === 'creation') return 'table-primary'
updateTransactions() {
this.$emit('update-transactions')
},
},
}

View File

@ -0,0 +1,21 @@
<template>
<div>
<b-list-group>
<b-list-group-item v-if="count > 5">
<router-link
to="/transactions"
v-html="$t('transaction.show_all', { count: count })"
></router-link>
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
export default {
name: 'GddTableFooter',
props: {
count: { count: Number },
},
}
</script>

View File

@ -68,6 +68,7 @@ export default {
if (item.status === 'earned') return 'table-primary'
},
toogle(item) {
// eslint-disable-next-line no-unused-vars
const temp =
'<b-collapse visible v-bind:id="item.id">xxx <small class="text-muted">porta</small></b-collapse>'
},

View File

@ -16,7 +16,7 @@
<b-row class="justify-content-center">
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="px-lg-5 py-lg-5">
<b-card-body class="p-4">
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<base-input

View File

@ -9,22 +9,22 @@ const localVue = global.localVue
describe('Login', () => {
let wrapper
let mocks = {
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
}
let state = {
const state = {
loginfail: false,
}
let store = new Vuex.Store({
const store = new Vuex.Store({
state,
})
let stubs = {
const stubs = {
RouterLink: RouterLinkStub,
}

View File

@ -1,9 +1,9 @@
<template>
<div class="login-form">
<!-- Header -->
<div class="header p-4">
<div class="p-3">
<b-container>
<div class="text-center mb-7">
<div class="text-center mb-7 header">
<b-row class="justify-content-center">
<b-col xl="5" lg="6" md="8" class="px-2">
<h1>Gradido</h1>
@ -14,11 +14,11 @@
</b-container>
</div>
<!-- Page content -->
<b-container class="mt--8 p-1">
<b-container class="mt--8">
<b-row class="justify-content-center">
<b-col lg="5" md="7">
<b-card no-body class="border-0 mb-0" style="background-color: #ebebeba3 !important">
<b-card-body class="py-lg-4 px-sm-0 px-0 px-md-2 px-lg-4">
<b-card-body class="p-4">
<div class="text-center text-muted mb-4">
<small>{{ $t('login') }}</small>
</div>
@ -44,24 +44,14 @@
v-model="model.password"
></base-input>
<b-alert v-show="loginfail" show variant="warning">
<b-alert v-show="loginfail" show dismissible variant="warning">
<span class="alert-text bv-example-row">
<b-row>
<b-col class="col-9 text-left">
<b-col class="col-9 text-left text-dark">
<strong>
Leider konnten wir keinen Account finden mit diesen Daten!
</strong>
</b-col>
<b-col class="text-right">
<a @click="closeAlert">
<div>
<b-icon-exclamation-triangle-fill
class="h2 mb-0"
></b-icon-exclamation-triangle-fill>
<b-icon-x class="h1 pl-2"></b-icon-x>
</div>
</a>
</b-col>
</b-row>
</span>
</b-alert>
@ -112,13 +102,15 @@ export default {
},
methods: {
async onSubmit() {
let loader = this.$loading.show({
// error info ausschalten
this.loginfail = false
const loader = this.$loading.show({
container: this.$refs.submitButton,
})
const result = await loginAPI.login(this.model.email, this.model.password)
if (result.success) {
this.$store.dispatch('login', {
session_id: result.result.data.session_id,
sessionId: result.result.data.session_id,
email: this.model.email,
})
this.$router.push('/overview')
@ -128,10 +120,6 @@ export default {
this.loginfail = true
}
},
closeAlert() {
loader.hide()
this.loginfail = false
},
},
}
</script>

View File

@ -9,22 +9,22 @@ const localVue = global.localVue
describe('Register', () => {
let wrapper
let mocks = {
const mocks = {
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
}
let state = {
const state = {
// loginfail: false,
}
let store = new Vuex.Store({
const store = new Vuex.Store({
state,
})
let stubs = {
const stubs = {
RouterLink: RouterLinkStub,
}

View File

@ -19,7 +19,7 @@
<b-row class="justify-content-center">
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="py-lg-4 px-sm-0 px-0 px-md-2 px-lg-4">
<b-card-body class="p-4">
<div class="text-center text-muted mb-4">
<small>{{ $t('signup') }}</small>
</div>
@ -110,6 +110,20 @@
</base-input>
</b-col>
</b-row>
<b-alert
v-if="showError"
show
dismissible
variant="warning"
@dismissed="closeAlert"
>
<span class="alert-icon"><i class="ni ni-point"></i></span>
<span class="alert-text">
<strong>{{ $t('error.error') }}!</strong>
{{ messageError }}
</span>
</b-alert>
<div
class="text-center"
v-if="
@ -160,6 +174,8 @@ export default {
checkPassword: '',
passwordVisible: false,
submitted: false,
showError: false,
messageError: '',
}
},
methods: {
@ -175,7 +191,7 @@ export default {
)
if (result.success) {
this.$store.dispatch('login', {
session_id: result.result.data.session_id,
sessionId: result.result.data.session_id,
email: this.model.email,
})
this.model.email = ''
@ -184,11 +200,18 @@ export default {
this.password = ''
this.$router.push('/thx')
} else {
// todo: Display a proper error message!
this.$store.dispatch('logout')
this.$router.push('/login')
this.showError = true
this.messageError = result.result.message
}
},
closeAlert() {
this.showError = false
this.messageError = ''
this.model.email = ''
this.model.firstname = ''
this.model.lastname = ''
this.password = ''
},
},
computed: {
samePasswords() {
@ -209,8 +232,8 @@ export default {
return this.model.email !== ''
},
passwordValidation() {
let errors = []
for (let condition of this.rules) {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.password)) {
errors.push(condition.message)
}

View File

@ -11,9 +11,9 @@ const router = new VueRouter({ routes })
describe('ResetPassword', () => {
let wrapper
let emailVerification = jest.fn()
const emailVerification = jest.fn()
let mocks = {
const mocks = {
$i18n: {
locale: 'en',
},
@ -49,13 +49,13 @@ describe('ResetPassword', () => {
expect(wrapper.find('div.resetpwd-form').exists()).toBeTruthy()
})
//describe('Register header', () => {
// describe('Register header', () => {
// it('has a welcome message', () => {
// expect(wrapper.find('div.header').text()).toBe('site.signup.title site.signup.subtitle')
// })
//})
// })
//describe('links', () => {
// describe('links', () => {
// it('has a link "Back"', () => {
// expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual('back')
// })
@ -63,9 +63,9 @@ describe('ResetPassword', () => {
// it('links to /login when clicking "Back"', () => {
// expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/login')
// })
//})
// })
//describe('Register form', () => {
// describe('Register form', () => {
// it('has a register form', () => {
// expect(wrapper.find('form').exists()).toBeTruthy()
// })
@ -108,7 +108,7 @@ describe('ResetPassword', () => {
// })
// //TODO test different invalid password combinations
//})
// })
// TODO test submit button
})

View File

@ -19,7 +19,7 @@
<b-row class="justify-content-center">
<b-col lg="6" md="8">
<b-card no-body class="border-0" style="background-color: #ebebeba3 !important">
<b-card-body class="py-lg-4 px-sm-0 px-0 px-md-2 px-lg-4">
<b-card-body class="p-4">
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<b-form-group :label="$t('form.password')">
@ -35,11 +35,8 @@
></b-form-input>
<b-input-group-append>
<b-button variant="outline-primary">
<b-icon
:icon="passwordVisible ? 'eye' : 'eye-slash'"
@click="togglePasswordVisibility"
/>
<b-button variant="outline-primary" @click="togglePasswordVisibility">
<b-icon :icon="passwordVisible ? 'eye' : 'eye-slash'" />
</b-button>
</b-input-group-append>
</b-input-group>
@ -104,7 +101,7 @@ export default {
passwordVisible: false,
submitted: false,
authenticated: false,
session_id: null,
sessionId: null,
email: null,
}
},
@ -113,7 +110,7 @@ export default {
this.passwordVisible = !this.passwordVisible
},
async onSubmit() {
const result = await loginAPI.changePassword(this.session_id, this.email, this.password)
const result = await loginAPI.changePassword(this.sessionId, this.email, this.password)
if (result.success) {
this.password = ''
this.$router.push('/thx')
@ -126,7 +123,7 @@ export default {
const result = await loginAPI.loginViaEmailVerificationCode(optin)
if (result.success) {
this.authenticated = true
this.session_id = result.result.data.session_id
this.sessionId = result.result.data.session_id
this.email = result.result.data.user.email
} else {
alert(result.result.message)
@ -141,8 +138,8 @@ export default {
return this.password !== '' && this.checkPassword !== ''
},
passwordValidation() {
let errors = []
for (let condition of this.rules) {
const errors = []
for (const condition of this.rules) {
if (!condition.regex.test(this.password)) {
errors.push(condition.message)
}

View File

@ -1,51 +0,0 @@
<template>
<div>
<div
class="header pb-8 pt-5 pt-lg-8 d-flex align-items-center profile-header"
style="
min-height: 600px;
background-image: url(img/theme/profile-cover.jpg);
background-size: cover;
background-position: center top;
"
>
<b-container fluid>
<b-container fluid class="d-flex align-items-center">
<b-row>
<b-col lg="7" md="10">
<h1 class="display-2 text-white">Hello {{ this.$store.state.email }}</h1>
<p class="text-white mt-0 mb-5">
This is your profile page. You can see the progress you've made with your work and
manage your projects or assigned tasks
</p>
<a href="#!" class="btn btn-info">Edit profile</a>
</b-col>
</b-row>
</b-container>
</b-container>
</div>
<b-container fluid class="mt--6">
<b-row>
<b-col xl="4" class="order-xl-2 mb-5">
<user-card></user-card>
</b-col>
<b-col xl="8" class="order-xl-1">
<edit-profile-form></edit-profile-form>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import EditProfileForm from './UserProfile/EditProfileForm.vue'
import UserCard from './UserProfile/UserCard.vue'
export default {
components: {
EditProfileForm,
UserCard,
},
}
</script>
<style></style>

View File

@ -43,11 +43,11 @@ export default {
},
onFileChange(fieldName, file) {
const { maxSize } = this
let imageFile = file[0]
const imageFile = file[0]
//check if user actually selected a file
// check if user actually selected a file
if (file.length > 0) {
let size = imageFile.size / maxSize / maxSize
const size = imageFile.size / maxSize / maxSize
if (!imageFile.type.match('image.*')) {
// check whether the upload is an image
this.errorDialog = true
@ -58,8 +58,8 @@ export default {
this.errorText = 'Your file is too big! Please select an image under 1MB'
} else {
// Append file into FormData & turn file into image URL
let formData = new FormData()
let imageURL = URL.createObjectURL(imageFile)
const formData = new FormData()
const imageURL = URL.createObjectURL(imageFile)
formData.append(fieldName, imageFile)
// Emit FormData & image URL to the parent component
this.$emit('input', { formData, imageURL })

View File

@ -36,7 +36,7 @@
</div>
<div>
<span class="heading">
{{ $n(this.$store.state.user.balance) }}
{{ $n(balance) }}
</span>
<span class="description">GDD</span>
</div>
@ -60,6 +60,9 @@ export default {
components: {
VueQrcode,
},
props: {
balance: { type: Number, default: 0 },
},
}
</script>
<style></style>

View File

@ -31,8 +31,8 @@
</div>
</template>
<script>
import GddWorkTable from '../../views/KontoOverview/GddWorkTable.vue'
import GddAddWork2 from '../../views/KontoOverview/GddAddWork2.vue'
import GddWorkTable from '../../views/Pages/AccountOverview/GddWorkTable.vue'
import GddAddWork2 from '../../views/Pages/AccountOverview/GddAddWork2.vue'
import * as chartConfigs from '@/components/Charts/config'
import LineChart from '@/components/Charts/LineChart'

Some files were not shown because too many files have changed in this diff Show More