WIP Reimbursements/expenses

This commit is contained in:
2020-09-28 14:13:17 +02:00
parent 4722064337
commit e4d2fdfce4
13 changed files with 271 additions and 168 deletions
@@ -0,0 +1,59 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import moment from 'moment';
export default class AddExpenseItemComponent extends Component {
// @tracked newExpense = Expense.create();
@tracked amount = '0';
@tracked currency = 'EUR';
@tracked date = moment().startOf('hour').toDate();
@tracked title = '';
@tracked description = '';
@tracked url = '';
@tracked tags = '';
defaultDate = moment().startOf('hour').toDate();
// TODO fetch/apply exchange rate to (W)BTC
currencies = [
{ code: 'EUR' },
{ code: 'USD' },
{ code: 'GBP' }
];
get submitButtonEnabled () {
return true;
}
get submitButtonDisabled () {
return !this.submitButtonEnabled;
}
setDefaultValues () {
}
@action
submit (e) {
e.preventDefault();
let dateInput = (this.date instanceof Array) ?
this.date[0] : this.date;
const [ date ] = dateInput.toISOString().split('T');
// TODO validate form
const expense = {
amount: parseFloat(this.amount),
currency: this.currency,
date: date,
title: this.title,
description: this.description,
url: this.url,
tags: this.tags.split(',').map(t => t.trim())
}
this.args.addExpenseItem(expense);
this.resetProperties();
}
}
@@ -0,0 +1,64 @@
<form id="add-expense-item" onsubmit={{action "submit"}}>
<fieldset class="horizontal">
<label>
<p class="label">Amount:</p>
<p>
{{input name="expense-amount" type="text"
placeholder="10" value=this.amount}}
</p>
</label>
<label>
<p class="label">Currency:</p>
<p>
<select required name="expense-currency"
onchange={{action (mut this.currency)
value="target.value"}}>
<option value="" selected disabled hidden></option>
{{#each this.currencies as |currency|}}
<option value={{currency.code}} selected={{eq this.currency currency.code}}>{{currency.code}}</option>
{{/each}}
</select>
</p>
</label>
</fieldset>
<label>
<p class="label">Date:</p>
<p>
{{ember-flatpickr
date=this.date
defaultDate=this.defaultDate
maxDate=this.defaultDate
enableTime=false
onChange=(action (mut this.date))}}
</p>
</label>
<label>
<p class="label">Title:</p>
<p>
{{input name="expense-title" type="text" value=this.title}}
</p>
</label>
<label>
<p class="label">Description (optional):</p>
<p>
{{input name="expense-description" type="text" value=this.description}}
</p>
</label>
<label>
<p class="label">URL (optional):</p>
<p>
{{input name="expense-url" type="text" value=this.url}}
</p>
</label>
<label>
<p class="label">Tags (comma-separated, optional):</p>
<p>
{{input name="expense-tags" type="text" value=this.tags}}
</p>
</label>
<p class="actions">
{{input type="submit" value="Add" disabled=this.submitButtonDisabled
class="green" title="Add item to reimbursement"}}
</p>
</form>
+17 -18
View File
@@ -5,40 +5,39 @@ import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { A } from '@ember/array'; import { A } from '@ember/array';
import Reimbursement from 'kredits-web/models/reimbursement'; import Reimbursement from 'kredits-web/models/reimbursement';
import Expense from 'kredits-web/models/expense';
export default class AddReimbursementComponent extends Component { export default class AddReimbursementComponent extends Component {
@service kredits; @service kredits;
@alias('kredits.contributorsSorted') contributors;
@tracked recipientId = null; @tracked recipientId = null;
@tracked title = ''; @tracked title = '';
@tracked total = '0'; @tracked total = '0';
@tracked expenses = A([]);; @tracked expenses = A([]);
@tracked newExpense = Expense.create(); @tracked expenseFormVisible = true;
// TODO fetch/apply exchange rate to (W)BTC get submitButtonEnabled () {
currencies = [
{ code: 'EUR' },
{ code: 'USD' },
{ code: 'GBP' }
];
get typeofTotal() {
return typeof this.total;
}
get submitButtonEnabled() {
return this.expenses.length > 0; return this.expenses.length > 0;
} }
get submitButtonDisabled() { get submitButtonDisabled () {
return !this.submitButtonEnabled; return !this.submitButtonEnabled;
} }
@alias('kredits.contributorsSorted') contributors; @action
showExpenseForm () {
this.expenseFormVisible = true;
}
@action @action
submit(e) { addExpenseItem (expenseItem) {
this.expenses.pushObject(expenseItem);
this.expenseFormVisible = false;
}
@action
submit (e) {
e.preventDefault(); e.preventDefault();
console.log('submit', e); console.log('submit', e);
// TODO // TODO
+29 -70
View File
@@ -17,82 +17,41 @@
</p> </p>
</label> </label>
<h3> <h3>Expense items</h3>
Expense items {{#if this.expenses}}
</h3> <ul class="expense-list">
{{#each this.expenses as |expense|}} {{#each this.expenses as |expense|}}
<table class="expense-list"> <li>
<tr> <div class="description" rowspan="2">
<td class="description">{{expense.title}} &ndash; {{expense.description}}</td> <h4>
<td class="amount">{{fmt-fiat-currency expense.amount expense.currency}}</td> <span class="date">{{expense.date}}:</span>
</tr> <span class="title">{{expense.title}}</span>
</table> </h4>
<p>TODO: date, url, tags</p> <p class="description">{{expense.description}}</p>
</div>
<div class="amount">
{{fmt-fiat-currency expense.amount expense.currency}}
</div>
<div class="actions">
<button class="danger small">delete</button>
</div>
</li>
{{/each}}
</ul>
<p class="actions">
<button onclick={{fn this.showExpenseForm}} class="green small">+ Add another item</button>
</p>
{{else}} {{else}}
<p>No line items yet.</p> <p>No line items yet.</p>
{{/each}} {{/if}}
<p class="actions"> <p class="actions">
{{input type="submit" value="Submit" disabled=this.submitButtonDisabled {{input type="submit" value="Submit" disabled=this.submitButtonDisabled
title="Submit/propose this reimbursement"}} title="Submit/propose this reimbursement"}}
</p> </p>
<h3> {{#if this.expenseFormVisible}}
New expense item <h3>New expense item</h3>
</h3> <AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
<form id="add-expense-item"> {{/if}}
<fieldset class="horizontal">
<label>
<p class="label">Amount:</p>
<p>
{{input type="text" placeholder="10" value=this.newExpense.amount}}
</p>
</label>
<label>
<p class="label">Currency:</p>
<p>
<select required onchange={{action (mut this.newExpense.currency) value="target.value"}}>
<option value="" selected disabled hidden></option>
{{#each this.currencies as |currency|}}
<option value={{currency.code}} selected={{eq this.newExpense.currency currency.code}}>{{currency.code}}</option>
{{/each}}
</select>
</p>
</label>
</fieldset>
<label>
<p class="label">Date:</p>
<p>
{{ember-flatpickr
date=this.newExpense.date
defaultDate=this.newExpense.defaultDate
maxDate=this.newExpense.defaultDate
enableTime=false
onChange=(action (mut this.newExpense.date))}}
</p>
</label>
<label>
<p class="label">Title:</p>
<p>
{{input type="text" value=this.newExpense.title}}
</p>
</label>
<label>
<p class="label">Description (optional):</p>
<p>
{{input type="text" value=this.newExpense.description}}
</p>
</label>
<label>
<p class="label">URL (optional):</p>
<p>
{{input type="text" value=this.newExpense.url}}
</p>
</label>
<p class="actions">
{{input type="submit" value="Add" disabled=this.addButtonDisabled
class="green" title="Add item to reimbursement"}}
</p>
</form>
</form> </form>
+14 -6
View File
@@ -11,14 +11,22 @@
<!-- <img src="/img/bitcoin.svg" class="currency&#45;logo"> --> <!-- <img src="/img/bitcoin.svg" class="currency&#45;logo"> -->
{{sats-to-btc reimbursement.amount}}</span>&#8239;<span class="symbol">WBTC</span> {{sats-to-btc reimbursement.amount}}</span>&#8239;<span class="symbol">WBTC</span>
</p> </p>
<table> <ul class="expense-list">
{{#each reimbursement.expenses as |expense|}} {{#each reimbursement.expenses as |expense|}}
<tr> <li>
<td class="description">{{expense.title}} &ndash; {{expense.description}}</td> <div class="description" rowspan="2">
<td class="amount">{{fmt-fiat-currency expense.amount expense.currency}}</td> <h4>
</tr> <span class="date">{{expense.date}}:</span>
<span class="title">{{expense.title}}</span>
</h4>
<p class="description">{{expense.description}}</p>
</div>
<div class="amount">
{{fmt-fiat-currency expense.amount expense.currency}}
</div>
</li>
{{/each}} {{/each}}
</table> </ul>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
-43
View File
@@ -1,43 +0,0 @@
import EmberObject, { computed } from '@ember/object';
import moment from 'moment';
import { A } from '@ember/array';
export default EmberObject.extend({
title: null,
description: null,
currency: null,
amount: null,
date: null,
url: null,
tags: null,
init () {
this._super(...arguments);
this.setDefaultValues();
},
iso8601Date: computed('date', 'time', function() {
return this.time ? `${this.date}T${this.time}` : this.date;
}),
jsDate: computed('iso8601Date', function() {
return moment(this.iso8601Date).toDate();
}),
setDefaultValues () {
this.set('defaultDate', moment().startOf('hour').toDate());
this.setProperties({
title: '',
description: '',
currency: 'EUR',
amount: 0,
date: this.defaultDate,
url: null,
tags: A([])
});
},
serialize () {
return JSON.stringify(this);
}
});
+6 -1
View File
@@ -13,18 +13,19 @@ section#signup {
p { p {
font-size: 1.2rem; font-size: 1.2rem;
margin-bottom: 1.5rem;
&.mg-bottom-md { &.mg-bottom-md {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
&.label { &.label {
margin-bottom: 1.5rem;
font-size: 1rem; font-size: 1rem;
margin-bottom: .5rem; margin-bottom: .5rem;
} }
&.actions { &.actions {
margin-bottom: 1.5rem;
padding-top: 1.5rem; padding-top: 1.5rem;
text-align: center; text-align: center;
a { a {
@@ -38,6 +39,10 @@ section#signup {
display: block; display: block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
opacity: 0.7; opacity: 0.7;
> p {
margin-bottom: 1.5rem;
}
} }
fieldset { fieldset {
+3 -3
View File
@@ -1,7 +1,7 @@
ul.item-list { ul.item-list {
list-style: none; list-style: none;
li { > li {
padding: 0.8rem 1.2rem; padding: 0.8rem 1.2rem;
font-size: 1.2rem; font-size: 1.2rem;
background-color: $item-background-color; background-color: $item-background-color;
@@ -19,7 +19,7 @@ ul.item-list {
&.loading { &.loading {
@include loading-border-top; @include loading-border-top;
li { > li {
&:first-of-type { &:first-of-type {
border-top: none; border-top: none;
} }
@@ -27,7 +27,7 @@ ul.item-list {
} }
&.spaced { &.spaced {
li { > li {
border-top: 1px solid $item-border-color; border-top: 1px solid $item-border-color;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
+1
View File
@@ -96,6 +96,7 @@ section {
@import "components/contribution-list"; @import "components/contribution-list";
@import "components/contributor-list"; @import "components/contributor-list";
@import "components/contributor-profile"; @import "components/contributor-profile";
@import "components/expense-list";
@import "components/external-account-link"; @import "components/external-account-link";
@import "components/loading-spinner"; @import "components/loading-spinner";
@import "components/reimbursement-list"; @import "components/reimbursement-list";
+1 -1
View File
@@ -25,7 +25,7 @@ section#funds {
text-align: right; text-align: right;
&.amount { &.amount {
font-size: 1.6rem; font-size: 1.5rem;
padding-right: 1.2rem; padding-right: 1.2rem;
} }
+48
View File
@@ -0,0 +1,48 @@
ul.expense-list {
grid-column-start: span 2;
width: 100%;
margin-top: 0.8rem;
border-collapse: collapse;
li {
display: grid;
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
padding-top: 1.2rem;
border-top: 1px solid $item-border-color;
font-size: 1.2rem;
&:not(:last-child) {
padding-bottom: 1.2rem;
}
}
.description {
grid-row-start: 1;
grid-row-end: 3;
}
.amount {
justify-self: end;
// font-weight: normal;
}
.actions {
justify-self: end;
}
h4 {
font-size: 1.2rem;
font-weight: normal;
line-height: 2rem;
}
p {
line-height: 2rem;
&.description {
font-size: 1rem;
opacity: 0.7;
}
}
}
+3 -26
View File
@@ -1,7 +1,7 @@
ul.reimbursement-list { ul.reimbursement-list {
width: 100%; width: 100%;
li { > li {
display: grid; display: grid;
grid-template-columns: auto 10rem; grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem; grid-row-gap: 0.5rem;
@@ -17,36 +17,13 @@ ul.reimbursement-list {
} }
.amount { .amount {
font-weight: 500; font-size: 1.5rem;
} }
.symbol { .symbol {
font-size: 0.8rem; font-size: 1rem;
padding-left: 0.2rem; padding-left: 0.2rem;
} }
} }
table {
grid-column-start: span 2;
width: 100%;
margin-top: 0.8rem;
border-collapse: collapse;
tr {
border-top: 1px solid $item-border-color;
}
td {
padding: 0.8rem 0;
&.amount {
text-align: right;
}
}
@include media-max(small) {
td.date { display: none; }
}
}
} }
} }
@@ -0,0 +1,26 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | add-expense-item', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`<AddExpenseItem />`);
assert.equal(this.element.textContent.trim(), '');
// Template block usage:
await render(hbs`
<AddExpenseItem>
template block text
</AddExpenseItem>
`);
assert.equal(this.element.textContent.trim(), 'template block text');
});
});