Introduce budget, reimbursements for expenses #195

Merged
raucao merged 57 commits from feature/expenses into master 2021-06-03 14:23:45 +00:00
13 changed files with 271 additions and 168 deletions
Showing only changes of commit e4d2fdfce4 - Show all commits
@@ -0,0 +1,59 @@
import Component from '@glimmer/component';
galfert commented 2020-11-13 14:49:01 +00:00 (Migrated from github.com)
Review

Is this intended? If it just returns true, why is it needed?

Is this intended? If it just returns true, why is it needed?
raucao commented 2020-11-17 12:39:47 +00:00 (Migrated from github.com)
Review

I guess it's just a leftover from before the proper form validation.

I guess it's just a leftover from before the proper form validation.
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 { A } from '@ember/array';
import Reimbursement from 'kredits-web/models/reimbursement';
import Expense from 'kredits-web/models/expense';
export default class AddReimbursementComponent extends Component {
@service kredits;
@alias('kredits.contributorsSorted') contributors;
@tracked recipientId = null;
@tracked title = '';
@tracked total = '0';
@tracked expenses = A([]);;
@tracked newExpense = Expense.create();
@tracked expenses = A([]);
@tracked expenseFormVisible = true;
// TODO fetch/apply exchange rate to (W)BTC
currencies = [
{ code: 'EUR' },
{ code: 'USD' },
{ code: 'GBP' }
];
get typeofTotal() {
return typeof this.total;
}
get submitButtonEnabled() {
get submitButtonEnabled () {
return this.expenses.length > 0;
}
get submitButtonDisabled() {
get submitButtonDisabled () {
return !this.submitButtonEnabled;
}
@alias('kredits.contributorsSorted') contributors;
@action
showExpenseForm () {
this.expenseFormVisible = true;
}
@action
submit(e) {
addExpenseItem (expenseItem) {
this.expenses.pushObject(expenseItem);
this.expenseFormVisible = false;
}
@action
submit (e) {
e.preventDefault();
console.log('submit', e);
// TODO
+29 -70
View File
@@ -17,82 +17,41 @@
</p>
</label>
<h3>
Expense items
</h3>
{{#each this.expenses as |expense|}}
<table class="expense-list">
<tr>
<td class="description">{{expense.title}} &ndash; {{expense.description}}</td>
<td class="amount">{{fmt-fiat-currency expense.amount expense.currency}}</td>
</tr>
</table>
<p>TODO: date, url, tags</p>
<h3>Expense items</h3>
{{#if this.expenses}}
<ul class="expense-list">
{{#each this.expenses as |expense|}}
<li>
<div class="description" rowspan="2">
<h4>
<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>
<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}}
<p>No line items yet.</p>
{{/each}}
{{/if}}
<p class="actions">
{{input type="submit" value="Submit" disabled=this.submitButtonDisabled
title="Submit/propose this reimbursement"}}
</p>
<h3>
New expense item
</h3>
<form id="add-expense-item">
<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>
{{#if this.expenseFormVisible}}
<h3>New expense item</h3>
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
{{/if}}
</form>
+14 -6
View File
@@ -11,14 +11,22 @@
<!-- <img src="/img/bitcoin.svg" class="currency&#45;logo"> -->
{{sats-to-btc reimbursement.amount}}</span>&#8239;<span class="symbol">WBTC</span>
</p>
<table>
<ul class="expense-list">
{{#each reimbursement.expenses as |expense|}}
<tr>
<td class="description">{{expense.title}} &ndash; {{expense.description}}</td>
<td class="amount">{{fmt-fiat-currency expense.amount expense.currency}}</td>
</tr>
<li>
<div class="description" rowspan="2">
<h4>
<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}}
</table>
</ul>
</li>
{{/each}}
</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 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.label {
margin-bottom: 1.5rem;
font-size: 1rem;
margin-bottom: .5rem;
}
&.actions {
margin-bottom: 1.5rem;
padding-top: 1.5rem;
text-align: center;
a {
@@ -38,6 +39,10 @@ section#signup {
display: block;
margin-bottom: 0.5rem;
opacity: 0.7;
> p {
margin-bottom: 1.5rem;
}
}
fieldset {
+3 -3
View File
@@ -1,7 +1,7 @@
ul.item-list {
list-style: none;
li {
> li {
padding: 0.8rem 1.2rem;
font-size: 1.2rem;
background-color: $item-background-color;
@@ -19,7 +19,7 @@ ul.item-list {
&.loading {
@include loading-border-top;
li {
> li {
&:first-of-type {
border-top: none;
}
@@ -27,7 +27,7 @@ ul.item-list {
}
&.spaced {
li {
> li {
border-top: 1px solid $item-border-color;
margin-bottom: 2rem;
}
+1
View File
@@ -96,6 +96,7 @@ section {
@import "components/contribution-list";
@import "components/contributor-list";
@import "components/contributor-profile";
@import "components/expense-list";
@import "components/external-account-link";
@import "components/loading-spinner";
@import "components/reimbursement-list";
+1 -1
View File
@@ -25,7 +25,7 @@ section#funds {
text-align: right;
&.amount {
font-size: 1.6rem;
font-size: 1.5rem;
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 {
width: 100%;
li {
> li {
display: grid;
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
@@ -17,36 +17,13 @@ ul.reimbursement-list {
}
.amount {
font-weight: 500;
font-size: 1.5rem;
}
.symbol {
font-size: 0.8rem;
font-size: 1rem;
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');
});
});