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:
parent
3eb961e1d8
commit
81b2b31187
@ -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',
|
||||||
|
@ -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: {
|
||||||
|
@ -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,
|
||||||
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user