Skip to content

Instantly share code, notes, and snippets.

@stravid
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stravid/0da9bd2e96b485eff0a4 to your computer and use it in GitHub Desktop.
Save stravid/0da9bd2e96b485eff0a4 to your computer and use it in GitHub Desktop.
Invoice Push Update Application
require 'sinatra'
require 'json'
current_invoice_id = 0
invoices = {}
before do
content_type :json
end
test('Invoice drafts are grouped and correctly displayed', function() {
visit('/');
andThen(function() {
equal(currentPath(), 'index');
});
});
import Ember from 'ember';
import startApp from '../helpers/start-app';
var dueAt = new Date();
dueAt.setTime(new Date().getTime() + 8 * 24 * 60 * 60 * 1000 + 60000);
var billedAt = new Date();
billedAt.setTime(new Date().getTime() - 1 * 24 * 60 * 60 * 1000 + 1000);
var paidAt = new Date(2014, 7, 12, 10);
var INVOICES = {
"1": {
id: 1,
title: 'Draft 1',
client: 'Company A',
description: '',
amount: 420,
status: 'draft',
due_at: null,
billed_at: null,
paid_at: null
},
"2": {
id: 2,
title: 'Draft 2',
client: 'Company B',
description: '',
amount: 4200,
status: 'draft',
due_at: null,
billed_at: null,
paid_at: null
},
"3": {
id: 3,
title: 'Open',
client: 'Company C',
description: '',
amount: 42200,
status: 'open',
due_at: dueAt,
billed_at: billedAt,
paid_at: null
},
"4": {
id: 4,
title: 'Paid',
client: 'Company D',
description: '',
amount: 422200,
status: 'paid',
due_at: dueAt,
billed_at: billedAt,
paid_at: paidAt
}
};
var App;
var server;
module('Acceptance: InvoiceOverview', {
setup: function() {
App = startApp();
server = new Pretender(function() {
this.get('/invoices', function(request) {
var result = JSON.stringify({ invoices: Object.keys(INVOICES).map(function(k) { return INVOICES[k]; }) });
return [200, { 'Content-Type': 'application/json' }, result];
});
});
},
teardown: function() {
Ember.run(App, 'destroy');
server.shutdown();
}
});
test('Invoice drafts are grouped and correctly displayed', function() {
visit('/');
andThen(function() {
equal(find('table.drafts tr:eq(1) td:eq(0)').text(), 'Draft 1');
equal(find('table.drafts tr:eq(1) td:eq(1)').text(), 'Company A');
equal(find('table.drafts tr:eq(1) td:eq(2)').text(), '€ 4,20');
equal(find('table.drafts tr:eq(2) td:eq(0)').text(), 'Draft 2');
equal(find('table.drafts tr:eq(2) td:eq(1)').text(), 'Company B');
equal(find('table.drafts tr:eq(2) td:eq(2)').text(), '€ 42,00');
equal(find('table.heading:eq(0) th:eq(1)').text(), '€ 46,20');
equal(find('table.drafts tr').length, 3);
});
});
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.find('invoice');
}
});
import DS from 'ember-data';
export default DS.ActiveModelAdapter.extend({
});
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
client: DS.attr('string'),
description: DS.attr('string'),
status: DS.attr('string'),
amount: DS.attr('number'),
dueAt: DS.attr('date'),
billedAt: DS.attr('date'),
paidAt: DS.attr('date')
});
<table class="heading">
<tr>
<th>Invoice Drafts</th>
<th>{{totalAmount}}</th>
</tr>
</table>
<table class="drafts">
<tr>
<th>Invoice</th>
<th>Client</th>
<th class="align-right">Amount</th>
<th></th>
</tr>
{{#each model}}
<tr>
<td>{{title}}</td>
<td>{{client}}</td>
<td class="align-right">{{amount}}</td>
<td>
<a href="#">
<svg class="icon edit-icon"><use xlink:href="#edit" /></svg>
</a>
</td>
</tr>
{{/each}}
</table>
import Ember from 'ember';
export default Ember.ArrayController.extend({
invoiceDrafts: function() {
return this.get('model').filterBy('status', 'draft');
}.property('model.@each.status'),
invoiceDraftAmounts: Ember.computed.mapBy('invoiceDrafts', 'amount'),
totalAmountForInvoiceDrafts: Ember.computed.sum('invoiceDraftAmounts')
});
import { numberToCurrency } from '../../../helpers/number-to-currency';
module('number-to-currency Handlebars Helper');
test('convert invalid input to zero', function() {
equal(numberToCurrency(null), '€ 0,00');
equal(numberToCurrency(undefined), '€ 0,00');
});
test('handle single digit input', function() {
equal(numberToCurrency(0), '€ 0,00');
equal(numberToCurrency(9), '€ 0,09');
});
test('handle two digit input', function() {
equal(numberToCurrency(99), '€ 0,99');
});
test('handle multi digit input', function() {
equal(numberToCurrency(199), '€ 1,99');
equal(numberToCurrency(10099), '€ 100,99');
equal(numberToCurrency(100099), '€ 1.000,99');
equal(numberToCurrency(100000099), '€ 1.000.000,99');
});
import Ember from 'ember';
function numberToCurrency(value) {
if (value === null) {
value = 0;
}
if (isNaN(value)) {
value = 0;
}
value = value.toString();
while (value.length < 3) {
value = '0' + value;
}
var fractionalPart = value.slice(-2);
var integerPart = value.slice(0, -2).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return '€ ' + integerPart + ',' + fractionalPart;
}
export { numberToCurrency };
export default Ember.Handlebars.makeBoundHelper(numberToCurrency);
post '/invoices' do
params = JSON.parse request.body.read, symbolize_names: true
current_invoice_id += 1
new_invoice = params[:invoice].merge!({ id: current_invoice_id })
invoices[current_invoice_id] = new_invoice
{ invoice: new_invoice }.to_json
end
test('Open invoices are grouped and correctly displayed', function() {
visit('/');
andThen(function() {
equal(find('table.open-invoices tr:eq(1) td:eq(0)').text(), 'Open');
equal(find('table.open-invoices tr:eq(1) td:eq(1)').text(), 'Company C');
equal(find('table.open-invoices tr:eq(1) td:eq(2)').text().trim(), 'Due in 8 days');
equal(find('table.open-invoices tr:eq(1) td:eq(3)').text(), '€ 422,00');
equal(find('table.heading:eq(1) th:eq(1)').text(), '€ 422,00');
equal(find('table.open-invoices tr').length, 2);
});
});
import Ember from 'ember';
export default Ember.ArrayController.extend({
invoiceDrafts: function() {
return this.get('model').filterBy('status', 'draft');
}.property('model.@each.status'),
invoiceDraftAmounts: Ember.computed.mapBy('invoiceDrafts', 'amount'),
totalAmountForInvoiceDrafts: Ember.computed.sum('invoiceDraftAmounts'),
openInvoices: function() {
return this.get('model').filterBy('status', 'open');
}.property('model.@each.status'),
openInvoiceAmounts: Ember.computed.mapBy('openInvoices', 'amount'),
totalAmountForOpenInvoices: Ember.computed.sum('openInvoiceAmounts')
});
<table class="heading">
<tr>
<th>Open Invoices</th>
<th>{{number-to-currency totalAmountForOpenInvoices}}</th>
</tr>
</table>
<table class="open-invoices">
<tr>
<th>Invoice</th>
<th>Client</th>
<th>Due in</th>
<th class="align-right">Amount</th>
<th></th>
</tr>
{{#each openInvoices}}
<tr>
<td>{{title}}</td>
<td>{{client}}</td>
<td>{{dueAt}}</td>
<td class="align-right">{{number-to-currency amount}}</td>
<td>
<a href="#">
<svg class="icon edit-icon"><use xlink:href="#edit" /></svg>
</a>
</td>
</tr>
{{/each}}
</table>
import Ember from 'ember';
import { test, moduleForComponent } from 'ember-qunit';
moduleForComponent('due-at-label');
var dayBasis = 24 * 60 * 60 * 1000;
function setDueAtToDaysFromNow(component, days) {
Ember.run(function() {
var dueAt = new Date();
dueAt.setTime(new Date().getTime() + days + 60000);
component.set('dueAt', dueAt);
});
}
test('displays correct text', function() {
var component = this.subject();
setDueAtToDaysFromNow(component, 8 * dayBasis);
equal(this.$().text().trim(), 'Due in 8 days');
setDueAtToDaysFromNow(component, 1 * dayBasis);
equal(this.$().text().trim(), 'Due in 1 day');
setDueAtToDaysFromNow(component, 0 * dayBasis);
equal(this.$().text().trim(), 'Due today');
setDueAtToDaysFromNow(component, -1 * dayBasis);
equal(this.$().text().trim(), '1 day overdue');
setDueAtToDaysFromNow(component, -8 * dayBasis);
equal(this.$().text().trim(), '8 days overdue');
});
test('is a span tag', function() {
this.subject().set('dueAt', new Date());
equal('SPAN', this.$().prop('tagName'));
});
test('has correct class', function() {
var component = this.subject();
setDueAtToDaysFromNow(component, 8 * dayBasis);
ok(this.$().hasClass('ok'));
setDueAtToDaysFromNow(component, 7 * dayBasis);
ok(this.$().hasClass('warning'));
setDueAtToDaysFromNow(component, 0 * dayBasis);
ok(this.$().hasClass('overdue'));
setDueAtToDaysFromNow(component, -1 * dayBasis);
ok(this.$().hasClass('overdue'));
});
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'span',
classNameBindings: [':due-label', 'isOverdue:overdue', 'isComingDue:warning', 'isLaterDue:ok'],
daysUntilDueDate: function() {
var difference = this.get('dueAt').getTime() - new Date().getTime();
return Math.floor(difference / (24 * 60 * 60 * 1000));
}.property('dueAt'),
isOverdue: function() {
return this.get('daysUntilDueDate') <= 0;
}.property('daysUntilDueDate'),
isComingDue: function() {
return this.get('daysUntilDueDate') > 0 && this.get('daysUntilDueDate') <= 7;
}.property('daysUntilDueDate'),
isLaterDue: function() {
return this.get('daysUntilDueDate') > 7;
}.property('daysUntilDueDate'),
label: function() {
var daysUntilDueDate = this.get('daysUntilDueDate');
var dayPart = Math.abs(daysUntilDueDate) > 1 ? 'days' : 'day';
if (daysUntilDueDate > 0) {
return 'Due in ' + daysUntilDueDate + ' ' + dayPart;
} else if (daysUntilDueDate === 0) {
return 'Due today';
} else {
return Math.abs(daysUntilDueDate) + ' ' + dayPart + ' overdue';
}
}.property('daysUntilDueDate')
});
test('Paid invoices are grouped and correctly displayed', function() {
visit('/');
andThen(function() {
equal(find('table.paid-invoices tr:eq(1) td:eq(0)').text(), 'Paid');
equal(find('table.paid-invoices tr:eq(1) td:eq(1)').text(), 'Company D');
equal(find('table.paid-invoices tr:eq(1) td:eq(2)').text(), '12.08.2014');
equal(find('table.paid-invoices tr:eq(1) td:eq(3)').text(), '€ 4.222,00');
equal(find('table.heading:eq(2) th:eq(1)').text(), '€ 4.222,00');
equal(find('table.paid-invoices tr').length, 2);
});
});
import Ember from 'ember';
export default Ember.ArrayController.extend({
invoiceDrafts: function() {
return this.get('model').filterBy('status', 'draft');
}.property('model.@each.status'),
invoiceDraftAmounts: Ember.computed.mapBy('invoiceDrafts', 'amount'),
totalAmountForInvoiceDrafts: Ember.computed.sum('invoiceDraftAmounts'),
openInvoices: function() {
return this.get('model').filterBy('status', 'open');
}.property('model.@each.status'),
openInvoiceAmounts: Ember.computed.mapBy('openInvoices', 'amount'),
totalAmountForOpenInvoices: Ember.computed.sum('openInvoiceAmounts'),
paidInvoices: function() {
return this.get('model').filterBy('status', 'paid');
}.property('model.@each.status'),
paidInvoiceAmounts: Ember.computed.mapBy('paidInvoices', 'amount'),
totalAmountForPaidInvoices: Ember.computed.sum('paidInvoiceAmounts')
});
<table class="heading">
<tr>
<th>Paid Invoices</th>
<th class="align-right">{{number-to-currency totalAmountForPaidInvoices}}</th>
</tr>
</table>
<table class="paid-invoices">
<tr>
<th>Invoice</th>
<th>Client</th>
<th>Paid on</th>
<th class="align-right">Amount</th>
<th></th>
</tr>
{{#each paidInvoices}}
<tr>
<td>{{title}}</td>
<td>{{client}}</td>
<td>{{paidAt}}</td>
<td class="align-right">{{number-to-currency amount}}</td>
<td>
<a href="">
<svg class="icon show-icon"><use xlink:href="#show" /></svg>
</a>
</td>
</tr>
{{/each}}
</table>
<div class="container">
{{outlet}}
</div>
As a small business owner
I want to create new invoice drafts
So that future me will not forget to bill a client
get '/invoices' do
{ invoices: invoices.map { |key, value| value }}.to_json
end
get '/invoices/:id' do |id|
{ invoice: invoices[id.to_i] }.to_json
end
import Ember from 'ember';
import startApp from '../helpers/start-app';
var INVOICES = {};
var CURRENT_INVOICE_ID = 1;
var App;
var server;
module('Acceptance: NewInvoiceDraft', {
setup: function() {
App = startApp();
server = new Pretender(function() {
this.get('/invoices', function(request) {
var result = JSON.stringify({ invoices: Object.keys(INVOICES).map(function(k) { return INVOICES[k]; }) });
return [200, { 'Content-Type': 'application/json' }, result];
});
this.post('/invoices', function(request) {
var invoice = JSON.parse(request.requestBody).invoice;
invoice.id = CURRENT_INVOICE_ID;
INVOICES[CURRENT_INVOICE_ID] = invoice;
var result = JSON.stringify({ invoice: invoice });
CURRENT_INVOICE_ID++;
return [200, { 'Content-Type': 'application/json' }, result];
});
});
},
teardown: function() {
Ember.run(App, 'destroy');
server.shutdown();
}
});
test('Create a new invoice draft', function() {
visit('/');
click('.heading-inline-link');
fillIn('#title', 'Writing Ember.js Learning Material');
fillIn('#client', 'Awesome Inc.');
fillIn('#amount', 4000);
fillIn('#description', 'A multi part article series detailing how to build an Ember.js Application with Push Updates');
andThen(function() {
equal(find('h2').text(), 'Writing Ember.js Learning Material');
click('#save-button');
andThen(function() {
equal(currentRouteName(), 'index');
equal(find('table.drafts tr:eq(1) td:eq(0)').text(), 'Writing Ember.js Learning Material');
equal(find('table.drafts tr:eq(1) td:eq(2)').text(), '€ 4.000,00');
});
});
});
Router.map(function() {
this.route('new');
});
<table class="heading">
<tr>
<th>Invoice Drafts &middot; {{link-to 'Create new draft' 'new' class='heading-inline-link'}}</th>
<th>{{number-to-currency totalAmountForInvoiceDrafts}}</th>
</tr>
</table>
<form>
{{#if title}}
<h2>{{title}}</h2>
{{else}}
<h2>Untitled Invoice Draft</h2>
{{/if}}
<div class="row">
<div class="column">
<div class="input-pair">
<label for="title">Title</label>
{{input value=title id="title"}}
</div>
<div class="input-pair">
<label for="client">Client</label>
{{input value=client id="client"}}
</div>
<div class="input-pair">
<label for="amount">Amount</label>
{{currency-input value=amount inputId="amount"}}
</div>
</div>
<div class="column">
<div class="input-pair">
<label for="description">Description</label>
{{textarea value=description id="description"}}
</div>
</div>
</div>
<div class="row button-row">
<a {{action discard}} class="destructive-link">Discard draft</a>
<button id="save-button" {{action save}}>Create draft</button>
</div>
</form>
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
save: function() {
this.modelFor('new').save();
this.transitionTo('index');
}
},
model: function() {
return this.store.createRecord('invoice');
}
});
import { test, moduleForComponent } from 'ember-qunit';
import Ember from 'ember';
moduleForComponent('currency-input');
test('passes ID to input', function() {
var component = this.subject();
Ember.run(function() {
component.set('inputId', 'test-id');
});
equal(this.$().find('input').attr('id'), 'test-id');
});
test('displays nothing as default value', function() {
equal(this.$().find('input').val(), '');
});
test('returns undefined as default value', function() {
equal(this.subject().get('value'), undefined);
});
test('displays formatted value in input', function() {
var component = this.subject();
Ember.run(function() {
component.set('value', 420000);
});
equal(this.$().find('input').val(), '4200');
Ember.run(function(){
component.set('value', 420);
});
equal(this.$().find('input').val(), '4,2');
});
test('converts euro input to cents', function() {
var component = this.subject();
var that = this;
Ember.run(function() {
that.$().find('input').val(100);
that.$().find('input').trigger('keyup');
});
equal(component.get('value'), 10000);
Ember.run(function() {
that.$().find('input').val('100,33');
that.$().find('input').trigger('keyup');
});
equal(component.get('value'), 10033);
Ember.run(function() {
that.$().find('input').val('4.2');
that.$().find('input').trigger('keyup');
});
equal(component.get('value'), 420);
});
import Ember from 'ember';
export default Ember.Component.extend({
inputValue: function(key, value) {
if (arguments.length > 1) {
this.set('value', parseFloat(value.toString().replace(',', '.')) * 100);
}
var displayValue = this.get('value') / 100;
if (isNaN(displayValue)) {
return '';
} else {
return displayValue.toString().replace('.', ',');
}
}.property('value')
});
test('Discard an unsaved invoice draft', function() {
visit('/');
click('.heading-inline-link');
fillIn('#title', 'Write an AngularJS book');
click('.destructive-link');
andThen(function() {
equal(currentRouteName(), 'index');
equal(find('table.drafts').text().indexOf('Write an AngularJS book'), -1);
});
});
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
save: function() {
this.modelFor('new').save();
this.transitionTo('index');
},
discard: function() {
this.modelFor('new').deleteRecord();
this.transitionTo('index');
}
},
model: function() {
return this.store.createRecord('invoice');
}
});
import Ember from 'ember';
import startApp from '../helpers/start-app';
var INVOICES = {
"1": {
id: 1,
title: 'Draft 1',
client: 'Company A',
description: '',
amount: 420,
status: 'draft',
due_at: null,
billed_at: null,
paid_at: null
}
};
var App;
var server;
module('Acceptance: EditInvoiceDraft', {
setup: function() {
App = startApp();
server = new Pretender(function() {
this.get('/invoices', function(request) {
var result = JSON.stringify({ invoices: Object.keys(INVOICES).map(function(k) { return INVOICES[k]; }) });
return [200, { 'Content-Type': 'application/json' }, result];
});
this.put('/invoices/1', function(request) {
var invoiceAttributes = JSON.parse(request.requestBody).invoice;
INVOICES['1'].title = invoiceAttributes.title;
var result = JSON.stringify({ invoice: INVOICES['1'] });
return [200, { 'Content-Type': 'application/json' }, result];
});
});
},
teardown: function() {
Ember.run(App, 'destroy');
server.shutdown();
}
});
test('Edit an existing invoice draft', function() {
visit('/');
click('table.drafts tr:eq(1) td:eq(3) a');
fillIn('#title', 'Writing Ember.js Learning Material');
click('#update-button');
andThen(function() {
equal(find('table.drafts tr:eq(1) td:eq(0)').text(), 'Writing Ember.js Learning Material');
});
});
put '/invoices/:id' do |id|
params = JSON.parse request.body.read, symbolize_names: true
invoices[id.to_i].merge!(params[:invoice])
{ invoice: invoices[id.to_i] }.to_json
end
{{#if title}}
<h2>{{title}}</h2>
{{else}}
<h2>Untitled Invoice Draft</h2>
{{/if}}
<div class="row">
<div class="column">
<div class="input-pair">
<label for="title">Title</label>
{{input value=title id="title"}}
</div>
<div class="input-pair">
<label for="client">Client</label>
{{input value=client id="client"}}
</div>
<div class="input-pair">
<label for="amount">Amount</label>
{{currency-input value=amount inputId="amount"}}
</div>
</div>
<div class="column">
<div class="input-pair">
<label for="description">Description</label>
{{textarea value=description id="description"}}
</div>
</div>
</div>
<form>
{{partial 'draft-input-fields'}}
<div class="row button-row">
<a {{action discard}} class="destructive-link">Discard draft</a>
<button id="save-button" {{action save}}>Create draft</button>
</div>
</form>
<form>
{{partial 'draft-input-fields'}}
<div class="row button-row">
<button id="update-button" {{action save}}>Update draft</button>
</div>
</form>
{{#link-to 'edit' this}}
<svg class="icon edit-icon"><use xlink:href="#edit" /></svg>
{{/link-to}}
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
save: function() {
this.modelFor('edit').save();
this.transitionTo('index');
}
}
});
import { formatDate } from '../../../helpers/format-date';
module('format-date Handlebars Helper');
test('handle non-Date objects as input', function() {
equal(formatDate(null), '');
equal(formatDate(undefined), '');
equal(formatDate(100), '');
equal(formatDate('a string'), '');
});
test('handle Date object as input', function() {
equal(formatDate(new Date(2014, 11, 12)), '12.12.2014');
});
test('prepend single digit days', function() {
equal(formatDate(new Date(2014, 11, 2)), '02.12.2014');
});
test('prepend single digit months', function() {
equal(formatDate(new Date(2014, 0, 12)), '12.01.2014');
});
import Ember from 'ember';
function formatDate(value) {
if (value === null || value === undefined) {
return '';
}
if (!(value instanceof Date)) {
return '';
}
var day = value.getDate();
var month = value.getMonth() + 1;
var year = value.getFullYear();
if (day < 10) {
day = '0' + day;
}
if (month < 10) {
month = '0' + month;
}
return day + '.' + month + '.' + year;
}
export { formatDate };
export default Ember.Handlebars.makeBoundHelper(formatDate);
delete '/invoices/:id' do |id|
invoices.delete id.to_i
200
end
As a small business owner
I want to create new invoice drafts
So that future me will not forget to bill a client
As a small business owner
I want to edit invoice drafts
So that an invoice draft always mirrors the actual project terms
As a small business owner
I want to turn an invoice draft into an open invoice
So that I remember the invoice is already billed
As a small business owner
I want to turn an open invoice into a paid invoice
So that I remember the invoice is paid
As a small business owner
I want to delete invoices
So that obsolete invoices do not clutter the system
As a small business owner
I want to see a grouped overview of all invoices in the system
So that I can get an insight into my business financials with one look
As a small business owner
I want to instantly see changes made by my cofounder
So that I do not accidently do things which are already taken care of
DEFINITIONS
Invoice - Represents an actual or future invoice of the business.
An invoice consists of a title, client, amount, description,
status ('draft', 'open' and 'paid'), billing date, due date and payment date.
Invoices can be only edited if they have the 'draft' status.
Client - Someone the small business owner is working with. Is only saved as single text field.
ROLES
Small business owner - Someone running a small business who wants to keep tabs on his invoices.
current_invoice_id = 4
invoices = {
1 => {
id: 1,
title: 'Developing the frontend',
client: 'edgy circle',
description: '',
amount: 890000,
status: 'draft',
due_at: nil,
billed_at: nil,
paid_at: nil
},
2 => {
id: 2,
title: 'Invoice backend development',
client: 'edgy circle',
description: '',
amount: '420000',
status: 'open',
due_at: (Date.today + 5).to_datetime,
billed_at: Date.today.to_datetime,
paid_at: nil
},
3 => {
id: 3,
title: 'Requirements Workshop "Invoice Application"',
client: 'edgy circle',
description: 'Note: Suppies were provided by the client.',
amount: 99900,
status: 'paid',
due_at: (Date.today - 10).to_datetime,
billed_at: (Date.today - 13).to_datetime,
paid_at: Date.today.to_datetime
}
}
Display the source blob
Display the rendered blob
Raw
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="show" viewBox="15 15 60 60">
<path d="M67.4,41C63.3,36.5,54.9,29,45,29s-18.3,7.5-22.4,12c-2.1,2.3-2.1,5.7,0,7.9c4.1,4.5,12.5,12,22.4,12s18.3-7.5,22.4-12 C69.5,46.7,69.5,43.3,67.4,41z M64.5,46.3C60.8,50.3,53.4,57,45,57s-15.8-6.7-19.5-10.7c-0.7-0.7-0.7-1.8,0-2.5 C29.2,39.7,36.6,33,45,33s15.8,6.7,19.5,10.7C65.2,44.5,65.2,45.5,64.5,46.3z"/>
<circle cx="45" cy="45" r="6"/>
</symbol>
<symbol id="edit" viewBox="15 15 60 60">
<path d="M64.5,22.7c-2.3-2.3-6.2-2.3-8.5,0L25.8,52.9c0,0,0,0,0,0.1c-0.1,0.1-0.2,0.3-0.3,0.5c0,0,0,0.1-0.1,0.1c0,0,0,0.1,0,0.1 L21,66.4c-0.2,0.7-0.1,1.5,0.5,2c0.4,0.4,0.9,0.6,1.4,0.6c0.2,0,0.4,0,0.6-0.1l12.7-4.2c0,0,0,0,0.1,0c0,0,0.1,0,0.1-0.1 c0.2-0.1,0.4-0.2,0.5-0.3c0,0,0,0,0.1,0L67.3,34c2.3-2.3,2.3-6.1,0-8.5L64.5,22.7z M35.7,60l-2.8-2.8L30,54.3l22.7-22.7l5.7,5.7 L35.7,60z M28,58l2,2l2,2l-5.9,2L28,58z M64.5,31.1l-3.3,3.3l-5.7-5.7l3.3-3.3c0.8-0.8,2.1-0.8,2.8,0l2.8,2.8 C65.3,29.1,65.3,30.4,64.5,31.1z"/>
</symbol>
</svg>
As a small business owner
I want to see a grouped overview of all invoices in the system
So that I can get an insight into my business financials with one look
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment