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 {
if (!field?.fieldtype) {
console.log(value, field);
console.trace();
}
switch (field.fieldtype) {
case FieldTypeEnum.Currency:
return toDocCurrency(value, field, fyo);
@ -102,6 +106,9 @@ export class Converter {
for (const fieldname in rawValueMap) {
const field = fieldValueMap[fieldname];
const rawValue = rawValueMap[fieldname];
if (!field) {
continue;
}
if (Array.isArray(rawValue)) {
const parentSchemaName = (field as TargetField).target;

View File

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

View File

@ -122,7 +122,7 @@
>
<div class="flex flex-col justify-center items-center">
<p>
{{ xi > -1 ? xLabels[xi] : '' }}
{{ xi > -1 ? formatX(xLabels[xi]) : '' }}
</p>
<p class="font-semibold">
{{ 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 { fyo } from 'src/initFyo';
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';
export default {
@ -89,8 +90,25 @@ export default {
},
methods: {
async setData() {
const { fromDate, toDate } = await getDatesAndPeriodicity(this.period);
this.data = await fyo.db.getCashflow(fromDate, toDate);
const { periodList, fromDate, toDate } = await getDatesAndPeriodList(
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() {
const accounts = await fyo.db.getAllRaw('Account', {

View File

@ -2,7 +2,7 @@
<div class="flex justify-between gap-10">
<div
class="flex-col justify-between flex-1"
v-for="invoice in invoices"
v-for="(invoice, i) in invoices"
:key="invoice.title"
>
<!-- Title and Period Selector -->
@ -10,7 +10,7 @@
<template #title>{{ invoice.title }}</template>
<template #action>
<PeriodSelector
v-if="invoice.count"
v-if="invoice.hasData"
:value="$data[invoice.periodKey]"
@change="(value) => ($data[invoice.periodKey] = value)"
/>
@ -53,7 +53,11 @@
</div>
<!-- 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
class="w-full h-4"
:class="
@ -74,14 +78,33 @@
</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>
</template>
<script>
import { t } from 'fyo';
import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue';
import MouseFollower from 'src/components/MouseFollower.vue';
import { fyo } from 'src/initFyo';
import { getDatesAndPeriodicity } from 'src/utils/misc';
import { getDatesAndPeriodList } from 'src/utils/misc';
import { routeTo } from 'src/utils/ui';
import PeriodSelector from './PeriodSelector.vue';
import SectionHeader from './SectionHeader.vue';
@ -92,16 +115,22 @@ export default {
PeriodSelector,
SectionHeader,
Button,
MouseFollower,
},
data: () => ({
idx: -1,
colors: ['#33A1FF', '#B7BFC6'],
invoices: [
{
title: t`Sales Invoices`,
schemaName: ModelNameEnum.SalesInvoice,
total: 0,
unpaid: 0,
hasData: false,
paid: 0,
count: 0,
unpaidCount: 0,
paidCount: 0,
color: 'blue',
periodKey: 'salesInvoicePeriod',
barWidth: 40,
@ -111,8 +140,11 @@ export default {
schemaName: ModelNameEnum.PurchaseInvoice,
total: 0,
unpaid: 0,
hasData: false,
paid: 0,
count: 0,
unpaidCount: 0,
paidCount: 0,
color: 'gray',
periodKey: 'purchaseInvoicePeriod',
barWidth: 60,
@ -131,7 +163,7 @@ export default {
methods: {
async calculateInvoiceTotals() {
for (const invoice of this.invoices) {
const { fromDate, toDate } = await getDatesAndPeriodicity(
const { fromDate, toDate } = await getDatesAndPeriodList(
this.$data[invoice.periodKey]
);
@ -141,14 +173,19 @@ export default {
toDate
);
const count = await fyo.db.count(invoice.schemaName, {
filters: { cancelled: false, submitted: true },
});
const { countTotal, countOutstanding } = await this.getCounts(
invoice.schemaName,
DateTime.fromISO(fromDate),
DateTime.fromISO(toDate)
);
invoice.total = total ?? 0;
invoice.unpaid = outstanding ?? 0;
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;
}
},
@ -156,6 +193,26 @@ export default {
let doc = await fyo.doc.getNewDoc(invoice.schemaName);
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>

View File

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

View File

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

View File

@ -7,9 +7,9 @@ import SetupWizardSchema from 'schemas/app/SetupWizard.json';
import { Schema } from 'schemas/types';
import { fyo } from 'src/initFyo';
export function getDatesAndPeriodicity(
export function getDatesAndPeriodList(
period: 'This Year' | 'This Quarter' | 'This Month'
): { fromDate: string; toDate: string } {
): { periodList: DateTime[]; fromDate: string; toDate: string } {
const toDate: DateTime = DateTime.now();
let fromDate: DateTime;
@ -23,7 +23,22 @@ export function getDatesAndPeriodicity(
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 {
periodList,
fromDate: fromDate.toISO(),
toDate: toDate.toISO(),
};

View File

@ -29,3 +29,21 @@ export function useKeys(callback?: (keys: Set<string>) => void) {
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;
}