2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

Merge pull request #492 from frappe/minor-ui-touchups

fix: minor ui touchups
This commit is contained in:
Alan 2023-01-05 00:01:14 -08:00 committed by GitHub
commit 694268f843
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 624 additions and 328 deletions

View File

@ -422,12 +422,21 @@ export default class DatabaseCore extends DatabaseBase {
this.#applyFiltersToBuilder(builder, filters);
if (options.orderBy) {
builder.orderBy(options.orderBy, options.order);
const { orderBy, groupBy, order } = options;
if (Array.isArray(orderBy)) {
builder.orderBy(orderBy.map((column) => ({ column, order })));
}
if (options.groupBy) {
builder.groupBy(options.groupBy);
if (typeof orderBy === 'string') {
builder.orderBy(orderBy, order);
}
if (Array.isArray(groupBy)) {
builder.groupBy(...groupBy);
}
if (typeof groupBy === 'string') {
builder.groupBy(groupBy);
}
if (options.offset) {

View File

@ -5,8 +5,8 @@ import { DatabaseManager } from './manager';
export interface GetQueryBuilderOptions {
offset?: number;
limit?: number;
groupBy?: string;
orderBy?: string;
groupBy?: string | string[];
orderBy?: string | string[];
order?: 'desc' | 'asc';
}

View File

@ -18,7 +18,7 @@ const childTableColumnMap = {
const defaultNumberSeriesMap = {
[ModelNameEnum.Payment]: 'PAY-',
[ModelNameEnum.JournalEntry]: 'JE-',
[ModelNameEnum.JournalEntry]: 'JV-',
[ModelNameEnum.SalesInvoice]: 'SINV-',
[ModelNameEnum.PurchaseInvoice]: 'PINV-',
} as Record<ModelNameEnum, string>;

View File

@ -2,19 +2,16 @@ import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import {
Action,
CurrenciesMap,
DefaultMap,
FiltersMap,
FormulaMap,
HiddenMap,
HiddenMap
} from 'fyo/model/types';
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors';
import {
getExchangeRate,
getInvoiceActions,
getNumberSeries,
getExchangeRate, getNumberSeries
} from 'models/helpers';
import { InventorySettings } from 'models/inventory/InventorySettings';
import { StockTransfer } from 'models/inventory/StockTransfer';

View File

@ -230,7 +230,7 @@ export abstract class AccountReport extends LedgerReport {
const toDate = dr.toDate.toMillis();
const fromDate = dr.fromDate.toMillis();
if (fromDate < entryDate && entryDate <= toDate) {
if (entryDate >= fromDate && entryDate < toDate) {
return dr;
}
}
@ -278,9 +278,9 @@ export abstract class AccountReport extends LedgerReport {
let fromDate: string;
if (this.basedOn === 'Until Date') {
toDate = this.toDate!;
toDate = DateTime.fromISO(this.toDate!).plus({ days: 1 }).toISODate();
const months = monthsMap[this.periodicity] * Math.max(this.count ?? 1, 1);
fromDate = DateTime.fromISO(toDate).minus({ months }).toISODate();
fromDate = DateTime.fromISO(this.toDate!).minus({ months }).toISODate();
} else {
const fy = await getFiscalEndpoints(
this.toYear!,
@ -299,7 +299,7 @@ export abstract class AccountReport extends LedgerReport {
const { fromDate, toDate } = await this._getFromAndToDates();
const dateFilter: string[] = [];
dateFilter.push('<=', toDate);
dateFilter.push('<', toDate);
dateFilter.push('>=', fromDate);
filters.date = dateFilter;
@ -342,17 +342,17 @@ export abstract class AccountReport extends LedgerReport {
let dateFilters = [
{
fieldtype: 'Int',
fieldname: 'toYear',
placeholder: t`To Year`,
label: t`To Year`,
fieldname: 'fromYear',
placeholder: t`From Year`,
label: t`From Year`,
minvalue: 2000,
required: true,
},
{
fieldtype: 'Int',
fieldname: 'fromYear',
placeholder: t`From Year`,
label: t`From Year`,
fieldname: 'toYear',
placeholder: t`To Year`,
label: t`To Year`,
minvalue: 2000,
required: true,
},
@ -407,16 +407,18 @@ export abstract class AccountReport extends LedgerReport {
const dateColumns = this._dateRanges!.sort(
(a, b) => b.toDate.toMillis() - a.toDate.toMillis()
).map(
(d) =>
({
label: this.fyo.format(d.toDate.toJSDate(), 'Date'),
fieldtype: 'Data',
fieldname: 'toDate',
align: 'right',
width: ACC_BAL_WIDTH,
} as ColumnField)
);
).map((d) => {
const toDate = d.toDate.minus({ days: 1 });
const label = this.fyo.format(toDate.toJSDate(), 'Date');
return {
label,
fieldtype: 'Data',
fieldname: 'toDate',
align: 'right',
width: ACC_BAL_WIDTH,
} as ColumnField;
});
return [columns, dateColumns].flat();
}

View File

@ -299,7 +299,7 @@ export class GeneralLedger extends LedgerReport {
},
{
fieldtype: 'DynamicLink',
label: t`Ref Name`,
label: t`Ref. Name`,
references: 'referenceType',
placeholder: t`Ref Name`,
emptyMessage: t`Change Ref Type`,

View File

@ -95,7 +95,7 @@ export abstract class LedgerReport extends Report {
{
fields,
filters,
orderBy: 'date',
orderBy: ['date', 'created'],
order: this.ascending ? 'asc' : 'desc',
}
)) as RawLedgerEntry[];

View File

@ -26,7 +26,7 @@ export async function getRawStockLedgerEntries(fyo: Fyo) {
return (await fyo.db.getAllRaw(ModelNameEnum.StockLedgerEntry, {
fields: fieldnames,
orderBy: 'date',
orderBy: ['date', 'created', 'name'],
order: 'asc',
})) as RawStockLedgerEntry[];
}

View File

@ -103,7 +103,7 @@ export default {
return this.labelClass;
}
return 'text-gray-600 text-sm';
return 'text-gray-600 text-base';
},
checked() {
return this.value;

View File

@ -1,28 +1,38 @@
<template>
<div
class="
fixed
top-0
left-0
w-screen
h-screen
z-20
flex
justify-center
items-center
"
style="background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(4px)"
@click="$emit('closemodal')"
v-if="openModal"
>
<Transition>
<div
class="bg-white rounded-lg shadow-2xl w-form border overflow-hidden"
v-bind="$attrs"
@click.stop
class="
fixed
top-0
left-0
w-screen
h-screen
z-20
flex
justify-center
items-center
"
style="background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(4px)"
@click="$emit('closemodal')"
v-if="openModal"
>
<slot></slot>
<div
class="
bg-white
rounded-lg
shadow-2xl
w-form
border
overflow-hidden
inner
"
v-bind="$attrs"
@click.stop
>
<slot></slot>
</div>
</div>
</div>
</Transition>
</template>
<script lang="ts">
@ -60,3 +70,28 @@ export default defineComponent({
},
});
</script>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: all 100ms ease-out;
}
.inner {
transition: all 150ms ease-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-enter-from .inner,
.v-leave-to .inner {
transform: translateY(-50px);
}
.v-enter-to .inner,
.v-leave-from .inner {
transform: translateY(0px);
}
</style>

View File

@ -6,6 +6,13 @@
platform !== 'Windows' ? 'window-drag' : '',
]"
>
<Transition name="spacer">
<div
v-if="!sidebar && platform === 'Mac'"
class="h-full"
:class="sidebar ? '' : 'w-tl mr-4 border-r'"
/>
</Transition>
<h1 class="text-xl font-semibold select-none" v-if="title">
{{ title }}
</h1>
@ -18,10 +25,12 @@
</div>
</template>
<script>
import { Transition } from 'vue';
import BackLink from './BackLink.vue';
import SearchBar from './SearchBar.vue';
export default {
inject: ['sidebar'],
props: {
title: { type: String, default: '' },
backLink: { type: Boolean, default: true },
@ -29,7 +38,7 @@ export default {
border: { type: Boolean, default: true },
searchborder: { type: Boolean, default: true },
},
components: { SearchBar, BackLink },
components: { SearchBar, BackLink, Transition },
computed: {
showBorder() {
return !!this.$slots.default && this.searchborder;
@ -37,3 +46,29 @@ export default {
},
};
</script>
<style scoped>
.w-tl {
width: var(--w-trafficlights);
}
.spacer-enter-from,
.spacer-leave-to {
opacity: 0;
width: 0px;
margin-right: 0px;
border-right-width: 0px;
}
.spacer-enter-to,
.spacer-leave-from {
opacity: 1;
width: var(--w-trafficlights);
margin-right: 1rem;
border-right-width: 1px;
}
.spacer-enter-active,
.spacer-leave-active {
transition: all 150ms ease-out;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div
class="py-2 h-full flex justify-between flex-col bg-gray-25"
class="py-2 h-full flex justify-between flex-col bg-gray-25 relative"
:class="{
'window-drag': platform !== 'Windows',
}"
@ -36,6 +36,7 @@
@click="onGroupClick(group)"
>
<Icon
class="flex-shrink-0"
:name="group.icon"
:size="group.iconSize || '18'"
:height="group.iconHeight"
@ -91,7 +92,7 @@
"
@click="openDocumentation"
>
<feather-icon name="help-circle" class="h-4 w-4" />
<feather-icon name="help-circle" class="h-4 w-4 flex-shrink-0" />
<p>
{{ t`Help` }}
</p>
@ -107,7 +108,7 @@
"
@click="$emit('change-db-file')"
>
<feather-icon name="database" class="h-4 w-4" />
<feather-icon name="database" class="h-4 w-4 flex-shrink-0" />
<p>{{ t`Change DB` }}</p>
</button>
@ -121,7 +122,7 @@
"
@click="() => reportIssue()"
>
<feather-icon name="flag" class="h-4 w-4" />
<feather-icon name="flag" class="h-4 w-4 flex-shrink-0" />
<p>
{{ t`Report Issue` }}
</p>
@ -134,6 +135,23 @@
dev mode
</p>
</div>
<!-- Hide Sidebar Button -->
<button
class="
absolute
bottom-0
right-0
text-gray-600
hover:bg-gray-100
rounded
p-1
m-4
"
@click="$emit('toggle-sidebar')"
>
<feather-icon name="chevrons-left" class="w-4 h-4" />
</button>
</div>
</template>
<script>
@ -148,7 +166,7 @@ import Icon from './Icon.vue';
export default {
components: [Button],
emits: ['change-db-file'],
emits: ['change-db-file', 'toggle-sidebar'],
data() {
return {
companyName: '',

View File

@ -1,6 +1,11 @@
<template>
<div class="flex flex-col h-full">
<PageHeader :title="t`Chart of Accounts`" />
<PageHeader :title="t`Chart of Accounts`">
<Button v-if="!isAllExpanded" @click="expand">{{ t`Expand` }}</Button>
<Button v-if="!isAllCollapsed" @click="collapse">{{
t`Collapse`
}}</Button>
</PageHeader>
<!-- Chart of Accounts -->
<div
@ -141,20 +146,24 @@
import { t } from 'fyo';
import { isCredit } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import PageHeader from 'src/components/PageHeader';
import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import { docsPath, openQuickEdit } from 'src/utils/ui';
import { getMapFromList, removeAtIndex } from 'utils/index';
import { nextTick } from 'vue';
import Button from '../components/Button.vue';
import { handleErrorWithDialog } from '../errorHandling';
export default {
components: {
Button,
PageHeader,
},
data() {
return {
isAllCollapsed: true,
isAllExpanded: false,
root: null,
accounts: [],
schemaName: 'Account',
@ -187,6 +196,33 @@ export default {
docsPath.value = '';
},
methods: {
async expand() {
await this.toggleAll(this.accounts, true);
this.isAllCollapsed = false;
this.isAllExpanded = true;
},
async collapse() {
await this.toggleAll(this.accounts, false);
this.isAllExpanded = false;
this.isAllCollapsed = true;
},
async toggleAll(accounts, expand) {
if (!Array.isArray(accounts)) {
await this.toggle(accounts, expand);
accounts = accounts.children ?? [];
}
for (const account of accounts) {
await this.toggleAll(account, expand);
}
},
async toggle(account, expand) {
if (account.expanded === expand || !account.isGroup) {
return;
}
await this.toggleChildren(account);
},
getBalance(account) {
const total = this.totals[account.name];
if (!total) {
@ -226,6 +262,14 @@ export default {
shouldOpen = !(await this.toggleChildren(account));
}
if (account.isGroup && account.expanded) {
this.isAllCollapsed = false;
}
if (account.isGroup && !account.expanded) {
this.isAllExpanded = false;
}
if (!shouldOpen) {
return;
}

View File

@ -0,0 +1,32 @@
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
period: 'This Year',
periodOptions: ['This Year', 'This Quarter', 'This Month'],
};
},
props: {
commonPeriod: String,
},
watch: {
period: 'periodChange',
commonPeriod(val) {
if (!this.periodOptions.includes(val)) {
return;
}
this.period = val;
},
},
methods: {
async periodChange() {
this.$emit('period-change', this.period);
await this.setData();
},
async setData() {},
},
});
</script>

View File

@ -20,7 +20,7 @@
<PeriodSelector
:value="period"
@change="(value) => (period = value)"
:options="['This Year', 'This Quarter']"
:options="periodOptions"
v-if="hasData"
/>
<div v-else class="w-20 h-5 bg-gray-200 rounded" />
@ -51,23 +51,22 @@ import { formatXLabels, getYMax } from 'src/utils/chart';
import { uicolors } from 'src/utils/colors';
import { getDatesAndPeriodList } from 'src/utils/misc';
import { getMapFromList } from 'utils/';
import PeriodSelector from './PeriodSelector';
import DashboardChartBase from './BaseDashboardChart.vue';
import PeriodSelector from './PeriodSelector.vue';
export default {
name: 'Cashflow',
extends: DashboardChartBase,
components: {
PeriodSelector,
LineChart,
},
data: () => ({
period: 'This Year',
data: [],
periodList: [],
periodOptions: ['This Year', 'This Quarter'],
hasData: false,
}),
watch: {
period: 'setData',
},
async activated() {
await this.setData();
if (!this.hasData) {

View File

@ -1,19 +1,61 @@
<template>
<div class="overflow-hidden h-screen" style="width: var(--w-desk)">
<PageHeader :title="t`Dashboard`" />
<PageHeader :title="t`Dashboard`">
<div
class="
border
rounded
bg-gray-50
focus-within:bg-gray-100
flex
items-center
"
>
<PeriodSelector
class="px-3"
:value="period"
:options="['This Year', 'This Quarter', 'This Month']"
@change="(value) => (period = value)"
/>
</div>
</PageHeader>
<div class="no-scrollbar overflow-auto h-full">
<div
style="min-width: var(--w-desk-fixed); min-height: var(--h-app)"
class="overflow-auto"
>
<Cashflow class="p-4" />
<Cashflow
class="p-4"
:common-period="period"
@period-change="handlePeriodChange"
/>
<hr />
<UnpaidInvoices />
<div class="flex w-full">
<UnpaidInvoices
:schema-name="'SalesInvoice'"
:common-period="period"
@period-change="handlePeriodChange"
class="border-r"
/>
<UnpaidInvoices
:schema-name="'PurchaseInvoice'"
:common-period="period"
@period-change="handlePeriodChange"
/>
</div>
<hr />
<div class="flex">
<ProfitAndLoss class="w-full p-4 border-r" />
<Expenses class="w-full p-4" />
<ProfitAndLoss
class="w-full p-4 border-r"
:common-period="period"
@period-change="handlePeriodChange"
/>
<Expenses
class="w-full p-4"
:common-period="period"
@period-change="handlePeriodChange"
/>
</div>
<hr />
</div>
@ -22,21 +64,26 @@
</template>
<script>
import PageHeader from 'src/components/PageHeader';
import PageHeader from 'src/components/PageHeader.vue';
import { docsPath } from 'src/utils/ui';
import Cashflow from './Cashflow';
import Expenses from './Expenses';
import ProfitAndLoss from './ProfitAndLoss';
import UnpaidInvoices from './UnpaidInvoices';
import UnpaidInvoices from './UnpaidInvoices.vue';
import Cashflow from './Cashflow.vue';
import Expenses from './Expenses.vue';
import PeriodSelector from './PeriodSelector.vue';
import ProfitAndLoss from './ProfitAndLoss.vue';
export default {
name: 'Dashboard',
components: {
PageHeader,
UnpaidInvoices,
Cashflow,
ProfitAndLoss,
Expenses,
PeriodSelector,
UnpaidInvoices,
},
data() {
return { period: 'This Year' };
},
activated() {
docsPath.value = 'analytics/dashboard';
@ -44,5 +91,14 @@ export default {
deactivated() {
docsPath.value = '';
},
methods: {
handlePeriodChange(period) {
if (period === this.period) {
return;
}
this.period = '';
},
},
};
</script>

View File

@ -58,26 +58,22 @@ import { fyo } from 'src/initFyo';
import { uicolors } from 'src/utils/colors';
import { getDatesAndPeriodList } from 'src/utils/misc';
import DonutChart from '../../components/Charts/DonutChart.vue';
import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader';
import DashboardChartBase from './BaseDashboardChart.vue';
import PeriodSelector from './PeriodSelector.vue';
import SectionHeader from './SectionHeader.vue';
export default {
name: 'Expenses',
extends: DashboardChartBase,
components: {
DonutChart,
PeriodSelector,
SectionHeader,
},
data: () => ({
period: 'This Year',
active: null,
expenses: [],
}),
watch: {
period() {
this.setData();
},
},
activated() {
this.setData();
},

View File

@ -13,7 +13,6 @@
text-sm
flex
focus:outline-none
text-gray-900
hover:text-gray-800
focus:text-gray-800
items-center
@ -22,6 +21,7 @@
leading-relaxed
cursor-pointer
"
:class="!value ? 'text-gray-600' : 'text-gray-900'"
@click="toggleDropdown()"
tabindex="0"
@keydown.down="highlightItemDown"
@ -37,7 +37,7 @@
<script>
import { t } from 'fyo';
import Dropdown from 'src/components/Dropdown';
import Dropdown from 'src/components/Dropdown.vue';
export default {
name: 'PeriodSelector',
@ -54,14 +54,17 @@ export default {
},
mounted() {
this.periodSelectorMap = {
'': t`Set Period`,
'This Year': t`This Year`,
'This Quarter': t`This Quarter`,
'This Month': t`This Month`,
};
this.periodOptions = this.options.map((option) => {
let label = this.periodSelectorMap[option] ?? option;
return {
label: this.periodSelectorMap[option] ?? option,
label,
action: () => this.selectOption(option),
};
});

View File

@ -5,7 +5,7 @@
<template #action>
<PeriodSelector
:value="period"
:options="['This Year', 'This Quarter']"
:options="periodOptions"
@change="(value) => (period = value)"
/>
</template>
@ -36,27 +36,26 @@ import { formatXLabels, getYMax, getYMin } from 'src/utils/chart';
import { uicolors } from 'src/utils/colors';
import { getDatesAndPeriodList } from 'src/utils/misc';
import { getValueMapFromList } from 'utils';
import DashboardChartBase from './BaseDashboardChart.vue';
import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader';
export default {
name: 'ProfitAndLoss',
extends: DashboardChartBase,
components: {
PeriodSelector,
SectionHeader,
BarChart,
},
data: () => ({
period: 'This Year',
data: [],
hasData: false,
periodOptions: ['This Year', 'This Quarter'],
}),
activated() {
this.setData();
},
watch: {
period: 'setData',
},
computed: {
chartData() {
const points = [this.data.map((d) => d.balance)];

View File

@ -1,209 +1,197 @@
<template>
<div class="flex">
<div
v-for="(invoice, i) in invoices"
class="flex-col justify-between w-full p-4"
:class="i === 0 ? 'border-r' : ''"
:key="invoice.title"
>
<!-- Title and Period Selector -->
<SectionHeader>
<template #title>{{ invoice.title }}</template>
<template #action>
<PeriodSelector
v-if="invoice.hasData"
:value="$data[invoice.periodKey]"
@change="(value) => ($data[invoice.periodKey] = value)"
/>
<Button
v-else
:icon="true"
type="primary"
@click="newInvoice(invoice)"
>
<feather-icon name="plus" class="w-4 h-4 text-white" />
</Button>
</template>
</SectionHeader>
<div class="flex-col justify-between w-full p-4">
<!-- Title and Period Selector -->
<SectionHeader>
<template #title>{{ title }}</template>
<template #action>
<PeriodSelector
v-if="hasData"
:value="period"
@change="(value) => (period = value)"
/>
<Button v-else :icon="true" type="primary" @click="newInvoice()">
<feather-icon name="plus" class="w-4 h-4 text-white" />
</Button>
</template>
</SectionHeader>
<!-- Widget Body -->
<div class="mt-4">
<!-- Paid & Unpaid Amounts -->
<div class="flex justify-between">
<!-- Paid -->
<div
class="text-sm font-medium"
:class="{ 'bg-gray-200 text-gray-200 rounded': !invoice.count }"
>
{{ fyo.format(invoice.paid, 'Currency') }}
<span :class="{ 'text-gray-900 font-normal': invoice.count }">{{
t`Paid`
}}</span>
</div>
<!-- Unpaid -->
<div
class="text-sm font-medium"
:class="{ 'bg-gray-200 text-gray-200 rounded': !invoice.count }"
>
{{ fyo.format(invoice.unpaid, 'Currency') }}
<span :class="{ 'text-gray-900 font-normal': invoice.count }">{{
t`Unpaid`
}}</span>
</div>
</div>
<!-- Widget Bar -->
<!-- Widget Body -->
<div class="mt-4">
<!-- Paid & Unpaid Amounts -->
<div class="flex justify-between">
<!-- Paid -->
<div
class="mt-2 relative rounded overflow-hidden"
@mouseenter="idx = i"
@mouseleave="idx = -1"
class="text-sm font-medium"
:class="{ 'bg-gray-200 text-gray-200 rounded': !count }"
>
<div
class="w-full h-4"
:class="
invoice.count && invoice.color == 'blue'
? 'bg-blue-200'
: invoice.hasData
? 'bg-pink-200'
: 'bg-gray-200'
"
></div>
<div
class="absolute inset-0 h-4"
:class="
invoice.count && invoice.color == 'blue'
? 'bg-blue-500'
: invoice.hasData
? 'bg-pink-500'
: 'bg-gray-400'
"
:style="`width: ${invoice.barWidth}%`"
></div>
{{ fyo.format(paid, 'Currency') }}
<span :class="{ 'text-gray-900 font-normal': count }">{{
t`Paid`
}}</span>
</div>
<!-- Unpaid -->
<div
class="text-sm font-medium"
:class="{ 'bg-gray-200 text-gray-200 rounded': !count }"
>
{{ fyo.format(unpaid, 'Currency') }}
<span :class="{ 'text-gray-900 font-normal': count }">{{
t`Unpaid`
}}</span>
</div>
</div>
<!-- Widget Bar -->
<div
class="mt-2 relative rounded overflow-hidden"
@mouseenter="show = true"
@mouseleave="show = false"
>
<div class="w-full h-4" :class="unpaidColor"></div>
<div
class="absolute inset-0 h-4"
:class="paidColor"
:style="`width: ${barWidth}%`"
></div>
</div>
</div>
<MouseFollower
v-if="invoices[0].hasData || invoices[1].hasData"
v-if="hasData"
:offset="15"
:show="idx >= 0"
:show="show"
placement="top"
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-l-4"
:style="{ borderColor: colors[idx] }"
:style="{ borderColor: colors }"
>
<div class="flex justify-between gap-4">
<p>{{ t`Paid` }}</p>
<p class="font-semibold">{{ invoices[idx]?.paidCount ?? 0 }}</p>
<p class="font-semibold">{{ paidCount ?? 0 }}</p>
</div>
<div
v-if="invoices[idx]?.unpaidCount > 0"
class="flex justify-between gap-4"
>
<div v-if="unpaidCount > 0" class="flex justify-between gap-4">
<p>{{ t`Unpaid` }}</p>
<p class="font-semibold">{{ invoices[idx]?.unpaidCount ?? 0 }}</p>
<p class="font-semibold">{{ unpaidCount ?? 0 }}</p>
</div>
</MouseFollower>
</div>
</template>
<script>
<script lang="ts">
import { t } from 'fyo';
import { DateTime } from 'luxon';
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 { uicolors } from 'src/utils/colors';
import { getDatesAndPeriodList } from 'src/utils/misc';
import { PeriodKey } from 'src/utils/types';
import { routeTo } from 'src/utils/ui';
import { safeParseFloat } from 'utils/index';
import { defineComponent } from 'vue';
import BaseDashboardChart from './BaseDashboardChart.vue';
import PeriodSelector from './PeriodSelector.vue';
import SectionHeader from './SectionHeader.vue';
export default {
export default defineComponent({
name: 'UnpaidInvoices',
extends: BaseDashboardChart,
components: {
PeriodSelector,
SectionHeader,
Button,
MouseFollower,
},
data: () => ({
idx: -1,
colors: [uicolors.blue['500'], uicolors.pink['500']],
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,
},
{
title: t`Purchase Invoices`,
schemaName: ModelNameEnum.PurchaseInvoice,
total: 0,
unpaid: 0,
hasData: false,
paid: 0,
count: 0,
unpaidCount: 0,
paidCount: 0,
color: 'pink',
periodKey: 'purchaseInvoicePeriod',
barWidth: 60,
},
],
salesInvoicePeriod: 'This Year',
purchaseInvoicePeriod: 'This Year',
}),
watch: {
salesInvoicePeriod: 'calculateInvoiceTotals',
purchaseInvoicePeriod: 'calculateInvoiceTotals',
props: {
schemaName: { type: String, required: true },
},
computed: {
title(): string {
return fyo.schemaMap[this.schemaName]?.label ?? '';
},
color(): 'blue' | 'pink' {
if (this.schemaName === ModelNameEnum.SalesInvoice) {
return 'blue';
}
return 'pink';
},
colors(): string {
return uicolors[this.color]['500'];
},
paidColor(): string {
if (!this.hasData) {
return 'bg-gray-400';
}
return `bg-${this.color}-500`;
},
unpaidColor(): string {
if (!this.hasData) {
return 'bg-gray-200';
}
return `bg-${this.color}-200`;
},
},
data() {
return {
show: false,
total: 0,
unpaid: 0,
hasData: false,
paid: 0,
count: 0,
unpaidCount: 0,
paidCount: 0,
barWidth: 40,
period: 'This Year',
} as {
show: boolean;
period: PeriodKey;
total: number;
unpaid: number;
hasData: boolean;
paid: number;
count: number;
unpaidCount: number;
paidCount: number;
barWidth: number;
};
},
activated() {
this.calculateInvoiceTotals();
this.setData();
},
methods: {
async calculateInvoiceTotals() {
for (const invoice of this.invoices) {
const { fromDate, toDate } = await getDatesAndPeriodList(
this.$data[invoice.periodKey]
);
async setData() {
const { fromDate, toDate } = getDatesAndPeriodList(this.period);
const { total, outstanding } = await fyo.db.getTotalOutstanding(
invoice.schemaName,
fromDate.toISO(),
toDate.toISO()
);
const { total, outstanding } = await fyo.db.getTotalOutstanding(
this.schemaName,
fromDate.toISO(),
toDate.toISO()
);
const { countTotal, countOutstanding } = await this.getCounts(
invoice.schemaName,
fromDate,
toDate
);
const { countTotal, countOutstanding } = await this.getCounts(
this.schemaName,
fromDate,
toDate
);
invoice.total = total ?? 0;
invoice.unpaid = outstanding ?? 0;
invoice.paid = total - outstanding;
invoice.hasData = countTotal > 0;
invoice.count = countTotal;
invoice.paidCount = countTotal - countOutstanding;
invoice.unpaidCount = countOutstanding;
invoice.barWidth = (invoice.paid / (invoice.total || 1)) * 100;
}
this.total = total ?? 0;
this.unpaid = outstanding ?? 0;
this.paid = total - outstanding;
this.hasData = countTotal > 0;
this.count = countTotal;
this.paidCount = countTotal - countOutstanding;
this.unpaidCount = countOutstanding;
this.barWidth = (this.paid / (this.total || 1)) * 100;
},
async newInvoice(invoice) {
let doc = await fyo.doc.getNewDoc(invoice.schemaName);
routeTo(`/edit/${invoice.schemaName}/${doc.name}`);
async newInvoice() {
const doc = fyo.doc.getNewDoc(this.schemaName);
await routeTo(`/edit/${this.schemaName}/${doc.name}`);
},
async getCounts(schemaName, fromDate, toDate) {
async getCounts(schemaName: string, fromDate: DateTime, toDate: DateTime) {
const outstandingAmounts = await fyo.db.getAllRaw(schemaName, {
fields: ['outstandingAmount'],
filters: {
@ -223,5 +211,5 @@ export default {
};
},
},
};
});
</script>

View File

@ -1,9 +1,14 @@
<template>
<div class="flex overflow-hidden">
<Sidebar
class="w-sidebar flex-shrink-0 border-r"
@change-db-file="$emit('change-db-file')"
/>
<Transition name="sidebar">
<Sidebar
v-show="sidebar"
class="flex-shrink-0 border-r whitespace-nowrap w-sidebar"
@change-db-file="$emit('change-db-file')"
@toggle-sidebar="sidebar = !sidebar"
/>
</Transition>
<div class="flex flex-1 overflow-y-hidden bg-white">
<router-view v-slot="{ Component }">
<keep-alive>
@ -11,36 +16,76 @@
</keep-alive>
</router-view>
<div class="flex" v-if="showQuickEdit">
<router-view name="edit" v-slot="{ Component }">
<router-view name="edit" v-slot="{ Component, route }">
<Transition name="quickedit">
<keep-alive>
<component
:is="Component"
class="w-quick-edit flex-1"
:key="$route.query.schemaName + $route.query.name"
/>
<div v-if="route?.query?.edit">
<component
:is="Component"
:key="route.query.schemaName + route.query.name"
/>
</div>
</keep-alive>
</router-view>
</div>
</Transition>
</router-view>
</div>
<!-- Show Sidebar Button -->
<button
v-show="!sidebar"
class="
absolute
bottom-0
left-0
text-gray-600
bg-gray-100
rounded
p-1
m-4
opacity-0
hover:opacity-100 hover:shadow-md
"
@click="sidebar = !sidebar"
>
<feather-icon name="chevrons-right" class="w-4 h-4" />
</button>
</div>
</template>
<script>
import Sidebar from '../components/Sidebar';
import { computed } from '@vue/reactivity';
import Sidebar from '../components/Sidebar.vue';
export default {
name: 'Desk',
emits: ['change-db-file'],
data() {
return { sidebar: true };
},
provide() {
return { sidebar: computed(() => this.sidebar) };
},
components: {
Sidebar,
},
computed: {
showQuickEdit() {
return (
this.$route.query.edit &&
this.$route.query.schemaName &&
this.$route.query.name
);
},
},
};
</script>
<style scoped>
.sidebar-enter-from,
.sidebar-leave-to {
opacity: 0;
transform: translateX(calc(-1 * var(--w-sidebar)));
width: 0px;
}
.sidebar-enter-to,
.sidebar-leave-from {
opacity: 1;
transform: translateX(0px);
width: var(--w-sidebar);
}
.sidebar-enter-active,
.sidebar-leave-active {
transition: all 150ms ease-out;
}
</style>

View File

@ -265,27 +265,31 @@
</div>
</template>
<template #quickedit v-if="quickEditDoc || linked">
<QuickEditForm
v-if="quickEditDoc && !linked"
class="w-quick-edit"
:name="quickEditDoc.name"
:show-name="false"
:show-save="false"
:source-doc="quickEditDoc"
:source-fields="quickEditFields"
:schema-name="quickEditDoc.schemaName"
:white="true"
:route-back="false"
:load-on-close="false"
@close="toggleQuickEditDoc(null)"
/>
<template #quickedit>
<Transition name="quickedit">
<QuickEditForm
v-if="quickEditDoc && !linked"
class="w-quick-edit"
:name="quickEditDoc.name"
:show-name="false"
:show-save="false"
:source-doc="quickEditDoc"
:source-fields="quickEditFields"
:schema-name="quickEditDoc.schemaName"
:white="true"
:route-back="false"
:load-on-close="false"
@close="toggleQuickEditDoc(null)"
/>
</Transition>
<LinkedEntryWidget
v-if="linked && !quickEditDoc"
:linked="linked"
@close-widget="linked = null"
/>
<Transition name="quickedit">
<LinkedEntryWidget
v-if="linked && !quickEditDoc"
:linked="linked"
@close-widget="linked = null"
/>
</Transition>
</template>
</FormContainer>
</template>

View File

@ -198,9 +198,10 @@ export default defineComponent({
filters = objectForEach(clone(filters), toRaw);
const orderBy = !!fyo.getField(this.schemaName, 'date')
? 'date'
: 'created';
const orderBy = ['created'];
if (fyo.db.fieldMap[this.schemaName]['date']) {
orderBy.unshift('date');
}
this.data = (
await fyo.db.getAll(this.schemaName, {

View File

@ -37,21 +37,23 @@
</div>
<!-- Printview Customizer -->
<div class="border-l w-quick-edit" v-if="showCustomiser">
<div
class="px-4 flex items-center justify-between h-row-largest border-b"
>
<h2 class="font-semibold">{{ t`Customise` }}</h2>
<Button :icon="true" @click="showCustomiser = false">
<feather-icon name="x" class="w-4 h-4" />
</Button>
<Transition name="quickedit">
<div class="border-l w-quick-edit" v-if="showCustomiser">
<div
class="px-4 flex items-center justify-between h-row-largest border-b"
>
<h2 class="font-semibold">{{ t`Customise` }}</h2>
<Button :icon="true" @click="showCustomiser = false">
<feather-icon name="x" class="w-4 h-4" />
</Button>
</div>
<TwoColumnForm
:doc="printSettings"
:autosave="true"
class="border-none"
/>
</div>
<TwoColumnForm
:doc="printSettings"
:autosave="true"
class="border-none"
/>
</div>
</Transition>
</div>
</template>
<script>

View File

@ -1,6 +1,6 @@
<template>
<div
class="border-l h-full overflow-auto"
class="border-l h-full overflow-auto w-quick-edit"
:class="white ? 'bg-white' : 'bg-gray-25'"
>
<!-- Quick edit Tool bar -->

View File

@ -22,7 +22,8 @@
v-for="field in report.filters"
:border="true"
size="small"
:show-label="field.fieldtype === 'Check'"
:class="[field.fieldtype === 'Check' ? 'self-end' : '']"
:show-label="true"
:key="field.fieldname + '-filter'"
:df="field"
:value="report.get(field.fieldname)"

View File

@ -64,6 +64,7 @@ input[type='number']::-webkit-inner-spin-button {
--w-desk-fixed: calc(var(--w-app) - var(--w-sidebar));
--w-quick-edit: 22rem;
--w-scrollbar: 0.6rem;
--w-trafficlights: 72px;
/* Row Heights */
--h-row-smallest: 2rem;
@ -144,3 +145,26 @@ input[type='number']::-webkit-inner-spin-button {
.custom-scroll::-webkit-scrollbar-thumb:hover {
background: theme('colors.gray.400');
}
/*
Transitions
*/
.quickedit-enter-from,
.quickedit-leave-to {
transform: translateX(var(--w-quick-edit));
width: 0px;
opacity: 0;
}
.quickedit-enter-to,
.quickedit-leave-from {
transform: translateX(0px);
width: var(--w-quick-edit);
opacity: 1;
}
.quickedit-enter-active,
.quickedit-leave-active {
transition: all 150ms ease-out;
}

View File

@ -260,9 +260,11 @@ async function getParentData(
limit: number | null,
fyo: Fyo
) {
const orderBy = !!fields.find((f) => f.fieldname === 'date')
? 'date'
: 'created';
const orderBy = ['created'];
if (fyo.db.fieldMap[schemaName]['date']) {
orderBy.unshift('date');
}
const options: GetAllOptions = { filters, orderBy, order: 'desc' };
if (limit) {
options.limit = limit;

View File

@ -7,10 +7,13 @@ import SetupWizardSchema from 'schemas/app/SetupWizard.json';
import { Schema } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { QueryFilter } from 'utils/db/types';
import { PeriodKey } from './types';
export function getDatesAndPeriodList(
period: 'This Year' | 'This Quarter' | 'This Month'
): { periodList: DateTime[]; fromDate: DateTime; toDate: DateTime } {
export function getDatesAndPeriodList(period: PeriodKey): {
periodList: DateTime[];
fromDate: DateTime;
toDate: DateTime;
} {
const toDate: DateTime = DateTime.now().plus({ days: 1 });
let fromDate: DateTime;

View File

@ -69,4 +69,5 @@ export interface ExportTableField {
fields: ExportField[];
}
export type ExportFormat = 'csv' | 'json';
export type ExportFormat = 'csv' | 'json';
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month'

View File

@ -74,7 +74,7 @@ export async function openQuickEdit({
router[method]({
query: {
edit: 1,
edit: '1',
schemaName,
name,
showFields,

View File

@ -59,8 +59,8 @@ export interface GetAllOptions {
filters?: QueryFilter;
offset?: number;
limit?: number;
groupBy?: string;
orderBy?: string;
groupBy?: string | string[];
orderBy?: string | string[];
order?: 'asc' | 'desc';
}