mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
fix: Refactor Dashboard
- Move charts to separate components
This commit is contained in:
parent
70b8e818c0
commit
3f411ba010
87
src/pages/Dashboard/Cashflow.vue
Normal file
87
src/pages/Dashboard/Cashflow.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-medium">{{ _('Cash Flow') }}</div>
|
||||
<div class="flex text-base">
|
||||
<div class="flex items-center">
|
||||
<span class="w-3 h-3 rounded inline-block bg-blue-500"></span>
|
||||
<span class="ml-2">{{ _('Inflow') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center ml-6">
|
||||
<span class="w-3 h-3 rounded inline-block bg-gray-500"></span>
|
||||
<span class="ml-2">{{ _('Outflow') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<PeriodSelector :value="period" @change="value => (period = value)" />
|
||||
</div>
|
||||
<div class="chart-wrapper" ref="cashflow"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import { Chart } from 'frappe-charts';
|
||||
import PeriodSelector from './PeriodSelector';
|
||||
import Cashflow from '../../../reports/Cashflow/Cashflow';
|
||||
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
|
||||
|
||||
export default {
|
||||
name: 'Cashflow',
|
||||
components: {
|
||||
PeriodSelector
|
||||
},
|
||||
data: () => ({ period: 'This Year' }),
|
||||
watch: {
|
||||
period: 'render'
|
||||
},
|
||||
mounted() {
|
||||
this.render();
|
||||
},
|
||||
methods: {
|
||||
async render() {
|
||||
let { fromDate, toDate, periodicity } = await getDatesAndPeriodicity(
|
||||
this.period
|
||||
);
|
||||
|
||||
let { data, periodList } = await new Cashflow().run({
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity
|
||||
});
|
||||
|
||||
const chart = new Chart(this.$refs['cashflow'], {
|
||||
title: '',
|
||||
type: 'line',
|
||||
animate: false,
|
||||
colors: ['#2490EF', '#B7BFC6'],
|
||||
axisOptions: {
|
||||
xAxisMode: 'tick',
|
||||
shortenYAxisNumbers: true
|
||||
},
|
||||
lineOptions: {
|
||||
regionFill: 1,
|
||||
hideDots: 1,
|
||||
heatLine: 1
|
||||
},
|
||||
tooltipOptions: {
|
||||
formatTooltipY: value => frappe.format(value, 'Currency')
|
||||
},
|
||||
data: {
|
||||
labels: periodList,
|
||||
datasets: [
|
||||
{
|
||||
name: 'Inflow',
|
||||
chartType: 'line',
|
||||
values: data.map(period => period.inflow)
|
||||
},
|
||||
{
|
||||
name: 'Outflow',
|
||||
chartType: 'line',
|
||||
values: data.map(period => period.outflow)
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -8,104 +8,16 @@
|
||||
</PageHeader>
|
||||
<div class="mt-4 px-8">
|
||||
<div class="border-t" />
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-medium">{{ _('Cash Flow') }}</div>
|
||||
<div class="flex text-base">
|
||||
<div class="flex items-center">
|
||||
<span class="w-3 h-3 rounded inline-block bg-blue-500"></span>
|
||||
<span class="ml-2">{{ _('Inflow') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center ml-6">
|
||||
<span class="w-3 h-3 rounded inline-block bg-gray-500"></span>
|
||||
<span class="ml-2">{{ _('Outflow') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<PeriodSelector
|
||||
:value="periods.cashflow"
|
||||
@change="value => periodChange('cashflow', value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="chart-wrapper" ref="cashflow"></div>
|
||||
</div>
|
||||
<Cashflow />
|
||||
<div class="my-10 border-t" />
|
||||
<div class="flex -mx-4">
|
||||
<div
|
||||
class="w-1/2 px-4"
|
||||
v-for="invoice in invoices"
|
||||
:key="invoice.title"
|
||||
>
|
||||
<SectionHeader>
|
||||
<template slot="title">{{ invoice.title }}</template>
|
||||
<PeriodSelector
|
||||
slot="action"
|
||||
:value="periods[invoice.periodKey]"
|
||||
@change="value => periodChange(invoice.periodKey, value)"
|
||||
/>
|
||||
</SectionHeader>
|
||||
<div class="mt-6 flex justify-between">
|
||||
<div class="text-sm">
|
||||
{{ frappe.format(invoice.paid, 'Currency') }}
|
||||
<span class="text-gray-600">{{ _('Paid') }}</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ frappe.format(invoice.unpaid, 'Currency') }}
|
||||
<span class="text-gray-600">{{ _('Unpaid') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 relative">
|
||||
<div
|
||||
class="w-full h-4 rounded"
|
||||
:class="invoice.color == 'blue' ? 'bg-blue-200' : 'bg-gray-200'"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 h-4 rounded"
|
||||
:class="invoice.color == 'blue' ? 'bg-blue-500' : 'bg-gray-500'"
|
||||
:style="`width: ${(invoice.paid / invoice.total) * 100}%`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UnpaidInvoices />
|
||||
<div class="my-10 border-t" />
|
||||
<div class="flex -mx-4">
|
||||
<div class="w-1/2 px-4">
|
||||
<SectionHeader>
|
||||
<template slot="title">{{ _('Profit and Loss') }}</template>
|
||||
<PeriodSelector
|
||||
slot="action"
|
||||
:value="periods.profitAndLoss"
|
||||
@change="value => periodChange('profitAndLoss', value)"
|
||||
/>
|
||||
</SectionHeader>
|
||||
<div class="chart-wrapper" ref="profit-and-loss"></div>
|
||||
<ProfitAndLoss />
|
||||
</div>
|
||||
<div class="w-1/2 px-4">
|
||||
<SectionHeader>
|
||||
<template slot="title" class="font-medium">{{
|
||||
_('Top Expenses')
|
||||
}}</template>
|
||||
<PeriodSelector
|
||||
slot="action"
|
||||
:value="periods.expenses"
|
||||
@change="value => periodChange('expenses', value)"
|
||||
/>
|
||||
</SectionHeader>
|
||||
<div class="flex">
|
||||
<div class="w-1/2">
|
||||
<div
|
||||
class="mt-5 flex justify-between items-center text-sm"
|
||||
v-for="d in expenses"
|
||||
:key="d.name"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded" :class="d.class"></div>
|
||||
<div class="ml-3">{{ d.account }}</div>
|
||||
</div>
|
||||
<div>{{ frappe.format(d.total, 'Currency') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 chart-wrapper" ref="top-expenses"></div>
|
||||
</div>
|
||||
<Expenses />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -113,276 +25,22 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import { Chart } from 'frappe-charts';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import SearchBar from '@/components/SearchBar';
|
||||
import PeriodSelector from './PeriodSelector';
|
||||
import SectionHeader from './SectionHeader';
|
||||
import { getData } from '../../../reports/FinancialStatements/FinancialStatements';
|
||||
import ProfitAndLoss from '../../../reports/ProfitAndLoss/ProfitAndLoss';
|
||||
import Cashflow from '../../../reports/Cashflow/Cashflow';
|
||||
import theme from '@/theme';
|
||||
import { DateTime } from 'luxon';
|
||||
import Cashflow from './Cashflow';
|
||||
import UnpaidInvoices from './UnpaidInvoices';
|
||||
import ProfitAndLoss from './ProfitAndLoss';
|
||||
import Expenses from './Expenses';
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
components: {
|
||||
PageHeader,
|
||||
SearchBar,
|
||||
PeriodSelector,
|
||||
SectionHeader
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
invoices: [],
|
||||
expenses: [],
|
||||
periods: {
|
||||
cashflow: 'This Year',
|
||||
receivables: 'This Year',
|
||||
payables: 'This Year',
|
||||
expenses: 'This Year',
|
||||
profitAndLoss: 'This Year'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
await this.generateCashflowChart();
|
||||
this.getInvoiceTotals();
|
||||
this.generateProfitAndLossChart();
|
||||
this.generateExpensesPieChart();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async getInvoiceTotals() {
|
||||
let promises = [
|
||||
{
|
||||
title: 'Sales Invoices',
|
||||
doctype: 'SalesInvoice',
|
||||
total: 0,
|
||||
unpaid: 0,
|
||||
paid: 0,
|
||||
color: 'blue',
|
||||
periodKey: 'receivables'
|
||||
},
|
||||
{
|
||||
title: 'Purchase Invoices',
|
||||
doctype: 'PurchaseInvoice',
|
||||
total: 0,
|
||||
unpaid: 0,
|
||||
paid: 0,
|
||||
color: 'gray',
|
||||
periodKey: 'payables'
|
||||
},
|
||||
].map(async d => {
|
||||
let { fromDate, toDate, periodicity } = await this.getDatesAndPeriodicity(
|
||||
this.periods[d.periodKey]
|
||||
);
|
||||
|
||||
let res = await frappe.db.sql(
|
||||
`
|
||||
select
|
||||
sum(baseGrandTotal) as total,
|
||||
sum(outstandingAmount) as outstanding
|
||||
from ${d.doctype}
|
||||
where date >= $fromDate and date <= $toDate
|
||||
`,
|
||||
{ $fromDate: fromDate, $toDate: toDate }
|
||||
);
|
||||
let { total, outstanding } = res[0];
|
||||
d.total = total;
|
||||
d.unpaid = outstanding;
|
||||
d.paid = total - outstanding;
|
||||
return d;
|
||||
});
|
||||
|
||||
this.invoices = await Promise.all(promises);
|
||||
},
|
||||
async generateCashflowChart() {
|
||||
let { fromDate, toDate, periodicity } = await this.getDatesAndPeriodicity(
|
||||
this.periods.cashflow
|
||||
);
|
||||
|
||||
let { data, periodList } = await new Cashflow().run({
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity
|
||||
});
|
||||
|
||||
const chart = new Chart(this.$refs['cashflow'], {
|
||||
title: '',
|
||||
type: 'line',
|
||||
animate: false,
|
||||
colors: ['#2490EF', '#B7BFC6'],
|
||||
axisOptions: {
|
||||
xAxisMode: 'tick',
|
||||
shortenYAxisNumbers: true
|
||||
},
|
||||
lineOptions: {
|
||||
regionFill: 1,
|
||||
hideDots: 1,
|
||||
heatLine: 1
|
||||
},
|
||||
tooltipOptions: {
|
||||
formatTooltipY: value => frappe.format(value, 'Currency')
|
||||
},
|
||||
data: {
|
||||
labels: periodList,
|
||||
datasets: [
|
||||
{
|
||||
name: 'Inflow',
|
||||
chartType: 'line',
|
||||
values: data.map(period => period.inflow)
|
||||
},
|
||||
{
|
||||
name: 'Outflow',
|
||||
chartType: 'line',
|
||||
values: data.map(period => period.outflow)
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
},
|
||||
async generateProfitAndLossChart() {
|
||||
let { fromDate, toDate, periodicity } = await this.getDatesAndPeriodicity(
|
||||
this.periods.profitAndLoss
|
||||
);
|
||||
|
||||
let pl = new ProfitAndLoss();
|
||||
let res = await pl.run({
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity
|
||||
});
|
||||
|
||||
let totalRow = res.rows[res.rows.length - 1];
|
||||
|
||||
const chart = new Chart(this.$refs['profit-and-loss'], {
|
||||
title: '',
|
||||
animate: false,
|
||||
type: 'bar',
|
||||
colors: ['#2490EF', '#B7BFC6'],
|
||||
axisOptions: {
|
||||
xAxisMode: 'tick',
|
||||
shortenYAxisNumbers: true,
|
||||
xIsSeries: true
|
||||
},
|
||||
tooltipOptions: {
|
||||
formatTooltipY: value => frappe.format(value, 'Currency')
|
||||
},
|
||||
data: {
|
||||
labels: res.columns,
|
||||
datasets: [
|
||||
{
|
||||
name: 'Income',
|
||||
chartType: 'bar',
|
||||
values: res.columns.map(key => totalRow[key])
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async generateExpensesPieChart() {
|
||||
let { fromDate, toDate } = await this.getDatesAndPeriodicity(
|
||||
this.periods.expenses
|
||||
);
|
||||
|
||||
let topExpenses = await frappe.db.sql(
|
||||
`
|
||||
select sum(debit) - sum(credit) as total, account from AccountingLedgerEntry
|
||||
where account in (
|
||||
select name from Account where rootType = "Expense"
|
||||
)
|
||||
and date >= $fromDate and date <= $toDate
|
||||
group by account
|
||||
order by total desc
|
||||
limit 5
|
||||
`,
|
||||
{ $fromDate: fromDate, $toDate: toDate }
|
||||
);
|
||||
|
||||
let shades = [
|
||||
{ class: 'bg-gray-800', hex: theme.backgroundColor.gray['800'] },
|
||||
{ class: 'bg-gray-600', hex: theme.backgroundColor.gray['600'] },
|
||||
{ class: 'bg-gray-400', hex: theme.backgroundColor.gray['400'] },
|
||||
{ class: 'bg-gray-200', hex: theme.backgroundColor.gray['200'] },
|
||||
{ class: 'bg-gray-100', hex: theme.backgroundColor.gray['100'] }
|
||||
];
|
||||
topExpenses = topExpenses.map((d, i) => {
|
||||
d.class = shades[i].class;
|
||||
d.color = shades[i].hex;
|
||||
return d;
|
||||
});
|
||||
|
||||
this.expenses = topExpenses;
|
||||
|
||||
let chart = new Chart(this.$refs['top-expenses'], {
|
||||
type: 'pie',
|
||||
colors: topExpenses.map(d => d.color),
|
||||
data: {
|
||||
labels: topExpenses.map(d => d.account),
|
||||
datasets: [
|
||||
{
|
||||
values: topExpenses.map(d => d.total)
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
periodChange(key, value) {
|
||||
this.periods[key] = value;
|
||||
if (key === 'cashflow') {
|
||||
this.generateCashflowChart();
|
||||
}
|
||||
if (key === 'profitAndLoss') {
|
||||
this.generateProfitAndLossChart();
|
||||
}
|
||||
if (key === 'expenses') {
|
||||
this.generateExpensesPieChart();
|
||||
}
|
||||
if (['receivables', 'payables'].includes(key)) {
|
||||
this.getInvoiceTotals();
|
||||
}
|
||||
},
|
||||
|
||||
async getDatesAndPeriodicity(period) {
|
||||
let fromDate, toDate;
|
||||
let periodicity = 'Monthly';
|
||||
let accountingSettings = await frappe.getSingle('AccountingSettings');
|
||||
|
||||
if (period === 'This Year') {
|
||||
fromDate = accountingSettings.fiscalYearStart;
|
||||
toDate = accountingSettings.fiscalYearEnd;
|
||||
} else if (period === 'This Quarter') {
|
||||
fromDate = DateTime.local()
|
||||
.startOf('quarter')
|
||||
.toISODate();
|
||||
toDate = DateTime.local()
|
||||
.endOf('quarter')
|
||||
.toISODate();
|
||||
} else if (period === 'This Month') {
|
||||
fromDate = DateTime.local()
|
||||
.startOf('month')
|
||||
.toISODate();
|
||||
toDate = DateTime.local()
|
||||
.endOf('month')
|
||||
.toISODate();
|
||||
}
|
||||
|
||||
return {
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity
|
||||
};
|
||||
}
|
||||
Cashflow,
|
||||
UnpaidInvoices,
|
||||
ProfitAndLoss,
|
||||
Expenses
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.chart-wrapper >>> .chart-legend {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
118
src/pages/Dashboard/Expenses.vue
Normal file
118
src/pages/Dashboard/Expenses.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div>
|
||||
<SectionHeader>
|
||||
<template slot="title" class="font-medium">{{
|
||||
_('Top Expenses')
|
||||
}}</template>
|
||||
<PeriodSelector
|
||||
slot="action"
|
||||
:value="period"
|
||||
@change="value => (period = value)"
|
||||
/>
|
||||
</SectionHeader>
|
||||
<div class="flex">
|
||||
<div class="w-1/2">
|
||||
<div
|
||||
class="mt-5 flex justify-between items-center text-sm"
|
||||
v-for="d in expenses"
|
||||
:key="d.name"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded" :class="d.class"></div>
|
||||
<div class="ml-3">{{ d.account }}</div>
|
||||
</div>
|
||||
<div>{{ frappe.format(d.total, 'Currency') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 relative">
|
||||
<div class="chart-wrapper" ref="top-expenses"></div>
|
||||
<div class="absolute text-base text-center font-semibold" style="right: 3.8rem; top: 32%;">
|
||||
<div>
|
||||
{{ frappe.format(totalExpense, 'Currency') }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">
|
||||
{{ _('Total Spending') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import { Chart } from 'frappe-charts';
|
||||
import theme from '@/theme';
|
||||
import PeriodSelector from './PeriodSelector';
|
||||
import SectionHeader from './SectionHeader';
|
||||
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
|
||||
|
||||
export default {
|
||||
name: 'Expenses',
|
||||
components: {
|
||||
PeriodSelector,
|
||||
SectionHeader
|
||||
},
|
||||
data: () => ({ period: 'This Year', expenses: [] }),
|
||||
mounted() {
|
||||
this.render();
|
||||
},
|
||||
watch: {
|
||||
period: 'render'
|
||||
},
|
||||
computed: {
|
||||
totalExpense() {
|
||||
return this.expenses.reduce((sum, expense) => sum + expense.total, 0);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async render() {
|
||||
let { fromDate, toDate } = await getDatesAndPeriodicity(this.period);
|
||||
|
||||
let topExpenses = await frappe.db.sql(
|
||||
`
|
||||
select sum(debit) - sum(credit) as total, account from AccountingLedgerEntry
|
||||
where account in (
|
||||
select name from Account where rootType = "Expense"
|
||||
)
|
||||
and date >= $fromDate and date <= $toDate
|
||||
group by account
|
||||
order by total desc
|
||||
limit 5
|
||||
`,
|
||||
{ $fromDate: fromDate, $toDate: toDate }
|
||||
);
|
||||
|
||||
let shades = [
|
||||
{ class: 'bg-gray-800', hex: theme.backgroundColor.gray['800'] },
|
||||
{ class: 'bg-gray-600', hex: theme.backgroundColor.gray['600'] },
|
||||
{ class: 'bg-gray-400', hex: theme.backgroundColor.gray['400'] },
|
||||
{ class: 'bg-gray-200', hex: theme.backgroundColor.gray['200'] },
|
||||
{ class: 'bg-gray-100', hex: theme.backgroundColor.gray['100'] }
|
||||
];
|
||||
topExpenses = topExpenses.map((d, i) => {
|
||||
d.class = shades[i].class;
|
||||
d.color = shades[i].hex;
|
||||
return d;
|
||||
});
|
||||
|
||||
this.expenses = topExpenses;
|
||||
|
||||
let chart = new Chart(this.$refs['top-expenses'], {
|
||||
type: 'donut',
|
||||
hoverRadio: 0.01,
|
||||
strokeWidth: 18,
|
||||
colors: topExpenses.map(d => d.color),
|
||||
data: {
|
||||
labels: topExpenses.map(d => d.account),
|
||||
datasets: [
|
||||
{
|
||||
values: topExpenses.map(d => d.total)
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
77
src/pages/Dashboard/ProfitAndLoss.vue
Normal file
77
src/pages/Dashboard/ProfitAndLoss.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<SectionHeader>
|
||||
<template slot="title">{{ _('Profit and Loss') }}</template>
|
||||
<PeriodSelector
|
||||
slot="action"
|
||||
:value="period"
|
||||
@change="value => (period = value)"
|
||||
/>
|
||||
</SectionHeader>
|
||||
<div class="chart-wrapper" ref="profit-and-loss"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import { Chart } from 'frappe-charts';
|
||||
import PeriodSelector from './PeriodSelector';
|
||||
import SectionHeader from './SectionHeader';
|
||||
import ProfitAndLoss from '../../../reports/ProfitAndLoss/ProfitAndLoss';
|
||||
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
|
||||
|
||||
export default {
|
||||
name: 'ProfitAndLoss',
|
||||
components: {
|
||||
PeriodSelector,
|
||||
SectionHeader
|
||||
},
|
||||
data: () => ({ period: 'This Year' }),
|
||||
mounted() {
|
||||
this.render();
|
||||
},
|
||||
watch: {
|
||||
period: 'render'
|
||||
},
|
||||
methods: {
|
||||
async render() {
|
||||
let { fromDate, toDate, periodicity } = await getDatesAndPeriodicity(
|
||||
this.period
|
||||
);
|
||||
|
||||
let pl = new ProfitAndLoss();
|
||||
let res = await pl.run({
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity
|
||||
});
|
||||
|
||||
let totalRow = res.rows[res.rows.length - 1];
|
||||
|
||||
const chart = new Chart(this.$refs['profit-and-loss'], {
|
||||
title: '',
|
||||
animate: false,
|
||||
type: 'bar',
|
||||
colors: ['#2490EF', '#B7BFC6'],
|
||||
axisOptions: {
|
||||
xAxisMode: 'tick',
|
||||
shortenYAxisNumbers: true,
|
||||
xIsSeries: true
|
||||
},
|
||||
tooltipOptions: {
|
||||
formatTooltipY: value => frappe.format(value, 'Currency')
|
||||
},
|
||||
data: {
|
||||
labels: res.columns,
|
||||
datasets: [
|
||||
{
|
||||
name: 'Income',
|
||||
chartType: 'bar',
|
||||
values: res.columns.map(key => totalRow[key])
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
107
src/pages/Dashboard/UnpaidInvoices.vue
Normal file
107
src/pages/Dashboard/UnpaidInvoices.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="flex -mx-4">
|
||||
<div class="w-1/2 px-4" v-for="invoice in invoices" :key="invoice.title">
|
||||
<SectionHeader>
|
||||
<template slot="title">{{ invoice.title }}</template>
|
||||
<PeriodSelector
|
||||
slot="action"
|
||||
:value="$data[invoice.periodKey]"
|
||||
@change="value => ($data[invoice.periodKey] = value)"
|
||||
/>
|
||||
</SectionHeader>
|
||||
<div class="mt-6 flex justify-between">
|
||||
<div class="text-sm">
|
||||
{{ frappe.format(invoice.paid, 'Currency') }}
|
||||
<span class="text-gray-600">{{ _('Paid') }}</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ frappe.format(invoice.unpaid, 'Currency') }}
|
||||
<span class="text-gray-600">{{ _('Unpaid') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 relative">
|
||||
<div
|
||||
class="w-full h-4 rounded"
|
||||
:class="invoice.color == 'blue' ? 'bg-blue-200' : 'bg-gray-200'"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 h-4 rounded"
|
||||
:class="invoice.color == 'blue' ? 'bg-blue-500' : 'bg-gray-500'"
|
||||
:style="`width: ${(invoice.paid / invoice.total) * 100}%`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import PeriodSelector from './PeriodSelector';
|
||||
import SectionHeader from './SectionHeader';
|
||||
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
|
||||
|
||||
export default {
|
||||
name: 'UnpaidInvoices',
|
||||
components: {
|
||||
PeriodSelector,
|
||||
SectionHeader
|
||||
},
|
||||
data: () => ({
|
||||
invoices: [],
|
||||
salesInvoicePeriod: 'This Year',
|
||||
purchaseInvoicePeriod: 'This Year'
|
||||
}),
|
||||
watch: {
|
||||
salesInvoicePeriod: 'calculateInvoiceTotals',
|
||||
purchaseInvoicePeriod: 'calculateInvoiceTotals'
|
||||
},
|
||||
mounted() {
|
||||
this.calculateInvoiceTotals();
|
||||
},
|
||||
methods: {
|
||||
async calculateInvoiceTotals() {
|
||||
let promises = [
|
||||
{
|
||||
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(
|
||||
this.$data[d.periodKey]
|
||||
);
|
||||
|
||||
let res = await frappe.db.sql(
|
||||
`
|
||||
select
|
||||
sum(baseGrandTotal) as total,
|
||||
sum(outstandingAmount) as outstanding
|
||||
from ${d.doctype}
|
||||
where date >= $fromDate and date <= $toDate
|
||||
`,
|
||||
{ $fromDate: fromDate, $toDate: toDate }
|
||||
);
|
||||
let { total, outstanding } = res[0];
|
||||
d.total = total;
|
||||
d.unpaid = outstanding;
|
||||
d.paid = total - outstanding;
|
||||
return d;
|
||||
});
|
||||
|
||||
this.invoices = await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
33
src/pages/Dashboard/getDatesAndPeriodicity.js
Normal file
33
src/pages/Dashboard/getDatesAndPeriodicity.js
Normal file
@ -0,0 +1,33 @@
|
||||
import frappe from 'frappejs';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export async function getDatesAndPeriodicity(period) {
|
||||
let fromDate, toDate;
|
||||
let periodicity = 'Monthly';
|
||||
let accountingSettings = await frappe.getSingle('AccountingSettings');
|
||||
|
||||
if (period === 'This Year') {
|
||||
fromDate = accountingSettings.fiscalYearStart;
|
||||
toDate = accountingSettings.fiscalYearEnd;
|
||||
} else if (period === 'This Quarter') {
|
||||
fromDate = DateTime.local()
|
||||
.startOf('quarter')
|
||||
.toISODate();
|
||||
toDate = DateTime.local()
|
||||
.endOf('quarter')
|
||||
.toISODate();
|
||||
} else if (period === 'This Month') {
|
||||
fromDate = DateTime.local()
|
||||
.startOf('month')
|
||||
.toISODate();
|
||||
toDate = DateTime.local()
|
||||
.endOf('month')
|
||||
.toISODate();
|
||||
}
|
||||
|
||||
return {
|
||||
fromDate,
|
||||
toDate,
|
||||
periodicity
|
||||
};
|
||||
}
|
@ -30,3 +30,7 @@ html {
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.frappe-chart .chart-legend {
|
||||
display: none;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user