2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 19:09:01 +00:00

incr: add MouseFollower

- unpaid invoices tooltips
- fix getstarted
This commit is contained in:
18alantom 2022-05-17 11:39:08 +05:30
parent 1e35635170
commit e9226f38c8
10 changed files with 197 additions and 44 deletions

View File

@ -56,6 +56,10 @@ export class Converter {
} }
static toDocValue(value: RawValue, field: Field, fyo: Fyo): DocValue { static toDocValue(value: RawValue, field: Field, fyo: Fyo): DocValue {
if (!field?.fieldtype) {
console.log(value, field);
console.trace();
}
switch (field.fieldtype) { switch (field.fieldtype) {
case FieldTypeEnum.Currency: case FieldTypeEnum.Currency:
return toDocCurrency(value, field, fyo); return toDocCurrency(value, field, fyo);
@ -102,6 +106,9 @@ export class Converter {
for (const fieldname in rawValueMap) { for (const fieldname in rawValueMap) {
const field = fieldValueMap[fieldname]; const field = fieldValueMap[fieldname];
const rawValue = rawValueMap[fieldname]; const rawValue = rawValueMap[fieldname];
if (!field) {
continue;
}
if (Array.isArray(rawValue)) { if (Array.isArray(rawValue)) {
const parentSchemaName = (field as TargetField).target; const parentSchemaName = (field as TargetField).target;

View File

@ -24,8 +24,13 @@
"fieldtype": "Check" "fieldtype": "Check"
}, },
{ {
"fieldname": "itemCreated", "fieldname": "salesItemCreated",
"label": "Item Created", "label": "Purchase Item Created",
"fieldtype": "Check"
},
{
"fieldname": "purchaseItemCreated",
"label": "Sales Item Created",
"fieldtype": "Check" "fieldtype": "Check"
}, },
{ {

View File

@ -122,7 +122,7 @@
> >
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">
<p> <p>
{{ xi > -1 ? xLabels[xi] : '' }} {{ xi > -1 ? formatX(xLabels[xi]) : '' }}
</p> </p>
<p class="font-semibold"> <p class="font-semibold">
{{ yi > -1 ? format(points[yi][xi]) : '' }} {{ yi > -1 ? format(points[yi][xi]) : '' }}

View File

@ -0,0 +1,35 @@
<template>
<Tooltip ref="tooltip"><slot></slot></Tooltip>
</template>
<script>
import { defineComponent } from 'vue';
import Tooltip from './Tooltip.vue';
export default defineComponent({
props: { show: { type: Boolean, default: false } },
components: { Tooltip },
watch: {
show(val) {
if (val) {
this.$refs.tooltip.create();
this.setListeners();
} else {
this.$refs.tooltip.destroy();
this.removeListener();
}
},
},
methods: {
mousemoveListener(e) {
this.$refs.tooltip.update(e);
},
setListeners() {
window.addEventListener('mousemove', this.mousemoveListener);
},
removeListener() {
window.removeEventListener('mousemove', this.mousemoveListener);
},
},
});
</script>

View File

@ -46,7 +46,8 @@ import { ModelNameEnum } from 'models/types';
import LineChart from 'src/components/Charts/LineChart.vue'; import LineChart from 'src/components/Charts/LineChart.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { formatXLabels, getYMax } from 'src/utils/chart'; import { formatXLabels, getYMax } from 'src/utils/chart';
import { getDatesAndPeriodicity } from 'src/utils/misc'; import { getDatesAndPeriodList } from 'src/utils/misc';
import { getMapFromList } from 'utils/';
import PeriodSelector from './PeriodSelector'; import PeriodSelector from './PeriodSelector';
export default { export default {
@ -89,8 +90,25 @@ export default {
}, },
methods: { methods: {
async setData() { async setData() {
const { fromDate, toDate } = await getDatesAndPeriodicity(this.period); const { periodList, fromDate, toDate } = await getDatesAndPeriodList(
this.data = await fyo.db.getCashflow(fromDate, toDate); this.period
);
const data = await fyo.db.getCashflow(fromDate, toDate);
const dataMap = getMapFromList(data, 'month-year');
this.data = periodList.map((p) => {
const key = p.toFormat('yyyy-MM');
const item = dataMap[key];
if (item) {
return item;
}
return {
inflow: 0,
outflow: 0,
'month-year': key,
};
});
}, },
async setHasData() { async setHasData() {
const accounts = await fyo.db.getAllRaw('Account', { const accounts = await fyo.db.getAllRaw('Account', {

View File

@ -2,7 +2,7 @@
<div class="flex justify-between gap-10"> <div class="flex justify-between gap-10">
<div <div
class="flex-col justify-between flex-1" class="flex-col justify-between flex-1"
v-for="invoice in invoices" v-for="(invoice, i) in invoices"
:key="invoice.title" :key="invoice.title"
> >
<!-- Title and Period Selector --> <!-- Title and Period Selector -->
@ -10,7 +10,7 @@
<template #title>{{ invoice.title }}</template> <template #title>{{ invoice.title }}</template>
<template #action> <template #action>
<PeriodSelector <PeriodSelector
v-if="invoice.count" v-if="invoice.hasData"
:value="$data[invoice.periodKey]" :value="$data[invoice.periodKey]"
@change="(value) => ($data[invoice.periodKey] = value)" @change="(value) => ($data[invoice.periodKey] = value)"
/> />
@ -53,7 +53,11 @@
</div> </div>
<!-- Widget Bar --> <!-- Widget Bar -->
<div class="mt-2 relative rounded overflow-hidden"> <div
class="mt-2 relative rounded overflow-hidden"
@mouseenter="idx = i"
@mouseleave="idx = -1"
>
<div <div
class="w-full h-4" class="w-full h-4"
:class=" :class="
@ -74,14 +78,33 @@
</div> </div>
</div> </div>
</div> </div>
<MouseFollower
v-if="invoices[0].hasData || invoices[1].hasData"
:show="idx >= 0"
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-l-2"
:style="{ borderColor: colors[idx] }"
>
<div class="flex justify-between gap-4">
<p>{{ t`Paid` }}</p>
<p class="font-semibold">{{ invoices[idx]?.paidCount ?? 0 }}</p>
</div>
<div
v-if="invoices[idx]?.unpaidCount > 0"
class="flex justify-between gap-4"
>
<p>{{ t`Unpaid` }}</p>
<p class="font-semibold">{{ invoices[idx]?.unpaidCount ?? 0 }}</p>
</div>
</MouseFollower>
</div> </div>
</template> </template>
<script> <script>
import { t } from 'fyo'; import { t } from 'fyo';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import MouseFollower from 'src/components/MouseFollower.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { getDatesAndPeriodicity } from 'src/utils/misc'; import { getDatesAndPeriodList } from 'src/utils/misc';
import { routeTo } from 'src/utils/ui'; import { routeTo } from 'src/utils/ui';
import PeriodSelector from './PeriodSelector.vue'; import PeriodSelector from './PeriodSelector.vue';
import SectionHeader from './SectionHeader.vue'; import SectionHeader from './SectionHeader.vue';
@ -92,16 +115,22 @@ export default {
PeriodSelector, PeriodSelector,
SectionHeader, SectionHeader,
Button, Button,
MouseFollower,
}, },
data: () => ({ data: () => ({
idx: -1,
colors: ['#33A1FF', '#B7BFC6'],
invoices: [ invoices: [
{ {
title: t`Sales Invoices`, title: t`Sales Invoices`,
schemaName: ModelNameEnum.SalesInvoice, schemaName: ModelNameEnum.SalesInvoice,
total: 0, total: 0,
unpaid: 0, unpaid: 0,
hasData: false,
paid: 0, paid: 0,
count: 0, count: 0,
unpaidCount: 0,
paidCount: 0,
color: 'blue', color: 'blue',
periodKey: 'salesInvoicePeriod', periodKey: 'salesInvoicePeriod',
barWidth: 40, barWidth: 40,
@ -111,8 +140,11 @@ export default {
schemaName: ModelNameEnum.PurchaseInvoice, schemaName: ModelNameEnum.PurchaseInvoice,
total: 0, total: 0,
unpaid: 0, unpaid: 0,
hasData: false,
paid: 0, paid: 0,
count: 0, count: 0,
unpaidCount: 0,
paidCount: 0,
color: 'gray', color: 'gray',
periodKey: 'purchaseInvoicePeriod', periodKey: 'purchaseInvoicePeriod',
barWidth: 60, barWidth: 60,
@ -131,7 +163,7 @@ export default {
methods: { methods: {
async calculateInvoiceTotals() { async calculateInvoiceTotals() {
for (const invoice of this.invoices) { for (const invoice of this.invoices) {
const { fromDate, toDate } = await getDatesAndPeriodicity( const { fromDate, toDate } = await getDatesAndPeriodList(
this.$data[invoice.periodKey] this.$data[invoice.periodKey]
); );
@ -141,14 +173,19 @@ export default {
toDate toDate
); );
const count = await fyo.db.count(invoice.schemaName, { const { countTotal, countOutstanding } = await this.getCounts(
filters: { cancelled: false, submitted: true }, invoice.schemaName,
}); DateTime.fromISO(fromDate),
DateTime.fromISO(toDate)
);
invoice.total = total ?? 0; invoice.total = total ?? 0;
invoice.unpaid = outstanding ?? 0; invoice.unpaid = outstanding ?? 0;
invoice.paid = total - outstanding; invoice.paid = total - outstanding;
invoice.count = count; invoice.hasData = countTotal > 0;
invoice.count = countTotal;
invoice.paidCount = countTotal - countOutstanding;
invoice.unpaidCount = countOutstanding;
invoice.barWidth = (invoice.paid / (invoice.total || 1)) * 100; invoice.barWidth = (invoice.paid / (invoice.total || 1)) * 100;
} }
}, },
@ -156,6 +193,26 @@ export default {
let doc = await fyo.doc.getNewDoc(invoice.schemaName); let doc = await fyo.doc.getNewDoc(invoice.schemaName);
routeTo(`/edit/${invoice.schemaName}/${doc.name}`); routeTo(`/edit/${invoice.schemaName}/${doc.name}`);
}, },
async getCounts(schemaName, fromDate, toDate) {
const outstandingAmounts = await fyo.db.getAllRaw(schemaName, {
fields: ['outstandingAmount'],
filters: {
cancelled: false,
submitted: true,
date: ['<=', toDate.toISO(), '>=', fromDate.toISO()],
},
});
const isOutstanding = outstandingAmounts.map((o) =>
parseFloat(o.outstandingAmount)
);
return {
countTotal: isOutstanding.length,
countOutstanding: isOutstanding.filter((o) => o > 0).length,
};
},
}, },
}; };
</script> </script>

View File

@ -166,43 +166,40 @@ export default {
return; return;
} }
if (!fyo.singles.GetStarted.itemCreated) { if (!fyo.singles.GetStarted.salesItemCreated) {
const count = await fyo.db.count('Item'); const count = await fyo.db.count('Item', { filters: { for: 'Sales' } });
if (count > 0) { toUpdate.salesItemCreated = count > 0;
toUpdate.itemCreated = 1;
} }
if (!fyo.singles.GetStarted.purchaseItemCreated) {
const count = await fyo.db.count('Item', {
filters: { for: 'Purchases' },
});
toUpdate.purchaseItemCreated = count > 0;
} }
if (!fyo.singles.GetStarted.invoiceCreated) { if (!fyo.singles.GetStarted.invoiceCreated) {
const count = await fyo.db.count('SalesInvoice'); const count = await fyo.db.count('SalesInvoice');
if (count > 0) { toUpdate.invoiceCreated = count > 0;
toUpdate.invoiceCreated = 1;
}
} }
if (!fyo.singles.GetStarted.customerCreated) { if (!fyo.singles.GetStarted.customerCreated) {
const count = fyo.db.count('Party', { const count = await fyo.db.count('Party', {
filters: { role: 'Customer' }, filters: { role: 'Customer' },
}); });
if (count > 0) { toUpdate.customerCreated = count > 0;
toUpdate.customerCreated = 1;
}
} }
if (!fyo.singles.GetStarted.billCreated) { if (!fyo.singles.GetStarted.billCreated) {
const count = await fyo.db.count('SalesInvoice'); const count = await fyo.db.count('SalesInvoice');
if (count > 0) { toUpdate.billCreated = count > 0;
toUpdate.billCreated = 1;
}
} }
if (!fyo.singles.GetStarted.supplierCreated) { if (!fyo.singles.GetStarted.supplierCreated) {
const count = fyo.db.count('Party', { const count = await fyo.db.count('Party', {
filters: { role: 'Supplier' }, filters: { role: 'Supplier' },
}); });
if (count > 0) { toUpdate.supplierCreated = count > 0;
toUpdate.supplierCreated = 1;
}
} }
await this.updateChecks(toUpdate); await this.updateChecks(toUpdate);
}, },
@ -211,10 +208,10 @@ export default {
await fyo.doc.getSingle('GetStarted'); await fyo.doc.getSingle('GetStarted');
}, },
isCompleted(item) { isCompleted(item) {
return fyo.singles.GetStarted.get(item.fieldname) || 0; return fyo.singles.GetStarted.get(item.fieldname) || false;
}, },
getIconComponent(item) { getIconComponent(item) {
let completed = fyo.singles.GetStarted[item.fieldname] || 0; let completed = fyo.singles.GetStarted[item.fieldname] || false;
let name = completed ? 'green-check' : item.icon; let name = completed ? 'green-check' : item.icon;
let size = completed ? '24' : '18'; let size = completed ? '24' : '18';
return { return {

View File

@ -84,8 +84,8 @@ export function getGetStartedConfig() {
label: t`Add Items`, label: t`Add Items`,
icon: 'item', icon: 'item',
description: t`Add products or services that you sell to your customers`, description: t`Add products or services that you sell to your customers`,
action: () => routeTo('/list/Item'), action: () => routeTo(`/list/Item/for/Sales/${t`Sales Items`}`),
fieldname: 'itemCreated', fieldname: 'salesItemCreated',
documentation: 'https://frappebooks.com/docs/setting-up#3-add-items', documentation: 'https://frappebooks.com/docs/setting-up#3-add-items',
}, },
{ {
@ -93,7 +93,7 @@ export function getGetStartedConfig() {
label: t`Add Customers`, label: t`Add Customers`,
icon: 'customer', icon: 'customer',
description: t`Add a few customers to create your first invoice`, description: t`Add a few customers to create your first invoice`,
action: () => routeTo('/list/Customer'), action: () => routeTo(`/list/Party/role/Customer/${t`Customers`}`),
fieldname: 'customerCreated', fieldname: 'customerCreated',
documentation: documentation:
'https://frappebooks.com/docs/setting-up#4-add-customers', 'https://frappebooks.com/docs/setting-up#4-add-customers',
@ -118,15 +118,16 @@ export function getGetStartedConfig() {
label: t`Add Items`, label: t`Add Items`,
icon: 'item', icon: 'item',
description: t`Add products or services that you buy from your suppliers`, description: t`Add products or services that you buy from your suppliers`,
action: () => routeTo('/list/Item'), action: () =>
fieldname: 'itemCreated', routeTo(`/list/Item/for/Purchases/${t`Purchase Items`}`),
fieldname: 'purchaseItemCreated',
}, },
{ {
key: 'Add Suppliers', key: 'Add Suppliers',
label: t`Add Suppliers`, label: t`Add Suppliers`,
icon: 'supplier', icon: 'supplier',
description: t`Add a few suppliers to create your first bill`, description: t`Add a few suppliers to create your first bill`,
action: () => routeTo('/list/Supplier'), action: () => routeTo(`/list/Party/role/Supplier/${t`Suppliers`}`),
fieldname: 'supplierCreated', fieldname: 'supplierCreated',
}, },
{ {

View File

@ -7,9 +7,9 @@ import SetupWizardSchema from 'schemas/app/SetupWizard.json';
import { Schema } from 'schemas/types'; import { Schema } from 'schemas/types';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
export function getDatesAndPeriodicity( export function getDatesAndPeriodList(
period: 'This Year' | 'This Quarter' | 'This Month' period: 'This Year' | 'This Quarter' | 'This Month'
): { fromDate: string; toDate: string } { ): { periodList: DateTime[]; fromDate: string; toDate: string } {
const toDate: DateTime = DateTime.now(); const toDate: DateTime = DateTime.now();
let fromDate: DateTime; let fromDate: DateTime;
@ -23,7 +23,22 @@ export function getDatesAndPeriodicity(
fromDate = toDate.minus({ days: 1 }); fromDate = toDate.minus({ days: 1 });
} }
/**
* periodList: Monthly decrements before toDate until fromDate
*/
const periodList: DateTime[] = [toDate];
while (true) {
const nextDate = periodList.at(0)!.minus({ months: 1 });
if (nextDate.toMillis() < fromDate.toMillis()) {
break;
}
periodList.unshift(nextDate);
}
periodList.shift();
return { return {
periodList,
fromDate: fromDate.toISO(), fromDate: fromDate.toISO(),
toDate: toDate.toISO(), toDate: toDate.toISO(),
}; };

View File

@ -29,3 +29,21 @@ export function useKeys(callback?: (keys: Set<string>) => void) {
return keys; return keys;
} }
export function useMouseLocation() {
const loc = ref({ clientX: 0, clientY: 0 });
const mousemoveListener = (e: MouseEvent) => {
loc.value.clientX = e.clientX;
loc.value.clientY = e.clientY;
};
onMounted(() => {
window.addEventListener('mousemove', mousemoveListener);
});
onUnmounted(() => {
window.removeEventListener('mousemove', mousemoveListener);
});
return loc;
}