2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 19:39:07 +00:00

fix(dashboard): Empty states

This commit is contained in:
Faris Ansari 2019-11-26 18:49:32 +05:30
parent 3eb961e1d8
commit 81b2b31187
4 changed files with 202 additions and 49 deletions

View File

@ -1,20 +1,96 @@
<template> <template>
<div class="mt-6"> <div class="mt-6">
<div class="flex items-center justify-between"> <template v-if="hasData">
<div class="font-medium">{{ _('Cash Flow') }}</div> <div class="flex items-center justify-between">
<div class="flex text-base"> <div class="font-medium">{{ _('Cashflow') }}</div>
<div class="flex items-center"> <div class="flex text-base">
<span class="w-3 h-3 rounded inline-block bg-blue-500"></span> <div class="flex items-center">
<span class="ml-2">{{ _('Inflow') }}</span> <span class="w-3 h-3 rounded inline-block bg-blue-500"></span>
</div> <span class="ml-2">{{ _('Inflow') }}</span>
<div class="flex items-center ml-6"> </div>
<span class="w-3 h-3 rounded inline-block bg-gray-500"></span> <div class="flex items-center ml-6">
<span class="ml-2">{{ _('Outflow') }}</span> <span class="w-3 h-3 rounded inline-block bg-gray-500"></span>
<span class="ml-2">{{ _('Outflow') }}</span>
</div>
</div> </div>
<PeriodSelector :value="period" @change="value => (period = value)" />
</div> </div>
<PeriodSelector :value="period" @change="value => (period = value)" /> <div class="chart-wrapper" ref="cashflow"></div>
</div> </template>
<div class="chart-wrapper" ref="cashflow"></div> <svg
v-else
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 889 240"
class="w-full h-full"
>
<defs>
<linearGradient x1="50%" y1="100%" x2="50%" y2=".889%" id="a">
<stop stop-color="#FFF" stop-opacity="0" offset="0%" />
<stop stop-color="#F4F4F6" offset="100%" />
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<text
font-family="Inter-Medium, Inter"
font-size="16"
font-weight="400"
fill="#112B42"
transform="translate(0 -3)"
>
<tspan x="10" y="16">{{ _('Cashflow') }}</tspan>
</text>
<g fill="#E9E9ED">
<path d="M371 2h12v12h-12zM391 2h53v12h-53z" />
<g>
<path d="M453 2h12v12h-12zM473 2h53v12h-53z" />
</g>
</g>
<path
fill="#E9E9ED"
d="M0 41h19v12H0zM4 121h15v12H4zM2 81h17v12H2zM4 201h15v12H4zM3 161h16v12H3z"
/>
<path
d="M37.25 211.25h849.5v1H37.25zM37.25 167.25h849.5v1H37.25zM37.25 127.25h849.5v1H37.25zM37.25 87.25h849.5v1H37.25zM37.25 47.25h849.5v1H37.25z"
stroke="#F6F7F9"
stroke-width=".5"
/>
<g fill="#E9E9ED">
<path
d="M49 228h31v12H49zM122 228h31v12h-31zM195 228h31v12h-31zM268 228h31v12h-31zM341 228h31v12h-31zM414 228h31v12h-31zM487 228h31v12h-31zM560 228h31v12h-31zM633 228h31v12h-31zM706 228h31v12h-31zM779 228h31v12h-31zM852 228h31v12h-31z"
/>
</g>
<g fill-rule="nonzero">
<path
fill="url(#a)"
opacity=".5"
d="M12 34l78 73 73 12 74-37 73 36.167L383 126l73-55.5L529.223 98 602 0l73 75 73 2 73 34 29 41v25H0V25z"
transform="translate(37 35)"
/>
<path
stroke="#E9E9ED"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
d="M37 60l12 9 78 73 73 12 74-37 73 36.167L420 161l73-55.5 73.223 27.5L639 35l73 75 73 2 73 34 29 42"
/>
<g>
<path
fill="url(#a)"
opacity=".5"
d="M12 44.599l78 31.345 73 5.152 74-26.192L310 .738l73 21.578 73 37.955 73.223 11.808L602 30l73 32.203 73 .86 73 14.598L850 58v48H0V40.734z"
transform="translate(37 106)"
/>
<path
stroke="#E9E9ED"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
d="M37 146.734l12 3.865 78 31.345 73 5.152 74-26.192 73-54.166 73 21.578 73 37.955 73.223 11.808L639 136l73 32.203 73 .86 73 14.598L887 164"
/>
</g>
</g>
</g>
</svg>
</div> </div>
</template> </template>
<script> <script>
@ -29,7 +105,7 @@ export default {
components: { components: {
PeriodSelector PeriodSelector
}, },
data: () => ({ period: 'This Year' }), data: () => ({ period: 'This Year', hasData: false }),
watch: { watch: {
period: 'render' period: 'render'
}, },
@ -48,6 +124,15 @@ export default {
periodicity periodicity
}); });
let totalInflow = data.reduce((sum, d) => d.inflow, 0);
let totalOutflow = data.reduce((sum, d) => d.outflow, 0);
this.hasData = !(totalInflow === 0 && totalOutflow === 0);
if (!this.hasData) return;
this.$nextTick(() => this.renderChart(periodList, data));
},
renderChart(periodList, data) {
const chart = new Chart(this.$refs['cashflow'], { const chart = new Chart(this.$refs['cashflow'], {
title: '', title: '',
type: 'line', type: 'line',

View File

@ -5,12 +5,13 @@
_('Top Expenses') _('Top Expenses')
}}</template> }}</template>
<PeriodSelector <PeriodSelector
v-if="hasData"
slot="action" slot="action"
:value="period" :value="period"
@change="value => (period = value)" @change="value => (period = value)"
/> />
</SectionHeader> </SectionHeader>
<div class="flex"> <div class="flex relative">
<div class="w-1/2"> <div class="w-1/2">
<div <div
class="mt-5 flex justify-between items-center text-sm" class="mt-5 flex justify-between items-center text-sm"
@ -26,7 +27,11 @@
</div> </div>
<div class="w-1/2 relative"> <div class="w-1/2 relative">
<div class="chart-wrapper" ref="top-expenses"></div> <div class="chart-wrapper" ref="top-expenses"></div>
<div class="absolute text-base text-center font-semibold" style="right: 3.8rem; top: 32%;"> <div
v-if="hasData"
class="absolute text-base text-center font-semibold"
style="right: 3.8rem; top: 32%;"
>
<div> <div>
{{ frappe.format(totalExpense, 'Currency') }} {{ frappe.format(totalExpense, 'Currency') }}
</div> </div>
@ -35,6 +40,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="absolute inset-0 flex justify-center items-center">
<span class="text-base text-gray-600">
{{ _('No transactions yet') }}
</span>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -53,7 +63,10 @@ export default {
PeriodSelector, PeriodSelector,
SectionHeader SectionHeader
}, },
data: () => ({ period: 'This Year', expenses: [] }), data: () => ({
period: 'This Year',
expenses: [{ account: 'Test', total: 0 }]
}),
mounted() { mounted() {
this.render(); this.render();
}, },
@ -63,6 +76,9 @@ export default {
computed: { computed: {
totalExpense() { totalExpense() {
return this.expenses.reduce((sum, expense) => sum + expense.total, 0); return this.expenses.reduce((sum, expense) => sum + expense.total, 0);
},
hasData() {
return this.totalExpense > 0;
} }
}, },
methods: { methods: {

View File

@ -1,14 +1,20 @@
<template> <template>
<div> <div class="flex flex-col h-full">
<SectionHeader> <SectionHeader>
<template slot="title">{{ _('Profit and Loss') }}</template> <template slot="title">{{ _('Profit and Loss') }}</template>
<PeriodSelector <PeriodSelector
v-if="hasData"
slot="action" slot="action"
:value="period" :value="period"
@change="value => (period = value)" @change="value => (period = value)"
/> />
</SectionHeader> </SectionHeader>
<div class="chart-wrapper" ref="profit-and-loss"></div> <div v-if="hasData" class="chart-wrapper" ref="profit-and-loss"></div>
<div class="flex-1 w-full h-full flex justify-center items-center" v-else>
<span class="text-base text-gray-600">
{{ _('No transactions yet') }}
</span>
</div>
</div> </div>
</template> </template>
<script> <script>
@ -25,7 +31,7 @@ export default {
PeriodSelector, PeriodSelector,
SectionHeader SectionHeader
}, },
data: () => ({ period: 'This Year' }), data: () => ({ period: 'This Year', hasData: false }),
mounted() { mounted() {
this.render(); this.render();
}, },
@ -46,7 +52,13 @@ export default {
}); });
let totalRow = res.rows[res.rows.length - 1]; let totalRow = res.rows[res.rows.length - 1];
this.hasData = res.columns.some(key => totalRow[key] > 0);
if (!this.hasData) return;
this.$nextTick(() => this.renderChart(res));
},
renderChart(res) {
let totalRow = res.rows[res.rows.length - 1];
const chart = new Chart(this.$refs['profit-and-loss'], { const chart = new Chart(this.$refs['profit-and-loss'], {
title: '', title: '',
animate: false, animate: false,

View File

@ -4,30 +4,58 @@
<SectionHeader> <SectionHeader>
<template slot="title">{{ invoice.title }}</template> <template slot="title">{{ invoice.title }}</template>
<PeriodSelector <PeriodSelector
v-if="invoice.hasData"
slot="action" slot="action"
:value="$data[invoice.periodKey]" :value="$data[invoice.periodKey]"
@change="value => ($data[invoice.periodKey] = value)" @change="value => ($data[invoice.periodKey] = value)"
/> />
<Button
v-else
slot="action"
:icon="true"
type="primary"
@click="newInvoice(invoice)"
>
<feather-icon name="plus" class="w-4 h-4 text-white" />
</Button>
</SectionHeader> </SectionHeader>
<div class="mt-6 flex justify-between"> <div class="mt-6 flex justify-between">
<div class="text-sm"> <div
class="text-sm"
:class="{ 'bg-gray-200 text-gray-200 rounded': !invoice.hasData }"
>
{{ frappe.format(invoice.paid, 'Currency') }} {{ frappe.format(invoice.paid, 'Currency') }}
<span class="text-gray-600">{{ _('Paid') }}</span> <span :class="{ 'text-gray-600': invoice.hasData }">{{
_('Paid')
}}</span>
</div> </div>
<div class="text-sm"> <div
class="text-sm"
:class="{ 'bg-gray-200 text-gray-200 rounded': !invoice.hasData }"
>
{{ frappe.format(invoice.unpaid, 'Currency') }} {{ frappe.format(invoice.unpaid, 'Currency') }}
<span class="text-gray-600">{{ _('Unpaid') }}</span> <span :class="{ 'text-gray-600': invoice.hasData }">{{
_('Unpaid')
}}</span>
</div> </div>
</div> </div>
<div class="mt-2 relative"> <div class="mt-2 relative">
<div <div
class="w-full h-4 rounded" class="w-full h-4 rounded"
:class="invoice.color == 'blue' ? 'bg-blue-200' : 'bg-gray-200'" :class="
invoice.hasData && invoice.color == 'blue'
? 'bg-blue-200'
: 'bg-gray-200'
"
></div> ></div>
<div <div
class="absolute inset-0 h-4 rounded" class="absolute inset-0 h-4 rounded"
:class="invoice.color == 'blue' ? 'bg-blue-500' : 'bg-gray-500'" :class="
:style="`width: ${(invoice.paid / invoice.total) * 100}%`" invoice.hasData && invoice.color == 'blue'
? 'bg-blue-500'
: 'bg-gray-500'
"
:style="`width: ${invoice.barWidth}%`"
></div> ></div>
</div> </div>
</div> </div>
@ -35,6 +63,7 @@
</template> </template>
<script> <script>
import frappe from 'frappejs'; import frappe from 'frappejs';
import Button from '@/components/Button';
import PeriodSelector from './PeriodSelector'; import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader'; import SectionHeader from './SectionHeader';
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity'; import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
@ -43,10 +72,34 @@ export default {
name: 'UnpaidInvoices', name: 'UnpaidInvoices',
components: { components: {
PeriodSelector, PeriodSelector,
SectionHeader SectionHeader,
Button
}, },
data: () => ({ data: () => ({
invoices: [], invoices: [
{
title: 'Sales Invoices',
doctype: 'SalesInvoice',
total: 0,
unpaid: 0,
paid: 0,
color: 'blue',
periodKey: 'salesInvoicePeriod',
hasData: false,
barWidth: 40
},
{
title: 'Purchase Invoices',
doctype: 'PurchaseInvoice',
total: 0,
unpaid: 0,
paid: 0,
color: 'gray',
periodKey: 'purchaseInvoicePeriod',
hasData: false,
barWidth: 60
}
],
salesInvoicePeriod: 'This Year', salesInvoicePeriod: 'This Year',
purchaseInvoicePeriod: 'This Year' purchaseInvoicePeriod: 'This Year'
}), }),
@ -59,26 +112,7 @@ export default {
}, },
methods: { methods: {
async calculateInvoiceTotals() { async calculateInvoiceTotals() {
let promises = [ let promises = this.invoices.map(async d => {
{
title: 'Sales Invoices',
doctype: 'SalesInvoice',
total: 0,
unpaid: 0,
paid: 0,
color: 'blue',
periodKey: 'salesInvoicePeriod'
},
{
title: 'Purchase Invoices',
doctype: 'PurchaseInvoice',
total: 0,
unpaid: 0,
paid: 0,
color: 'gray',
periodKey: 'purchaseInvoicePeriod'
}
].map(async d => {
let { fromDate, toDate, periodicity } = await getDatesAndPeriodicity( let { fromDate, toDate, periodicity } = await getDatesAndPeriodicity(
this.$data[d.periodKey] this.$data[d.periodKey]
); );
@ -97,10 +131,16 @@ export default {
d.total = total; d.total = total;
d.unpaid = outstanding; d.unpaid = outstanding;
d.paid = total - outstanding; d.paid = total - outstanding;
d.hasData = (d.total || 0) !== 0;
d.barWidth = (d.paid / d.total) * 100;
return d; return d;
}); });
this.invoices = await Promise.all(promises); this.invoices = await Promise.all(promises);
},
async newInvoice(invoice) {
let doc = await frappe.getNewDoc(invoice.doctype);
this.$router.push(`/edit/${invoice.doctype}/${doc.name}`);
} }
} }
}; };