mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
Merge branch 'frappe:master' into master
This commit is contained in:
commit
5363a5a8f3
@ -150,7 +150,7 @@ If you want to contribute code then you can fork this repo, make changes and rai
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| French | [DeepL](https://www.deepl.com/), [mael-chouteau](https://github.com/mael-chouteau), [joandreux](https://github.com/joandreux) |
|
||||
| German | [DeepL](https://www.deepl.com/), [barredterra](https://github.com/barredterra), [promexio](https://github.com/promexio), [C2H6-383](https://github.com/C2H6-383) |
|
||||
| Portuguese | [DeepL](https://www.deepl.com/) |
|
||||
| Portuguese | [DeepL](https://www.deepl.com/), [Valdir Amaral](https://github.com/valdir-amaral) |
|
||||
| Arabic | [taha2002](https://github.com/taha2002), [Faridget](https://github.com/faridget) |
|
||||
| Catalan | Dídac E. Jiménez |
|
||||
| Dutch | [FastAct](https://github.com/FastAct) |
|
||||
@ -158,9 +158,10 @@ If you want to contribute code then you can fork this repo, make changes and rai
|
||||
| Gujarati | [dhruvilxcode](https://github.com/dhruvilxcode), [4silvertooth](https://github.com/4silvertooth) |
|
||||
| Hindi | [bnsinghgit](https://github.com/bnsinghgit) |
|
||||
| Korean | [Isaac-Kwon](https://github.com/Isaac-Kwon) |
|
||||
| Simplified Chinese | [wcxu21](https://github.com/wcxu21), [wolone](https://github.com/wolone) |
|
||||
| Swedish | [papplo](https://github.com/papplo) |
|
||||
| Simplified Chinese | [wcxu21](https://github.com/wcxu21), [wolone](https://github.com/wolone), [Ji Qu](https://github.com/winkidney) |
|
||||
| Swedish | [papplo](https://github.com/papplo), [Crims-on](https://github.com/Crims-on) |
|
||||
| Turkish | Eyuq, [XTechnology-TR](https://github.com/XTechnology-TR) |
|
||||
| Danish | [Tummas Joensen](https://github.com/slang123) |
|
||||
|
||||
## License
|
||||
|
||||
|
@ -11,6 +11,9 @@
|
||||
"600": "#7C7C7C",
|
||||
"700": "#525252",
|
||||
"800": "#383838",
|
||||
"850": "#282828",
|
||||
"875": "#212121",
|
||||
"890": "#1C1C1C",
|
||||
"900": "#171717"
|
||||
},
|
||||
"red": {
|
||||
|
7
main.ts
7
main.ts
@ -102,13 +102,6 @@ export class Main {
|
||||
resizable: true,
|
||||
};
|
||||
|
||||
if (!this.isMac) {
|
||||
options.titleBarOverlay = {
|
||||
color: '#FFFFFF',
|
||||
height: 26,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.isDevelopment || this.isLinux) {
|
||||
Object.assign(options, { icon: this.icon });
|
||||
}
|
||||
|
@ -27,6 +27,42 @@ const ipc = {
|
||||
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
||||
},
|
||||
|
||||
minimizeWindow() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.MINIMIZE_MAIN_WINDOW);
|
||||
},
|
||||
|
||||
toggleMaximize() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.MAXIMIZE_MAIN_WINDOW);
|
||||
},
|
||||
|
||||
isMaximized() {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.send(IPC_MESSAGES.ISMAXIMIZED_MAIN_WINDOW);
|
||||
ipcRenderer.once(
|
||||
IPC_MESSAGES.ISMAXIMIZED_RESULT,
|
||||
(_event, isMaximized) => {
|
||||
resolve(isMaximized);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
isFullscreen() {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.send(IPC_MESSAGES.ISFULLSCREEN_MAIN_WINDOW);
|
||||
ipcRenderer.once(
|
||||
IPC_MESSAGES.ISFULLSCREEN_RESULT,
|
||||
(_event, isFullscreen) => {
|
||||
resolve(isFullscreen);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
closeWindow() {
|
||||
return ipcRenderer.send(IPC_MESSAGES.CLOSE_MAIN_WINDOW);
|
||||
},
|
||||
|
||||
async getCreds() {
|
||||
return (await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS)) as Creds;
|
||||
},
|
||||
|
@ -21,6 +21,30 @@ export default function registerIpcMainMessageListeners(main: Main) {
|
||||
main.mainWindow!.reload();
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.MINIMIZE_MAIN_WINDOW, () => {
|
||||
main.mainWindow!.minimize();
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.MAXIMIZE_MAIN_WINDOW, () => {
|
||||
main.mainWindow!.isMaximized()
|
||||
? main.mainWindow!.unmaximize()
|
||||
: main.mainWindow!.maximize();
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.ISMAXIMIZED_MAIN_WINDOW, (event) => {
|
||||
const isMaximized = main.mainWindow?.isMaximized() ?? false;
|
||||
event.sender.send(IPC_MESSAGES.ISMAXIMIZED_RESULT, isMaximized);
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.ISFULLSCREEN_MAIN_WINDOW, (event) => {
|
||||
const isFullscreen = main.mainWindow?.isFullScreen() ?? false;
|
||||
event.sender.send(IPC_MESSAGES.ISFULLSCREEN_RESULT, isFullscreen);
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.CLOSE_MAIN_WINDOW, () => {
|
||||
main.mainWindow!.close();
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.OPEN_EXTERNAL, (_, link: string) => {
|
||||
shell.openExternal(link).catch((err) => emitMainProcessError(err));
|
||||
});
|
||||
|
@ -18,6 +18,7 @@ export class AccountingSettings extends Doc {
|
||||
enableLead?: boolean;
|
||||
enableFormCustomization?: boolean;
|
||||
enableInvoiceReturns?: boolean;
|
||||
enableLoyaltyProgram?: boolean;
|
||||
enablePricingRule?: boolean;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
@ -56,6 +57,9 @@ export class AccountingSettings extends Doc {
|
||||
enableInvoiceReturns: () => {
|
||||
return !!this.enableInvoiceReturns;
|
||||
},
|
||||
enableLoyaltyProgram: () => {
|
||||
return !!this.enableLoyaltyProgram;
|
||||
},
|
||||
};
|
||||
|
||||
override hidden: HiddenMap = {
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export class CollectionRulesItems extends Doc {
|
||||
tierName?: string;
|
||||
collectionFactor?: number;
|
||||
minimumTotalSpent?: Money;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { DocValueMap } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
@ -14,10 +14,13 @@ import { Transactional } from 'models/Transactional/Transactional';
|
||||
import {
|
||||
addItem,
|
||||
canApplyPricingRule,
|
||||
createLoyaltyPointEntry,
|
||||
filterPricingRules,
|
||||
getAddedLPWithGrandTotal,
|
||||
getExchangeRate,
|
||||
getNumberSeries,
|
||||
getPricingRulesConflicts,
|
||||
removeLoyaltyPoint,
|
||||
roundFreeItemQty,
|
||||
} from 'models/helpers';
|
||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||
@ -38,6 +41,7 @@ import { AccountFieldEnum, PaymentTypeEnum } from '../Payment/types';
|
||||
import { PricingRule } from '../PricingRule/PricingRule';
|
||||
import { ApplicablePricingRules } from './types';
|
||||
import { PricingRuleDetail } from '../PricingRuleDetail/PricingRuleDetail';
|
||||
import { LoyaltyProgram } from '../LoyaltyProgram/LoyaltyProgram';
|
||||
|
||||
export type TaxDetail = {
|
||||
account: string;
|
||||
@ -69,8 +73,10 @@ export abstract class Invoice extends Transactional {
|
||||
setDiscountAmount?: boolean;
|
||||
discountAmount?: Money;
|
||||
discountPercent?: number;
|
||||
loyaltyPoints?: number;
|
||||
discountAfterTax?: boolean;
|
||||
stockNotTransferred?: number;
|
||||
loyaltyProgram?: string;
|
||||
backReference?: string;
|
||||
|
||||
submitted?: boolean;
|
||||
@ -179,17 +185,31 @@ export abstract class Invoice extends Transactional {
|
||||
async afterSubmit() {
|
||||
await super.afterSubmit();
|
||||
|
||||
if (this.isReturn) {
|
||||
await this._removeLoyaltyPointEntry();
|
||||
}
|
||||
|
||||
if (this.isQuote) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lpAddedBaseGrandTotal: Money | undefined;
|
||||
|
||||
if (this.redeemLoyaltyPoints) {
|
||||
lpAddedBaseGrandTotal = await this.getLPAddedBaseGrandTotal();
|
||||
}
|
||||
|
||||
// update outstanding amounts
|
||||
await this.fyo.db.update(this.schemaName, {
|
||||
name: this.name as string,
|
||||
outstandingAmount: this.baseGrandTotal!,
|
||||
outstandingAmount: lpAddedBaseGrandTotal! || this.baseGrandTotal!,
|
||||
});
|
||||
|
||||
const party = (await this.fyo.doc.getDoc('Party', this.party)) as Party;
|
||||
const party = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
this.party
|
||||
)) as Party;
|
||||
|
||||
await party.updateOutstandingAmount();
|
||||
|
||||
if (this.makeAutoPayment && this.autoPaymentAccount) {
|
||||
@ -207,6 +227,7 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
|
||||
await this._updateIsItemsReturned();
|
||||
await this._createLoyaltyPointEntry();
|
||||
}
|
||||
|
||||
async afterCancel() {
|
||||
@ -214,6 +235,14 @@ export abstract class Invoice extends Transactional {
|
||||
await this._cancelPayments();
|
||||
await this._updatePartyOutStanding();
|
||||
await this._updateIsItemsReturned();
|
||||
await this._removeLoyaltyPointEntry();
|
||||
}
|
||||
|
||||
async _removeLoyaltyPointEntry() {
|
||||
if (!this.loyaltyProgram) {
|
||||
return;
|
||||
}
|
||||
await removeLoyaltyPoint(this);
|
||||
}
|
||||
|
||||
async _cancelPayments() {
|
||||
@ -538,6 +567,28 @@ export abstract class Invoice extends Transactional {
|
||||
await invoiceDoc.submit();
|
||||
}
|
||||
|
||||
async _createLoyaltyPointEntry() {
|
||||
if (!this.loyaltyProgram) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loyaltyProgramDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
this.loyaltyProgram
|
||||
)) as LoyaltyProgram;
|
||||
|
||||
const expiryDate = this.date as Date;
|
||||
const fromDate = loyaltyProgramDoc.fromDate as Date;
|
||||
const toDate = loyaltyProgramDoc.toDate as Date;
|
||||
|
||||
if (fromDate <= expiryDate && toDate >= expiryDate) {
|
||||
const party = (await this.loadAndGetLink('party')) as Party;
|
||||
|
||||
await createLoyaltyPointEntry(this);
|
||||
await party.updateLoyaltyPoints();
|
||||
}
|
||||
}
|
||||
|
||||
async _validateHasLinkedReturnInvoices() {
|
||||
if (!this.name || this.isReturn || this.isQuote) {
|
||||
return;
|
||||
@ -560,6 +611,16 @@ export abstract class Invoice extends Transactional {
|
||||
);
|
||||
}
|
||||
|
||||
async getLPAddedBaseGrandTotal() {
|
||||
const totalLotaltyAmount = await getAddedLPWithGrandTotal(
|
||||
this.fyo,
|
||||
this.loyaltyProgram as string,
|
||||
this.loyaltyPoints as number
|
||||
);
|
||||
|
||||
return totalLotaltyAmount.sub(this.baseGrandTotal as Money).abs();
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
account: {
|
||||
formula: async () => {
|
||||
@ -571,6 +632,16 @@ export abstract class Invoice extends Transactional {
|
||||
},
|
||||
dependsOn: ['party'],
|
||||
},
|
||||
loyaltyProgram: {
|
||||
formula: async () => {
|
||||
const partyDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
this.party
|
||||
);
|
||||
return partyDoc?.loyaltyProgram as string;
|
||||
},
|
||||
dependsOn: ['party', 'name'],
|
||||
},
|
||||
currency: {
|
||||
formula: async () => {
|
||||
const currency = (await this.fyo.getValue(
|
||||
@ -611,12 +682,15 @@ export abstract class Invoice extends Transactional {
|
||||
dependsOn: ['grandTotal', 'exchangeRate'],
|
||||
},
|
||||
outstandingAmount: {
|
||||
formula: () => {
|
||||
formula: async () => {
|
||||
if (this.submitted) {
|
||||
return;
|
||||
}
|
||||
if (this.redeemLoyaltyPoints) {
|
||||
return await this.getLPAddedBaseGrandTotal();
|
||||
}
|
||||
|
||||
return this.baseGrandTotal!;
|
||||
return this.baseGrandTotal;
|
||||
},
|
||||
},
|
||||
stockNotTransferred: {
|
||||
@ -726,6 +800,9 @@ export abstract class Invoice extends Transactional {
|
||||
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
|
||||
backReference: () => !this.backReference,
|
||||
quote: () => !this.quote,
|
||||
loyaltyProgram: () => !this.loyaltyProgram,
|
||||
loyaltyPoints: () => !this.redeemLoyaltyPoints || this.isReturn,
|
||||
redeemLoyaltyPoints: () => !this.loyaltyProgram || this.isReturn,
|
||||
priceList: () =>
|
||||
!this.fyo.singles.AccountingSettings?.enablePriceList ||
|
||||
(!this.canEdit && !this.priceList),
|
||||
@ -1121,19 +1198,15 @@ export abstract class Invoice extends Transactional {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pricingRuleDetail?.length || !this.pricingRuleDetail.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.items = this.items.filter((item) => !item.isFreeItem);
|
||||
|
||||
for (const item of this.items) {
|
||||
const pricingRuleDetailForItem = this.pricingRuleDetail.filter(
|
||||
const pricingRuleDetailForItem = this.pricingRuleDetail?.filter(
|
||||
(doc) => doc.referenceItem === item.item
|
||||
);
|
||||
|
||||
if (!pricingRuleDetailForItem.length) {
|
||||
return;
|
||||
if (!pricingRuleDetailForItem?.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pricingRuleDoc = (await this.fyo.doc.getDoc(
|
||||
@ -1179,6 +1252,7 @@ export abstract class Invoice extends Transactional {
|
||||
item: pricingRuleDoc.freeItem as string,
|
||||
quantity: freeItemQty,
|
||||
isFreeItem: true,
|
||||
pricingRule: pricingRuleDoc.title,
|
||||
rate: pricingRuleDoc.freeItemRate,
|
||||
unit: pricingRuleDoc.freeItemUnit,
|
||||
});
|
||||
@ -1189,6 +1263,7 @@ export abstract class Invoice extends Transactional {
|
||||
if (!this.isSales || !this.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pricingRules: ApplicablePricingRules[] = [];
|
||||
|
||||
for (const item of this.items) {
|
||||
@ -1196,6 +1271,23 @@ export abstract class Invoice extends Transactional {
|
||||
continue;
|
||||
}
|
||||
|
||||
const duplicatePricingRule = this.pricingRuleDetail?.filter(
|
||||
(pricingrule: PricingRuleDetail) =>
|
||||
pricingrule.referenceItem == item.item
|
||||
);
|
||||
|
||||
if (duplicatePricingRule && duplicatePricingRule?.length >= 2) {
|
||||
const { showToast } = await import('src/utils/interactive');
|
||||
const message = t`Pricing Rule '${
|
||||
duplicatePricingRule[0]?.referenceName as string
|
||||
}' is already applied to item '${
|
||||
item.item as string
|
||||
}' in another batch.`;
|
||||
showToast({ type: 'error', message });
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const pricingRuleDocNames = (
|
||||
await this.fyo.db.getAll(ModelNameEnum.PricingRuleItem, {
|
||||
fields: ['parent'],
|
||||
@ -1230,10 +1322,7 @@ export abstract class Invoice extends Transactional {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isPricingRuleHasConflicts = getPricingRulesConflicts(
|
||||
filtered,
|
||||
item.item as string
|
||||
);
|
||||
const isPricingRuleHasConflicts = getPricingRulesConflicts(filtered);
|
||||
|
||||
if (isPricingRuleHasConflicts) {
|
||||
continue;
|
||||
|
21
models/baseModels/LoyaltyPointEntry/LoyaltyPointEntry.ts
Normal file
21
models/baseModels/LoyaltyPointEntry/LoyaltyPointEntry.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { ListViewSettings } from 'fyo/model/types';
|
||||
|
||||
export class LoyaltyPointEntry extends Doc {
|
||||
loyaltyProgram?: string;
|
||||
customer?: string;
|
||||
invoice?: string;
|
||||
purchaseAmount?: number;
|
||||
expiryDate?: Date;
|
||||
|
||||
static override getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: [
|
||||
'loyaltyProgram',
|
||||
'customer',
|
||||
'purchaseAmount',
|
||||
'loyaltyPoints',
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
22
models/baseModels/LoyaltyProgram/LoyaltyProgram.ts
Normal file
22
models/baseModels/LoyaltyProgram/LoyaltyProgram.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, ListViewSettings } from 'fyo/model/types';
|
||||
import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems';
|
||||
import { AccountRootTypeEnum } from '../Account/types';
|
||||
|
||||
export class LoyaltyProgram extends Doc {
|
||||
collectionRules?: CollectionRulesItems[];
|
||||
expiryDuration?: number;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
expenseAccount: () => ({
|
||||
rootType: AccountRootTypeEnum.Liability,
|
||||
isGroup: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: ['name', 'fromDate', 'toDate', 'expiryDuration'],
|
||||
};
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ export class Party extends Doc {
|
||||
party?: string;
|
||||
fromLead?: string;
|
||||
defaultAccount?: string;
|
||||
loyaltyPoints?: number;
|
||||
outstandingAmount?: Money;
|
||||
async updateOutstandingAmount() {
|
||||
/**
|
||||
@ -54,6 +55,40 @@ export class Party extends Doc {
|
||||
await this.setAndSync({ outstandingAmount });
|
||||
}
|
||||
|
||||
async updateLoyaltyPoints() {
|
||||
let loyaltyPoints = 0;
|
||||
|
||||
if (this.role === 'Customer' || this.role === 'Both') {
|
||||
loyaltyPoints = await this._getTotalLoyaltyPoints();
|
||||
}
|
||||
|
||||
await this.setAndSync({ loyaltyPoints });
|
||||
}
|
||||
|
||||
async _getTotalLoyaltyPoints() {
|
||||
const data = (await this.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, {
|
||||
fields: ['name', 'loyaltyPoints', 'expiryDate', 'postingDate'],
|
||||
filters: {
|
||||
customer: this.name as string,
|
||||
},
|
||||
})) as {
|
||||
name: string;
|
||||
loyaltyPoints: number;
|
||||
expiryDate: Date;
|
||||
postingDate: Date;
|
||||
}[];
|
||||
|
||||
const totalLoyaltyPoints = data.reduce((total, entry) => {
|
||||
if (entry.expiryDate > entry.postingDate) {
|
||||
return total + entry.loyaltyPoints;
|
||||
}
|
||||
|
||||
return total;
|
||||
}, 0);
|
||||
|
||||
return totalLoyaltyPoints;
|
||||
}
|
||||
|
||||
async _getTotalOutstandingAmount(
|
||||
schemaName: 'SalesInvoice' | 'PurchaseInvoice'
|
||||
) {
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { Action, ListViewSettings } from 'fyo/model/types';
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { Action, ListViewSettings, ValidationMap } from 'fyo/model/types';
|
||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers';
|
||||
import {
|
||||
getAddedLPWithGrandTotal,
|
||||
getInvoiceActions,
|
||||
getTransactionStatusColumn,
|
||||
} from '../../helpers';
|
||||
import { Invoice } from '../Invoice/Invoice';
|
||||
import { SalesInvoiceItem } from '../SalesInvoiceItem/SalesInvoiceItem';
|
||||
import { LoyaltyProgram } from '../LoyaltyProgram/LoyaltyProgram';
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Party } from '../Party/Party';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
|
||||
export class SalesInvoice extends Invoice {
|
||||
items?: SalesInvoiceItem[];
|
||||
@ -26,6 +34,26 @@ export class SalesInvoice extends Invoice {
|
||||
await posting.credit(item.account!, item.amount!.mul(exchangeRate));
|
||||
}
|
||||
|
||||
if (this.redeemLoyaltyPoints) {
|
||||
const loyaltyProgramDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
this.loyaltyProgram
|
||||
)) as LoyaltyProgram;
|
||||
|
||||
const totalAmount = await getAddedLPWithGrandTotal(
|
||||
this.fyo,
|
||||
this.loyaltyProgram as string,
|
||||
this.loyaltyPoints as number
|
||||
);
|
||||
|
||||
await posting.debit(
|
||||
loyaltyProgramDoc.expenseAccount as string,
|
||||
totalAmount
|
||||
);
|
||||
|
||||
await posting.credit(this.account!, totalAmount);
|
||||
}
|
||||
|
||||
if (this.taxes) {
|
||||
for (const tax of this.taxes) {
|
||||
if (this.isReturn) {
|
||||
@ -51,6 +79,50 @@ export class SalesInvoice extends Invoice {
|
||||
return posting;
|
||||
}
|
||||
|
||||
validations: ValidationMap = {
|
||||
loyaltyPoints: async (value: DocValue) => {
|
||||
if (!this.redeemLoyaltyPoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
const partyDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
this.party
|
||||
)) as Party;
|
||||
|
||||
if ((value as number) <= 0) {
|
||||
throw new ValidationError(t`Points must be greather than 0`);
|
||||
}
|
||||
|
||||
if ((value as number) > (partyDoc?.loyaltyPoints || 0)) {
|
||||
throw new ValidationError(
|
||||
t`${this.party as string} only has ${
|
||||
partyDoc.loyaltyPoints as number
|
||||
} points`
|
||||
);
|
||||
}
|
||||
|
||||
const loyaltyProgramDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
this.loyaltyProgram
|
||||
)) as LoyaltyProgram;
|
||||
|
||||
if (!this?.grandTotal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loyaltyPoint =
|
||||
((value as number) || 0) *
|
||||
((loyaltyProgramDoc?.conversionFactor as number) || 0);
|
||||
|
||||
if (this.grandTotal?.lt(loyaltyPoint)) {
|
||||
throw new ValidationError(
|
||||
t`no need ${value as number} points to purchase this item`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: [
|
||||
|
260
models/baseModels/tests/testLoyaltyProgram.spec.ts
Normal file
260
models/baseModels/tests/testLoyaltyProgram.spec.ts
Normal file
@ -0,0 +1,260 @@
|
||||
import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Party } from '../Party/Party';
|
||||
import { SalesInvoice } from '../SalesInvoice/SalesInvoice';
|
||||
import { getLoyaltyProgramTier } from 'models/helpers';
|
||||
import { CollectionRulesItems } from '../CollectionRulesItems/CollectionRulesItems';
|
||||
|
||||
const fyo = getTestFyo();
|
||||
setupTestFyo(fyo, __filename);
|
||||
|
||||
const accountData = {
|
||||
name: 'Loyalty Point Redemption',
|
||||
rootType: 'Liability',
|
||||
parentAccount: 'Accounts Payable',
|
||||
isGroup: false,
|
||||
};
|
||||
|
||||
const itemData = {
|
||||
name: 'Pen',
|
||||
rate: 4000,
|
||||
for: 'Both',
|
||||
};
|
||||
|
||||
const partyData = {
|
||||
name: 'John Whoe',
|
||||
email: 'john@whoe.com',
|
||||
};
|
||||
|
||||
const loyaltyProgramData = {
|
||||
name: 'program',
|
||||
fromDate: new Date(Date.now()),
|
||||
toDate: new Date(Date.now()),
|
||||
email: 'sample@gmail.com',
|
||||
mobile: '1234567890',
|
||||
expenseAccount: accountData.name,
|
||||
};
|
||||
|
||||
const collectionRulesData = [
|
||||
{
|
||||
tierName: 'Silver',
|
||||
collectionFactor: 0.5,
|
||||
minimumTotalSpent: 2000,
|
||||
},
|
||||
{ tierName: 'Gold', collectionFactor: 0.5, minimumTotalSpent: 3000 },
|
||||
];
|
||||
|
||||
test('create test docs', async (t) => {
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Item, itemData).sync();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.Item, itemData.name),
|
||||
`dummy item ${itemData.name} exists`
|
||||
);
|
||||
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Party, partyData).sync();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.Party, partyData.name),
|
||||
`dummy party ${partyData.name} exists`
|
||||
);
|
||||
|
||||
await fyo.doc.getNewDoc(ModelNameEnum.Account, accountData).sync();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.Account, accountData.name),
|
||||
`dummy account ${accountData.name} exists`
|
||||
);
|
||||
});
|
||||
|
||||
test('create a Loyalty Program document', async (t) => {
|
||||
const loyaltyProgramDoc = fyo.doc.getNewDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
loyaltyProgramData
|
||||
);
|
||||
|
||||
await loyaltyProgramDoc.append('collectionRules', collectionRulesData[0]);
|
||||
|
||||
await loyaltyProgramDoc.append('collectionRules', collectionRulesData[1]);
|
||||
|
||||
await loyaltyProgramDoc.sync();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.LoyaltyProgram, loyaltyProgramData.name),
|
||||
`Loyalty Program '${loyaltyProgramData.name}' exists `
|
||||
);
|
||||
|
||||
const partyDoc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
partyData.name
|
||||
)) as Party;
|
||||
|
||||
await partyDoc.setAndSync('loyaltyProgram', loyaltyProgramData.name);
|
||||
|
||||
t.equals(
|
||||
partyDoc.loyaltyProgram,
|
||||
loyaltyProgramData.name,
|
||||
`Loyalty Program '${loyaltyProgramData.name}' successfully added to Party '${partyData.name}'`
|
||||
);
|
||||
});
|
||||
|
||||
async function loyaltyPointEntryDoc(sinvName: string) {
|
||||
const loyaltyPointEntryData = (await fyo.db.getAll(
|
||||
ModelNameEnum.LoyaltyPointEntry,
|
||||
{
|
||||
fields: ['name', 'customer', 'loyaltyPoints', 'loyaltyProgramTier'],
|
||||
filters: { invoice: sinvName! },
|
||||
}
|
||||
)) as {
|
||||
name?: string;
|
||||
customer?: string;
|
||||
loyaltyPoints?: number;
|
||||
loyaltyProgramTier?: string;
|
||||
}[];
|
||||
|
||||
if (loyaltyPointEntryData) {
|
||||
return loyaltyPointEntryData[0];
|
||||
}
|
||||
}
|
||||
|
||||
async function createSalesInvoice() {
|
||||
const sinvDoc = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
|
||||
account: 'Debtors',
|
||||
party: partyData.name,
|
||||
items: [
|
||||
{
|
||||
item: itemData.name,
|
||||
rate: itemData.rate,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
}) as SalesInvoice;
|
||||
|
||||
return sinvDoc;
|
||||
}
|
||||
|
||||
test('create Sales Invoice and verify loyalty points are created correctly', async (t) => {
|
||||
const sinvDoc = await createSalesInvoice();
|
||||
|
||||
await sinvDoc.sync();
|
||||
await sinvDoc.submit();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.SalesInvoice, sinvDoc.name),
|
||||
`Sales Invoice '${sinvDoc.name}' exists`
|
||||
);
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.SalesInvoice, sinvDoc.loyaltyProgram),
|
||||
`Loyalty Program '${sinvDoc.loyaltyProgram}' should be linked to Sales Invoice '${sinvDoc.name}'`
|
||||
);
|
||||
|
||||
t.equals(
|
||||
sinvDoc.loyaltyProgram,
|
||||
loyaltyProgramData.name,
|
||||
`Loyalty Program '${sinvDoc.loyaltyProgram}' linked to Sales Invoice'`
|
||||
);
|
||||
|
||||
const loyaltyPointEntryData = await loyaltyPointEntryDoc(
|
||||
sinvDoc.name as string
|
||||
);
|
||||
|
||||
const loyaltyProgramDoc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
sinvDoc.loyaltyProgram
|
||||
)) as Party;
|
||||
|
||||
const selectedTier: CollectionRulesItems | undefined = getLoyaltyProgramTier(
|
||||
loyaltyProgramDoc,
|
||||
fyo.pesa(itemData.rate)
|
||||
);
|
||||
|
||||
t.equals(
|
||||
loyaltyPointEntryData?.loyaltyProgramTier,
|
||||
selectedTier?.tierName,
|
||||
`tier name ${loyaltyPointEntryData?.loyaltyProgramTier} matches.'`
|
||||
);
|
||||
|
||||
const tierData = collectionRulesData.find((rule) => {
|
||||
return rule.tierName === loyaltyPointEntryData?.loyaltyProgramTier;
|
||||
});
|
||||
|
||||
t.equals(
|
||||
loyaltyPointEntryData?.loyaltyPoints,
|
||||
itemData.rate * (tierData?.collectionFactor as number),
|
||||
`calculation of ${loyaltyPointEntryData?.loyaltyPoints} loyalty Point is correct'`
|
||||
);
|
||||
});
|
||||
|
||||
test('create SINV with future date and verify loyalty points are not created', async (t) => {
|
||||
const futureDate = new Date(new Date().setDate(new Date().getDate() + 20));
|
||||
|
||||
const sinvDoc = await createSalesInvoice();
|
||||
sinvDoc.date = futureDate;
|
||||
|
||||
await sinvDoc.sync();
|
||||
await sinvDoc.submit();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.SalesInvoice, sinvDoc.name),
|
||||
`Sales Invoice '${sinvDoc.name}' exists`
|
||||
);
|
||||
|
||||
const loyaltyPointEntryData = await loyaltyPointEntryDoc(
|
||||
sinvDoc.name as string
|
||||
);
|
||||
t.equals(
|
||||
loyaltyPointEntryData,
|
||||
undefined,
|
||||
'Loyalty points should not be created for a future-dated Sales Invoice'
|
||||
);
|
||||
});
|
||||
|
||||
test('redeem loyalty points and verify a new loyalty point entry doc is created', async (t) => {
|
||||
const sinvDoc = await createSalesInvoice();
|
||||
|
||||
sinvDoc.redeemLoyaltyPoints = true;
|
||||
sinvDoc.loyaltyPoints = 1000;
|
||||
|
||||
await sinvDoc.sync();
|
||||
await sinvDoc.submit();
|
||||
|
||||
t.ok(
|
||||
fyo.db.exists(ModelNameEnum.SalesInvoice, sinvDoc.name),
|
||||
`Sales Invoice '${sinvDoc.name}' exists`
|
||||
);
|
||||
|
||||
const loyaltyPointEntryData = await loyaltyPointEntryDoc(
|
||||
sinvDoc.name as string
|
||||
);
|
||||
|
||||
t.ok(
|
||||
await fyo.db.exists(
|
||||
ModelNameEnum.LoyaltyPointEntry,
|
||||
loyaltyPointEntryData?.name
|
||||
),
|
||||
`Negative Loyalty Point Entry '${loyaltyPointEntryData?.name}' should be created against Sales Invoice '${sinvDoc.name}'`
|
||||
);
|
||||
|
||||
t.equals(
|
||||
loyaltyPointEntryData?.loyaltyPoints,
|
||||
-1000,
|
||||
`redeemed loyalty point matches the loyalty points used`
|
||||
);
|
||||
|
||||
const partyDoc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
partyData.name
|
||||
)) as Party;
|
||||
|
||||
const totalPoints = await partyDoc._getTotalLoyaltyPoints();
|
||||
|
||||
t.equals(
|
||||
totalPoints,
|
||||
itemData.rate * collectionRulesData[1].collectionFactor + -1000,
|
||||
`Customer '${partyData.name}' should have a total of ${totalPoints} loyalty points after redemption`
|
||||
);
|
||||
});
|
||||
|
||||
closeTestFyo(fyo, __filename);
|
@ -23,8 +23,11 @@ import { StockTransfer } from './inventory/StockTransfer';
|
||||
import { InvoiceStatus, ModelNameEnum } from './types';
|
||||
import { Lead } from './baseModels/Lead/Lead';
|
||||
import { PricingRule } from './baseModels/PricingRule/PricingRule';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ApplicablePricingRules } from './baseModels/Invoice/types';
|
||||
import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram';
|
||||
import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { Party } from './baseModels/Party/Party';
|
||||
|
||||
export function getQuoteActions(
|
||||
fyo: Fyo,
|
||||
@ -663,15 +666,139 @@ export async function addItem<M extends ModelsWithItems>(name: string, doc: M) {
|
||||
await item.set('item', name);
|
||||
}
|
||||
|
||||
export async function createLoyaltyPointEntry(doc: Invoice) {
|
||||
const loyaltyProgramDoc = (await doc.fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
doc?.loyaltyProgram
|
||||
)) as LoyaltyProgram;
|
||||
|
||||
if (!loyaltyProgramDoc.isEnabled) {
|
||||
return;
|
||||
}
|
||||
const expiryDate = new Date(Date.now());
|
||||
|
||||
expiryDate.setDate(
|
||||
expiryDate.getDate() + (loyaltyProgramDoc.expiryDuration || 0)
|
||||
);
|
||||
|
||||
let loyaltyProgramTier;
|
||||
let loyaltyPoint: number;
|
||||
|
||||
if (doc.redeemLoyaltyPoints) {
|
||||
loyaltyPoint = -(doc.loyaltyPoints || 0);
|
||||
} else {
|
||||
loyaltyProgramTier = getLoyaltyProgramTier(
|
||||
loyaltyProgramDoc,
|
||||
doc?.grandTotal as Money
|
||||
) as CollectionRulesItems;
|
||||
|
||||
if (!loyaltyProgramTier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collectionFactor = loyaltyProgramTier.collectionFactor as number;
|
||||
loyaltyPoint = Math.round(doc?.grandTotal?.float || 0) * collectionFactor;
|
||||
}
|
||||
|
||||
const newLoyaltyPointEntry = doc.fyo.doc.getNewDoc(
|
||||
ModelNameEnum.LoyaltyPointEntry,
|
||||
{
|
||||
loyaltyProgram: doc.loyaltyProgram,
|
||||
customer: doc.party,
|
||||
invoice: doc.name,
|
||||
postingDate: doc.date,
|
||||
purchaseAmount: doc.grandTotal,
|
||||
expiryDate: expiryDate,
|
||||
loyaltyProgramTier: loyaltyProgramTier?.tierName,
|
||||
loyaltyPoints: loyaltyPoint,
|
||||
}
|
||||
);
|
||||
|
||||
return await newLoyaltyPointEntry.sync();
|
||||
}
|
||||
|
||||
export async function getAddedLPWithGrandTotal(
|
||||
fyo: Fyo,
|
||||
loyaltyProgram: string,
|
||||
loyaltyPoints: number
|
||||
) {
|
||||
const loyaltyProgramDoc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyProgram,
|
||||
loyaltyProgram
|
||||
)) as LoyaltyProgram;
|
||||
|
||||
const conversionFactor = loyaltyProgramDoc.conversionFactor as number;
|
||||
|
||||
return fyo.pesa((loyaltyPoints || 0) * conversionFactor);
|
||||
}
|
||||
|
||||
export function getLoyaltyProgramTier(
|
||||
loyaltyProgramData: LoyaltyProgram,
|
||||
grandTotal: Money
|
||||
): CollectionRulesItems | undefined {
|
||||
if (!loyaltyProgramData.collectionRules) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loyaltyProgramTier: CollectionRulesItems | undefined;
|
||||
|
||||
for (const row of loyaltyProgramData.collectionRules) {
|
||||
if (isPesa(row.minimumTotalSpent)) {
|
||||
const minimumSpent = row.minimumTotalSpent;
|
||||
|
||||
if (!minimumSpent.lte(grandTotal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!loyaltyProgramTier ||
|
||||
minimumSpent.gt(loyaltyProgramTier.minimumTotalSpent as Money)
|
||||
) {
|
||||
loyaltyProgramTier = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
return loyaltyProgramTier;
|
||||
}
|
||||
|
||||
export async function removeLoyaltyPoint(doc: Doc) {
|
||||
const data = (await doc.fyo.db.getAll(ModelNameEnum.LoyaltyPointEntry, {
|
||||
fields: ['name', 'loyaltyPoints', 'expiryDate'],
|
||||
filters: {
|
||||
loyaltyProgram: doc.loyaltyProgram as string,
|
||||
invoice: doc.isReturn
|
||||
? (doc.returnAgainst as string)
|
||||
: (doc.name as string),
|
||||
},
|
||||
})) as { name: string; loyaltyPoints: number; expiryDate: Date }[];
|
||||
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loyalityPointEntryDoc = await doc.fyo.doc.getDoc(
|
||||
ModelNameEnum.LoyaltyPointEntry,
|
||||
data[0].name
|
||||
);
|
||||
|
||||
const party = (await doc.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
doc.party as string
|
||||
)) as Party;
|
||||
|
||||
await loyalityPointEntryDoc.delete();
|
||||
await party.updateLoyaltyPoints();
|
||||
}
|
||||
|
||||
export async function getPricingRule(
|
||||
doc: Invoice
|
||||
): Promise<ApplicablePricingRules[] | null> {
|
||||
): Promise<ApplicablePricingRules[] | undefined> {
|
||||
if (
|
||||
!doc.fyo.singles.AccountingSettings?.enablePricingRule ||
|
||||
!doc.isSales ||
|
||||
!doc.items
|
||||
) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const pricingRules: ApplicablePricingRules[] = [];
|
||||
@ -715,10 +842,7 @@ export async function getPricingRule(
|
||||
continue;
|
||||
}
|
||||
|
||||
const isPricingRuleHasConflicts = getPricingRulesConflicts(
|
||||
filtered,
|
||||
item.item as string
|
||||
);
|
||||
const isPricingRuleHasConflicts = getPricingRulesConflicts(filtered);
|
||||
|
||||
if (isPricingRuleHasConflicts) {
|
||||
continue;
|
||||
@ -729,7 +853,6 @@ export async function getPricingRule(
|
||||
pricingRule: filtered[0],
|
||||
});
|
||||
}
|
||||
|
||||
return pricingRules;
|
||||
}
|
||||
|
||||
@ -802,11 +925,9 @@ export function canApplyPricingRule(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getPricingRulesConflicts(
|
||||
pricingRules: PricingRule[],
|
||||
item: string
|
||||
): string[] | undefined {
|
||||
pricingRules: PricingRule[]
|
||||
): undefined | boolean {
|
||||
const pricingRuleDocs = Array.from(pricingRules);
|
||||
|
||||
const firstPricingRule = pricingRuleDocs.shift();
|
||||
@ -827,13 +948,7 @@ export function getPricingRulesConflicts(
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ValidationError(
|
||||
t`Pricing Rules ${
|
||||
firstPricingRule.name as string
|
||||
}, ${conflictingPricingRuleNames.join(
|
||||
', '
|
||||
)} has the same Priority for the Item ${item}.`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function roundFreeItemQty(
|
||||
|
@ -9,6 +9,9 @@ import { JournalEntry } from './baseModels/JournalEntry/JournalEntry';
|
||||
import { JournalEntryAccount } from './baseModels/JournalEntryAccount/JournalEntryAccount';
|
||||
import { Misc } from './baseModels/Misc';
|
||||
import { Party } from './baseModels/Party/Party';
|
||||
import { LoyaltyProgram } from './baseModels/LoyaltyProgram/LoyaltyProgram';
|
||||
import { LoyaltyPointEntry } from './baseModels/LoyaltyPointEntry/LoyaltyPointEntry';
|
||||
import { CollectionRulesItems } from './baseModels/CollectionRulesItems/CollectionRulesItems';
|
||||
import { Lead } from './baseModels/Lead/Lead';
|
||||
import { Payment } from './baseModels/Payment/Payment';
|
||||
import { PaymentFor } from './baseModels/PaymentFor/PaymentFor';
|
||||
@ -58,6 +61,9 @@ export const models = {
|
||||
Misc,
|
||||
Lead,
|
||||
Party,
|
||||
LoyaltyProgram,
|
||||
LoyaltyPointEntry,
|
||||
CollectionRulesItems,
|
||||
Payment,
|
||||
PaymentFor,
|
||||
PrintSettings,
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { HiddenMap } from 'fyo/model/types';
|
||||
import { Party as BaseParty } from 'models/baseModels/Party/Party';
|
||||
import { GSTType } from './types';
|
||||
import { PartyRole } from 'models/baseModels/Party/types';
|
||||
|
||||
export class Party extends BaseParty {
|
||||
gstin?: string;
|
||||
fromLead?: string;
|
||||
role?: PartyRole;
|
||||
gstType?: GSTType;
|
||||
loyaltyProgram?: string;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async beforeSync() {
|
||||
@ -19,6 +21,12 @@ export class Party extends BaseParty {
|
||||
|
||||
hidden: HiddenMap = {
|
||||
gstin: () => (this.gstType as GSTType) !== 'Registered Regular',
|
||||
fromLead: () => !this.fromLead,
|
||||
loyaltyProgram: () => {
|
||||
if (!this.fyo.singles.AccountingSettings?.enableLoyaltyProgram) {
|
||||
return true;
|
||||
}
|
||||
return this.role === 'Supplier';
|
||||
},
|
||||
loyaltyPoints: () => !this.loyaltyProgram || this.role === 'Supplier',
|
||||
};
|
||||
}
|
||||
|
@ -19,6 +19,9 @@ export enum ModelNameEnum {
|
||||
NumberSeries = 'NumberSeries',
|
||||
Lead = 'Lead',
|
||||
Party = 'Party',
|
||||
LoyaltyProgram = 'LoyaltyProgram',
|
||||
LoyaltyPointEntry = 'LoyaltyPointEntry',
|
||||
CollectionRulesItems = 'CollectionRulesItems',
|
||||
Payment = 'Payment',
|
||||
PaymentFor = 'PaymentFor',
|
||||
PriceList = 'PriceList',
|
||||
|
@ -114,6 +114,13 @@
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "enableLoyaltyProgram",
|
||||
"label": "Enable Loyalty Program",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"section": "Features"
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscalYearStart",
|
||||
"label": "Fiscal Year Start Date",
|
||||
|
23
schemas/app/CollectionRulesItems.json
Normal file
23
schemas/app/CollectionRulesItems.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "CollectionRulesItems",
|
||||
"label": "Collection Rules",
|
||||
"isChild": true,
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "tierName",
|
||||
"label": "Tier Name",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "collectionFactor",
|
||||
"label": "Collection Factor (=1 LP)",
|
||||
"fieldtype": "Float"
|
||||
},
|
||||
{
|
||||
"fieldname": "minimumTotalSpent",
|
||||
"label": "Minimum Total Spent",
|
||||
"fieldtype": "Currency"
|
||||
}
|
||||
],
|
||||
"tableFields": ["tierName", "collectionFactor", "minimumTotalSpent"]
|
||||
}
|
70
schemas/app/LoyaltyPointEntry.json
Normal file
70
schemas/app/LoyaltyPointEntry.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "LoyaltyPointEntry",
|
||||
"label": "Loyalty Point Entry",
|
||||
"create": false,
|
||||
"naming": "random",
|
||||
"fields": [
|
||||
{
|
||||
"label": "Entry No.",
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Data",
|
||||
"required": true,
|
||||
"readOnly": true,
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "loyaltyProgram",
|
||||
"readOnly": true,
|
||||
"label": "Loyalty Program",
|
||||
"fieldtype": "Data",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "loyaltyProgramTier",
|
||||
"readOnly": true,
|
||||
"label": "Loyalty Program Tier",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"readOnly": true,
|
||||
"label": "Customer",
|
||||
"fieldtype": "Link",
|
||||
"target": "Party",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice",
|
||||
"readOnly": true,
|
||||
"label": "Invoice",
|
||||
"fieldtype": "Link",
|
||||
"target": "SalesInvoice",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "loyaltyPoints",
|
||||
"readOnly": true,
|
||||
"label": "Loyalty Points",
|
||||
"fieldtype": "Int"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchaseAmount",
|
||||
"readOnly": true,
|
||||
"label": "Purchase Amount",
|
||||
"fieldtype": "Currency",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "expiryDate",
|
||||
"readOnly": true,
|
||||
"label": "Expiry Date",
|
||||
"fieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "postingDate",
|
||||
"readOnly": true,
|
||||
"label": "Posting Date",
|
||||
"fieldtype": "Date"
|
||||
}
|
||||
]
|
||||
}
|
72
schemas/app/LoyaltyProgram.json
Normal file
72
schemas/app/LoyaltyProgram.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "LoyaltyProgram",
|
||||
"label": "Loyalty Program",
|
||||
"naming": "manual",
|
||||
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"label": "Name",
|
||||
"fieldtype": "Data",
|
||||
"required": true,
|
||||
"placeholder": "Name",
|
||||
"section": "Default"
|
||||
},
|
||||
{
|
||||
"fieldname": "fromDate",
|
||||
"label": "From Date",
|
||||
"fieldtype": "Date",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "toDate",
|
||||
"label": "To Date",
|
||||
"fieldtype": "Date",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "isEnabled",
|
||||
"label": "Is Enabled",
|
||||
"fieldtype": "Check",
|
||||
"default": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "collectionRules",
|
||||
"label": "Collection Rules",
|
||||
"fieldtype": "Table",
|
||||
"target": "CollectionRulesItems",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"fieldname": "conversionFactor",
|
||||
"label": "Conversion Factor",
|
||||
"fieldtype": "Float",
|
||||
"default": 1,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "expiryDuration",
|
||||
"label": "Expiry Duration",
|
||||
"fieldtype": "Int",
|
||||
"default": 1,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"fieldname": "expenseAccount",
|
||||
"label": "Expense Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
"name",
|
||||
"fromDate",
|
||||
"toDate",
|
||||
"conversionFactor",
|
||||
"expenseAccount",
|
||||
"expiryDuration"
|
||||
],
|
||||
"keywordFields": ["name"]
|
||||
}
|
@ -86,6 +86,23 @@
|
||||
"readOnly": true,
|
||||
"section": "References"
|
||||
},
|
||||
|
||||
{
|
||||
"fieldname": "loyaltyProgram",
|
||||
"label": "Loyalty Program",
|
||||
"fieldtype": "Link",
|
||||
"target": "LoyaltyProgram",
|
||||
"create": true,
|
||||
"section": "Loyalty Program"
|
||||
},
|
||||
{
|
||||
"fieldname": "loyaltyPoints",
|
||||
"label": "Loyalty Points",
|
||||
"fieldtype": "Int",
|
||||
"readOnly": true,
|
||||
"default": 0,
|
||||
"section": "Loyalty Program"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxId",
|
||||
"label": "Tax ID",
|
||||
@ -104,6 +121,7 @@
|
||||
"phone",
|
||||
"address",
|
||||
"defaultAccount",
|
||||
"loyaltyProgram",
|
||||
"currency",
|
||||
"role",
|
||||
"taxId"
|
||||
|
@ -70,6 +70,27 @@
|
||||
"label": "Return Against",
|
||||
"section": "References"
|
||||
},
|
||||
{
|
||||
"fieldname": "loyaltyProgram",
|
||||
"fieldtype": "Link",
|
||||
"target": "LoyaltyProgram",
|
||||
"label": "Loyalty Program",
|
||||
"section": "References",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "redeemLoyaltyPoints",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"label": "Redeem Loyalty Points",
|
||||
"section": "Loyalty Points Redemption"
|
||||
},
|
||||
{
|
||||
"fieldname": "loyaltyPoints",
|
||||
"fieldtype": "Int",
|
||||
"label": "Loyalty Points",
|
||||
"section": "Loyalty Points Redemption"
|
||||
},
|
||||
{
|
||||
"fieldname": "isPOS",
|
||||
"fieldtype": "Check",
|
||||
|
@ -8,6 +8,11 @@
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"fieldname": "pricingRule",
|
||||
"fieldtype": "Data",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -117,6 +117,14 @@
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true,
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"fieldname": "darkMode",
|
||||
"label": "Dark mode",
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
"description": "Sets the theme of the app.",
|
||||
"section": "Theme"
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
|
@ -16,6 +16,9 @@ import Misc from './app/Misc.json';
|
||||
import NumberSeries from './app/NumberSeries.json';
|
||||
import Party from './app/Party.json';
|
||||
import Lead from './app/Lead.json';
|
||||
import LoyaltyProgram from './app/LoyaltyProgram.json';
|
||||
import LoyaltyPointEntry from './app/LoyaltyPointEntry.json';
|
||||
import CollectionRulesItems from './app/CollectionRulesItems.json';
|
||||
import Payment from './app/Payment.json';
|
||||
import PaymentFor from './app/PaymentFor.json';
|
||||
import PriceList from './app/PriceList.json';
|
||||
@ -106,6 +109,10 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
||||
UOM as Schema,
|
||||
UOMConversionItem as Schema,
|
||||
|
||||
LoyaltyProgram as Schema,
|
||||
LoyaltyPointEntry as Schema,
|
||||
CollectionRulesItems as Schema,
|
||||
|
||||
Payment as Schema,
|
||||
PaymentFor as Schema,
|
||||
|
||||
|
16
src/App.vue
16
src/App.vue
@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
id="app"
|
||||
class="h-screen flex flex-col font-sans overflow-hidden antialiased"
|
||||
class="
|
||||
dark:bg-gray-900
|
||||
h-screen
|
||||
flex flex-col
|
||||
font-sans
|
||||
overflow-hidden
|
||||
antialiased
|
||||
"
|
||||
:dir="languageDirection"
|
||||
:language="language"
|
||||
>
|
||||
@ -14,6 +21,7 @@
|
||||
<Desk
|
||||
v-if="activeScreen === 'Desk'"
|
||||
class="flex-1"
|
||||
:darkMode="darkMode"
|
||||
@change-db-file="showDbSelector"
|
||||
/>
|
||||
<DatabaseSelector
|
||||
@ -61,6 +69,7 @@ import { Search } from './utils/search';
|
||||
import { Shortcuts } from './utils/shortcuts';
|
||||
import { routeTo } from './utils/ui';
|
||||
import { useKeys } from './utils/vueUtils';
|
||||
import { setDarkMode } from 'src/utils/theme';
|
||||
|
||||
enum Screen {
|
||||
Desk = 'Desk',
|
||||
@ -106,10 +115,12 @@ export default defineComponent({
|
||||
activeScreen: null,
|
||||
dbPath: '',
|
||||
companyName: '',
|
||||
darkMode: false,
|
||||
} as {
|
||||
activeScreen: null | Screen;
|
||||
dbPath: string;
|
||||
companyName: string;
|
||||
darkMode: boolean | undefined;
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -124,6 +135,9 @@ export default defineComponent({
|
||||
},
|
||||
async mounted() {
|
||||
await this.setInitialScreen();
|
||||
const { darkMode } = await fyo.doc.getDoc('SystemSettings');
|
||||
setDarkMode(!!darkMode);
|
||||
this.darkMode = !!darkMode;
|
||||
},
|
||||
methods: {
|
||||
async setInitialScreen(): Promise<void> {
|
||||
|
@ -15,6 +15,7 @@
|
||||
items-center
|
||||
justify-center
|
||||
text-white
|
||||
dark:text-gray-900
|
||||
w-full
|
||||
text-base
|
||||
uppercase
|
||||
|
@ -39,10 +39,12 @@ export default defineComponent({
|
||||
_class() {
|
||||
return {
|
||||
'opacity-50 cursor-not-allowed pointer-events-none': this.disabled,
|
||||
'text-white': this.type === 'primary',
|
||||
'bg-black': this.type === 'primary' && this.background,
|
||||
'text-gray-700': this.type !== 'primary',
|
||||
'bg-gray-200': this.type !== 'primary' && this.background,
|
||||
'text-white dark:text-black': this.type === 'primary',
|
||||
'bg-black dark:bg-gray-300 dark:font-semibold':
|
||||
this.type === 'primary' && this.background,
|
||||
'text-gray-700 dark:text-gray-200': this.type !== 'primary',
|
||||
'bg-gray-200 dark:bg-gray-900':
|
||||
this.type !== 'primary' && this.background,
|
||||
'h-8': this.background,
|
||||
'px-3': this.padding && this.icon,
|
||||
'px-6': this.padding && !this.icon,
|
||||
|
@ -118,7 +118,17 @@
|
||||
ref="tooltip"
|
||||
:offset="15"
|
||||
placement="top"
|
||||
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-s-4"
|
||||
class="
|
||||
text-sm
|
||||
shadow-md
|
||||
px-2
|
||||
py-1
|
||||
bg-white
|
||||
dark:bg-gray-900
|
||||
text-gray-900
|
||||
dark:text-gray-200
|
||||
border-s-4
|
||||
"
|
||||
:style="{ borderColor: activeColor }"
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
|
@ -45,7 +45,7 @@
|
||||
:key="i"
|
||||
clip-path="url(#donut-hole)"
|
||||
:d="getArcPath(cx, cy, radius, start_, theta)"
|
||||
:stroke="sectors[i].color"
|
||||
:stroke="getSectorColor(i)"
|
||||
:stroke-width="thickness + (active === i ? 4 : 0)"
|
||||
:style="{ transformOrigin: `${cx}px ${cy}px` }"
|
||||
class="sector"
|
||||
@ -57,7 +57,11 @@
|
||||
:x="cx"
|
||||
:y="cy"
|
||||
text-anchor="middle"
|
||||
style="font-size: 6px; font-weight: bold; fill: #415668"
|
||||
:style="{
|
||||
fontSize: '6px',
|
||||
fontWeight: 'bold',
|
||||
fill: darkMode ? '#FFFFFF' : '#415668',
|
||||
}"
|
||||
>
|
||||
{{
|
||||
valueFormatter(
|
||||
@ -101,6 +105,7 @@ export default {
|
||||
offsetY: { default: 0, type: Number },
|
||||
textOffsetX: { default: 0, type: Number },
|
||||
textOffsetY: { default: 0, type: Number },
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['change'],
|
||||
computed: {
|
||||
@ -150,6 +155,13 @@ export default {
|
||||
|
||||
return `M ${startX} ${startY} A ${r} ${r} 0 ${largeArcFlag} ${sweepFlag} ${endX} ${endY}`;
|
||||
},
|
||||
getSectorColor(index) {
|
||||
if (this.darkMode) {
|
||||
return this.sectors[index].color.darkColor;
|
||||
} else {
|
||||
return this.sectors[index].color.color;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -117,7 +117,17 @@
|
||||
ref="tooltip"
|
||||
:offset="15"
|
||||
placement="top"
|
||||
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-s-4"
|
||||
class="
|
||||
text-sm
|
||||
shadow-md
|
||||
px-2
|
||||
py-1
|
||||
bg-white
|
||||
dark:bg-gray-900
|
||||
text-gray-900
|
||||
dark:text-gray-200
|
||||
border-s-4
|
||||
"
|
||||
:style="{ borderColor: colors[yi] }"
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
|
@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative bg-white border flex-center overflow-hidden group"
|
||||
class="
|
||||
relative
|
||||
bg-white
|
||||
dark:bg-gray-900
|
||||
border
|
||||
dark:border-gray-800
|
||||
flex-center
|
||||
overflow-hidden
|
||||
group
|
||||
"
|
||||
:class="{
|
||||
rounded: size === 'form',
|
||||
'w-20 h-20 rounded-full': size !== 'small' && size !== 'form',
|
||||
@ -19,6 +28,7 @@
|
||||
items-center
|
||||
justify-center
|
||||
text-gray-400
|
||||
dark:text-gray-600
|
||||
font-semibold
|
||||
w-full
|
||||
text-4xl
|
||||
@ -29,7 +39,7 @@
|
||||
</div>
|
||||
<svg
|
||||
v-else
|
||||
class="w-6 h-6 text-gray-300"
|
||||
class="w-6 h-6 text-gray-300 dark:text-gray-600"
|
||||
viewBox="0 0 24 21"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
@ -45,10 +55,13 @@
|
||||
:class="[!isReadOnly ? 'group-hover:flex' : '']"
|
||||
style="background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(2px)"
|
||||
>
|
||||
<button class="bg-gray-100 p-0.5 rounded mb-1" @click="handleClick">
|
||||
<button
|
||||
class="bg-gray-100 dark:bg-gray-800 p-0.5 rounded mb-1"
|
||||
@click="handleClick"
|
||||
>
|
||||
<FeatherIcon
|
||||
:name="shouldClear ? 'x' : 'upload'"
|
||||
class="w-4 h-4 text-gray-600"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -7,7 +7,10 @@
|
||||
<label
|
||||
for="attachment"
|
||||
class="block whitespace-nowrap overflow-auto no-scrollbar"
|
||||
:class="[inputClasses, !value ? 'text-gray-600' : 'cursor-default']"
|
||||
:class="[
|
||||
inputClasses,
|
||||
!value ? 'text-gray-600 dark:text-gray-400' : 'cursor-default',
|
||||
]"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<input
|
||||
@ -24,12 +27,18 @@
|
||||
<div class="me-2 flex gap-1">
|
||||
<!-- Upload Button -->
|
||||
<button v-if="!value" class="p-0.5 rounded" @click="upload">
|
||||
<FeatherIcon name="upload" class="h-4 w-4 text-gray-600" />
|
||||
<FeatherIcon
|
||||
name="upload"
|
||||
class="h-4 w-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Download Button -->
|
||||
<button v-if="value" class="p-0.5 rounded" @click="download">
|
||||
<FeatherIcon name="download" class="h-4 w-4 text-gray-600" />
|
||||
<FeatherIcon
|
||||
name="download"
|
||||
class="h-4 w-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Clear Button -->
|
||||
@ -38,7 +47,10 @@
|
||||
class="p-0.5 rounded"
|
||||
@click="clear"
|
||||
>
|
||||
<FeatherIcon name="x" class="h-4 w-4 text-gray-600" />
|
||||
<FeatherIcon
|
||||
name="x"
|
||||
class="h-4 w-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,7 +47,9 @@
|
||||
<path
|
||||
d="M1 2.636L2.636 1l1.637 1.636M1 7.364L2.636 9l1.637-1.636"
|
||||
class="stroke-current"
|
||||
:class="showMandatory ? 'text-red-400' : 'text-gray-400'"
|
||||
:class="
|
||||
showMandatory ? 'text-red-400 dark:text-red-600' : 'text-gray-400'
|
||||
"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
@ -70,7 +72,7 @@
|
||||
<template #target>
|
||||
<feather-icon
|
||||
name="chevron-right"
|
||||
class="w-4 h-4 text-gray-600"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
|
@ -8,7 +8,9 @@
|
||||
rounded
|
||||
px-2
|
||||
bg-gray-50
|
||||
dark:bg-gray-890
|
||||
focus-within:bg-gray-100
|
||||
dark:focus-within:bg-gray-900
|
||||
"
|
||||
>
|
||||
<input
|
||||
@ -20,7 +22,7 @@
|
||||
/>
|
||||
<feather-icon
|
||||
name="maximize"
|
||||
class="w-3 h-3 text-gray-600 cursor-text"
|
||||
class="w-3 h-3 text-gray-600 dark:text-gray-400 cursor-text"
|
||||
@click="() => ($refs.scanner as HTMLInputElement).focus()"
|
||||
/>
|
||||
</div>
|
||||
|
@ -80,7 +80,7 @@ export default defineComponent({
|
||||
return 'text';
|
||||
},
|
||||
labelClasses(): string {
|
||||
return 'text-gray-600 text-sm mb-1';
|
||||
return 'text-gray-600 dark:text-gray-500 text-sm mb-1';
|
||||
},
|
||||
inputClasses(): string[] {
|
||||
/**
|
||||
@ -115,10 +115,10 @@ export default defineComponent({
|
||||
},
|
||||
inputReadOnlyClasses(): string {
|
||||
if (this.isReadOnly) {
|
||||
return 'text-gray-800 cursor-default';
|
||||
return 'text-gray-800 dark:text-gray-300 cursor-default';
|
||||
}
|
||||
|
||||
return 'text-gray-900';
|
||||
return 'text-gray-900 dark:text-gray-100';
|
||||
},
|
||||
containerClasses(): string[] {
|
||||
/**
|
||||
@ -136,7 +136,7 @@ export default defineComponent({
|
||||
},
|
||||
containerReadOnlyClasses(): string {
|
||||
if (!this.isReadOnly) {
|
||||
return 'focus-within:bg-gray-100';
|
||||
return 'focus-within:bg-gray-100 dark:focus-within:bg-gray-850';
|
||||
}
|
||||
|
||||
return '';
|
||||
@ -146,10 +146,10 @@ export default defineComponent({
|
||||
return '';
|
||||
}
|
||||
|
||||
const border = 'border border-gray-200';
|
||||
let background = 'bg-gray-25';
|
||||
const border = 'border border-gray-200 dark:border-gray-800';
|
||||
let background = 'bg-gray-25 dark:bg-gray-875';
|
||||
if (this.isReadOnly) {
|
||||
background = 'bg-gray-50';
|
||||
background = 'bg-gray-50 dark:bg-gray-850';
|
||||
}
|
||||
|
||||
return border + ' ' + background;
|
||||
|
@ -52,8 +52,9 @@
|
||||
y="0.5"
|
||||
width="13"
|
||||
height="13"
|
||||
rx="3.5"
|
||||
:stroke="offBorderColor"
|
||||
rx="3"
|
||||
:stroke="color"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@ -94,8 +95,7 @@ export default defineComponent({
|
||||
emits: ['focus'],
|
||||
data() {
|
||||
return {
|
||||
offBorderColor: 'rgba(17, 43, 66, 0.201322)',
|
||||
offColor: '#FFFFFF',
|
||||
offColor: '#0000',
|
||||
color: '#A1ABB4',
|
||||
};
|
||||
},
|
||||
|
@ -19,7 +19,7 @@
|
||||
<span v-if="value">
|
||||
{{ selectedColorLabel }}
|
||||
</span>
|
||||
<span v-else class="text-gray-400">
|
||||
<span v-else class="text-gray-400 dark:text-gray-600">
|
||||
{{ inputPlaceholder }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -27,7 +27,7 @@
|
||||
<p
|
||||
v-if="!isEmpty"
|
||||
:class="[baseInputClasses]"
|
||||
class="overflow-auto no-scrollbar whitespace-nowrap"
|
||||
class="overflow-auto no-scrollbar whitespace-nowrap dark:text-gray-100"
|
||||
>
|
||||
{{ formattedValue }}
|
||||
</p>
|
||||
@ -39,7 +39,9 @@
|
||||
<FeatherIcon
|
||||
name="calendar"
|
||||
class="w-4 h-4"
|
||||
:class="showMandatory ? 'text-red-600' : 'text-gray-600'"
|
||||
:class="
|
||||
showMandatory ? 'text-red-600' : 'text-gray-600 dark:text-gray-400'
|
||||
"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -84,10 +86,10 @@ export default defineComponent({
|
||||
return '';
|
||||
}
|
||||
|
||||
const border = 'border border-gray-200';
|
||||
let background = 'bg-gray-25';
|
||||
const border = 'border border-gray-200 dark:border-gray-800';
|
||||
let background = 'bg-gray-25 dark:bg-gray-875';
|
||||
if (this.isReadOnly) {
|
||||
background = 'bg-gray-50';
|
||||
background = 'bg-gray-50 dark:bg-gray-850';
|
||||
}
|
||||
|
||||
if (this.showInput) {
|
||||
|
@ -1,18 +1,37 @@
|
||||
<template>
|
||||
<div class="flex items-center bg-gray-50 rounded-md text-sm p-1 border">
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
bg-gray-50
|
||||
dark:bg-gray-890
|
||||
rounded-md
|
||||
text-sm
|
||||
p-1
|
||||
border
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="rate-container"
|
||||
:class="disabled ? 'bg-gray-100' : 'bg-gray-25'"
|
||||
:class="
|
||||
disabled
|
||||
? 'bg-gray-100 dark:bg-gray-850'
|
||||
: 'bg-gray-25 dark:bg-gray-890'
|
||||
"
|
||||
>
|
||||
<input v-model="fromValue" type="number" :disabled="disabled" min="0" />
|
||||
<p>{{ left }}</p>
|
||||
</div>
|
||||
|
||||
<p class="mx-1 text-gray-600">=</p>
|
||||
<p class="mx-1 text-gray-600 dark:text-gray-400">=</p>
|
||||
|
||||
<div
|
||||
class="rate-container"
|
||||
:class="disabled ? 'bg-gray-100' : 'bg-gray-25'"
|
||||
:class="
|
||||
disabled
|
||||
? 'bg-gray-100 dark:bg-gray-850'
|
||||
: 'bg-gray-25 dark:bg-gray-890'
|
||||
"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@ -26,10 +45,22 @@
|
||||
|
||||
<button
|
||||
v-if="!disabled"
|
||||
class="bg-green100 px-2 ms-1 -me-0.5 h-full border-s"
|
||||
class="
|
||||
bg-green-100
|
||||
dark:bg-green-600
|
||||
px-2
|
||||
ms-1
|
||||
-me-0.5
|
||||
h-full
|
||||
border-s
|
||||
dark:border-gray-800
|
||||
"
|
||||
@click="swap"
|
||||
>
|
||||
<feather-icon name="refresh-cw" class="w-3 h-3 text-gray-600" />
|
||||
<feather-icon
|
||||
name="refresh-cw"
|
||||
class="w-3 h-3 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -97,7 +97,7 @@ export default {
|
||||
{
|
||||
component: markRaw({
|
||||
template:
|
||||
'<span class="text-gray-600">{{ t`No results found` }}</span>',
|
||||
'<span class="text-gray-600 dark:text-gray-400">{{ t`No results found` }}</span>',
|
||||
}),
|
||||
action: () => {},
|
||||
actionOnly: true,
|
||||
|
@ -14,6 +14,7 @@
|
||||
focus:outline-none
|
||||
w-11/12
|
||||
cursor-pointer
|
||||
custom-scroll custom-scroll-thumb2
|
||||
"
|
||||
:class="{
|
||||
'pointer-events-none': isReadOnly,
|
||||
@ -28,6 +29,7 @@
|
||||
value=""
|
||||
disabled
|
||||
selected
|
||||
class="text-black dark:text-gray-200 dark:bg-gray-850"
|
||||
>
|
||||
{{ inputPlaceholder }}
|
||||
</option>
|
||||
@ -35,7 +37,7 @@
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
class="text-black"
|
||||
class="text-black dark:text-gray-200 dark:bg-gray-850"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
@ -50,7 +52,11 @@
|
||||
<path
|
||||
d="M1 2.636L2.636 1l1.637 1.636M1 7.364L2.636 9l1.637-1.636"
|
||||
class="stroke-current"
|
||||
:class="showMandatory ? 'text-red-400' : 'text-gray-400'"
|
||||
:class="
|
||||
showMandatory
|
||||
? 'text-red-400 dark:text-red-600'
|
||||
: 'text-gray-400 dark:text-gray-600'
|
||||
"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke-linecap="round"
|
||||
|
@ -1,14 +1,23 @@
|
||||
<template>
|
||||
<div v-if="tableFields?.length">
|
||||
<div v-if="showLabel" class="text-gray-600 text-sm mb-1">
|
||||
<div v-if="showLabel" class="text-gray-600 dark:text-gray-400 text-sm mb-1">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
|
||||
<div :class="border ? 'border rounded-md' : ''">
|
||||
<div :class="border ? 'border dark:border-gray-800 rounded-md' : ''">
|
||||
<!-- Title Row -->
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border-b px-2 text-gray-600 w-full flex items-center"
|
||||
class="
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
px-2
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
w-full
|
||||
flex
|
||||
items-center
|
||||
"
|
||||
>
|
||||
<div class="flex items-center ps-2">#</div>
|
||||
<div
|
||||
@ -29,14 +38,14 @@
|
||||
<!-- Data Rows -->
|
||||
<div
|
||||
v-if="value"
|
||||
class="overflow-auto custom-scroll"
|
||||
class="overflow-auto custom-scroll custom-scroll-thumb1"
|
||||
:style="{ 'max-height': maxHeight }"
|
||||
>
|
||||
<TableRow
|
||||
v-for="(row, idx) of value"
|
||||
ref="table-row"
|
||||
:key="row.name"
|
||||
:class="idx < value.length - 1 ? 'border-b' : ''"
|
||||
:class="idx < value.length - 1 ? 'border-b dark:border-gray-800' : ''"
|
||||
v-bind="{ row, tableFields, size, ratio, isNumeric }"
|
||||
:read-only="isReadOnly"
|
||||
:can-edit-row="canEditRow"
|
||||
@ -58,7 +67,7 @@
|
||||
flex
|
||||
items-center
|
||||
"
|
||||
:class="value.length > 0 ? 'border-t' : ''"
|
||||
:class="value.length > 0 ? 'border-t dark:border-gray-800' : ''"
|
||||
@click="addRow"
|
||||
>
|
||||
<div class="flex items-center ps-1">
|
||||
|
@ -2,10 +2,10 @@
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="w-full px-2 group flex items-center justify-center h-row-mid"
|
||||
:class="readOnly ? '' : 'hover:bg-gray-25'"
|
||||
:class="readOnly ? '' : 'hover:bg-gray-25 dark:hover:bg-gray-900'"
|
||||
>
|
||||
<!-- Index or Remove button -->
|
||||
<div class="flex items-center ps-2 text-gray-600">
|
||||
<div class="flex items-center ps-2 text-gray-600 dark:text-gray-400">
|
||||
<span class="hidden" :class="{ 'group-hover:inline-block': !readOnly }">
|
||||
<feather-icon
|
||||
name="x"
|
||||
@ -35,7 +35,10 @@
|
||||
:background="false"
|
||||
@click="openRowQuickEdit"
|
||||
>
|
||||
<feather-icon name="edit" class="w-4 h-4 text-gray-600" />
|
||||
<feather-icon
|
||||
name="edit"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<!-- Error Display -->
|
||||
|
@ -7,7 +7,7 @@
|
||||
<textarea
|
||||
ref="input"
|
||||
:rows="rows"
|
||||
:class="['resize-none', inputClasses, containerClasses]"
|
||||
:class="['resize-none bg-transparent', inputClasses, containerClasses]"
|
||||
:value="value"
|
||||
:placeholder="inputPlaceholder"
|
||||
style="vertical-align: top"
|
||||
|
@ -7,9 +7,12 @@
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
border
|
||||
dark:border-gray-800
|
||||
rounded-lg
|
||||
text-gray-900
|
||||
dark:text-gray-25
|
||||
p-4
|
||||
shadow-2xl
|
||||
w-dialog
|
||||
|
@ -15,14 +15,34 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="bg-white rounded w-full min-w-40 overflow-hidden">
|
||||
<div class="p-1 max-h-64 overflow-auto custom-scroll text-sm">
|
||||
<div v-if="isLoading" class="p-2 text-gray-600 italic">
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850 dark:text-white
|
||||
rounded
|
||||
w-full
|
||||
min-w-40
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
p-1
|
||||
max-h-64
|
||||
overflow-auto
|
||||
custom-scroll custom-scroll-thumb2
|
||||
text-sm
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="p-2 text-gray-600 dark:text-gray-400 italic"
|
||||
>
|
||||
{{ t`Loading...` }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="dropdownItems.length === 0"
|
||||
class="p-2 text-gray-600 italic"
|
||||
class="p-2 text-gray-600 dark:text-gray-400 italic"
|
||||
>
|
||||
{{ getEmptyMessage() }}
|
||||
</div>
|
||||
@ -41,6 +61,7 @@
|
||||
text-xs
|
||||
uppercase
|
||||
text-gray-700
|
||||
dark:text-gray-400
|
||||
font-semibold
|
||||
tracking-wider
|
||||
"
|
||||
@ -58,7 +79,11 @@
|
||||
cursor-pointer
|
||||
truncate
|
||||
"
|
||||
:class="index === highlightedIndex ? 'bg-gray-100' : ''"
|
||||
:class="
|
||||
index === highlightedIndex
|
||||
? 'bg-gray-100 dark:bg-gray-875'
|
||||
: ''
|
||||
"
|
||||
@mouseenter="highlightedIndex = index"
|
||||
@mousedown.prevent
|
||||
@click="selectItem(d)"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<!-- Export Wizard Header -->
|
||||
<FormHeader :form-title="label" :form-sub-title="t`Export Wizard`" />
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<!-- Export Config -->
|
||||
<div class="grid grid-cols-3 p-4 gap-4">
|
||||
@ -31,23 +31,23 @@
|
||||
@change="(value: number) => (limit = value)"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<!-- Fields Selection -->
|
||||
<div class="max-h-80 overflow-auto custom-scroll">
|
||||
<div class="max-h-80 overflow-auto custom-scroll custom-scroll-thumb2">
|
||||
<!-- Main Fields -->
|
||||
<div class="p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-800">
|
||||
<h2 class="text-sm font-semibold text-gray-800 dark:text-gray-300">
|
||||
{{ fyo.schemaMap[schemaName]?.label ?? schemaName }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-3 border rounded mt-1">
|
||||
<div class="grid grid-cols-3 border dark:border-gray-800 rounded mt-1">
|
||||
<Check
|
||||
v-for="ef of fields"
|
||||
:key="ef.fieldname"
|
||||
:label-class="
|
||||
ef.fieldtype === 'Table'
|
||||
? 'text-sm text-gray-600 font-semibold'
|
||||
: 'text-sm text-gray-600'
|
||||
? 'text-sm text-gray-600 dark:text-gray-300 font-semibold'
|
||||
: 'text-sm text-gray-600 dark:text-gray-400'
|
||||
"
|
||||
:df="getField(ef)"
|
||||
:show-label="true"
|
||||
@ -59,13 +59,14 @@
|
||||
|
||||
<!-- Table Fields -->
|
||||
<div v-for="efs of filteredTableFields" :key="efs.fieldname" class="p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-800">
|
||||
<h2 class="text-sm font-semibold text-gray-800 dark:text-gray-300">
|
||||
{{ fyo.schemaMap[efs.target]?.label ?? schemaName }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-3 border rounded mt-1">
|
||||
<div class="grid grid-cols-3 border dark:border-gray-800 rounded mt-1">
|
||||
<Check
|
||||
v-for="ef of efs.fields"
|
||||
:key="ef.fieldname"
|
||||
label-class="text-gray-600 dark:text-gray-300"
|
||||
:df="getField(ef)"
|
||||
:show-label="true"
|
||||
:value="ef.export"
|
||||
@ -76,9 +77,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Export Button -->
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<div class="p-4 flex justify-between items-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`${numSelected} fields selected` }}
|
||||
</p>
|
||||
<Button type="primary" @click="exportData">{{ t`Export` }}</Button>
|
||||
|
@ -7,7 +7,11 @@
|
||||
<template #target="{ togglePopover }">
|
||||
<Button :icon="true" @click="togglePopover()">
|
||||
<span class="flex items-center">
|
||||
<Icon name="filter" size="12" class="stroke-current text-gray-700" />
|
||||
<Icon
|
||||
name="filter"
|
||||
size="12"
|
||||
class="stroke-current text-gray-700 dark:text-gray-400"
|
||||
/>
|
||||
<span class="ms-1">
|
||||
<template v-if="activeFilterCount > 0">
|
||||
{{ filterAppliedMessage }}
|
||||
@ -38,7 +42,9 @@
|
||||
items-center
|
||||
justify-center
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-300
|
||||
rounded-md
|
||||
group
|
||||
"
|
||||
@ -100,7 +106,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-base text-gray-600">{{
|
||||
<span class="text-base text-gray-600 dark:text-gray-500">{{
|
||||
t`No filters selected`
|
||||
}}</span>
|
||||
</template>
|
||||
@ -109,12 +115,15 @@
|
||||
class="
|
||||
text-base
|
||||
border-t
|
||||
dark:border-gray-800
|
||||
p-2
|
||||
flex
|
||||
items-center
|
||||
text-gray-600
|
||||
dark:text-gray-500
|
||||
cursor-pointer
|
||||
hover:bg-gray-100
|
||||
dark:hover:bg-gray-875
|
||||
"
|
||||
@click="addNewFilter"
|
||||
>
|
||||
|
@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<div class="flex bg-gray-25 overflow-x-auto">
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
bg-gray-25
|
||||
dark:bg-gray-875
|
||||
overflow-x-auto
|
||||
custom-scroll custom-scroll-thumb1
|
||||
"
|
||||
>
|
||||
<div class="flex flex-1 flex-col">
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<PageHeader
|
||||
@ -16,11 +24,18 @@
|
||||
|
||||
<!-- Common Form -->
|
||||
<div
|
||||
class="flex flex-col self-center h-full overflow-auto bg-white"
|
||||
class="
|
||||
flex flex-col
|
||||
self-center
|
||||
h-full
|
||||
overflow-auto
|
||||
bg-white
|
||||
dark:bg-gray-890
|
||||
"
|
||||
:class="
|
||||
useFullWidth
|
||||
? 'w-full border-t'
|
||||
: 'w-form border rounded-lg shadow-lg mb-4 mx-4'
|
||||
? 'w-full border-t dark:border-gray-800'
|
||||
: 'w-form border dark:border-gray-800 rounded-lg shadow-lg mb-4 mx-4'
|
||||
"
|
||||
>
|
||||
<slot name="body" />
|
||||
|
@ -11,9 +11,9 @@
|
||||
flex-shrink-0
|
||||
"
|
||||
>
|
||||
<h1 v-if="formTitle">{{ formTitle }}</h1>
|
||||
<h1 v-if="formTitle" class="dark:text-gray-25">{{ formTitle }}</h1>
|
||||
<slot />
|
||||
<p v-if="formSubTitle" class="text-gray-600">
|
||||
<p v-if="formSubTitle" class="text-gray-600 dark:text-gray-400">
|
||||
{{ formSubTitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
ref="hr"
|
||||
class="h-full bg-gray-300 transition-opacity hover:opacity-100"
|
||||
class="
|
||||
h-full
|
||||
bg-gray-300
|
||||
dark:bg-gray-700
|
||||
transition-opacity
|
||||
hover:opacity-100
|
||||
"
|
||||
:class="resizing ? 'opacity-100' : 'opacity-0'"
|
||||
style="width: 3px; cursor: col-resize; margin-left: -3px"
|
||||
@mousedown="onMouseDown"
|
||||
@ -13,6 +19,7 @@
|
||||
px-1
|
||||
py-0.5
|
||||
border
|
||||
dark:border-gray-800
|
||||
rounded-md
|
||||
shadow
|
||||
text-sm text-center
|
||||
|
@ -4,6 +4,7 @@
|
||||
:is="iconComponent"
|
||||
:class="iconClasses"
|
||||
:active="active"
|
||||
:darkMode="darkMode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -26,6 +27,7 @@ export default {
|
||||
props: {
|
||||
name: { type: String, required: true },
|
||||
active: { type: Boolean, default: false },
|
||||
darkMode: { type: Boolean, default: false },
|
||||
size: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#FFF" d="M-589-585H611v907H-589z" />
|
||||
<path
|
||||
d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12C23.98 5.38 18.62.02 12 0z"
|
||||
fill="#92D336"
|
||||
|
@ -1,15 +1,33 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { uicolors } from 'src/utils/colors';
|
||||
|
||||
export default {
|
||||
name: 'IconBase',
|
||||
props: ['active'],
|
||||
props: {
|
||||
active: Boolean,
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
lightColor() {
|
||||
return this.active ? uicolors.gray['600'] : uicolors.gray['400'];
|
||||
lightColor(): string {
|
||||
const activeGray = this.darkMode
|
||||
? uicolors.gray['500']
|
||||
: uicolors.gray['600'];
|
||||
const passiveGray = this.darkMode
|
||||
? uicolors.gray['700']
|
||||
: uicolors.gray['400'];
|
||||
return this.active ? activeGray : passiveGray;
|
||||
},
|
||||
darkColor() {
|
||||
return this.active ? uicolors.gray['800'] : uicolors.gray['600'];
|
||||
darkColor(): string {
|
||||
const activeGray = this.darkMode
|
||||
? uicolors.gray['200']
|
||||
: uicolors.gray['800'];
|
||||
const passiveGray = this.darkMode
|
||||
? uicolors.gray['500']
|
||||
: uicolors.gray['600'];
|
||||
return this.active ? activeGray : passiveGray;
|
||||
},
|
||||
bgColor(): string {
|
||||
return this.darkMode ? uicolors.gray['900'] : uicolors.gray['100'];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -8,7 +8,9 @@
|
||||
<div
|
||||
class="
|
||||
border
|
||||
dark:border-gray-800
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
shadow-lg
|
||||
px-3
|
||||
py-3
|
||||
@ -16,11 +18,15 @@
|
||||
w-96
|
||||
z-10
|
||||
bg-white
|
||||
dark:bg-gray-900
|
||||
rounded-lg
|
||||
"
|
||||
>
|
||||
<!-- Message -->
|
||||
<p v-if="message?.length" class="text-base text-gray-600 pb-2">
|
||||
<p
|
||||
v-if="message?.length"
|
||||
class="text-base text-gray-600 dark:text-gray-400 pb-2"
|
||||
>
|
||||
{{ message }}
|
||||
</p>
|
||||
|
||||
@ -29,12 +35,16 @@
|
||||
<!-- Loading Bar BG -->
|
||||
<div
|
||||
class="w-full h-3 me-2 rounded"
|
||||
:class="percent >= 0 ? 'bg-gray-200' : 'bg-gray-300'"
|
||||
:class="
|
||||
percent >= 0
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: 'bg-gray-300 dark:bg-gray-700'
|
||||
"
|
||||
>
|
||||
<!-- Loading Bar -->
|
||||
<div
|
||||
v-if="percent >= 0"
|
||||
class="h-3 rounded bg-gray-800"
|
||||
class="h-3 rounded bg-gray-800 dark:bg-gray-200"
|
||||
:style="{ width: `${percent * 100}%` }"
|
||||
></div>
|
||||
</div>
|
||||
@ -48,8 +58,10 @@
|
||||
h-4
|
||||
ms-auto
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
cursor-pointer
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-200
|
||||
"
|
||||
@click="closeToast"
|
||||
/>
|
||||
|
@ -6,7 +6,16 @@
|
||||
@click="$emit('closemodal')"
|
||||
>
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-2xl border overflow-hidden inner"
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
rounded-lg
|
||||
shadow-2xl
|
||||
border
|
||||
dark:border-gray-800
|
||||
overflow-hidden
|
||||
inner
|
||||
"
|
||||
v-bind="$attrs"
|
||||
@click.stop
|
||||
>
|
||||
|
@ -17,9 +17,12 @@
|
||||
w-full
|
||||
font-medium
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
bg-gray-25
|
||||
dark:bg-gray-850
|
||||
rounded-lg
|
||||
border border-gray-200
|
||||
dark:border-gray-800
|
||||
appearance-none
|
||||
focus:outline-none focus:ring-0
|
||||
peer
|
||||
|
@ -17,9 +17,12 @@
|
||||
w-full
|
||||
font-medium
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
bg-gray-25
|
||||
dark:bg-gray-850
|
||||
rounded-lg
|
||||
border border-gray-200
|
||||
dark:border-gray-800
|
||||
appearance-none
|
||||
focus:outline-none focus:ring-0
|
||||
peer
|
||||
|
@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border flex items-center mt-4 px-2 rounded-t-md text-gray-600 w-full"
|
||||
class="
|
||||
border
|
||||
dark:border-gray-800
|
||||
flex
|
||||
items-center
|
||||
mt-4
|
||||
px-2
|
||||
rounded-t-md
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
w-full
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="df in tableFields"
|
||||
@ -26,10 +37,12 @@
|
||||
:border="true"
|
||||
class="
|
||||
border-b border-l border-r
|
||||
dark:border-gray-800
|
||||
flex
|
||||
group
|
||||
h-row-mid
|
||||
hover:bg-gray-25
|
||||
dark:bg-gray-890
|
||||
items-center
|
||||
justify-center
|
||||
px-2
|
||||
|
@ -5,17 +5,27 @@
|
||||
@click="isExapanded = !isExapanded"
|
||||
/>
|
||||
|
||||
<Link
|
||||
:df="{
|
||||
fieldname: 'item',
|
||||
fieldtype: 'Data',
|
||||
label: 'item',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.item"
|
||||
:read-only="true"
|
||||
/>
|
||||
<div class="relative">
|
||||
<Link
|
||||
class="pt-2"
|
||||
:df="{
|
||||
fieldname: 'item',
|
||||
fieldtype: 'Data',
|
||||
label: 'item',
|
||||
}"
|
||||
size="small"
|
||||
:border="false"
|
||||
:value="row.item"
|
||||
:read-only="true"
|
||||
/>
|
||||
<p
|
||||
v-if="row.isFreeItem"
|
||||
class="absolute flex top-0 font-medium text-xs ml-2 text-green-800"
|
||||
style="font-size: 0.6rem"
|
||||
>
|
||||
{{ row.pricingRule }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Int
|
||||
:df="{
|
||||
@ -69,7 +79,7 @@
|
||||
<feather-icon
|
||||
name="trash"
|
||||
class="w-4 text-xl text-red-500"
|
||||
@click="$emit('removeItem', row.idx)"
|
||||
@click="removeAddedItem(row)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -88,8 +98,8 @@
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:value="row.quantity"
|
||||
@change="(value:number) => (row.quantity = value)"
|
||||
:read-only="false"
|
||||
@change="(value:number) => setQuantity((row.quantity = value))"
|
||||
:read-only="isReadOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -106,13 +116,15 @@
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.transferUnit"
|
||||
@change="(value:string) => setTransferUnit((row.transferUnit = value))"
|
||||
@change="(value:string) => row.set('transferUnit', value)"
|
||||
:read-only="isReadOnly"
|
||||
/>
|
||||
<feather-icon
|
||||
v-if="isUOMConversionEnabled"
|
||||
name="refresh-ccw"
|
||||
class="w-3.5 ml-2 mt-4 text-blue-500"
|
||||
@click="row.transferUnit = row.unit"
|
||||
:read-only="isReadOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -128,8 +140,8 @@
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:value="row.transferQuantity"
|
||||
@change="(value:number) => setTransferQty((row.transferQuantity = value))"
|
||||
:read-only="false"
|
||||
@change="(value:string) => row.set('transferQuantity', value)"
|
||||
:read-only="isReadOnly"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -147,8 +159,8 @@
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.rate"
|
||||
:read-only="false"
|
||||
@change="(value:Money) => (row.rate = value)"
|
||||
:read-only="isReadOnly"
|
||||
@change="(value:Money) => setRate((row.rate = value))"
|
||||
/>
|
||||
<feather-icon
|
||||
name="refresh-ccw"
|
||||
@ -169,7 +181,7 @@
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.itemDiscountAmount"
|
||||
:read-only="row.itemDiscountPercent as number > 0"
|
||||
:read-only="row.itemDiscountPercent as number > 0 || isReadOnly"
|
||||
@change="(value:number) => setItemDiscount('amount', value)"
|
||||
/>
|
||||
</div>
|
||||
@ -186,7 +198,7 @@
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
:value="row.itemDiscountPercent"
|
||||
:read-only="!row.itemDiscountAmount?.isZero()"
|
||||
:read-only="!row.itemDiscountAmount?.isZero() || isReadOnly"
|
||||
@change="(value:number) => setItemDiscount('percent', value)"
|
||||
/>
|
||||
</div>
|
||||
@ -204,7 +216,7 @@
|
||||
target: 'Batch',
|
||||
label: t`Batch`,
|
||||
}"
|
||||
value=""
|
||||
:value="row.batch"
|
||||
:border="true"
|
||||
:show-label="true"
|
||||
:read-only="false"
|
||||
@ -264,6 +276,7 @@ import { Money } from 'pesa';
|
||||
import { DiscountType } from './types';
|
||||
import { t } from 'fyo';
|
||||
import { validateSerialNumberCount } from 'src/utils/pos';
|
||||
import { ApplicablePricingRules } from 'models/baseModels/Invoice/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SelectedItemRow',
|
||||
@ -271,7 +284,7 @@ export default defineComponent({
|
||||
props: {
|
||||
row: { type: SalesInvoiceItem, required: true },
|
||||
},
|
||||
emits: ['removeItem', 'runSinvFormulas', 'setItemSerialNumbers'],
|
||||
emits: ['runSinvFormulas', 'setItemSerialNumbers', 'addItem'],
|
||||
setup() {
|
||||
return {
|
||||
isDiscountingEnabled: inject('isDiscountingEnabled') as boolean,
|
||||
@ -296,6 +309,9 @@ export default defineComponent({
|
||||
hasSerialNumber(): boolean {
|
||||
return !!(this.row.links?.item && this.row.links?.item.hasSerialNumber);
|
||||
},
|
||||
isReadOnly() {
|
||||
return this.row.isFreeItem;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getAvailableQtyInBatch(): Promise<number> {
|
||||
@ -314,7 +330,7 @@ export default defineComponent({
|
||||
);
|
||||
},
|
||||
async setBatch(batch: string) {
|
||||
this.row.batch = batch;
|
||||
this.row.set('batch', batch);
|
||||
this.availableQtyInBatch = await this.getAvailableQtyInBatch();
|
||||
},
|
||||
setSerialNumber(serialNumber: string) {
|
||||
@ -331,23 +347,45 @@ export default defineComponent({
|
||||
},
|
||||
setItemDiscount(type: DiscountType, value: Money | number) {
|
||||
if (type === 'percent') {
|
||||
this.row.setItemDiscountAmount = false;
|
||||
this.row.itemDiscountPercent = value as number;
|
||||
this.$emit('runSinvFormulas');
|
||||
this.row.set('setItemDiscountAmount', false);
|
||||
this.row.set('itemDiscountPercent', value as number);
|
||||
return;
|
||||
}
|
||||
this.row.setItemDiscountAmount = true;
|
||||
this.row.itemDiscountAmount = value as Money;
|
||||
this.row.set('setItemDiscountAmount', true);
|
||||
this.row.set('itemDiscountAmount', value as Money);
|
||||
},
|
||||
setRate(rate: Money) {
|
||||
this.row.setRate = rate;
|
||||
this.$emit('runSinvFormulas');
|
||||
},
|
||||
setTransferUnit(unit: string) {
|
||||
this.row.setTransferUnit = unit;
|
||||
this.row._applyFormula('transferUnit');
|
||||
async setQuantity(quantity: number) {
|
||||
this.row.set('quantity', quantity);
|
||||
|
||||
if (!this.row.isFreeItem) {
|
||||
await this.updatePricingRuleItem();
|
||||
this.$emit('runSinvFormulas');
|
||||
}
|
||||
},
|
||||
setTransferQty(quantity: number) {
|
||||
this.row.transferQuantity = quantity;
|
||||
this.row._applyFormula('transferQuantity');
|
||||
this.$emit('runSinvFormulas');
|
||||
async removeAddedItem(row: SalesInvoiceItem) {
|
||||
this.row.parentdoc?.remove('items', row?.idx as number);
|
||||
|
||||
if (!row.isFreeItem) {
|
||||
await this.updatePricingRuleItem();
|
||||
}
|
||||
},
|
||||
async updatePricingRuleItem() {
|
||||
const pricingRule =
|
||||
(await this.row.parentdoc?.getPricingRule()) as ApplicablePricingRules[];
|
||||
|
||||
let appliedPricingRuleCount: number;
|
||||
setTimeout(async () => {
|
||||
if (appliedPricingRuleCount !== pricingRule?.length) {
|
||||
appliedPricingRuleCount = pricingRule?.length;
|
||||
await this.row.parentdoc?.appendPricingRuleDetail(pricingRule);
|
||||
|
||||
await this.row.parentdoc?.applyProductDiscount();
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,7 +1,18 @@
|
||||
<template>
|
||||
<Row
|
||||
:ratio="ratio"
|
||||
class="border rounded-t px-2 text-gray-600 w-full flex items-center mt-4"
|
||||
class="
|
||||
border
|
||||
dark:border-gray-800
|
||||
rounded-t
|
||||
px-2
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
w-full
|
||||
flex
|
||||
items-center
|
||||
mt-4
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="tableFields"
|
||||
@ -25,6 +36,7 @@
|
||||
:ratio="ratio"
|
||||
class="
|
||||
border
|
||||
dark:border-gray-800
|
||||
w-full
|
||||
px-2
|
||||
py-2
|
||||
@ -33,11 +45,11 @@
|
||||
items-center
|
||||
justify-center
|
||||
hover:bg-gray-25
|
||||
dark:bg-gray-890
|
||||
"
|
||||
>
|
||||
<SelectedItemRow
|
||||
:row="(row as SalesInvoiceItem)"
|
||||
@remove-item="removeItem"
|
||||
@run-sinv-formulas="runSinvFormulas"
|
||||
/>
|
||||
</Row>
|
||||
@ -138,9 +150,6 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
removeItem(idx: number) {
|
||||
this.sinvDoc.remove('items', idx);
|
||||
},
|
||||
async runSinvFormulas() {
|
||||
await this.sinvDoc.runFormulas();
|
||||
},
|
||||
|
@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
class="px-4 flex justify-between items-center h-row-largest flex-shrink-0"
|
||||
class="
|
||||
px-4
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
h-row-largest
|
||||
flex-shrink-0
|
||||
dark:bg-gray-875
|
||||
"
|
||||
:class="[
|
||||
border ? 'border-b' : '',
|
||||
border ? 'border-b dark:border-gray-800' : '',
|
||||
platform !== 'Windows' ? 'window-drag' : '',
|
||||
]"
|
||||
>
|
||||
@ -22,7 +30,13 @@
|
||||
<PageHeaderNavGroup />
|
||||
<h1
|
||||
v-if="title"
|
||||
class="text-xl font-semibold select-none whitespace-nowrap"
|
||||
class="
|
||||
text-xl
|
||||
font-semibold
|
||||
select-none
|
||||
whitespace-nowrap
|
||||
dark:text-white
|
||||
"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
|
@ -4,9 +4,15 @@
|
||||
<!-- Back Button -->
|
||||
<a
|
||||
ref="backlink"
|
||||
class="nav-link border-l border-r border-white"
|
||||
class="
|
||||
nav-link
|
||||
border-l border-r border-white
|
||||
dark:border-gray-850 dark:bg-gray-900
|
||||
"
|
||||
:class="
|
||||
historyState.back ? 'text-gray-700 cursor-pointer' : 'text-gray-400'
|
||||
historyState.back
|
||||
? 'text-gray-700 dark:text-gray-300 cursor-pointer'
|
||||
: 'text-gray-400 dark:text-gray-700'
|
||||
"
|
||||
@click="$router.back()"
|
||||
>
|
||||
@ -14,9 +20,11 @@
|
||||
</a>
|
||||
<!-- Forward Button -->
|
||||
<a
|
||||
class="nav-link rounded-md rounded-l-none"
|
||||
class="nav-link rounded-md rounded-l-none dark:bg-gray-900"
|
||||
:class="
|
||||
historyState.forward ? 'text-gray-700 cursor-pointer' : 'text-gray-400'
|
||||
historyState.forward
|
||||
? 'text-gray-700 dark:text-gray-400 cursor-pointer'
|
||||
: 'text-gray-400 dark:text-gray-700'
|
||||
"
|
||||
@click="$router.forward()"
|
||||
>
|
||||
|
@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-3 text-gray-800 text-sm select-none items-center"
|
||||
class="
|
||||
grid grid-cols-3
|
||||
text-gray-800
|
||||
dark:text-gray-100
|
||||
text-sm
|
||||
select-none
|
||||
items-center
|
||||
"
|
||||
style="height: 50px"
|
||||
>
|
||||
<!-- Length Display -->
|
||||
@ -16,14 +23,23 @@
|
||||
name="chevron-left"
|
||||
class="w-4 h-4 rtl-rotate-180"
|
||||
:class="
|
||||
pageNo > 1 ? 'text-gray-600 cursor-pointer' : 'text-transparent'
|
||||
pageNo > 1
|
||||
? 'text-gray-600 dark:text-gray-500 cursor-pointer'
|
||||
: 'text-transparent'
|
||||
"
|
||||
@click="() => setPageNo(Math.max(1, pageNo - 1))"
|
||||
/>
|
||||
<div class="flex gap-1 bg-gray-100 rounded">
|
||||
<div class="flex gap-1 bg-gray-100 dark:bg-gray-890 rounded">
|
||||
<input
|
||||
type="number"
|
||||
class="w-7 text-end outline-none bg-transparent focus:text-gray-900"
|
||||
class="
|
||||
w-7
|
||||
text-end
|
||||
outline-none
|
||||
bg-transparent
|
||||
focus:text-gray-900
|
||||
dark:focus:text-gray-25
|
||||
"
|
||||
:value="pageNo"
|
||||
min="1"
|
||||
:max="maxPages"
|
||||
@ -40,7 +56,7 @@
|
||||
class="w-4 h-4 rtl-rotate-180"
|
||||
:class="
|
||||
pageNo < maxPages
|
||||
? 'text-gray-600 cursor-pointer'
|
||||
? 'text-gray-600 dark:text-gray-500 cursor-pointer'
|
||||
: 'text-transparent'
|
||||
"
|
||||
@click="() => setPageNo(Math.min(maxPages, pageNo + 1))"
|
||||
@ -50,14 +66,20 @@
|
||||
<!-- Count Selector -->
|
||||
<div
|
||||
v-if="filteredCounts.length"
|
||||
class="border border-gray-100 rounded flex justify-self-end"
|
||||
class="
|
||||
border border-gray-100
|
||||
dark:border-gray-800
|
||||
rounded
|
||||
flex
|
||||
justify-self-end
|
||||
"
|
||||
>
|
||||
<template v-for="c in filteredCounts" :key="c + '-count'">
|
||||
<button
|
||||
class="w-9"
|
||||
:class="
|
||||
count === c || (count === itemCount && c === -1)
|
||||
? 'bg-gray-100'
|
||||
? 'rounded bg-gray-100 dark:bg-gray-890'
|
||||
: ''
|
||||
"
|
||||
@click="setCount(c)"
|
||||
|
@ -14,8 +14,10 @@
|
||||
:class="popoverClass"
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
rounded-md
|
||||
border
|
||||
dark:border-gray-875
|
||||
shadow-lg
|
||||
popover-container
|
||||
relative
|
||||
|
@ -2,22 +2,28 @@
|
||||
<div style="min-width: 192px; max-width: 300px">
|
||||
<div
|
||||
class="p-2 flex justify-between"
|
||||
:class="values.length ? 'border-b' : ''"
|
||||
:class="values.length ? 'border-b dark:border-gray-800' : ''"
|
||||
>
|
||||
<p
|
||||
v-if="schema?.naming !== 'random' && !schema?.isChild"
|
||||
class="font-semibold text-base text-gray-900"
|
||||
class="font-semibold text-base text-gray-900 dark:text-gray-25"
|
||||
>
|
||||
{{ name }}
|
||||
</p>
|
||||
<p class="font-semibold text-base text-gray-600">
|
||||
<p class="font-semibold text-base text-gray-600 dark:text-gray-400">
|
||||
{{ schema?.label ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="values.length" class="flex gap-2 p-2 flex-wrap">
|
||||
<p v-for="v of values" :key="v.label" class="pill bg-gray-200">
|
||||
<span class="text-gray-600">{{ v.label }}</span>
|
||||
<span class="text-gray-800 ml-1.5">{{ v.value }}</span>
|
||||
<p
|
||||
v-for="v of values"
|
||||
:key="v.label"
|
||||
class="pill bg-gray-200 dark:bg-gray-800"
|
||||
>
|
||||
<span class="text-gray-600 dark:text-gray-500">{{ v.label }}</span>
|
||||
<span class="text-gray-800 dark:text-gray-300 ml-1.5">{{
|
||||
v.value
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,16 @@
|
||||
<!--Title Row -->
|
||||
<div
|
||||
ref="titlerow"
|
||||
class="w-full overflow-x-hidden flex items-center border-b px-4"
|
||||
class="
|
||||
w-full
|
||||
overflow-x-hidden
|
||||
flex
|
||||
items-center
|
||||
dark:text-gray-25
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
px-4
|
||||
"
|
||||
:style="{
|
||||
height: `${hconst}px`,
|
||||
paddingRight: 'calc(var(--w-scrollbar) + 1rem)',
|
||||
@ -43,8 +52,10 @@
|
||||
minWidth: `calc(var(--w-desk) - var(--w-scrollbar))`,
|
||||
}"
|
||||
:class="[
|
||||
r !== pageEnd - 1 ? 'border-b' : '',
|
||||
row.isGroup ? 'hover:bg-gray-50 cursor-pointer' : '',
|
||||
r !== pageEnd - 1 ? 'border-b dark:border-gray-800' : '',
|
||||
row.isGroup
|
||||
? 'hover:bg-gray-50 dark:hover:bg-gray-890 cursor-pointer'
|
||||
: '',
|
||||
]"
|
||||
@click="() => onRowClick(row, r)"
|
||||
>
|
||||
@ -70,7 +81,17 @@
|
||||
</WithScroll>
|
||||
<!-- Report Rows Container -->
|
||||
</div>
|
||||
<p v-else class="w-full text-center mt-20 text-gray-800 text-base">
|
||||
<p
|
||||
v-else
|
||||
class="
|
||||
w-full
|
||||
text-center
|
||||
mt-20
|
||||
text-gray-800
|
||||
dark:text-gray-100
|
||||
text-base
|
||||
"
|
||||
>
|
||||
{{ report.loading ? t`Loading Report...` : t`No Values to be Displayed` }}
|
||||
</p>
|
||||
|
||||
@ -203,23 +224,23 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (!cell.rawValue) {
|
||||
return 'text-gray-600';
|
||||
return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
|
||||
if (typeof cell.rawValue !== 'number') {
|
||||
return 'text-gray-900';
|
||||
return 'text-gray-900 dark:text-gray-100';
|
||||
}
|
||||
|
||||
if (cell.rawValue === 0) {
|
||||
return 'text-gray-600';
|
||||
return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
|
||||
const prec = this.fyo?.singles?.displayPrecision ?? 2;
|
||||
if (Number(cell.rawValue.toFixed(prec)) === 0) {
|
||||
return 'text-gray-600';
|
||||
return 'text-gray-600 dark:text-gray-500';
|
||||
}
|
||||
|
||||
return 'text-gray-900';
|
||||
return 'text-gray-900 dark:text-gray-300';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,8 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Search Bar Button -->
|
||||
<Button class="px-3 py-2 rounded-r-none" :padding="false" @click="open">
|
||||
<feather-icon name="search" class="w-4 h-4 text-gray-700" />
|
||||
<Button
|
||||
class="px-3 py-2 rounded-r-none dark:bg-gray-900"
|
||||
:padding="false"
|
||||
@click="open"
|
||||
>
|
||||
<feather-icon
|
||||
name="search"
|
||||
class="w-4 h-4 text-gray-700 dark:text-gray-300"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -24,11 +31,13 @@
|
||||
:placeholder="t`Type to search...`"
|
||||
class="
|
||||
bg-gray-100
|
||||
dark:bg-gray-800
|
||||
text-2xl
|
||||
focus:outline-none
|
||||
w-full
|
||||
placeholder-gray-500
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
rounded-md
|
||||
p-3
|
||||
"
|
||||
@ -38,19 +47,23 @@
|
||||
@keydown.esc="close"
|
||||
/>
|
||||
</div>
|
||||
<hr v-if="suggestions.length" />
|
||||
<hr v-if="suggestions.length" class="dark:border-gray-800" />
|
||||
|
||||
<!-- Search List -->
|
||||
<div
|
||||
:style="`max-height: ${49 * 6 - 1}px`"
|
||||
class="overflow-auto custom-scroll"
|
||||
class="overflow-auto custom-scroll custom-scroll-thumb2"
|
||||
>
|
||||
<div
|
||||
v-for="(si, i) in suggestions"
|
||||
:key="`${i}-${si.label}`"
|
||||
:data-index="`search-suggestion-${i}`"
|
||||
class="hover:bg-gray-50 cursor-pointer"
|
||||
:class="idx === i ? 'border-gray-700 bg-gray-50 border-s-4' : ''"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-875 cursor-pointer"
|
||||
:class="
|
||||
idx === i
|
||||
? 'border-gray-700 dark:border-gray-200 bg-gray-50 dark:bg-gray-875 border-s-4'
|
||||
: ''
|
||||
"
|
||||
@click="select(i)"
|
||||
>
|
||||
<!-- Search List Item -->
|
||||
@ -60,12 +73,19 @@
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<p
|
||||
:class="idx === i ? 'text-gray-900' : 'text-gray-700'"
|
||||
:class="
|
||||
idx === i
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-700 dark:text-gray-400'
|
||||
"
|
||||
:style="idx === i ? 'margin-left: -4px' : ''"
|
||||
>
|
||||
{{ si.label }}
|
||||
</p>
|
||||
<p v-if="si.group === 'Docs'" class="text-gray-600 text-sm ms-3">
|
||||
<p
|
||||
v-if="si.group === 'Docs'"
|
||||
class="text-gray-600 dark:text-gray-400 text-sm ms-3"
|
||||
>
|
||||
{{ si.more.filter(Boolean).join(', ') }}
|
||||
</p>
|
||||
</div>
|
||||
@ -79,12 +99,15 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr v-if="i !== suggestions.length - 1" />
|
||||
<hr
|
||||
v-if="i !== suggestions.length - 1"
|
||||
class="dark:border-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
|
||||
<!-- Group Filters -->
|
||||
<div class="flex justify-between">
|
||||
@ -92,7 +115,7 @@
|
||||
<button
|
||||
v-for="g in searchGroups"
|
||||
:key="g"
|
||||
class="border px-1 py-0.5 rounded-lg"
|
||||
class="border dark:border-gray-800 px-1 py-0.5 rounded-lg"
|
||||
:class="getGroupFilterButtonClass(g)"
|
||||
@click="searcher!.set(g, !searcher!.filters.groupFilters[g])"
|
||||
>
|
||||
@ -100,7 +123,14 @@
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
|
||||
class="
|
||||
hover:text-gray-900
|
||||
dark:hover:text-gray-25
|
||||
py-0.5
|
||||
rounded
|
||||
text-gray-700
|
||||
dark:text-gray-300
|
||||
"
|
||||
@click="showMore = !showMore"
|
||||
>
|
||||
{{ showMore ? t`Less Filters` : t`More Filters` }}
|
||||
@ -110,11 +140,11 @@
|
||||
<!-- Additional Filters -->
|
||||
<div v-if="showMore" class="-mt-1">
|
||||
<!-- Group Skip Filters -->
|
||||
<div class="flex gap-1 text-gray-800">
|
||||
<div class="flex gap-1 text-gray-800 dark:text-gray-200">
|
||||
<button
|
||||
v-for="s in ['skipTables', 'skipTransactions'] as const"
|
||||
:key="s"
|
||||
class="border px-1 py-0.5 rounded-lg"
|
||||
class="border dark:border-gray-800 px-1 py-0.5 rounded-lg"
|
||||
:class="{ 'bg-gray-200': searcher?.filters[s] }"
|
||||
@click="searcher?.set(s, !searcher?.filters[s])"
|
||||
>
|
||||
@ -125,7 +155,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Schema Name Filters -->
|
||||
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
|
||||
<div
|
||||
class="flex mt-1 gap-1 text-blue-500 dark:text-blue-100 flex-wrap"
|
||||
>
|
||||
<button
|
||||
v-for="sf in schemaFilters"
|
||||
:key="sf.value"
|
||||
@ -135,10 +167,12 @@
|
||||
py-0.5
|
||||
rounded-lg
|
||||
border-blue-100
|
||||
dark:border-blue-800
|
||||
whitespace-nowrap
|
||||
"
|
||||
:class="{
|
||||
'bg-blue-100': searcher?.filters.schemaFilters[sf.value],
|
||||
'bg-blue-100 dark:bg-blue-800':
|
||||
searcher?.filters.schemaFilters[sf.value],
|
||||
}"
|
||||
@click="
|
||||
searcher?.set(
|
||||
@ -159,7 +193,12 @@
|
||||
<p>↩ {{ t`Select` }}</p>
|
||||
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p>
|
||||
<button
|
||||
class="flex items-center hover:text-gray-800"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-300
|
||||
"
|
||||
@click="openDocs"
|
||||
>
|
||||
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
|
||||
@ -173,7 +212,14 @@
|
||||
|
||||
<div
|
||||
v-if="(searcher?.numSearches ?? 0) > 50"
|
||||
class="border border-gray-100 rounded flex justify-self-end ms-2"
|
||||
class="
|
||||
border border-gray-100
|
||||
dark:border-gray-875
|
||||
rounded
|
||||
flex
|
||||
justify-self-end
|
||||
ms-2
|
||||
"
|
||||
>
|
||||
<template
|
||||
v-for="c in allowedLimits.filter(
|
||||
@ -183,7 +229,9 @@
|
||||
>
|
||||
<button
|
||||
class="w-9"
|
||||
:class="limit === c ? 'bg-gray-100' : ''"
|
||||
:class="
|
||||
limit === c ? 'bg-gray-100 dark:bg-gray-875 rounded' : ''
|
||||
"
|
||||
@click="limit = Number(c)"
|
||||
>
|
||||
{{ c === -1 ? t`All` : c }}
|
||||
@ -390,10 +438,12 @@ export default defineComponent({
|
||||
const isOn = this.searcher.filters.groupFilters[g];
|
||||
const color = this.groupColorMap[g];
|
||||
if (isOn) {
|
||||
return `${getBgTextColorClass(color)} border-${color}-100`;
|
||||
return `${getBgTextColorClass(
|
||||
color
|
||||
)} border-${color}-100 dark:border-${color}-800`;
|
||||
}
|
||||
|
||||
return `text-${color}-600 border-${color}-100`;
|
||||
return `text-${color}-600 dark:text-${color}-400 border-${color}-100 dark:border-${color}-800`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<FormHeader :form-title="t`Shortcuts`" />
|
||||
<hr />
|
||||
<div class="h-96 overflow-y-auto text-gray-900">
|
||||
<hr class="dark:border-gray-800" />
|
||||
<div
|
||||
class="
|
||||
h-96
|
||||
overflow-y-auto
|
||||
custom-scroll custom-scroll-thumb2
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
"
|
||||
>
|
||||
<template v-for="g in groups" :key="g.label">
|
||||
<div class="p-4 w-full">
|
||||
<!-- Shortcut Group Header -->
|
||||
@ -27,13 +35,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Shortcut count if collapsed -->
|
||||
<div v-else class="text-base text-gray-600">
|
||||
<div v-else class="text-base text-gray-600 dark:text-gray-400">
|
||||
{{ t`${g.shortcuts.length} shortcuts` }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
</template>
|
||||
<div class="p-4 text-base text-gray-600">
|
||||
<div class="p-4 text-base text-gray-600 dark:text-gray-400">
|
||||
{{ t`More shortcuts will be added soon.` }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<div
|
||||
class="py-2 h-full flex justify-between flex-col bg-gray-25 relative"
|
||||
class="
|
||||
py-2
|
||||
h-full
|
||||
flex
|
||||
justify-between
|
||||
flex-col
|
||||
bg-gray-25
|
||||
dark:bg-gray-900
|
||||
relative
|
||||
"
|
||||
:class="{
|
||||
'window-drag': platform !== 'Windows',
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<!-- Company name and DB Switcher -->
|
||||
<!-- Company name -->
|
||||
<div
|
||||
class="px-4 flex flex-row items-center justify-between mb-4"
|
||||
:class="
|
||||
@ -17,6 +26,7 @@
|
||||
data-testid="company-name"
|
||||
class="
|
||||
font-semibold
|
||||
dark:text-gray-200
|
||||
whitespace-nowrap
|
||||
overflow-auto
|
||||
no-scrollbar
|
||||
@ -30,10 +40,18 @@
|
||||
<!-- Sidebar Items -->
|
||||
<div v-for="group in groups" :key="group.label">
|
||||
<div
|
||||
class="px-4 flex items-center cursor-pointer hover:bg-gray-100 h-10"
|
||||
class="
|
||||
px-4
|
||||
flex
|
||||
items-center
|
||||
cursor-pointer
|
||||
hover:bg-gray-100
|
||||
dark:hover:bg-gray-875
|
||||
h-10
|
||||
"
|
||||
:class="
|
||||
isGroupActive(group) && !group.items
|
||||
? 'bg-gray-100 border-s-4 border-gray-800'
|
||||
? 'bg-gray-100 dark:bg-gray-875 border-s-4 border-gray-800 dark:border-gray-100'
|
||||
: ''
|
||||
"
|
||||
@click="routeToSidebarItem(group)"
|
||||
@ -44,11 +62,16 @@
|
||||
:size="group.iconSize || '18'"
|
||||
:height="group.iconHeight ?? 0"
|
||||
:active="!!isGroupActive(group)"
|
||||
:darkMode="darkMode"
|
||||
:class="isGroupActive(group) && !group.items ? '-ms-1' : ''"
|
||||
/>
|
||||
<div
|
||||
class="ms-2 text-lg text-gray-700"
|
||||
:class="isGroupActive(group) && !group.items && 'text-gray-900'"
|
||||
:class="
|
||||
isGroupActive(group) && !group.items
|
||||
? 'text-gray-900 dark:text-gray-25'
|
||||
: 'dark:text-gray-300'
|
||||
"
|
||||
>
|
||||
{{ group.label }}
|
||||
</div>
|
||||
@ -67,11 +90,12 @@
|
||||
flex
|
||||
items-center
|
||||
hover:bg-gray-100
|
||||
dark:hover:bg-gray-875
|
||||
"
|
||||
:class="
|
||||
isItemActive(item)
|
||||
? 'bg-gray-100 text-gray-900 border-s-4 border-gray-800'
|
||||
: 'text-gray-700'
|
||||
? 'bg-gray-100 dark:bg-gray-875 text-gray-900 dark:text-gray-100 border-s-4 border-gray-800 dark:border-gray-100'
|
||||
: 'text-gray-700 dark:text-gray-400'
|
||||
"
|
||||
@click="routeToSidebarItem(item)"
|
||||
>
|
||||
@ -83,13 +107,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Report Issue and App Version -->
|
||||
<!-- Report Issue and DB Switcher -->
|
||||
<div class="window-no-drag flex flex-col gap-2 py-2 px-4">
|
||||
<button
|
||||
class="
|
||||
flex
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-500
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-400
|
||||
gap-1
|
||||
items-center
|
||||
"
|
||||
@ -105,7 +131,9 @@
|
||||
class="
|
||||
flex
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-500
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-400
|
||||
gap-1
|
||||
items-center
|
||||
"
|
||||
@ -120,7 +148,9 @@
|
||||
class="
|
||||
flex
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-500
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-400
|
||||
gap-1
|
||||
items-center
|
||||
"
|
||||
@ -134,7 +164,9 @@
|
||||
class="
|
||||
flex
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-500
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-400
|
||||
gap-1
|
||||
items-center
|
||||
"
|
||||
@ -163,7 +195,9 @@
|
||||
bottom-0
|
||||
end-0
|
||||
text-gray-600
|
||||
dark:text-gray-500
|
||||
hover:bg-gray-100
|
||||
dark:hover:bg-gray-875
|
||||
rounded
|
||||
p-1
|
||||
m-4
|
||||
@ -201,7 +235,10 @@ export default defineComponent({
|
||||
Modal,
|
||||
ShortcutsHelper,
|
||||
},
|
||||
emits: ['change-db-file'],
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['change-db-file', 'toggle-darkmode'],
|
||||
setup() {
|
||||
return {
|
||||
languageDirection: inject(languageDirectionKey),
|
||||
|
@ -6,6 +6,7 @@
|
||||
class="
|
||||
inner
|
||||
text-gray-900
|
||||
dark:text-gray-25
|
||||
shadow-lg
|
||||
px-3
|
||||
py-2
|
||||
@ -15,6 +16,7 @@
|
||||
w-toast
|
||||
z-30
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
rounded-lg
|
||||
border
|
||||
"
|
||||
@ -30,7 +32,12 @@
|
||||
<p class="text-base">{{ message }}</p>
|
||||
<button
|
||||
v-if="actionText"
|
||||
class="text-sm text-gray-700 hover:text-gray-800"
|
||||
class="
|
||||
text-sm text-gray-700
|
||||
dark:text-gray-300
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-200
|
||||
"
|
||||
>
|
||||
{{ actionText }}
|
||||
</button>
|
||||
@ -42,8 +49,10 @@
|
||||
h-4
|
||||
ms-auto
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
cursor-pointer
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-200
|
||||
"
|
||||
@click="closeToast"
|
||||
/>
|
||||
|
@ -16,13 +16,13 @@
|
||||
<div
|
||||
v-else
|
||||
:key="`${df.fieldname}-regular`"
|
||||
class="grid items-center border-b"
|
||||
class="grid items-center border-b dark:border-gray-800"
|
||||
:style="{
|
||||
...style,
|
||||
height: getFieldHeight(df),
|
||||
}"
|
||||
>
|
||||
<div class="ps-4 flex text-gray-600">
|
||||
<div class="ps-4 flex text-gray-600 dark:text-gray-400">
|
||||
{{ df.label }}
|
||||
</div>
|
||||
|
||||
|
@ -1,12 +1,65 @@
|
||||
<template>
|
||||
<div
|
||||
class="window-drag flex items-center border-b text-gray-900 border-gray-100"
|
||||
class="
|
||||
relative
|
||||
window-drag
|
||||
flex
|
||||
items-center
|
||||
border-b
|
||||
dark:bg-gray-900
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
border-gray-100
|
||||
dark:border-gray-800
|
||||
"
|
||||
style="height: 28px"
|
||||
>
|
||||
<Fb class="ms-2" />
|
||||
<p v-if="companyName && dbPath" class="mx-auto text-sm">
|
||||
{{ companyName }} - {{ dbPath }}
|
||||
</p>
|
||||
<div
|
||||
v-if="!isFullscreen"
|
||||
class="absolute window-no-drag flex h-full items-center right-0"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
px-4
|
||||
h-full
|
||||
hover:bg-gray-300
|
||||
dark:hover:bg-gray-875
|
||||
"
|
||||
@click="minimizeWindow"
|
||||
>
|
||||
<feather-icon name="minus" class="h-4 w-4 flex-shrink-0" />
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
px-4
|
||||
h-full
|
||||
hover:bg-gray-300
|
||||
dark:hover:bg-gray-875
|
||||
"
|
||||
@click="toggleMaximize"
|
||||
>
|
||||
<feather-icon
|
||||
v-if="isMax"
|
||||
name="minimize"
|
||||
class="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
<feather-icon v-else name="square" class="h-3 w-3 flex-shrink-0" />
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center px-4 h-full hover:bg-red-600 hover:text-white"
|
||||
@click="closeWindow"
|
||||
>
|
||||
<feather-icon name="x" class="h-4 w-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -20,5 +73,62 @@ export default {
|
||||
dbPath: String,
|
||||
companyName: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMax: Boolean,
|
||||
isFullscreen: Boolean,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getIsMaximized();
|
||||
this.getIsFullscreen();
|
||||
window.addEventListener('resize', this.getIsFullscreen);
|
||||
document.addEventListener('webkitfullscreenchange', this.getIsFullscreen);
|
||||
document.addEventListener('mozfullscreenchange', this.getIsFullscreen);
|
||||
document.addEventListener('fullscreenchange', this.getIsFullscreen);
|
||||
document.addEventListener('MSFullscreenChange', this.getIsFullscreen);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.getIsFullscreen);
|
||||
document.removeEventListener(
|
||||
'webkitfullscreenchange',
|
||||
this.getIsFullscreen
|
||||
);
|
||||
document.removeEventListener('mozfullscreenchange', this.getIsFullscreen);
|
||||
document.removeEventListener('fullscreenchange', this.getIsFullscreen);
|
||||
document.removeEventListener('MSFullscreenChange', this.getIsFullscreen);
|
||||
},
|
||||
methods: {
|
||||
minimizeWindow() {
|
||||
ipc.minimizeWindow();
|
||||
},
|
||||
toggleMaximize() {
|
||||
ipc.toggleMaximize();
|
||||
this.getIsMaximized();
|
||||
},
|
||||
closeWindow() {
|
||||
ipc.closeWindow();
|
||||
},
|
||||
getIsMaximized() {
|
||||
ipc
|
||||
.isMaximized()
|
||||
.then((result) => {
|
||||
this.isMax = result;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
getIsFullscreen() {
|
||||
ipc
|
||||
.isFullscreen()
|
||||
.then((result) => {
|
||||
this.isFullscreen = result;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="custom-scroll">
|
||||
<div class="custom-scroll custom-scroll-thumb1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -10,7 +10,12 @@
|
||||
<!-- Chart of Accounts -->
|
||||
<div
|
||||
v-if="root"
|
||||
class="flex-1 flex flex-col overflow-y-auto mb-4 custom-scroll"
|
||||
class="
|
||||
flex-1 flex flex-col
|
||||
overflow-y-auto
|
||||
mb-4
|
||||
custom-scroll custom-scroll-thumb1
|
||||
"
|
||||
>
|
||||
<!-- Chart of Accounts Indented List -->
|
||||
<template v-for="account in allAccounts" :key="account.name">
|
||||
@ -20,16 +25,18 @@
|
||||
py-2
|
||||
cursor-pointer
|
||||
hover:bg-gray-50
|
||||
dark:hover:bg-gray-890 dark:text-gray-25
|
||||
group
|
||||
flex
|
||||
items-center
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
flex-shrink-0
|
||||
pe-4
|
||||
"
|
||||
:class="[
|
||||
account.level !== 0 ? 'text-base' : 'text-lg',
|
||||
isQuickEditOpen(account) ? 'bg-gray-200' : '',
|
||||
isQuickEditOpen(account) ? 'bg-gray-200 dark:bg-gray-900' : '',
|
||||
]"
|
||||
:style="getItemStyle(account.level)"
|
||||
@click="onClick(account)"
|
||||
@ -49,7 +56,9 @@
|
||||
v-if="account.isGroup"
|
||||
class="
|
||||
text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
hover:text-gray-900
|
||||
dark:hover:text-gray-100
|
||||
focus:outline-none
|
||||
"
|
||||
@click.stop="addAccount(account, 'addingAccount')"
|
||||
@ -61,7 +70,9 @@
|
||||
class="
|
||||
ms-3
|
||||
text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
hover:text-gray-900
|
||||
dark:hover:text-gray-100
|
||||
focus:outline-none
|
||||
"
|
||||
@click.stop="addAccount(account, 'addingGroupAccount')"
|
||||
@ -72,7 +83,9 @@
|
||||
class="
|
||||
ms-3
|
||||
text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
hover:text-gray-900
|
||||
dark:hover:text-gray-100
|
||||
focus:outline-none
|
||||
"
|
||||
@click.stop="deleteAccount(account)"
|
||||
@ -83,7 +96,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Account Balance String -->
|
||||
<p v-if="!account.isGroup" class="ms-auto text-base text-gray-800">
|
||||
<p
|
||||
v-if="!account.isGroup"
|
||||
class="ms-auto text-base text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{{ getBalanceString(account) }}
|
||||
</p>
|
||||
</div>
|
||||
@ -95,8 +111,10 @@
|
||||
class="
|
||||
px-4
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
cursor-pointer
|
||||
hover:bg-gray-50
|
||||
dark:hover:bg-gray-890
|
||||
group
|
||||
flex
|
||||
items-center
|
||||
@ -109,8 +127,12 @@
|
||||
<input
|
||||
:ref="account.name"
|
||||
v-model="newAccountName"
|
||||
class="focus:outline-none bg-transparent"
|
||||
:class="{ 'text-gray-600': insertingAccount }"
|
||||
class="
|
||||
focus:outline-none
|
||||
bg-transparent
|
||||
dark:placeholder-gray-600 dark:text-gray-400
|
||||
"
|
||||
:class="{ 'text-gray-600 dark:text-gray-400': insertingAccount }"
|
||||
:placeholder="t`New Account`"
|
||||
type="text"
|
||||
:disabled="insertingAccount"
|
||||
@ -124,7 +146,9 @@
|
||||
class="
|
||||
ms-4
|
||||
text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
hover:text-gray-900
|
||||
dark:hover:text-gray-100
|
||||
focus:outline-none
|
||||
"
|
||||
@click="
|
||||
@ -138,7 +162,9 @@
|
||||
class="
|
||||
ms-4
|
||||
text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
hover:text-gray-900
|
||||
dark:hover:text-gray-100
|
||||
focus:outline-none
|
||||
"
|
||||
@click="cancelAddingAccount(account)"
|
||||
@ -194,6 +220,9 @@ export default defineComponent({
|
||||
Button,
|
||||
PageHeader,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
languageDirection: inject(languageDirectionKey),
|
||||
@ -527,36 +556,38 @@ export default defineComponent({
|
||||
return !!(edit && schemaName === 'Account' && name === account.name);
|
||||
},
|
||||
getIconComponent(isGroup: boolean, name?: string): Component {
|
||||
let lightColor = this.darkMode ? uicolors.gray[600] : uicolors.gray[400];
|
||||
let darkColor = this.darkMode ? uicolors.gray[400] : uicolors.gray[700];
|
||||
let icons = {
|
||||
'Application of Funds (Assets)': `<svg class="w-4 h-4" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M15.333 5.333H.667A.667.667 0 000 6v9.333c0 .368.299.667.667.667h14.666a.667.667 0 00.667-.667V6a.667.667 0 00-.667-.667zM8 12.667a2 2 0 110-4 2 2 0 010 4z" fill="${uicolors.gray[700]}" fill-rule="nonzero"/>
|
||||
<path d="M14 2.667V4H2V2.667h12zM11.333 0v1.333H4.667V0h6.666z" fill="${uicolors.gray[400]}"/>
|
||||
<path d="M15.333 5.333H.667A.667.667 0 000 6v9.333c0 .368.299.667.667.667h14.666a.667.667 0 00.667-.667V6a.667.667 0 00-.667-.667zM8 12.667a2 2 0 110-4 2 2 0 010 4z" fill="${darkColor}" fill-rule="nonzero"/>
|
||||
<path d="M14 2.667V4H2V2.667h12zM11.333 0v1.333H4.667V0h6.666z" fill="${lightColor}"/>
|
||||
</g>
|
||||
</svg>`,
|
||||
Expenses: `<svg class="w-4 h-4" viewBox="0 0 14 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.668 0v15.333a.666.666 0 01-.666.667h-12a.666.666 0 01-.667-.667V0l2.667 2 2-2 2 2 2-2 2 2 2.666-2zM9.964 4.273H4.386l-.311 1.133h1.62c.933 0 1.474.362 1.67.963H4.373l-.298 1.053h3.324c-.175.673-.767 1.044-1.705 1.044H4.182l.008.83L7.241 13h1.556v-.072L6.01 9.514c1.751-.106 2.574-.942 2.748-2.092h.904l.298-1.053H8.75a2.375 2.375 0 00-.43-1.044l1.342.009.302-1.061z" fill="${uicolors.gray[700]}" fill-rule="evenodd"/>
|
||||
<path d="M13.668 0v15.333a.666.666 0 01-.666.667h-12a.666.666 0 01-.667-.667V0l2.667 2 2-2 2 2 2-2 2 2 2.666-2zM9.964 4.273H4.386l-.311 1.133h1.62c.933 0 1.474.362 1.67.963H4.373l-.298 1.053h3.324c-.175.673-.767 1.044-1.705 1.044H4.182l.008.83L7.241 13h1.556v-.072L6.01 9.514c1.751-.106 2.574-.942 2.748-2.092h.904l.298-1.053H8.75a2.375 2.375 0 00-.43-1.044l1.342.009.302-1.061z" fill="${darkColor}" fill-rule="evenodd"/>
|
||||
</svg>`,
|
||||
Income: `<svg class="w-4 h-4" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M16 12.859V14c0 1.105-2.09 2-4.667 2-2.494 0-4.531-.839-4.66-1.894L6.667 14v-1.141C7.73 13.574 9.366 14 11.333 14c1.968 0 3.602-.426 4.667-1.141zm0-3.334v1.142c0 1.104-2.09 2-4.667 2-2.494 0-4.531-.839-4.66-1.894l-.006-.106V9.525c1.064.716 2.699 1.142 4.666 1.142 1.968 0 3.602-.426 4.667-1.142zm-4.667-4.192c2.578 0 4.667.896 4.667 2 0 1.105-2.09 2-4.667 2s-4.666-.895-4.666-2c0-1.104 2.089-2 4.666-2z" fill="${uicolors.gray[700]}"/>
|
||||
<path d="M0 10.859C1.065 11.574 2.7 12 4.667 12l.337-.005.33-.013v1.995c-.219.014-.44.023-.667.023-2.495 0-4.532-.839-4.66-1.894L0 12v-1.141zm0-2.192V7.525c1.065.716 2.7 1.142 4.667 1.142l.337-.005.33-.013v1.995c-.219.013-.44.023-.667.023-2.495 0-4.532-.839-4.66-1.894L0 8.667V7.525zm0-4.475c1.065.715 2.7 1.141 4.667 1.141.694 0 1.345-.056 1.946-.156-.806.56-1.27 1.292-1.278 2.134-.219.013-.441.022-.668.022-2.578 0-4.667-.895-4.667-2zM4.667 0c2.577 0 4.666.895 4.666 2S7.244 4 4.667 4C2.089 4 0 3.105 0 2s2.09-2 4.667-2z" fill="${uicolors.gray[400]}"/>
|
||||
<path d="M16 12.859V14c0 1.105-2.09 2-4.667 2-2.494 0-4.531-.839-4.66-1.894L6.667 14v-1.141C7.73 13.574 9.366 14 11.333 14c1.968 0 3.602-.426 4.667-1.141zm0-3.334v1.142c0 1.104-2.09 2-4.667 2-2.494 0-4.531-.839-4.66-1.894l-.006-.106V9.525c1.064.716 2.699 1.142 4.666 1.142 1.968 0 3.602-.426 4.667-1.142zm-4.667-4.192c2.578 0 4.667.896 4.667 2 0 1.105-2.09 2-4.667 2s-4.666-.895-4.666-2c0-1.104 2.089-2 4.666-2z" fill="${darkColor}"/>
|
||||
<path d="M0 10.859C1.065 11.574 2.7 12 4.667 12l.337-.005.33-.013v1.995c-.219.014-.44.023-.667.023-2.495 0-4.532-.839-4.66-1.894L0 12v-1.141zm0-2.192V7.525c1.065.716 2.7 1.142 4.667 1.142l.337-.005.33-.013v1.995c-.219.013-.44.023-.667.023-2.495 0-4.532-.839-4.66-1.894L0 8.667V7.525zm0-4.475c1.065.715 2.7 1.141 4.667 1.141.694 0 1.345-.056 1.946-.156-.806.56-1.27 1.292-1.278 2.134-.219.013-.441.022-.668.022-2.578 0-4.667-.895-4.667-2zM4.667 0c2.577 0 4.666.895 4.666 2S7.244 4 4.667 4C2.089 4 0 3.105 0 2s2.09-2 4.667-2z" fill="${lightColor}"/>
|
||||
</g>
|
||||
</svg>`,
|
||||
'Source of Funds (Liabilities)': `<svg class="w-4 h-4" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M7.332 11.36l4.666-3.734 2 1.6V.666A.667.667 0 0013.332 0h-12a.667.667 0 00-.667.667v14.666c0 .369.298.667.667.667h6v-4.64zm-4-7.36H11.3v1.333H3.332V4zm2.666 8H3.332v-1.333h2.666V12zM3.332 8.667V7.333h5.333v1.334H3.332z" fill="${uicolors.gray[700]}"/>
|
||||
<path d="M15.332 12l-3.334-2.667L8.665 12v3.333c0 .369.298.667.667.667h2v-2h1.333v2h2a.667.667 0 00.667-.667V12z" fill="${uicolors.gray[400]}"/>
|
||||
<path d="M7.332 11.36l4.666-3.734 2 1.6V.666A.667.667 0 0013.332 0h-12a.667.667 0 00-.667.667v14.666c0 .369.298.667.667.667h6v-4.64zm-4-7.36H11.3v1.333H3.332V4zm2.666 8H3.332v-1.333h2.666V12zM3.332 8.667V7.333h5.333v1.334H3.332z" fill="${darkColor}"/>
|
||||
<path d="M15.332 12l-3.334-2.667L8.665 12v3.333c0 .369.298.667.667.667h2v-2h1.333v2h2a.667.667 0 00.667-.667V12z" fill="${lightColor}"/>
|
||||
</g>
|
||||
</svg>`,
|
||||
};
|
||||
|
||||
let leaf = `<svg class="w-2 h-2" viewBox="0 0 8 8" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle stroke="${uicolors.gray[700]}" cx="4" cy="4" r="3.5" fill="none" fill-rule="evenodd"/>
|
||||
<circle stroke="${darkColor}" cx="4" cy="4" r="3.5" fill="none" fill-rule="evenodd"/>
|
||||
</svg>`;
|
||||
|
||||
let folder = `<svg class="w-3 h-3" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.333 3.367L6.333.7H.667A.667.667 0 000 1.367v12a2 2 0 002 2h12a2 2 0 002-2V4.033a.667.667 0 00-.667-.666h-7z" fill="${uicolors.gray[700]}" fill-rule="evenodd"/>
|
||||
<path d="M8.333 3.367L6.333.7H.667A.667.667 0 000 1.367v12a2 2 0 002 2h12a2 2 0 002-2V4.033a.667.667 0 00-.667-.666h-7z" fill="${darkColor}" fill-rule="evenodd"/>
|
||||
</svg>`;
|
||||
|
||||
let icon = isGroup ? folder : leaf;
|
||||
|
@ -74,18 +74,35 @@
|
||||
}}</Button>
|
||||
</template>
|
||||
<template #body>
|
||||
<FormHeader :form-title="title" class="sticky top-0 bg-white border-b">
|
||||
<FormHeader
|
||||
:form-title="title"
|
||||
class="
|
||||
sticky
|
||||
top-0
|
||||
bg-white
|
||||
dark:bg-gray-890
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
"
|
||||
>
|
||||
<StatusPill v-if="hasDoc" :doc="doc" />
|
||||
</FormHeader>
|
||||
|
||||
<!-- Section Container -->
|
||||
<div v-if="hasDoc" class="overflow-auto custom-scroll">
|
||||
<div
|
||||
v-if="hasDoc"
|
||||
class="overflow-auto custom-scroll custom-scroll-thumb1"
|
||||
>
|
||||
<CommonFormSection
|
||||
v-for="([n, fields], idx) in activeGroup.entries()"
|
||||
:key="n + idx"
|
||||
ref="section"
|
||||
class="p-4"
|
||||
:class="idx !== 0 && activeGroup.size > 1 ? 'border-t' : ''"
|
||||
:class="
|
||||
idx !== 0 && activeGroup.size > 1
|
||||
? 'border-t dark:border-gray-800'
|
||||
: ''
|
||||
"
|
||||
:show-title="activeGroup.size > 1 && n !== t`Default`"
|
||||
:title="n"
|
||||
:fields="fields"
|
||||
@ -107,10 +124,12 @@
|
||||
flex
|
||||
gap-8
|
||||
border-t
|
||||
dark:border-gray-800
|
||||
flex-shrink-0
|
||||
sticky
|
||||
bottom-0
|
||||
bg-white
|
||||
dark:bg-gray-875
|
||||
"
|
||||
>
|
||||
<div
|
||||
@ -119,8 +138,8 @@
|
||||
class="text-sm cursor-pointer"
|
||||
:class="
|
||||
key === activeTab
|
||||
? 'text-gray-900 font-semibold border-t-2 border-gray-800'
|
||||
: 'text-gray-700'
|
||||
? 'text-gray-900 dark:text-gray-25 font-semibold border-t-2 border-gray-800 dark:border-gray-100'
|
||||
: 'text-gray-700 dark:text-gray-200 '
|
||||
"
|
||||
:style="{
|
||||
paddingTop: key === activeTab ? 'calc(1rem - 2px)' : '1rem',
|
||||
|
@ -6,13 +6,13 @@
|
||||
:class="[collapsed ? '' : 'mb-4', collapsible ? 'cursor-pointer' : '']"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<h2 class="text-base text-gray-900 font-semibold">
|
||||
<h2 class="text-base text-gray-900 dark:text-gray-25 font-semibold">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<feather-icon
|
||||
v-if="collapsible"
|
||||
:name="collapsed ? 'chevron-up' : 'chevron-down'"
|
||||
class="w-4 h-4 text-gray-600"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="grid gap-4 gap-x-8 grid-cols-2">
|
||||
|
@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<div class="w-quick-edit bg-white border-l">
|
||||
<div
|
||||
class="
|
||||
w-quick-edit
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
border-l
|
||||
dark:border-gray-800
|
||||
overflow-y-auto
|
||||
custom-scroll custom-scroll-thumb2
|
||||
"
|
||||
>
|
||||
<!-- Page Header -->
|
||||
<div
|
||||
class="
|
||||
@ -11,6 +21,7 @@
|
||||
sticky
|
||||
top-0
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
"
|
||||
style="z-index: 1"
|
||||
>
|
||||
@ -18,7 +29,7 @@
|
||||
<Button :icon="true" @click="$emit('close')">
|
||||
<feather-icon name="x" class="w-4 h-4" />
|
||||
</Button>
|
||||
<p class="text-xl font-semibold text-gray-600">
|
||||
<p class="text-xl font-semibold text-gray-600 dark:text-gray-400">
|
||||
{{ t`Linked Entries` }}
|
||||
</p>
|
||||
</div>
|
||||
@ -27,17 +38,33 @@
|
||||
<!-- Linked Entry List -->
|
||||
<div
|
||||
v-if="sequence.length"
|
||||
class="w-full overflow-y-auto custom-scroll border-t"
|
||||
style="height: calc(100vh - var(--h-row-largest) - 1px)"
|
||||
class="
|
||||
w-full
|
||||
overflow-y-auto
|
||||
custom-scroll custom-scroll-thumb2
|
||||
border-t
|
||||
dark:border-gray-800
|
||||
"
|
||||
>
|
||||
<div v-for="sn of sequence" :key="sn" class="border-b p-4">
|
||||
<div
|
||||
v-for="sn of sequence"
|
||||
:key="sn"
|
||||
class="border-b dark:border-gray-800 p-4 overflow-auto"
|
||||
>
|
||||
<!-- Header with count and schema label -->
|
||||
<div
|
||||
class="flex justify-between cursor-pointer"
|
||||
:class="entries[sn].collapsed ? '' : 'pb-4'"
|
||||
@click="entries[sn].collapsed = !entries[sn].collapsed"
|
||||
>
|
||||
<h2 class="text-base text-gray-600 font-semibold select-none">
|
||||
<h2
|
||||
class="
|
||||
text-base text-gray-600
|
||||
dark:text-gray-400
|
||||
font-semibold
|
||||
select-none
|
||||
"
|
||||
>
|
||||
{{ fyo.schemaMap[sn]?.label ?? sn
|
||||
}}<span class="font-normal">{{
|
||||
` – ${entries[sn].details.length}`
|
||||
@ -45,30 +72,45 @@
|
||||
</h2>
|
||||
<feather-icon
|
||||
:name="entries[sn].collapsed ? 'chevron-up' : 'chevron-down'"
|
||||
class="w-4 h-4 text-gray-600"
|
||||
class="w-4 h-4 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Entry list -->
|
||||
<div
|
||||
v-show="!entries[sn].collapsed"
|
||||
class="entry-container rounded-md border overflow-hidden"
|
||||
class="
|
||||
entry-container
|
||||
rounded-md
|
||||
border
|
||||
dark:border-gray-800
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
<!-- Entry -->
|
||||
<div
|
||||
v-for="e of entries[sn].details"
|
||||
:key="String(e.name) + sn"
|
||||
class="p-2 text-sm cursor-pointer hover:bg-gray-50"
|
||||
class="
|
||||
p-2
|
||||
text-sm
|
||||
cursor-pointer
|
||||
border-b
|
||||
last:border-0
|
||||
dark:border-gray-800
|
||||
hover:bg-gray-50
|
||||
dark:hover:bg-gray-875
|
||||
"
|
||||
@click="routeTo(sn, String(e.name))"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<!-- Name -->
|
||||
<p class="font-semibold">
|
||||
<p class="font-semibold dark:text-gray-25">
|
||||
{{ e.name }}
|
||||
</p>
|
||||
|
||||
<!-- Date -->
|
||||
<p v-if="e.date" class="text-xs text-gray-600">
|
||||
<p v-if="e.date" class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ fyo.format(e.date, 'Date') }}
|
||||
</p>
|
||||
</div>
|
||||
@ -153,7 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="p-4 text-sm text-gray-600">
|
||||
<p v-else class="p-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`No linked entries found` }}
|
||||
</p>
|
||||
</div>
|
||||
@ -304,13 +346,6 @@ const linkEntryDisplayFields: Record<string, string[]> = {
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.entry-container > div {
|
||||
@apply border-b;
|
||||
}
|
||||
.entry-container > div:last-child {
|
||||
@apply border-0;
|
||||
}
|
||||
|
||||
.pill-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,9 +1,28 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-s h-full overflow-auto w-quick-edit bg-white custom-scroll"
|
||||
class="
|
||||
border-s
|
||||
dark:border-gray-800
|
||||
h-full
|
||||
overflow-auto
|
||||
w-quick-edit
|
||||
bg-white
|
||||
dark:bg-gray-890
|
||||
custom-scroll custom-scroll-thumb2
|
||||
"
|
||||
>
|
||||
<!-- Row Edit Tool bar -->
|
||||
<div class="sticky top-0 border-b bg-white" style="z-index: 1">
|
||||
<div
|
||||
class="
|
||||
sticky
|
||||
top-0
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
bg-white
|
||||
dark:bg-gray-890
|
||||
"
|
||||
style="z-index: 1"
|
||||
>
|
||||
<div class="flex items-center justify-between px-4 h-row-largest">
|
||||
<!-- Close Button -->
|
||||
<Button :icon="true" @click="$emit('close')">
|
||||
@ -25,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<FormHeader
|
||||
class="border-t"
|
||||
class="border-t dark:border-gray-800"
|
||||
:form-title="t`Row ${index + 1}`"
|
||||
:form-sub-title="fieldlabel"
|
||||
/>
|
||||
|
@ -17,6 +17,7 @@
|
||||
w-full
|
||||
gap-2
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
p-4
|
||||
"
|
||||
>
|
||||
@ -27,7 +28,7 @@
|
||||
fieldtype: 'AutoComplete',
|
||||
options: customizableSchemas,
|
||||
}"
|
||||
input-class="bg-transparent text-gray-900 text-base"
|
||||
input-class="bg-transparent text-gray-900 dark:text-gray-100 text-base"
|
||||
class="w-40"
|
||||
:border="true"
|
||||
:value="formType"
|
||||
@ -38,7 +39,10 @@
|
||||
<p v-if="errorMessage" class="text-base ms-2 text-red-500">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<p v-else-if="helpMessage" class="text-base ms-2 text-gray-700">
|
||||
<p
|
||||
v-else-if="helpMessage"
|
||||
class="text-base ms-2 text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ helpMessage }}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -2,20 +2,26 @@
|
||||
<div>
|
||||
<!-- Title and Period Selector -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-base">{{ t`Cashflow` }}</div>
|
||||
<div class="font-semibold text-base dark:text-white">
|
||||
{{ t`Cashflow` }}
|
||||
</div>
|
||||
|
||||
<!-- Chart Legend -->
|
||||
<div v-if="hasData" class="flex text-base gap-8">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm inline-block bg-blue-500" />
|
||||
<span class="text-gray-900">{{ t`Inflow` }}</span>
|
||||
<span
|
||||
class="w-3 h-3 rounded-sm inline-block bg-blue-500 dark:bg-blue-600"
|
||||
/>
|
||||
<span class="text-gray-900 dark:text-gray-25">{{ t`Inflow` }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-sm inline-block bg-pink-500" />
|
||||
<span class="text-gray-900">{{ t`Outflow` }}</span>
|
||||
<span
|
||||
class="w-3 h-3 rounded-sm inline-block bg-pink-500 dark:bg-pink-600"
|
||||
/>
|
||||
<span class="text-gray-900 dark:text-gray-25">{{ t`Outflow` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-16 h-5 bg-gray-200 rounded" />
|
||||
<div v-else class="w-16 h-5 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
|
||||
<PeriodSelector
|
||||
v-if="hasData"
|
||||
@ -23,7 +29,7 @@
|
||||
:options="periodOptions"
|
||||
@change="(value) => (period = value)"
|
||||
/>
|
||||
<div v-else class="w-20 h-5 bg-gray-200 rounded" />
|
||||
<div v-else class="w-20 h-5 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
</div>
|
||||
|
||||
<!-- Line Chart -->
|
||||
@ -32,6 +38,8 @@
|
||||
class="mt-4"
|
||||
:aspect-ratio="4.15"
|
||||
:colors="chartData.colors"
|
||||
:gridColor="chartData.gridColor"
|
||||
:fontColor="chartData.fontColor"
|
||||
:points="chartData.points"
|
||||
:x-labels="chartData.xLabels"
|
||||
:format="chartData.format"
|
||||
@ -68,6 +76,9 @@ export default defineComponent({
|
||||
PeriodSelector,
|
||||
LineChart,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
extends: DashboardChartBase,
|
||||
data: () => ({
|
||||
data: [] as { inflow: number; outflow: number; yearmonth: string }[],
|
||||
@ -78,10 +89,16 @@ export default defineComponent({
|
||||
computed: {
|
||||
chartData() {
|
||||
let data = this.data;
|
||||
let colors = [uicolors.blue['500'], uicolors.pink['500']];
|
||||
let colors = [
|
||||
uicolors.blue[this.darkMode ? '600' : '500'],
|
||||
uicolors.pink[this.darkMode ? '600' : '500'],
|
||||
];
|
||||
if (!this.hasData) {
|
||||
data = dummyData;
|
||||
colors = [uicolors.gray['200'], uicolors.gray['100']];
|
||||
colors = [
|
||||
this.darkMode ? uicolors.gray['700'] : uicolors.gray['200'],
|
||||
this.darkMode ? uicolors.gray['800'] : uicolors.gray['100'],
|
||||
];
|
||||
}
|
||||
|
||||
const xLabels = data.map((cf) => cf.yearmonth);
|
||||
@ -91,7 +108,16 @@ export default defineComponent({
|
||||
|
||||
const format = (value: number) => fyo.format(value ?? 0, 'Currency');
|
||||
const yMax = getYMax(points);
|
||||
return { points, xLabels, colors, format, yMax, formatX: formatXLabels };
|
||||
return {
|
||||
points,
|
||||
xLabels,
|
||||
colors,
|
||||
format,
|
||||
yMax,
|
||||
formatX: formatXLabels,
|
||||
gridColor: this.darkMode ? 'rgba(200, 200, 200, 0.2)' : undefined,
|
||||
fontColor: this.darkMode ? uicolors.gray['400'] : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
|
@ -4,9 +4,12 @@
|
||||
<div
|
||||
class="
|
||||
border
|
||||
dark:border-gray-900
|
||||
rounded
|
||||
bg-gray-50
|
||||
dark:bg-gray-890
|
||||
focus-within:bg-gray-100
|
||||
dark:focus-within:bg-gray-900
|
||||
flex
|
||||
items-center
|
||||
"
|
||||
@ -21,43 +24,48 @@
|
||||
</PageHeader>
|
||||
|
||||
<div
|
||||
class="no-scrollbar overflow-auto"
|
||||
class="no-scrollbar overflow-auto dark:bg-gray-875"
|
||||
style="height: calc(100vh - var(--h-row-largest) - 1px)"
|
||||
>
|
||||
<div style="min-width: var(--w-desk-fixed)" class="overflow-auto">
|
||||
<Cashflow
|
||||
class="p-4"
|
||||
:common-period="period"
|
||||
:darkMode="darkMode"
|
||||
@period-change="handlePeriodChange"
|
||||
/>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<div class="flex w-full">
|
||||
<UnpaidInvoices
|
||||
:schema-name="'SalesInvoice'"
|
||||
:common-period="period"
|
||||
class="border-e"
|
||||
:darkMode="darkMode"
|
||||
class="border-e dark:border-gray-800"
|
||||
@period-change="handlePeriodChange"
|
||||
/>
|
||||
<UnpaidInvoices
|
||||
:schema-name="'PurchaseInvoice'"
|
||||
:common-period="period"
|
||||
:darkMode="darkMode"
|
||||
@period-change="handlePeriodChange"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<div class="flex">
|
||||
<ProfitAndLoss
|
||||
class="w-full p-4 border-e"
|
||||
class="w-full p-4 border-e dark:border-gray-800"
|
||||
:common-period="period"
|
||||
:darkMode="darkMode"
|
||||
@period-change="handlePeriodChange"
|
||||
/>
|
||||
<Expenses
|
||||
class="w-full p-4"
|
||||
:common-period="period"
|
||||
:darkMode="darkMode"
|
||||
@period-change="handlePeriodChange"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,6 +90,9 @@ export default {
|
||||
PeriodSelector,
|
||||
UnpaidInvoices,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return { period: 'This Year' };
|
||||
},
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
<div v-show="hasData" class="flex relative">
|
||||
<!-- Chart Legend -->
|
||||
<div class="w-1/2 flex flex-col gap-4 justify-center">
|
||||
<div class="w-1/2 flex flex-col gap-4 justify-center dark:text-gray-25">
|
||||
<!-- Ledgend Item -->
|
||||
<div
|
||||
v-for="(d, i) in expenses"
|
||||
@ -36,6 +36,7 @@
|
||||
:text-offset-x="6.5"
|
||||
:value-formatter="(value: number) => fyo.format(value, 'Currency')"
|
||||
:total-label="t`Total Spending`"
|
||||
:darkMode="darkMode"
|
||||
@change="(value: number) => (active = value)"
|
||||
/>
|
||||
</div>
|
||||
@ -45,7 +46,7 @@
|
||||
v-if="expenses.length === 0"
|
||||
class="flex-1 w-full h-full flex-center my-20"
|
||||
>
|
||||
<span class="text-base text-gray-600">
|
||||
<span class="text-base text-gray-600 dark:text-gray-500">
|
||||
{{ t`No expenses in this period` }}
|
||||
</span>
|
||||
</div>
|
||||
@ -64,8 +65,8 @@ import PeriodSelector from './PeriodSelector.vue';
|
||||
import SectionHeader from './SectionHeader.vue';
|
||||
|
||||
// Linting broken in this file cause of `extends: ...`
|
||||
/*
|
||||
eslint-disable @typescript-eslint/no-unsafe-argument,
|
||||
/*
|
||||
eslint-disable @typescript-eslint/no-unsafe-argument,
|
||||
@typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/restrict-plus-operands
|
||||
*/
|
||||
@ -76,14 +77,17 @@ export default defineComponent({
|
||||
PeriodSelector,
|
||||
SectionHeader,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
extends: DashboardChartBase,
|
||||
data: () => ({
|
||||
active: null as null | number,
|
||||
expenses: [] as {
|
||||
account: string;
|
||||
total: number;
|
||||
color: string;
|
||||
class: string;
|
||||
color: { color: string; darkColor: string };
|
||||
class: { class: string; darkClass: string };
|
||||
}[],
|
||||
}),
|
||||
computed: {
|
||||
@ -93,7 +97,11 @@ export default defineComponent({
|
||||
hasData(): boolean {
|
||||
return this.expenses.length > 0;
|
||||
},
|
||||
sectors(): { color: string; label: string; value: number }[] {
|
||||
sectors(): {
|
||||
color: { color: string; darkColor: string };
|
||||
label: string;
|
||||
value: number;
|
||||
}[] {
|
||||
return this.expenses.map(({ account, color, total }) => ({
|
||||
color,
|
||||
label: truncate(account, { length: 21 }),
|
||||
@ -111,7 +119,6 @@ export default defineComponent({
|
||||
fromDate.toISO(),
|
||||
toDate.toISO()
|
||||
);
|
||||
|
||||
const shades = [
|
||||
{ class: 'bg-pink-500', hex: uicolors.pink['500'] },
|
||||
{ class: 'bg-pink-400', hex: uicolors.pink['400'] },
|
||||
@ -120,13 +127,25 @@ export default defineComponent({
|
||||
{ class: 'bg-pink-100', hex: uicolors.pink['100'] },
|
||||
];
|
||||
|
||||
const darkshades = [
|
||||
{ class: 'bg-pink-600', hex: uicolors.pink['600'] },
|
||||
{ class: 'bg-pink-500', hex: uicolors.pink['500'] },
|
||||
{ class: 'bg-pink-400', hex: uicolors.pink['400'] },
|
||||
{ class: 'bg-pink-300', hex: uicolors.pink['300'] },
|
||||
{
|
||||
class: 'bg-pink-200 dark:bg-opacity-80',
|
||||
hex: uicolors.pink['200'] + 'CC',
|
||||
},
|
||||
];
|
||||
|
||||
this.expenses = topExpenses
|
||||
.filter((e) => e.total > 0)
|
||||
.map((d, i) => {
|
||||
return {
|
||||
...d,
|
||||
color: shades[i].hex,
|
||||
class: shades[i].class,
|
||||
account: d.account,
|
||||
total: d.total,
|
||||
color: { color: shades[i].hex, darkColor: darkshades[i].hex },
|
||||
class: { class: shades[i].class, darkClass: darkshades[i].class },
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -14,14 +14,20 @@
|
||||
flex
|
||||
focus:outline-none
|
||||
hover:text-gray-800
|
||||
dark:hover:text-gray-100
|
||||
focus:text-gray-800
|
||||
dark:focus:text-gray-100
|
||||
items-center
|
||||
py-1
|
||||
rounded-md
|
||||
leading-relaxed
|
||||
cursor-pointer
|
||||
"
|
||||
:class="!value ? 'text-gray-600' : 'text-gray-900'"
|
||||
:class="
|
||||
!value
|
||||
? 'text-gray-600 dark:text-gray-500'
|
||||
: 'text-gray-900 dark:text-gray-300'
|
||||
"
|
||||
tabindex="0"
|
||||
@click="toggleDropdown()"
|
||||
@keydown.down="highlightItemDown"
|
||||
|
@ -15,6 +15,8 @@
|
||||
class="mt-4"
|
||||
:aspect-ratio="2.05"
|
||||
:colors="chartData.colors"
|
||||
:gridColor="chartData.gridColor"
|
||||
:fontColor="chartData.fontColor"
|
||||
:points="chartData.points"
|
||||
:x-labels="chartData.xLabels"
|
||||
:format="chartData.format"
|
||||
@ -23,7 +25,7 @@
|
||||
:y-min="chartData.yMin"
|
||||
/>
|
||||
<div v-else class="flex-1 w-full h-full flex-center my-20">
|
||||
<span class="text-base text-gray-600">
|
||||
<span class="text-base text-gray-600 dark:text-gray-500">
|
||||
{{ t`No transactions yet` }}
|
||||
</span>
|
||||
</div>
|
||||
@ -42,8 +44,8 @@ import SectionHeader from './SectionHeader.vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
// Linting broken in this file cause of `extends: ...`
|
||||
/*
|
||||
eslint-disable @typescript-eslint/no-unsafe-argument,
|
||||
/*
|
||||
eslint-disable @typescript-eslint/no-unsafe-argument,
|
||||
@typescript-eslint/no-unsafe-return
|
||||
*/
|
||||
export default defineComponent({
|
||||
@ -53,6 +55,9 @@ export default defineComponent({
|
||||
SectionHeader,
|
||||
BarChart,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
extends: DashboardChartBase,
|
||||
data: () => ({
|
||||
data: [] as { yearmonth: string; balance: number }[],
|
||||
@ -63,7 +68,10 @@ export default defineComponent({
|
||||
chartData() {
|
||||
const points = [this.data.map((d) => d.balance)];
|
||||
const colors = [
|
||||
{ positive: uicolors.blue['500'], negative: uicolors.pink['500'] },
|
||||
{
|
||||
positive: uicolors.blue[this.darkMode ? '600' : '500'],
|
||||
negative: uicolors.pink[this.darkMode ? '600' : '500'],
|
||||
},
|
||||
];
|
||||
const format = (value: number) => fyo.format(value ?? 0, 'Currency');
|
||||
const yMax = getYMax(points);
|
||||
@ -76,6 +84,9 @@ export default defineComponent({
|
||||
yMax,
|
||||
yMin,
|
||||
formatX: formatXLabels,
|
||||
gridColor: this.darkMode ? 'rgba(200, 200, 200, 0.2)' : undefined,
|
||||
fontColor: this.darkMode ? uicolors.gray['400'] : undefined,
|
||||
zeroLineColor: this.darkMode ? uicolors.gray['400'] : undefined,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<div class="flex items-baseline justify-between dark:text-white">
|
||||
<span class="font-semibold text-base"><slot name="title"></slot></span>
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
|
@ -14,34 +14,38 @@
|
||||
<div class="flex justify-between">
|
||||
<!-- Paid -->
|
||||
<div
|
||||
class="text-sm font-medium"
|
||||
class="text-sm font-medium dark:text-gray-25"
|
||||
:class="{
|
||||
'bg-gray-200 text-gray-200 rounded': !count,
|
||||
'bg-gray-200 dark:bg-gray-700 text-gray-200 dark:text-gray-700 rounded':
|
||||
!count,
|
||||
'cursor-pointer': paidCount > 0,
|
||||
}"
|
||||
:title="paidCount > 0 ? t`View Paid Invoices` : ''"
|
||||
@click="() => routeToInvoices('paid')"
|
||||
>
|
||||
{{ fyo.format(paid, 'Currency') }}
|
||||
<span :class="{ 'text-gray-900 font-normal': count }">{{
|
||||
t`Paid`
|
||||
}}</span>
|
||||
<span
|
||||
:class="{ 'text-gray-900 dark:text-gray-200 font-normal': count }"
|
||||
>{{ t`Paid` }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Unpaid -->
|
||||
<div
|
||||
class="text-sm font-medium"
|
||||
class="text-sm font-medium dark:text-gray-25"
|
||||
:class="{
|
||||
'bg-gray-200 text-gray-200 rounded': !count,
|
||||
'bg-gray-200 dark:bg-gray-700 text-gray-200 dark:text-gray-700 rounded':
|
||||
!count,
|
||||
'cursor-pointer': unpaidCount > 0,
|
||||
}"
|
||||
:title="unpaidCount > 0 ? t`View Unpaid Invoices` : ''"
|
||||
@click="() => routeToInvoices('unpaid')"
|
||||
>
|
||||
{{ fyo.format(unpaid, 'Currency') }}
|
||||
<span :class="{ 'text-gray-900 font-normal': count }">{{
|
||||
t`Unpaid`
|
||||
}}</span>
|
||||
<span
|
||||
:class="{ 'text-gray-900 dark:text-gray-200 font-normal': count }"
|
||||
>{{ t`Unpaid` }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -64,7 +68,17 @@
|
||||
:offset="15"
|
||||
:show="show"
|
||||
placement="top"
|
||||
class="text-sm shadow-md px-2 py-1 bg-white text-gray-900 border-s-4"
|
||||
class="
|
||||
text-sm
|
||||
shadow-md
|
||||
px-2
|
||||
py-1
|
||||
bg-white
|
||||
dark:bg-gray-900
|
||||
text-gray-900
|
||||
dark:text-white
|
||||
border-s-4
|
||||
"
|
||||
:style="{ borderColor: colors }"
|
||||
>
|
||||
<div class="flex justify-between gap-4">
|
||||
@ -110,6 +124,7 @@ export default defineComponent({
|
||||
extends: BaseDashboardChart,
|
||||
props: {
|
||||
schemaName: { type: String as PropType<string>, required: true },
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -144,25 +159,24 @@ export default defineComponent({
|
||||
if (this.schemaName === ModelNameEnum.SalesInvoice) {
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
return 'pink';
|
||||
},
|
||||
colors(): string {
|
||||
return uicolors[this.color]['500'];
|
||||
return uicolors[this.color][this.darkMode ? '600' : '500'];
|
||||
},
|
||||
paidColor(): string {
|
||||
if (!this.hasData) {
|
||||
return 'bg-gray-400';
|
||||
return this.darkMode ? 'bg-gray-700' : 'bg-gray-400';
|
||||
}
|
||||
|
||||
return `bg-${this.color}-500`;
|
||||
return `bg-${this.color}-${this.darkMode ? '600' : '500'}`;
|
||||
},
|
||||
unpaidColor(): string {
|
||||
if (!this.hasData) {
|
||||
return 'bg-gray-200';
|
||||
return `bg-gray-${this.darkMode ? '800' : '200'}`;
|
||||
}
|
||||
|
||||
return `bg-${this.color}-200`;
|
||||
return `bg-${this.color}-${this.darkMode ? '700 bg-opacity-20' : '200'}`;
|
||||
},
|
||||
},
|
||||
async activated() {
|
||||
|
@ -1,45 +1,61 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 flex justify-center items-center bg-gray-25"
|
||||
class="flex-1 flex justify-center items-center bg-gray-25 dark:bg-gray-900"
|
||||
:class="{
|
||||
'pointer-events-none': loadingDatabase,
|
||||
'window-drag': platform !== 'Windows',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="w-full w-form shadow-lg rounded-lg border relative bg-white"
|
||||
class="
|
||||
w-full w-form
|
||||
shadow-lg
|
||||
rounded-lg
|
||||
border
|
||||
dark:border-gray-800
|
||||
relative
|
||||
bg-white
|
||||
dark:bg-gray-875
|
||||
"
|
||||
style="height: 700px"
|
||||
>
|
||||
<!-- Welcome to Frappe Books -->
|
||||
<div class="px-4 py-4">
|
||||
<h1 class="text-2xl font-semibold select-none">
|
||||
<h1 class="text-2xl font-semibold select-none dark:text-gray-25">
|
||||
{{ t`Welcome to Frappe Books` }}
|
||||
</h1>
|
||||
<p class="text-gray-600 text-base select-none">
|
||||
<p class="text-gray-600 dark:text-gray-400 text-base select-none">
|
||||
{{
|
||||
t`Create a new company or select an existing one from your computer`
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<!-- New File (Blue Icon) -->
|
||||
<div
|
||||
data-testid="create-new-file"
|
||||
class="px-4 h-row-largest flex flex-row items-center gap-4 p-2"
|
||||
:class="creatingDemo ? '' : 'hover:bg-gray-50 cursor-pointer'"
|
||||
:class="
|
||||
creatingDemo
|
||||
? ''
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-890 cursor-pointer'
|
||||
"
|
||||
@click="newDatabase"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-full bg-blue-500 relative flex-center">
|
||||
<feather-icon name="plus" class="text-white w-5 h-5" />
|
||||
<feather-icon
|
||||
name="plus"
|
||||
class="text-white dark:text-gray-900 w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
<p class="font-medium dark:text-gray-200">
|
||||
{{ t`New Company` }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`Create a new company and store it on your computer` }}
|
||||
</p>
|
||||
</div>
|
||||
@ -48,17 +64,34 @@
|
||||
<!-- Existing File (Green Icon) -->
|
||||
<div
|
||||
class="px-4 h-row-largest flex flex-row items-center gap-4 p-2"
|
||||
:class="creatingDemo ? '' : 'hover:bg-gray-50 cursor-pointer'"
|
||||
:class="
|
||||
creatingDemo
|
||||
? ''
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-890 cursor-pointer'
|
||||
"
|
||||
@click="existingDatabase"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-full bg-green-500 relative flex-center">
|
||||
<feather-icon name="upload" class="w-4 h-4 text-white" />
|
||||
<div
|
||||
class="
|
||||
w-8
|
||||
h-8
|
||||
rounded-full
|
||||
bg-green-500
|
||||
dark:bg-green-600
|
||||
relative
|
||||
flex-center
|
||||
"
|
||||
>
|
||||
<feather-icon
|
||||
name="upload"
|
||||
class="w-4 h-4 text-white dark:text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
<p class="font-medium dark:text-gray-200">
|
||||
{{ t`Existing Company` }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`Load an existing company from your computer` }}
|
||||
</p>
|
||||
</div>
|
||||
@ -68,22 +101,36 @@
|
||||
<div
|
||||
v-if="!files?.length"
|
||||
class="px-4 h-row-largest flex flex-row items-center gap-4 p-2"
|
||||
:class="creatingDemo ? '' : 'hover:bg-gray-50 cursor-pointer'"
|
||||
:class="
|
||||
creatingDemo
|
||||
? ''
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-890 cursor-pointer'
|
||||
"
|
||||
@click="createDemo"
|
||||
>
|
||||
<div class="w-8 h-8 rounded-full bg-pink-500 relative flex-center">
|
||||
<div
|
||||
class="
|
||||
w-8
|
||||
h-8
|
||||
rounded-full
|
||||
bg-pink-500
|
||||
dark:bg-pink-600
|
||||
relative
|
||||
flex-center
|
||||
"
|
||||
>
|
||||
<feather-icon name="monitor" class="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">
|
||||
<p class="font-medium dark:text-gray-200">
|
||||
{{ t`Create Demo` }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`Create a demo company to try out Frappe Books` }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<!-- File List -->
|
||||
<div class="overflow-y-auto" style="max-height: 340px">
|
||||
@ -91,7 +138,11 @@
|
||||
v-for="(file, i) in files"
|
||||
:key="file.dbPath"
|
||||
class="h-row-largest px-4 flex gap-4 items-center"
|
||||
:class="creatingDemo ? '' : 'hover:bg-gray-50 cursor-pointer'"
|
||||
:class="
|
||||
creatingDemo
|
||||
? ''
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-890 cursor-pointer'
|
||||
"
|
||||
:title="t`${file.companyName} stored at ${file.dbPath}`"
|
||||
@click="selectFile(file)"
|
||||
>
|
||||
@ -104,6 +155,7 @@
|
||||
justify-center
|
||||
items-center
|
||||
bg-gray-200
|
||||
dark:bg-gray-800
|
||||
text-gray-500
|
||||
font-semibold
|
||||
flex-shrink-0
|
||||
@ -114,16 +166,23 @@
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between overflow-x-auto items-baseline">
|
||||
<h2 class="font-medium">
|
||||
<h2 class="font-medium dark:text-gray-200">
|
||||
{{ file.companyName }}
|
||||
</h2>
|
||||
<p class="whitespace-nowrap text-sm text-gray-600">
|
||||
<p
|
||||
class="
|
||||
whitespace-nowrap
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-400
|
||||
"
|
||||
>
|
||||
{{ formatDate(file.modified) }}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
class="
|
||||
text-sm text-gray-600
|
||||
dark:text-gray-400
|
||||
overflow-x-auto
|
||||
no-scrollbar
|
||||
whitespace-nowrap
|
||||
@ -137,11 +196,14 @@
|
||||
ms-auto
|
||||
p-2
|
||||
hover:bg-red-200
|
||||
dark:hover:bg-red-900 dark:hover:bg-opacity-40
|
||||
rounded-full
|
||||
w-8
|
||||
h-8
|
||||
text-gray-600
|
||||
dark:text-gray-400
|
||||
hover:text-red-400
|
||||
dark:hover:text-red-200
|
||||
"
|
||||
@click.stop="() => deleteDb(i)"
|
||||
>
|
||||
@ -149,7 +211,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr v-if="files?.length" />
|
||||
<hr v-if="files?.length" class="dark:border-gray-800" />
|
||||
|
||||
<!-- Language Selector -->
|
||||
<div
|
||||
@ -161,6 +223,7 @@
|
||||
absolute
|
||||
p-4
|
||||
text-gray-900
|
||||
dark:text-gray-100
|
||||
"
|
||||
style="top: 100%; transform: translateY(-100%)"
|
||||
>
|
||||
@ -170,7 +233,9 @@
|
||||
class="
|
||||
text-sm
|
||||
bg-gray-100
|
||||
dark:bg-gray-890
|
||||
hover:bg-gray-200
|
||||
dark:hover:bg-gray-900
|
||||
rounded
|
||||
px-4
|
||||
py-1.5
|
||||
@ -198,21 +263,25 @@
|
||||
|
||||
<!-- Base Count Selection when Dev -->
|
||||
<Modal :open-modal="openModal" @closemodal="openModal = false">
|
||||
<div class="p-4 text-gray-900 w-form">
|
||||
<div class="p-4 text-gray-900 dark:text-gray-100 w-form">
|
||||
<h2 class="text-xl font-semibold select-none">Set Base Count</h2>
|
||||
<p class="text-base mt-2">
|
||||
Base Count is a lower bound on the number of entries made when
|
||||
creating the dummy instance.
|
||||
</p>
|
||||
<div class="flex my-12 justify-center items-baseline gap-4 text-base">
|
||||
<label for="basecount" class="text-gray-600">Base Count</label>
|
||||
<label for="basecount" class="text-gray-600 dark:text-gray-400"
|
||||
>Base Count</label
|
||||
>
|
||||
<input
|
||||
v-model="baseCount"
|
||||
type="number"
|
||||
name="basecount"
|
||||
class="
|
||||
bg-gray-100
|
||||
dark:bg-gray-875
|
||||
focus:bg-gray-200
|
||||
dark:focus:bg-gray-890
|
||||
rounded-md
|
||||
px-2
|
||||
py-1
|
||||
|
@ -8,15 +8,35 @@ import { toggleSidebar } from 'src/utils/ui';
|
||||
<!-- eslint-disable vue/require-explicit-emits -->
|
||||
<Sidebar
|
||||
v-show="showSidebar"
|
||||
class="flex-shrink-0 border-e whitespace-nowrap w-sidebar"
|
||||
class="
|
||||
flex-shrink-0
|
||||
border-e
|
||||
dark:border-gray-800
|
||||
whitespace-nowrap
|
||||
w-sidebar
|
||||
"
|
||||
:darkMode="darkMode"
|
||||
@change-db-file="$emit('change-db-file')"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div class="flex flex-1 overflow-y-hidden bg-white">
|
||||
<div
|
||||
class="
|
||||
flex flex-1
|
||||
overflow-y-hidden
|
||||
custom-scroll custom-scroll-thumb1
|
||||
bg-white
|
||||
dark:bg-gray-875
|
||||
"
|
||||
>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive>
|
||||
<component :is="Component" :key="$route.path" class="flex-1" />
|
||||
<component
|
||||
:is="Component"
|
||||
:key="$route.path"
|
||||
:darkMode="darkMode"
|
||||
class="flex-1"
|
||||
/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
|
||||
@ -26,6 +46,7 @@ import { toggleSidebar } from 'src/utils/ui';
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.query.schemaName + route.query.name"
|
||||
:darkMode="darkMode"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
@ -40,7 +61,9 @@ import { toggleSidebar } from 'src/utils/ui';
|
||||
bottom-0
|
||||
start-0
|
||||
text-gray-600
|
||||
bg-gray-100
|
||||
dark:text-gray-400
|
||||
hover:bg-gray-100
|
||||
dark:hover:bg-gray-900
|
||||
rounded
|
||||
rtl-rotate-180
|
||||
p-1
|
||||
@ -62,6 +85,9 @@ export default defineComponent({
|
||||
components: {
|
||||
Sidebar,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ['change-db-file'],
|
||||
});
|
||||
</script>
|
||||
|
@ -1,13 +1,19 @@
|
||||
<template>
|
||||
<div class="flex flex-col overflow-y-hidden">
|
||||
<PageHeader :title="t`Set Up Your Workspace`" />
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden custom-scroll">
|
||||
<div
|
||||
class="
|
||||
flex-1
|
||||
overflow-y-auto overflow-x-hidden
|
||||
custom-scroll custom-scroll-thumb1
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="section in sections"
|
||||
:key="section.label"
|
||||
class="p-4 border-b"
|
||||
class="p-4 border-b dark:border-gray-800"
|
||||
>
|
||||
<h2 class="font-medium">{{ section.label }}</h2>
|
||||
<h2 class="font-medium dark:text-gray-25">{{ section.label }}</h2>
|
||||
<div class="flex mt-4 gap-4">
|
||||
<div
|
||||
v-for="item in section.items"
|
||||
@ -15,7 +21,15 @@
|
||||
class="w-full md:w-1/3 sm:w-1/2"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-between h-40 p-4 border rounded-lg"
|
||||
class="
|
||||
flex flex-col
|
||||
justify-between
|
||||
h-40
|
||||
p-4
|
||||
border
|
||||
dark:border-gray-800 dark:text-gray-50
|
||||
rounded-lg
|
||||
"
|
||||
@mouseenter="() => (activeCard = item.key)"
|
||||
@mouseleave="() => (activeCard = null)"
|
||||
>
|
||||
@ -32,7 +46,7 @@
|
||||
class="w-5 h-5 mb-4"
|
||||
/>
|
||||
<h3 class="font-medium">{{ item.label }}</h3>
|
||||
<p class="mt-2 text-sm text-gray-800">
|
||||
<p class="mt-2 text-sm text-gray-800 dark:text-gray-400">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
@ -42,23 +56,19 @@
|
||||
>
|
||||
<Button
|
||||
v-if="item.action"
|
||||
class="leading-tight"
|
||||
class="leading-tight text-base"
|
||||
type="primary"
|
||||
@click="handleAction(item)"
|
||||
>
|
||||
<span class="text-base text-white">
|
||||
{{ t`Set Up` }}
|
||||
</span>
|
||||
{{ t`Set Up` }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="item.documentation"
|
||||
class="leading-tight"
|
||||
class="leading-tight text-base"
|
||||
:class="{ 'ms-4': item.action }"
|
||||
@click="handleDocumentation(item)"
|
||||
>
|
||||
<span class="text-base">
|
||||
{{ t`Documentation` }}
|
||||
</span>
|
||||
{{ t`Documentation` }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -88,6 +98,9 @@ export default defineComponent({
|
||||
Button,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
darkMode: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeCard: null as string | null,
|
||||
|
@ -56,6 +56,7 @@
|
||||
w-full
|
||||
gap-2
|
||||
border-b
|
||||
dark:border-gray-800
|
||||
p-4
|
||||
"
|
||||
>
|
||||
@ -69,7 +70,6 @@
|
||||
label: fyo.schemaMap[value]?.label ?? value,
|
||||
})),
|
||||
}"
|
||||
input-class="bg-transparent text-gray-900 text-base"
|
||||
class="w-40"
|
||||
:border="true"
|
||||
:value="importType"
|
||||
@ -83,7 +83,11 @@
|
||||
<p
|
||||
v-else
|
||||
class="text-base ms-2"
|
||||
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
||||
:class="
|
||||
fileName
|
||||
? 'text-gray-900 dark:text-gray-25 font-semibold'
|
||||
: 'text-gray-700 dark:text-gray-200'
|
||||
"
|
||||
>
|
||||
<span v-if="fileName" class="font-normal">{{ t`Selected` }} </span>
|
||||
{{ helperMessage }}{{ fileName ? ',' : '' }}
|
||||
@ -106,12 +110,23 @@
|
||||
<!-- Assignment Row and Value Grid container -->
|
||||
<div
|
||||
v-if="hasImporter"
|
||||
class="overflow-auto custom-scroll"
|
||||
class="overflow-auto custom-scroll custom-scroll-thumb1"
|
||||
style="max-height: calc(100vh - (2 * var(--h-row-largest)) - 2px)"
|
||||
>
|
||||
<!-- Column Assignment Row -->
|
||||
<div
|
||||
class="grid sticky top-0 py-4 pe-4 bg-white border-b gap-4"
|
||||
class="
|
||||
grid
|
||||
sticky
|
||||
top-0
|
||||
py-4
|
||||
pe-4
|
||||
bg-white
|
||||
dark:bg-gray-875
|
||||
border-b border-e
|
||||
dark:border-gray-800
|
||||
gap-4
|
||||
"
|
||||
style="z-index: 1; width: fit-content"
|
||||
:style="gridTemplateColumn"
|
||||
>
|
||||
@ -131,7 +146,17 @@
|
||||
<!-- Values Grid -->
|
||||
<div
|
||||
v-if="importer.valueMatrix.length"
|
||||
class="grid py-4 pe-4 bg-white gap-4"
|
||||
class="
|
||||
grid
|
||||
py-4
|
||||
pe-4
|
||||
bg-white
|
||||
dark:bg-gray-875
|
||||
gap-4
|
||||
border-e
|
||||
last:border-b
|
||||
dark:border-gray-800
|
||||
"
|
||||
style="width: fit-content"
|
||||
:style="gridTemplateColumn"
|
||||
>
|
||||
@ -180,7 +205,11 @@
|
||||
<!-- FormControl Field if Column is Assigned -->
|
||||
<FormControl
|
||||
v-else
|
||||
:class="val.error ? 'border border-red-300 rounded-md' : ''"
|
||||
:class="
|
||||
val.error
|
||||
? 'border border-red-300 dark:border-red-600 rounded-md'
|
||||
: ''
|
||||
"
|
||||
:title="getFieldTitle(val)"
|
||||
:df="
|
||||
importer.templateFieldsMap.get(
|
||||
@ -203,7 +232,15 @@
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="ps-4 text-gray-700 sticky left-0 flex items-center"
|
||||
class="
|
||||
ps-4
|
||||
text-gray-700
|
||||
dark:text-gray-300
|
||||
sticky
|
||||
left-0
|
||||
flex
|
||||
items-center
|
||||
"
|
||||
style="height: 62.5px"
|
||||
>
|
||||
{{ t`No rows added. Select a file or add rows.` }}
|
||||
@ -227,18 +264,20 @@
|
||||
<div class="w-form">
|
||||
<!-- Pick Column Header -->
|
||||
<FormHeader :form-title="t`Pick Import Columns`" />
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<!-- Pick Column Checkboxes -->
|
||||
<div
|
||||
v-for="[key, value] of columnPickerFieldsMap.entries()"
|
||||
:key="key"
|
||||
class="p-4 max-h-80 overflow-auto custom-scroll"
|
||||
class="p-4 max-h-80 overflow-auto custom-scroll custom-scroll-thumb1"
|
||||
>
|
||||
<h2 class="text-sm font-semibold text-gray-800">
|
||||
<h2 class="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
{{ key }}
|
||||
</h2>
|
||||
<div class="grid grid-cols-3 border rounded mt-1">
|
||||
<div
|
||||
class="grid grid-cols-3 border dark:border-gray-800 rounded mt-1"
|
||||
>
|
||||
<div
|
||||
v-for="tf of value"
|
||||
:key="tf.fieldKey"
|
||||
@ -261,9 +300,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Pick Column Footer -->
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<div class="p-4 flex justify-between items-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`${numColumnsPicked} fields selected` }}
|
||||
</p>
|
||||
<Button type="primary" @click="showColumnPicker = false">{{
|
||||
@ -278,13 +317,15 @@
|
||||
<div class="w-form">
|
||||
<!-- Import Completed Header -->
|
||||
<FormHeader :form-title="t`Import Complete`" />
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<!-- Success -->
|
||||
<div v-if="success.length > 0">
|
||||
<!-- Success Section Header -->
|
||||
<div class="flex justify-between px-4 pt-4 pb-1">
|
||||
<p class="text-base font-semibold">{{ t`Success` }}</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-base font-semibold dark:text-gray-200">
|
||||
{{ t`Success` }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{
|
||||
success.length === 1
|
||||
? t`${success.length} entry imported`
|
||||
@ -293,7 +334,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- Success Body -->
|
||||
<div class="max-h-40 overflow-auto text-gray-900">
|
||||
<div class="max-h-40 overflow-auto text-gray-900 dark:text-gray-50">
|
||||
<div
|
||||
v-for="(name, i) of success"
|
||||
:key="name"
|
||||
@ -306,7 +347,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
</div>
|
||||
|
||||
<!-- Failed -->
|
||||
@ -314,7 +355,7 @@
|
||||
<!-- Failed Section Header -->
|
||||
<div class="flex justify-between px-4 pt-4 pb-1">
|
||||
<p class="text-base font-semibold">{{ t`Failed` }}</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{
|
||||
failed.length === 1
|
||||
? t`${failed.length} entry failed`
|
||||
@ -323,7 +364,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- Failed Body -->
|
||||
<div class="max-h-40 overflow-auto text-gray-900">
|
||||
<div class="max-h-40 overflow-auto text-gray-900 dark:text-gray-50">
|
||||
<div
|
||||
v-for="(f, i) of failed"
|
||||
:key="f.name"
|
||||
@ -345,7 +386,7 @@
|
||||
<!-- Fallback Div -->
|
||||
<div
|
||||
v-if="failed.length === 0 && success.length === 0"
|
||||
class="p-4 text-base"
|
||||
class="p-4 text-base dark:text-gray-200"
|
||||
>
|
||||
{{ t`No entries were imported.` }}
|
||||
</div>
|
||||
@ -964,6 +1005,6 @@ export default defineComponent({
|
||||
</script>
|
||||
<style scoped>
|
||||
.index-cell {
|
||||
@apply flex pe-4 justify-end items-center border-e bg-white sticky left-0 -my-4 text-gray-600;
|
||||
@apply flex pe-4 justify-end items-center border-e last:border-b dark:border-gray-800 bg-white dark:bg-gray-875 sticky left-0 -my-4 text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
</style>
|
||||
|
@ -7,9 +7,9 @@
|
||||
paddingRight: dataSlice.length > 13 ? 'var(--w-scrollbar)' : '',
|
||||
}"
|
||||
>
|
||||
<p class="w-8 text-end me-4 text-gray-700">#</p>
|
||||
<p class="w-8 text-end me-4 text-gray-700 dark:text-gray-400">#</p>
|
||||
<Row
|
||||
class="flex-1 text-gray-700 h-row-mid"
|
||||
class="flex-1 text-gray-700 dark:text-gray-400 h-row-mid"
|
||||
:column-count="columns.length"
|
||||
gap="1rem"
|
||||
>
|
||||
@ -33,19 +33,32 @@
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
|
||||
<!-- Data Rows -->
|
||||
<div v-if="dataSlice.length !== 0" class="overflow-y-auto custom-scroll">
|
||||
<div
|
||||
v-if="dataSlice.length !== 0"
|
||||
class="
|
||||
overflow-y-auto
|
||||
dark:dark-scroll
|
||||
custom-scroll custom-scroll-thumb1
|
||||
"
|
||||
>
|
||||
<div v-for="(row, i) in dataSlice" :key="(row.name as string)">
|
||||
<!-- Row Content -->
|
||||
<div class="flex hover:bg-gray-50 items-center">
|
||||
<p class="w-8 text-end me-4 text-gray-900">
|
||||
<div class="flex hover:bg-gray-50 dark:hover:bg-gray-850 items-center">
|
||||
<p class="w-8 text-end me-4 text-gray-900 dark:text-gray-25">
|
||||
{{ i + pageStart + 1 }}
|
||||
</p>
|
||||
<Row
|
||||
gap="1rem"
|
||||
class="cursor-pointer text-gray-900 flex-1 h-row-mid"
|
||||
class="
|
||||
cursor-pointer
|
||||
text-gray-900
|
||||
dark:text-gray-300
|
||||
flex-1
|
||||
h-row-mid
|
||||
"
|
||||
:column-count="columns.length"
|
||||
@click="$emit('openDoc', row.name)"
|
||||
>
|
||||
@ -61,13 +74,16 @@
|
||||
/>
|
||||
</Row>
|
||||
</div>
|
||||
<hr v-if="!(i === dataSlice.length - 1 && i > 13)" />
|
||||
<hr
|
||||
v-if="!(i === dataSlice.length - 1 && i > 13)"
|
||||
class="dark:border-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Footer -->
|
||||
<div v-if="data?.length" class="mt-auto">
|
||||
<hr />
|
||||
<hr class="dark:border-gray-800" />
|
||||
<Paginator
|
||||
:item-count="data.length"
|
||||
class="px-4"
|
||||
@ -81,13 +97,10 @@
|
||||
class="flex flex-col items-center justify-center my-auto"
|
||||
>
|
||||
<img src="../../assets/img/list-empty-state.svg" alt="" class="w-24" />
|
||||
<p class="my-3 text-gray-800">{{ t`No entries found` }}</p>
|
||||
<Button
|
||||
v-if="canCreate"
|
||||
type="primary"
|
||||
class="text-white"
|
||||
@click="$emit('makeNewDoc')"
|
||||
>
|
||||
<p class="my-3 text-gray-800 dark:text-gray-200">
|
||||
{{ t`No entries found` }}
|
||||
</p>
|
||||
<Button v-if="canCreate" type="primary" @click="$emit('makeNewDoc')">
|
||||
{{ t`Make Entry` }}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<Modal :open-modal="openModal" class="w-3/6 p-4">
|
||||
<h1 class="text-xl font-semibold text-center pb-4">Close POS Shift</h1>
|
||||
<h1 class="text-xl font-semibold text-center dark:text-gray-100 pb-4">
|
||||
Close POS Shift
|
||||
</h1>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-medium">Closing Cash</h2>
|
||||
<h2 class="mt-4 mb-2 text-lg font-medium dark:text-gray-100">
|
||||
Closing Cash
|
||||
</h2>
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="text-base"
|
||||
@ -14,7 +18,9 @@
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<h2 class="mt-6 mb-2 text-lg font-medium">Closing Amounts</h2>
|
||||
<h2 class="mt-6 mb-2 text-lg dark:text-gray-100 font-medium">
|
||||
Closing Amounts
|
||||
</h2>
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
class="text-base"
|
||||
@ -26,9 +32,9 @@
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 flex items-end">
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 items-end">
|
||||
<Button
|
||||
class="w-full py-5 bg-red-500"
|
||||
class="w-full py-5 bg-red-500 dark:bg-red-700"
|
||||
@click="$emit('toggleModal', 'ShiftClose', false)"
|
||||
>
|
||||
<slot>
|
||||
@ -38,7 +44,10 @@
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button class="w-full py-5 bg-green-500" @click="handleSubmit">
|
||||
<Button
|
||||
class="w-full py-5 bg-green-500 dark:bg-green-700"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
|
@ -1,10 +1,14 @@
|
||||
<template>
|
||||
<Modal class="w-3/6 p-4">
|
||||
<h1 class="text-xl font-semibold text-center pb-4">Open POS Shift</h1>
|
||||
<h1 class="text-xl font-semibold text-center dark:text-gray-100 pb-4">
|
||||
Open POS Shift
|
||||
</h1>
|
||||
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<div class="col-span-6">
|
||||
<h2 class="text-lg font-medium">Cash In Denominations</h2>
|
||||
<h2 class="text-lg font-medium dark:text-gray-100">
|
||||
Cash In Denominations
|
||||
</h2>
|
||||
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
@ -18,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-span-6">
|
||||
<h2 class="text-lg font-medium">Opening Amount</h2>
|
||||
<h2 class="text-lg font-medium dark:text-gray-100">Opening Amount</h2>
|
||||
|
||||
<Table
|
||||
v-if="isValuesSeeded"
|
||||
@ -32,8 +36,11 @@
|
||||
@row-change="handleChange"
|
||||
/>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 flex items-end">
|
||||
<Button class="w-full py-5 bg-red-500" @click="$router.back()">
|
||||
<div class="mt-4 grid grid-cols-2 gap-4 items-end">
|
||||
<Button
|
||||
class="w-full py-5 bg-red-500 dark:bg-red-700"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Back` }}
|
||||
@ -41,7 +48,10 @@
|
||||
</slot>
|
||||
</Button>
|
||||
|
||||
<Button class="w-full py-5 bg-green-500" @click="handleSubmit">
|
||||
<Button
|
||||
class="w-full py-5 bg-green-500 dark:bg-green-700"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<slot>
|
||||
<p class="uppercase text-lg text-white font-semibold">
|
||||
{{ t`Submit` }}
|
||||
|
@ -2,7 +2,10 @@
|
||||
<div class="">
|
||||
<PageHeader :title="t`Point of Sale`">
|
||||
<slot>
|
||||
<Button class="bg-red-500" @click="toggleModal('ShiftClose')">
|
||||
<Button
|
||||
class="bg-red-500 dark:bg-red-700"
|
||||
@click="toggleModal('ShiftClose')"
|
||||
>
|
||||
<span class="font-medium text-white">{{ t`Close POS Shift ` }}</span>
|
||||
</Button>
|
||||
</slot>
|
||||
@ -30,10 +33,19 @@
|
||||
/>
|
||||
|
||||
<div
|
||||
class="bg-gray-25 gap-2 grid grid-cols-12 p-4"
|
||||
class="bg-gray-25 dark:bg-gray-875 gap-2 grid grid-cols-12 p-4"
|
||||
style="height: calc(100vh - var(--h-row-largest))"
|
||||
>
|
||||
<div class="bg-white border col-span-5 rounded-md">
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
border
|
||||
dark:border-gray-800
|
||||
col-span-5
|
||||
rounded-md
|
||||
"
|
||||
>
|
||||
<div class="rounded-md p-4 col-span-5">
|
||||
<div class="flex gap-x-2">
|
||||
<!-- Item Search -->
|
||||
@ -74,7 +86,18 @@
|
||||
|
||||
<div class="col-span-7">
|
||||
<div class="flex flex-col gap-3" style="height: calc(100vh - 6rem)">
|
||||
<div class="bg-white border grow h-full p-4 rounded-md">
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
border
|
||||
dark:border-gray-800
|
||||
grow
|
||||
h-full
|
||||
p-4
|
||||
rounded-md
|
||||
"
|
||||
>
|
||||
<!-- Customer Search -->
|
||||
<Link
|
||||
v-if="sinvDoc.fieldMap"
|
||||
@ -88,7 +111,16 @@
|
||||
<SelectedItemTable />
|
||||
</div>
|
||||
|
||||
<div class="bg-white border p-4 rounded-md">
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
border
|
||||
dark:border-gray-800
|
||||
p-4
|
||||
rounded-md
|
||||
"
|
||||
>
|
||||
<div class="w-full grid grid-cols-2 gap-y-2 gap-x-3">
|
||||
<div class="">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@ -146,7 +178,7 @@
|
||||
|
||||
<div class="">
|
||||
<Button
|
||||
class="w-full bg-red-500 py-6"
|
||||
class="w-full bg-red-500 dark:bg-red-700 py-6"
|
||||
:disabled="!sinvDoc.items?.length"
|
||||
@click="clearValues"
|
||||
>
|
||||
@ -158,7 +190,7 @@
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
class="mt-4 w-full bg-green-500 py-6"
|
||||
class="mt-4 w-full bg-green-500 dark:bg-green-700 py-6"
|
||||
:disabled="disablePayButton"
|
||||
@click="toggleModal('Payment', true)"
|
||||
>
|
||||
@ -417,6 +449,9 @@ export default defineComponent({
|
||||
invItem.quantity = (invItem.quantity as number) + 1;
|
||||
invItem.rate = item.rate as Money;
|
||||
|
||||
await this.applyPricingRule();
|
||||
await this.sinvDoc.runFormulas();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -432,6 +467,7 @@ export default defineComponent({
|
||||
message: t`${error as string}`,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -447,6 +483,9 @@ export default defineComponent({
|
||||
rate: item.rate as Money,
|
||||
item: item.name,
|
||||
});
|
||||
|
||||
await this.applyPricingRule();
|
||||
await this.sinvDoc.runFormulas();
|
||||
},
|
||||
async createTransaction(shouldPrint = false) {
|
||||
try {
|
||||
@ -599,8 +638,16 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sinvDoc.appendPricingRuleDetail(hasPricingRules);
|
||||
await this.sinvDoc.applyProductDiscount();
|
||||
setTimeout(async () => {
|
||||
const appliedPricingRuleCount = this.sinvDoc?.items?.filter(
|
||||
(val) => val.isFreeItem
|
||||
).length;
|
||||
|
||||
if (appliedPricingRuleCount !== hasPricingRules?.length) {
|
||||
await this.sinvDoc.appendPricingRuleDetail(hasPricingRules);
|
||||
await this.sinvDoc.applyProductDiscount();
|
||||
}
|
||||
}, 1);
|
||||
},
|
||||
|
||||
getItem,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 bg-gray-25">
|
||||
<PageHeader :border="true" :title="t`Print View`" class="bg-white">
|
||||
<div class="flex flex-col flex-1 bg-gray-25 dark:bg-gray-875">
|
||||
<PageHeader :border="true" :title="t`Print View`">
|
||||
<AutoComplete
|
||||
v-if="templateList.length"
|
||||
:df="{
|
||||
@ -22,9 +22,12 @@
|
||||
</PageHeader>
|
||||
|
||||
<!-- Template Display Area -->
|
||||
<div class="overflow-auto custom-scroll p-4">
|
||||
<div class="overflow-auto custom-scroll custom-scroll-thumb1 p-4">
|
||||
<!-- Display Hints -->
|
||||
<div v-if="helperMessage" class="text-sm text-gray-700">
|
||||
<div
|
||||
v-if="helperMessage"
|
||||
class="text-sm text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{{ helperMessage }}
|
||||
</div>
|
||||
|
||||
|
@ -6,10 +6,20 @@
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<div class="outer-container">
|
||||
<div
|
||||
class="outer-container overflow-y-auto custom-scroll custom-scroll-thumb1"
|
||||
>
|
||||
<!-- Report Print Display Area -->
|
||||
<div
|
||||
class="p-4 bg-gray-25 overflow-auto flex justify-center custom-scroll"
|
||||
class="
|
||||
p-4
|
||||
bg-gray-25
|
||||
dark:bg-gray-890
|
||||
overflow-auto
|
||||
flex
|
||||
justify-center
|
||||
custom-scroll custom-scroll-thumb1
|
||||
"
|
||||
>
|
||||
<!-- Report Print Display Container -->
|
||||
<ScaledContainer
|
||||
@ -57,8 +67,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Report Print Settings -->
|
||||
<div v-if="report" class="border-l flex flex-col">
|
||||
<p class="p-4 text-sm text-gray-600">
|
||||
<div v-if="report" class="border-l dark:border-gray-800 flex flex-col">
|
||||
<p class="p-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{{
|
||||
[
|
||||
t`Hidden values will be visible on Print on.`,
|
||||
@ -67,7 +77,7 @@
|
||||
}}
|
||||
</p>
|
||||
<!-- Row Selection -->
|
||||
<div class="p-4 border-t">
|
||||
<div class="p-4 border-t dark:border-gray-800">
|
||||
<Int
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
@ -98,7 +108,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Size Selection -->
|
||||
<div class="border-t p-4">
|
||||
<div class="border-t dark:border-gray-800 p-4">
|
||||
<Select
|
||||
:show-label="true"
|
||||
:border="true"
|
||||
@ -121,11 +131,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Pick Columns -->
|
||||
<div class="border-t p-4">
|
||||
<h2 class="text-sm text-gray-600">
|
||||
<div class="border-t dark:border-gray-800 p-4">
|
||||
<h2 class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ t`Pick Columns` }}
|
||||
</h2>
|
||||
<div class="border rounded grid grid-cols-2 mt-1">
|
||||
<div
|
||||
class="border dark:border-gray-800 rounded grid grid-cols-2 mt-1"
|
||||
>
|
||||
<Check
|
||||
v-for="(col, i) of report?.columns"
|
||||
:key="col.fieldname"
|
||||
|
@ -1,5 +1,15 @@
|
||||
<template>
|
||||
<div class="border-s h-full overflow-auto w-quick-edit bg-white">
|
||||
<div
|
||||
class="
|
||||
border-s
|
||||
dark:border-gray-800
|
||||
h-full
|
||||
overflow-auto
|
||||
w-quick-edit
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
"
|
||||
>
|
||||
<!-- Quick edit Tool bar -->
|
||||
<div
|
||||
class="
|
||||
@ -11,6 +21,7 @@
|
||||
sticky
|
||||
top-0
|
||||
bg-white
|
||||
dark:bg-gray-850
|
||||
"
|
||||
style="z-index: 1"
|
||||
>
|
||||
@ -20,20 +31,13 @@
|
||||
</Button>
|
||||
|
||||
<!-- Save & Submit Buttons -->
|
||||
<Button
|
||||
v-if="doc?.canSave"
|
||||
:icon="true"
|
||||
type="primary"
|
||||
class="text-white text-xs"
|
||||
@click="sync"
|
||||
>
|
||||
<Button v-if="doc?.canSave" :icon="true" type="primary" @click="sync">
|
||||
{{ t`Save` }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="doc?.canSubmit"
|
||||
:icon="true"
|
||||
type="primary"
|
||||
class="text-white text-xs"
|
||||
@click="submit"
|
||||
>
|
||||
{{ t`Submit` }}
|
||||
@ -43,7 +47,7 @@
|
||||
<!-- Name and image -->
|
||||
<div
|
||||
v-if="doc && (titleField || imageField)"
|
||||
class="items-center border-b border-t"
|
||||
class="items-center border-b border-t dark:border-gray-800"
|
||||
:class="imageField ? 'grid' : 'flex justify-center'"
|
||||
:style="{
|
||||
height: `calc(var(--h-row-mid) * ${!!imageField ? '2 + 1px' : '1'})`,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user