Upgrade to vue3 (#16)

* upgrade to vue 3.x and framework7 8.x
* change calendar plugin to vue-datepicker
* disable export button when user does not hava any transaction
* implement new pin code input
* append thousands separator in amount in exchange rates page
This commit is contained in:
mayswind
2023-04-21 01:45:00 +08:00
committed by GitHub
parent 4b0f7d45e8
commit b1c765eb51
89 changed files with 8353 additions and 16671 deletions
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
'root': true,
'env': {
'node': true
},
'extends': [
'eslint:recommended',
'plugin:vue/vue3-essential'
],
'rules': {
"vue/multi-word-component-names": "off",
"vue/no-use-v-if-with-v-for": "off"
}
}
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
-1
View File
@@ -135,7 +135,6 @@ build_frontend() {
npm install
echo "Building frontend files ($RELEASE_TYPE)..."
export NODE_OPTIONS=--openssl-legacy-provider
npm run build -- "$frontend_build_arguments"
}
+5 -1
View File
@@ -110,13 +110,17 @@ func startWebServer(c *cli.Context) error {
router.Static("/mobile/css", filepath.Join(config.StaticRootPath, "css"))
router.Static("/mobile/img", filepath.Join(config.StaticRootPath, "img"))
router.Static("/mobile/fonts", filepath.Join(config.StaticRootPath, "fonts"))
router.Static("/mobile/sw", filepath.Join(config.StaticRootPath, "sw"))
router.StaticFile("/mobile/favicon.ico", filepath.Join(config.StaticRootPath, "favicon.ico"))
router.StaticFile("/mobile/favicon.png", filepath.Join(config.StaticRootPath, "favicon.png"))
router.StaticFile("/mobile/touchicon.png", filepath.Join(config.StaticRootPath, "touchicon.png"))
router.StaticFile("/mobile/manifest.json", filepath.Join(config.StaticRootPath, "manifest.json"))
router.StaticFile("/mobile/sw.js", filepath.Join(config.StaticRootPath, "sw.js"))
workboxFileNames := utils.ListFileNamesWithPrefixAndSuffix(config.StaticRootPath, "workbox-", ".js")
for i := 0; i < len(workboxFileNames); i++ {
router.StaticFile("/mobile/"+workboxFileNames[i], filepath.Join(config.StaticRootPath, workboxFileNames[i]))
}
desktopEntryRoute := router.Group("/desktop")
desktopEntryRoute.Use(bindMiddleware(middlewares.ServerSettingsCookie(config)))
{
+3217 -12325
View File
File diff suppressed because it is too large Load Diff
+24 -37
View File
@@ -12,56 +12,43 @@
"url": "https://github.com/mayswind/ezbookkeeping/issues"
},
"scripts": {
"serve": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "set NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
"lint": "vue-cli-service lint"
"serve": "cross-env NODE_ENV=development vite",
"build": "cross-env NODE_ENV=production vite build",
"serve:dist": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
},
"dependencies": {
"axios": "^1.3.5",
"@vuepic/vue-datepicker": "^4.4.0",
"axios": "^1.3.6",
"cbor-js": "^0.1.0",
"core-js": "^3.30.0",
"clipboard": "^2.0.11",
"crypto-js": "^4.1.1",
"framework7": "^5.7.14",
"dom7": "^4.0.6",
"framework7": "^8.0.3",
"framework7-icons": "^5.0.5",
"framework7-vue": "^5.7.14",
"framework7-vue": "^8.0.3",
"js-cookie": "^3.0.1",
"line-awesome": "^1.3.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1",
"swiper": "^9.2.3",
"ua-parser-js": "^1.0.35",
"vue": "^2.7.14",
"vue-clipboard2": "^0.3.3",
"vue-i18n": "^8.28.2",
"vue-pincode-input": "^0.4.0",
"vuex": "^3.6.2"
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.19",
"@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-plugin-pwa": "^4.5.19",
"@vue/cli-service": "^4.5.19",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^6.2.2",
"git-revision-webpack-plugin": "^3.0.6",
"moment-locales-webpack-plugin": "^1.2.0",
"vue-template-compiler": "^2.7.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
"@vitejs/plugin-vue": "^4.1.0",
"@vue/compiler-sfc": "^3.2.47",
"cross-env": "^7.0.3",
"eslint": "^8.38.0",
"eslint-plugin-vue": "^9.11.0",
"git-rev-sync": "^3.0.2",
"postcss-preset-env": "^8.3.2",
"vite": "^4.2.2",
"vite-plugin-pwa": "^0.14.7"
},
"browserslist": [
"> 1%",
+30
View File
@@ -3,8 +3,38 @@ package utils
import (
"io"
"os"
"strings"
)
// ListFileNamesWithPrefixAndSuffix returns file name list which has specified prefix and suffix
func ListFileNamesWithPrefixAndSuffix(path string, prefix string, suffix string) []string {
dir, err := os.Open(path)
if err != nil {
return nil
}
fileInfos, err := dir.Readdir(0)
if err != nil {
return nil
}
var fileNames []string
for i := 0; i < len(fileInfos); i++ {
fileInfo := fileInfos[i]
if !fileInfo.IsDir() &&
strings.HasPrefix(fileInfo.Name(), prefix) &&
strings.HasSuffix(fileInfo.Name(), suffix) {
fileNames = append(fileNames, fileInfo.Name())
}
}
return fileNames
}
// IsExists returns whether specified file or directory path exits
func IsExists(path string) (bool, error) {
_, err := os.Stat(path)
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'postcss-preset-env': {},
},
};
+199 -44
View File
@@ -1,10 +1,11 @@
<template>
<f7-app :params="f7params">
<f7-app v-bind="f7params">
<f7-view id="main-view" class="safe-areas" main url="/"></f7-view>
</f7-app>
</template>
<script>
import { f7ready } from 'framework7-vue';
import routes from './router/mobile.js';
export default {
@@ -14,10 +15,20 @@ export default {
return {
f7params: {
name: 'ezBookkeeping',
id: 'net.mayswind.ezbookkeeping',
theme: 'ios',
autoDarkTheme: self.$settings.isEnableAutoDarkMode(),
colors: {
primary: '#c67e48'
},
routes: routes,
darkMode: self.$settings.isEnableAutoDarkMode() ? 'auto' : false,
touch: {
disableContextMenu: true,
tapHold: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
scope: './',
},
actions: {
animate: self.$settings.isEnableAnimate(),
backdrop: true,
@@ -46,27 +57,16 @@ export default {
smartSelect: {
routableModals: false
},
touch: {
tapHold: true,
disableContextMenu: true
},
view: {
animate: self.$settings.isEnableAnimate(),
pushState: !self.isiOSHomeScreenMode(),
pushStateAnimate: false,
browserHistory: !self.isiOSHomeScreenMode(),
browserHistoryInitialMatch: true,
browserHistoryAnimate: false,
iosSwipeBackAnimateShadow: false,
mdSwipeBackAnimateShadow: false
},
calendar: {
locale: 'en',
openIn: 'customModal',
backdrop: true
},
serviceWorker: {
path: self.$settings.isProduction() ? './sw.js' : undefined,
scope: './',
}
}
},
isDarkMode: undefined
}
},
created() {
@@ -82,6 +82,27 @@ export default {
}
}
},
mounted() {
f7ready((f7) => {
this.isDarkMode = f7.darkMode;
this.setThemeColorMeta(f7.darkMode);
f7.on('pageBeforeOut', () => {
if (this.$ui.isModalShowing()) {
f7.actions.close('.actions-modal.modal-in', false);
f7.dialog.close('.dialog.modal-in', false);
f7.popover.close('.popover.modal-in', false);
f7.popup.close('.popup.modal-in', false);
f7.sheet.close('.sheet-modal.modal-in', false);
}
});
f7.on('darkModeChange', (isDarkMode) => {
this.isDarkMode = isDarkMode;
this.setThemeColorMeta(isDarkMode);
});
});
},
methods: {
isiOSHomeScreenMode() {
if ((/iphone|ipod|ipad/gi).test(navigator.platform) && (/Safari/i).test(navigator.appVersion) &&
@@ -91,6 +112,13 @@ export default {
}
return false;
},
setThemeColorMeta(isDarkMode) {
if (isDarkMode) {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#121212');
} else {
document.querySelector('meta[name=theme-color]').setAttribute('content', '#f6f6f8');
}
}
}
}
@@ -134,6 +162,10 @@ body {
pointer-events: none !important;
}
.skeleton-text {
pointer-events: none !important;
}
.segmented.readonly .button:not(.button-active) > span,
.list.readonly .item-content .item-title.item-label,
.list.readonly .item-content .item-title > .item-header {
@@ -142,19 +174,21 @@ body {
/** Replacing the default style of framework7 **/
:root {
--f7-theme-color: #c67e48;
--f7-theme-color-rgb: 198, 126, 72;
--f7-theme-color-shade: #af6a36;
--f7-theme-color-tint: #d09467;
--default-icon-color: var(--f7-text-color);
}
:root .theme-dark {
:root .dark {
--default-icon-color: var(--f7-text-color);
}
.ios .theme-dark, .ios.theme-dark {
.color-gray {
--f7-theme-color: #8e8e93;
--f7-theme-color-rgb: 142, 142, 147;
--f7-theme-color-shade: #79797f;
--f7-theme-color-tint: #a3a3a7;
}
.ios .dark, .ios.dark {
--f7-list-item-header-text-color: inherit !important;
}
@@ -166,6 +200,15 @@ i.icon.la, i.icon.las, i.icon.lab {
border: 0;
}
/** Replacing the default style of vue-datepicker **/
.dp__theme_light {
--dp-primary-color: #c67e48;
}
.dp__theme_dark {
--dp-primary-color: #c67e48;
}
/** Common class for replacing the default style of framework7 **/
.navbar .navbar-compact-icons.right a + a {
margin-left: 0;
@@ -195,6 +238,12 @@ i.icon.la, i.icon.las, i.icon.lab {
white-space: nowrap;
}
.block-title .accordion-item-toggle .icon {
color: var(--f7-list-chevron-icon-color);
font-size: var(--f7-list-chevron-icon-font-size);
font-weight: bolder;
}
.list-item-media-valign-middle .item-media {
align-self: normal !important;
}
@@ -227,7 +276,7 @@ i.icon.la, i.icon.las, i.icon.lab {
font-weight: bold;
}
.theme-dark .list .item-content .list-item-showing {
.dark .list .item-content .list-item-showing {
color: rgba(255, 255, 255, 0.2);
}
@@ -235,9 +284,34 @@ i.icon.la, i.icon.las, i.icon.lab {
font-weight: bold;
}
.list.list-dividers li.list-group-title:first-child {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.list.list-dividers li.list-group-title:first-child:before {
background-color: transparent;
}
.list.list-dividers li:last-child > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.list.inset li.list-group-title:first-child > a.button {
border-radius: var(--f7-button-border-radius);
}
.list .item-content .list-item-checked-icon {
font-size: 20px;
color: var(--f7-radio-active-color, var(--f7-theme-color));
margin-right: calc(var(--f7-list-item-media-margin) + var(--f7-checkbox-extra-margin));
}
.popover .popover-inner .list .item-content .list-item-checked-icon {
margin-right: 0;
}
.list li.no-margin .item-content.item-input {
margin: 0;
}
.ebk-list-item-error-info div.item-footer {
@@ -261,12 +335,6 @@ i.icon.la, i.icon.las, i.icon.lab {
opacity: 0.6;
}
.card-chevron-icon {
color: var(--f7-list-chevron-icon-color);
font-size: var(--f7-list-chevron-icon-font-size);
font-weight: bolder;
}
.icon-after-text {
margin-left: 6px;
}
@@ -284,6 +352,100 @@ i.icon.la, i.icon.las, i.icon.lab {
height: 13px;
}
/** Swipe handler **/
.swipe-handler {
height: 16px;
position: absolute;
left: 0;
width: 100%;
top: 0;
cursor: pointer;
z-index: 10
}
.swipe-handler:after {
content: "";
width: 36px;
height: 6px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -18px;
margin-top: -3px;
border-radius: 3px;
background: #666
}
/** list-item-with-multi-item for framework7 **/
.list-item-with-multi-item .item-content,
.list-item-with-multi-item .item-inner {
padding: 0;
}
.list-item-with-multi-item .item-inner > div {
width: 100%;
}
.list-item-with-multi-item > .item-content > .item-inner:after {
background-color: transparent;
}
.list-item-with-multi-item .list-item-subitem:first-child .item-content {
padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left));
}
.list-item-with-multi-item .list-item-subitem .item-inner {
display: block;
width: 100%;
padding-left: calc(var(--f7-list-item-padding-horizontal) + var(--f7-safe-area-left));
padding-top: var(--f7-list-item-padding-vertical);
padding-bottom: var(--f7-list-item-padding-vertical);
}
.list-item-with-multi-item .list-item-subitem:first-child .item-inner {
padding-left: 0;
}
/** Combination list for framework7 **/
.combination-list-wrapper {
margin: 0;
padding: 0;
}
.combination-list-wrapper .block-title {
margin-top: 0;
margin-bottom: 0;
}
.combination-list-wrapper .list.combination-list-header {
margin: 0;
}
.combination-list-wrapper .list.combination-list-header .item-title {
width: 100%;
display: flex;
}
.combination-list-wrapper .list.combination-list-header > ul {
background-color: var(--f7-list-group-title-bg-color);
}
.combination-list-wrapper .list.combination-list-header.combination-list-opened > ul {
border-radius: var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius) 0 0;
}
.combination-list-wrapper .list.combination-list-header.combination-list-closed > ul {
border-radius: var(--f7-list-inset-border-radius);
}
.combination-list-wrapper .list.combination-list-header .combination-list-chevron-icon {
margin-left: auto;
}
.combination-list-wrapper .list.combination-list-content.inset > ul {
border-radius: 0 0 var(--f7-list-inset-border-radius) var(--f7-list-inset-border-radius);
}
/** Nested List item for framework7 **/
.nested-list-item .item-title {
width: 100%;
@@ -329,18 +491,11 @@ i.icon.la, i.icon.las, i.icon.lab {
text-overflow: ellipsis;
}
.nested-list-item:last-child > .swipeout-content > .item-link > .item-content > .item-inner:after {
background-color: transparent;
}
.sortable-enabled .nested-list-item .nested-list-item-child .item-inner {
padding-right: var(--f7-safe-area-right) !important;
}
/** Replacing the default style of Vue-pincode-input **/
.vue-pincode-input {
margin: 3px !important;
padding: 5px !important;
box-shadow: 0 0 2px rgba(0,0,0,.5) !important;
}
.theme-dark .vue-pincode-input {
box-shadow: 0 0 2px rgba(255,255,255,.5) !important;
}
</style>
+21 -17
View File
@@ -1,27 +1,27 @@
<template>
<f7-sheet :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-block class="margin-vertical">
<f7-row class="padding-vertical padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
v-for="(row, idx) in allColorRows" :key="idx">
<f7-col class="text-align-center" v-for="colorInfo in row" :key="colorInfo.color">
<f7-icon f7="app_fill"
:style="colorInfo.color | iconStyle('default', 'var(--default-icon-color)')"
@click.native="onColorClicked(colorInfo)">
<f7-block class="margin-vertical no-padding">
<div class="grid grid-cols-7 padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
:key="idx" v-for="(row, idx) in allColorRows">
<div class="text-align-center" :key="colorInfo.color" v-for="colorInfo in row">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="colorInfo.color" @click="onColorClicked(colorInfo)">
<f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === colorInfo.color">
<f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge>
</f7-icon>
</f7-col>
<f7-col v-for="idx in (itemPerRow - row.length)" :key="idx"></f7-col>
</f7-row>
</ItemIcon>
</div>
</div>
</f7-block>
</f7-page-content>
</f7-sheet>
@@ -30,16 +30,20 @@
<script>
export default {
props: [
'value',
'modelValue',
'columnCount',
'show',
'allColorInfos'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value,
currentValue: self.modelValue,
itemPerRow: self.columnCount || 7
}
},
@@ -64,11 +68,11 @@ export default {
methods: {
onColorClicked(colorInfo) {
this.currentValue = colorInfo.color;
this.$emit('input', this.currentValue);
this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false);
},
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -1,56 +1,39 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content>
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">{{ hint }}</p>
<p class="no-margin-top" v-if="hint">{{ hint }}</p>
<p class="no-margin-top margin-bottom" v-if="beginDateTime && endDateTime">
<span>{{ beginDateTime }}</span>
<span> - </span>
<span>{{ endDateTime }}</span>
</p>
<slot></slot>
<f7-list no-hairlines inline-labels class="no-margin-top margin-bottom">
<f7-list-input
:label="$t('Begin Time')"
type="datepicker"
class="date-range-sheet-time-item"
:calendar-params="{
timePicker: true,
dateFormat: $t('input-format.datetime.long'),
firstDay: defaultFirstDayOfWeek,
toolbarCloseText: $t('Done'),
timePickerPlaceholder: $t('Select Time'),
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(),
monthNames: $locale.getAllLongMonthNames(),
monthNamesShort: $locale.getAllShortMonthNames(),
dayNames: $locale.getAllLongWeekdayNames(),
dayNamesShort: $locale.getAllShortWeekdayNames()}"
:value="currentMinDate"
@calendar:change="currentMinDate = $event"
>
</f7-list-input>
<f7-list-input
:label="$t('End Time')"
type="datepicker"
class="date-range-sheet-time-item"
:calendar-params="{
timePicker: true,
dateFormat: $t('input-format.datetime.long'),
firstDay: defaultFirstDayOfWeek,
toolbarCloseText: $t('Done'),
timePickerPlaceholder: $t('Select Time'),
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(),
monthNames: $locale.getAllLongMonthNames(),
monthNamesShort: $locale.getAllShortMonthNames(),
dayNames: $locale.getAllLongWeekdayNames(),
dayNamesShort: $locale.getAllShortWeekdayNames()}"
:value="currentMaxDate"
@calendar:change="currentMaxDate = $event"
>
</f7-list-input>
</f7-list>
<VueDatePicker range inline enable-seconds six-weeks
auto-apply month-name-format="long"
class="margin-bottom"
:dark="isDarkMode"
:week-start="firstDayOfWeek"
:year-range="yearRange"
:day-names="dayNames"
:is24="is24Hour"
:partial-range="false"
:preset-ranges="presetRanges"
v-model="dateRange">
<template #month="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
<template #month-overlay-value="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
</VueDatePicker>
<f7-button large fill
:class="{ 'disabled': !currentMinDate || !currentMaxDate }"
:class="{ 'disabled': !dateRange[0] || !dateRange[1] }"
:text="$t('Continue')"
@click="confirm">
</f7-button>
@@ -71,6 +54,10 @@ export default {
'hint',
'show'
],
emits: [
'update:show',
'dateRange:change'
],
data() {
const self = this;
let minDate = self.$utilities.getTodayFirstUnixTime();
@@ -84,65 +71,87 @@ export default {
maxDate = self.maxTime;
}
minDate = self.$utilities.getDummyUnixTimeForLocalUsage(minDate, self.$utilities.getTimezoneOffsetMinutes(), self.$utilities.getBrowserTimezoneOffsetMinutes());
maxDate = self.$utilities.getDummyUnixTimeForLocalUsage(maxDate, self.$utilities.getTimezoneOffsetMinutes(), self.$utilities.getBrowserTimezoneOffsetMinutes());
return {
currentMinDate: [self.$utilities.getLocalDatetimeFromUnixTime(minDate)],
currentMaxDate: [self.$utilities.getLocalDatetimeFromUnixTime(maxDate)]
yearRange: [
2000,
this.$utilities.getYear(this.$utilities.getCurrentDateTime()) + 1
],
dateRange: [
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(minDate, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes())),
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(maxDate, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()))
]
}
},
computed: {
defaultFirstDayOfWeek() {
return this.$store.getters.currentUserFirstDayOfWeek;
}
},
watch: {
'currentMinDate': function (newValue) {
if (!newValue) {
this.currentMinDate = [this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())];
}
isDarkMode() {
return this.$root.isDarkMode;
},
'currentMaxDate': function (newValue) {
if (!newValue) {
this.currentMaxDate = [this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())];
}
firstDayOfWeek() {
return this.$store.getters.currentUserFirstDayOfWeek;
},
dayNames() {
return this.$locale.getAllMinWeekdayNames();
},
is24Hour() {
const datetimeFormat = this.$t('format.datetime.long');
return this.$utilities.is24HourFormat(datetimeFormat);
},
beginDateTime() {
const actualBeginUnixTime = this.$utilities.getActualUnixTimeForStore(this.$utilities.getUnixTime(this.dateRange[0]), this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
return this.$utilities.formatUnixTime(actualBeginUnixTime, this.$t('format.datetime.long'));
},
endDateTime() {
const actualEndUnixTime = this.$utilities.getActualUnixTimeForStore(this.$utilities.getUnixTime(this.dateRange[1]), this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
return this.$utilities.formatUnixTime(actualEndUnixTime, this.$t('format.datetime.long'));
},
presetRanges() {
const presetRanges = [];
[
this.$constants.datetime.allDateRanges.Today,
this.$constants.datetime.allDateRanges.LastSevenDays,
this.$constants.datetime.allDateRanges.LastThirtyDays,
this.$constants.datetime.allDateRanges.ThisWeek,
this.$constants.datetime.allDateRanges.ThisMonth,
this.$constants.datetime.allDateRanges.ThisYear
].forEach(dateRangeType => {
const dateRange = this.$utilities.getDateRangeByDateType(dateRangeType.type, this.firstDayOfWeek);
presetRanges.push({
label: this.$t(dateRangeType.name),
range: [
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(dateRange.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes())),
this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(dateRange.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()))
]
});
});
return presetRanges;
}
},
methods: {
onSheetOpen() {
if (this.minTime) {
const minTime = this.$utilities.getDummyUnixTimeForLocalUsage(this.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
this.currentMinDate = [this.$utilities.getLocalDatetimeFromUnixTime(minTime)];
this.dateRange[0] = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(this.minTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()));
}
if (this.maxTime) {
const maxTime = this.$utilities.getDummyUnixTimeForLocalUsage(this.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes());
this.currentMaxDate = [this.$utilities.getLocalDatetimeFromUnixTime(maxTime)];
this.dateRange[1] = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getDummyUnixTimeForLocalUsage(this.maxTime, this.$utilities.getTimezoneOffsetMinutes(), this.$utilities.getBrowserTimezoneOffsetMinutes()));
}
},
onSheetClosed() {
this.$emit('update:show', false);
},
confirm() {
if (!this.currentMinDate || !this.currentMaxDate) {
if (!this.dateRange[0] || !this.dateRange[1]) {
return;
}
let currentMinDate = this.currentMinDate;
const currentMinDate = this.dateRange[0];
const currentMaxDate = this.dateRange[1];
if (this.$utilities.isArray(this.currentMinDate)) {
currentMinDate = this.currentMinDate[0];
}
let currentMaxDate = this.currentMaxDate;
if (this.$utilities.isArray(this.currentMaxDate)) {
currentMaxDate = this.currentMaxDate[0];
}
let minUnixTime = this.$utilities.getMinuteFirstUnixTime(currentMinDate);
let maxUnixTime = this.$utilities.getMinuteLastUnixTime(currentMaxDate);
let minUnixTime = this.$utilities.getUnixTime(currentMinDate);
let maxUnixTime = this.$utilities.getUnixTime(currentMaxDate);
if (minUnixTime < 0 || maxUnixTime < 0) {
this.$toast('Date is too early');
@@ -160,9 +169,3 @@ export default {
}
}
</script>
<style>
.list .date-range-sheet-time-item > .item-content {
padding-left: 0;
}
</style>
@@ -0,0 +1,110 @@
<template>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" class="date-time-selection-sheet" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left">
<f7-link :text="$t('Current Time')" @click="setCurrentTime"></f7-link>
</div>
<div class="right">
<f7-link :text="$t('Done')" @click="confirm"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<VueDatePicker inline enable-seconds
auto-apply month-name-format="long"
class="justify-content-center"
:dark="isDarkMode"
:week-start="firstDayOfWeek"
:year-range="yearRange"
:day-names="dayNames"
:is24="is24Hour"
v-model="dateTime">
<template #month="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
<template #month-overlay-value="{ text }">
{{ $t(`datetime.${text}.short`) }}
</template>
</VueDatePicker>
</f7-page-content>
</f7-sheet>
</template>
<script>
export default {
props: [
'modelValue',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
let value = self.$utilities.getCurrentUnixTime();
if (self.modelValue) {
value = self.modelValue;
}
return {
yearRange: [
2000,
this.$utilities.getYear(this.$utilities.getCurrentDateTime()) + 1
],
dateTime: this.$utilities.getLocalDatetimeFromUnixTime(value),
}
},
computed: {
isDarkMode() {
return this.$root.isDarkMode;
},
firstDayOfWeek() {
return this.$store.getters.currentUserFirstDayOfWeek;
},
dayNames() {
return this.$locale.getAllMinWeekdayNames();
},
is24Hour() {
const datetimeFormat = this.$t('format.datetime.long');
return this.$utilities.is24HourFormat(datetimeFormat);
}
},
methods: {
onSheetOpen() {
if (this.modelValue) {
this.dateTime = this.$utilities.getLocalDatetimeFromUnixTime(this.modelValue)
}
},
onSheetClosed() {
this.$emit('update:show', false);
},
setCurrentTime() {
this.dateTime = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime())
},
confirm() {
if (!this.dateTime) {
return;
}
const unixTime = this.$utilities.getUnixTime(this.dateTime);
if (unixTime < 0) {
this.$toast('Date is too early');
return;
}
this.$emit('update:modelValue', unixTime);
this.$emit('update:show', false);
}
}
}
</script>
<style>
.date-time-selection-sheet .dp__menu {
border: 0;
}
</style>
+33 -19
View File
@@ -1,27 +1,27 @@
<template>
<f7-sheet :class="{ 'icon-selection-huge-sheet': hugeIconRows }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:class="heightClass" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-block class="margin-vertical">
<f7-row class="padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
v-for="(row, idx) in allIconRows" :key="idx">
<f7-col class="text-align-center" v-for="iconInfo in row" :key="iconInfo.id">
<f7-icon :icon="iconInfo.icon"
:style="color | iconStyle('default', 'var(--default-icon-color)')"
@click.native="onIconClicked(iconInfo)">
<f7-block class="margin-vertical no-padding">
<div class="grid grid-cols-7 padding-vertical-half padding-horizontal-half"
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
:key="idx" v-for="(row, idx) in allIconRows">
<div class="text-align-center" :key="iconInfo.id" v-for="iconInfo in row">
<ItemIcon icon-type="fixed" :icon-id="iconInfo.icon" :color="color" @click="onIconClicked(iconInfo)">
<f7-badge color="default" class="right-bottom-icon" v-if="currentValue && currentValue === iconInfo.id">
<f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge>
</f7-icon>
</f7-col>
<f7-col v-for="idx in (itemPerRow - row.length)" :key="idx"></f7-col>
</f7-row>
</ItemIcon>
</div>
</div>
</f7-block>
</f7-page-content>
</f7-sheet>
@@ -30,17 +30,21 @@
<script>
export default {
props: [
'value',
'modelValue',
'color',
'columnCount',
'show',
'allIconInfos'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value,
currentValue: self.modelValue,
itemPerRow: self.columnCount || 7
}
},
@@ -71,18 +75,24 @@ export default {
return ret;
},
hugeIconRows() {
return this.allIconRows.length > 10;
heightClass() {
if (this.allIconRows.length > 10) {
return 'icon-selection-huge-sheet';
} else if (this.allIconRows.length > 6) {
return 'icon-selection-large-sheet';
} else {
return '';
}
}
},
methods: {
onIconClicked(iconInfo) {
this.currentValue = iconInfo.id;
this.$emit('input', this.currentValue);
this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false);
},
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -128,6 +138,10 @@ export default {
<style>
@media (min-height: 630px) {
.icon-selection-large-sheet {
height: 310px;
}
.icon-selection-huge-sheet {
height: 400px;
}
+51 -9
View File
@@ -1,18 +1,21 @@
<template>
<f7-sheet style="height:auto" :opened="show" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">
<span>{{ hint }}</span>
<f7-link class="icon-after-text"
<f7-link id="copy-to-clipboard-icon" ref="copyToClipboardIcon"
class="icon-after-text"
icon-only icon-f7="doc_on_doc" icon-size="16px"
v-if="enableCopy"
v-clipboard:copy="information" v-clipboard:success="onCopied"></f7-link>
></f7-link>
</p>
<textarea class="information-content full-line" :rows="rowCount" readonly="readonly" v-model="information"></textarea>
<textarea class="information-content full-line" :rows="rowCount" :value="information"></textarea>
<div class="margin-top text-align-center">
<f7-link @click="cancel" :text="$t('Close')"></f7-link>
</div>
@@ -31,14 +34,53 @@ export default {
'enableCopy',
'show'
],
emits: [
'update:show',
'info:copied'
],
data() {
return {
clipboardHolder: null
}
},
mounted() {
this.makeCopyToClipboardClickable();
},
updated() {
this.makeCopyToClipboardClickable();
},
watch: {
'information': function (newValue) {
if (this.clipboardHolder) {
this.$utilities.changeClipboardObjectText(this.clipboardHolder, newValue);
}
}
},
methods: {
onSheetClosed() {
this.$emit('update:show', false);
},
onCopied() {
this.$emit('info:copied');
this.close();
},
cancel() {
this.close();
},
makeCopyToClipboardClickable() {
const self = this;
if (self.clipboardHolder) {
return;
}
if (self.$refs.copyToClipboardIcon) {
self.clipboardHolder = self.$utilities.makeButtonCopyToClipboard({
el: '#copy-to-clipboard-icon',
text: self.information,
successCallback: function () {
self.$emit('info:copied');
}
});
}
},
close() {
this.$emit('update:show', false);
}
}
+127
View File
@@ -0,0 +1,127 @@
<template>
<f7-icon :f7="f7Icon" :icon="icon" :style="style">
<slot></slot>
</f7-icon>
</template>
<script>
export default {
props: [
'iconType',
'iconId',
'color',
'defaultColor',
'additionalColorAttr'
],
computed: {
f7Icon() {
if (this.iconType === 'fixed-f7') {
return this.iconId;
} else {
return '';
}
},
icon() {
if (this.iconType === 'account') {
return this.getAccountIcon(this.iconId);
} else if (this.iconType === 'category') {
return this.getCategoryIcon(this.iconId);
} else if (this.iconType === 'fixed') {
return this.iconId;
} else {
return '';
}
},
style() {
let defaultColor = 'var(--default-icon-color)';
if (this.defaultColor) {
defaultColor = this.defaultColor;
}
if (this.iconType === 'account') {
return this.getAccountIconStyle(this.color, defaultColor, this.additionalColorAttr);
} else if (this.iconType === 'category') {
return this.getCategoryIconStyle(this.color, defaultColor, this.additionalColorAttr);
} else {
return this.getDefaultIconStyle(this.color, defaultColor, this.additionalColorAttr);
}
}
},
methods: {
getAccountIcon(iconId) {
if (this.$utilities.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!this.$constants.icons.allAccountIcons[iconId]) {
return this.$constants.icons.defaultAccountIcon.icon;
}
return this.$constants.icons.allAccountIcons[iconId].icon;
},
getCategoryIcon(iconId) {
if (this.$utilities.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!this.$constants.icons.allCategoryIcons[iconId]) {
return this.$constants.icons.defaultCategoryIcon.icon;
}
return this.$constants.icons.allCategoryIcons[iconId].icon;
},
getAccountIconStyle(color, defaultColor, additionalColorAttr) {
if (color && color !== this.$constants.colors.defaultAccountColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalColorAttr) {
ret[additionalColorAttr] = color;
}
return ret;
},
getCategoryIconStyle(color, defaultColor, additionalColorAttr) {
if (color && color !== this.$constants.colors.defaultCategoryColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalColorAttr) {
ret[additionalColorAttr] = color;
}
return ret;
},
getDefaultIconStyle(color, defaultColor, additionalColorAttr) {
if (color && color !== this.$constants.colors.defaultColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalColorAttr) {
ret[additionalColorAttr] = color;
}
return ret;
}
}
}
</script>
@@ -1,25 +1,29 @@
<template>
<f7-sheet :class="{ 'list-item-selection-huge-sheet': hugeListItemRows }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:class="{ 'list-item-selection-huge-sheet': hugeListItemRows }" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-list no-hairlines class="no-margin-top no-margin-bottom">
<f7-list dividers no-hairlines class="no-margin-vertical">
<f7-list-item link="#" no-chevron
v-for="(item, index) in items"
:key="item | itemKeyValue(index, keyField, valueType)"
:title="$tIf((titleField ? item[titleField] : item), titleI18n)"
:value="getItemValue(item, index, valueField, valueType)"
:class="{ 'list-item-selected': isSelected(item, index) }"
:value="item | itemKeyValue(index, valueField, valueType)"
:title="item | itemFieldContent(titleField, item, titleI18n)"
:key="getItemValue(item, index, keyField, valueType)"
v-for="(item, index) in items"
@click="onItemClicked(item, index)">
<f7-icon slot="media"
:icon="item[iconField] | icon(iconType)"
:style="item[colorField] | iconStyle(iconType, 'var(--default-icon-color)')"
v-if="iconField"></f7-icon>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="isSelected(item, index)"></f7-icon>
<template #content-start>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" :style="{ 'color': isSelected(item, index) ? '' : 'transparent' }"></f7-icon>
</template>
<template #media v-if="iconField">
<ItemIcon :icon-type="iconType" :icon-id="item[iconField]" :color="item[colorField]"></ItemIcon>
</template>
</f7-list-item>
</f7-list>
</f7-page-content>
@@ -29,7 +33,7 @@
<script>
export default {
props: [
'value',
'modelValue',
'valueType', // item or index
'keyField', // for value type == item
'valueField', // for value type == item
@@ -41,11 +45,15 @@ export default {
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value
currentValue: self.modelValue
}
},
computed: {
@@ -54,6 +62,15 @@ export default {
}
},
methods: {
getItemValue(item, index, fieldName, valueType) {
if (valueType === 'index') {
return index;
} else if (fieldName) {
return item[fieldName];
} else {
return item;
}
},
onItemClicked(item, index) {
if (this.valueType === 'index') {
this.currentValue = index;
@@ -65,15 +82,15 @@ export default {
}
}
this.$emit('input', this.currentValue);
this.$emit('update:show', false);
this.$emit('update:modelValue', this.currentValue);
this.close();
},
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
isSelected(item, index) {
if (this.valueType === 'index') {
@@ -106,17 +123,9 @@ export default {
}
container.scrollTop(targetPos);
}
},
filters: {
itemKeyValue(item, index, fieldName, valueType) {
if (valueType === 'index') {
return index;
} else if (fieldName) {
return item[fieldName];
} else {
return item;
}
},
close() {
this.$emit('update:show', false);
}
}
}
+38 -30
View File
@@ -1,53 +1,55 @@
<template>
<f7-sheet class="numpad-sheet" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content class="no-margin no-padding-top">
<f7-row class="numpad-values">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" class="numpad-sheet" style="height: auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="numpad-values">
<span class="numpad-value" :style="{ fontSize: currentDisplayFontSize + 'px' }">{{ currentDisplay }}</span>
</f7-row>
<f7-row class="numpad-buttons">
<f7-button class="numpad-button numpad-button-num" @click="inputNum(7)">
</div>
<div class="numpad-buttons">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(7)">
<span class="numpad-button-text numpad-button-text-normal">7</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(8)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(8)">
<span class="numpad-button-text numpad-button-text-normal">8</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(9)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(9)">
<span class="numpad-button-text numpad-button-text-normal">9</span>
</f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('×')">
<f7-button class="numpad-button numpad-button-function no-right-border" @mousedown="setSymbol('×')">
<span class="numpad-button-text numpad-button-text-normal">&times;</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(4)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(4)">
<span class="numpad-button-text numpad-button-text-normal">4</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(5)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(5)">
<span class="numpad-button-text numpad-button-text-normal">5</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(6)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(6)">
<span class="numpad-button-text numpad-button-text-normal">6</span>
</f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('')">
<f7-button class="numpad-button numpad-button-function no-right-border" @mousedown="setSymbol('')">
<span class="numpad-button-text numpad-button-text-normal">&minus;</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(1)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(1)">
<span class="numpad-button-text numpad-button-text-normal">1</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(2)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(2)">
<span class="numpad-button-text numpad-button-text-normal">2</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(3)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(3)">
<span class="numpad-button-text numpad-button-text-normal">3</span>
</f7-button>
<f7-button class="numpad-button numpad-button-function no-right-border" @click="setSymbol('+')">
<f7-button class="numpad-button numpad-button-function no-right-border" @mousedown="setSymbol('+')">
<span class="numpad-button-text numpad-button-text-normal">&plus;</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputDot()">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputDot()">
<span class="numpad-button-text numpad-button-text-normal">.</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="inputNum(0)">
<f7-button class="numpad-button numpad-button-num" @mousedown="inputNum(0)">
<span class="numpad-button-text numpad-button-text-normal">0</span>
</f7-button>
<f7-button class="numpad-button numpad-button-num" @click="backspace" @taphold.native="clear()">
<f7-button class="numpad-button numpad-button-num" @mousedown="backspace" @taphold="clear()">
<span class="numpad-button-text numpad-button-text-normal">
<f7-icon f7="delete_left"></f7-icon>
</span>
@@ -55,7 +57,7 @@
<f7-button class="numpad-button numpad-button-confirm no-right-border no-bottom-border" fill @click="confirm()">
<span :class="{ 'numpad-button-text': true, 'numpad-button-text-confirm': !currentSymbol }">{{ confirmText }}</span>
</f7-button>
</f7-row>
</div>
</f7-page-content>
</f7-sheet>
</template>
@@ -63,18 +65,22 @@
<script>
export default {
props: [
'value',
'modelValue',
'minValue',
'maxValue',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
previousValue: '',
currentSymbol: '',
currentValue: self.getStringValue(self.value)
currentValue: self.getStringValue(self.modelValue)
}
},
computed: {
@@ -103,7 +109,7 @@ export default {
if (this.currentSymbol) {
return '=';
} else {
return this.$i18n.t('OK');
return this.$t('OK');
}
}
},
@@ -291,17 +297,20 @@ export default {
} else {
const value = this.$utilities.stringCurrencyToNumeric(this.currentValue);
this.$emit('input', value);
this.$emit('update:show', false);
this.$emit('update:modelValue', value);
this.close();
return true;
}
},
close() {
this.$emit('update:show', false);
},
onSheetOpen() {
this.currentValue = this.getStringValue(this.value);
this.currentValue = this.getStringValue(this.modelValue);
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
}
}
}
@@ -322,7 +331,6 @@ export default {
padding-left: 16px;
line-height: 1;
height: 50px;
justify-content: center;
align-items: center;
box-sizing: border-box;
user-select: none;
@@ -371,7 +379,7 @@ export default {
color: var(--f7-color-black);
}
.theme-dark .numpad-button-text-normal {
.dark .numpad-button-text-normal {
color: var(--f7-color-white);
}
+22 -11
View File
@@ -1,23 +1,26 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">{{ hint }}</p>
<p class="no-margin" v-if="hint">{{ hint }}</p>
<slot></slot>
<f7-list no-hairlines class="no-margin-top margin-bottom">
<f7-list no-hairlines strong class="no-margin">
<f7-list-input
type="number"
autocomplete="one-time-code"
outline
floating-label
clear-button
class="no-margin no-padding-bottom"
:label="$t('Password')"
:placeholder="$t('Passcode')"
:value="currentPasscode"
@input="currentPasscode = $event.target.value"
@keyup.enter.native="confirm()"
v-model:value="currentPasscode"
@keyup.enter="confirm()"
></f7-list-input>
</f7-list>
<f7-button large fill
@@ -36,13 +39,18 @@
<script>
export default {
props: [
'value',
'modelValue',
'title',
'hint',
'confirmDisabled',
'cancelDisabled',
'show'
],
emits: [
'update:modelValue',
'update:show',
'passcode:confirm'
],
data() {
return {
currentPasscode: ''
@@ -53,17 +61,20 @@ export default {
this.currentPasscode = '';
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
confirm() {
if (!this.currentPasscode || this.confirmDisabled) {
return;
}
this.$emit('input', this.currentPasscode);
this.$emit('update:modelValue', this.currentPasscode);
this.$emit('passcode:confirm', this.currentPasscode);
},
cancel() {
this.close();
},
close() {
this.$emit('update:show', false);
}
}
+22 -11
View File
@@ -1,22 +1,25 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px" v-if="title"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half" v-if="hint">{{ hint }}</p>
<f7-list no-hairlines class="no-margin-top margin-bottom">
<p class="no-margin" v-if="hint">{{ hint }}</p>
<f7-list no-hairlines strong class="no-margin">
<f7-list-input
type="password"
autocomplete="current-password"
outline
floating-label
clear-button
class="no-margin no-padding-bottom"
:label="$t('Password')"
:placeholder="$t('Password')"
:value="currentPassword"
@input="currentPassword = $event.target.value"
@keyup.enter.native="confirm()"
v-model:value="currentPassword"
@keyup.enter="confirm()"
></f7-list-input>
</f7-list>
<f7-button large fill
@@ -35,13 +38,18 @@
<script>
export default {
props: [
'value',
'modelValue',
'title',
'hint',
'confirmDisabled',
'cancelDisabled',
'show'
],
emits: [
'update:modelValue',
'update:show',
'password:confirm'
],
data() {
return {
currentPassword: ''
@@ -52,17 +60,20 @@ export default {
this.currentPassword = '';
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
confirm() {
if (!this.currentPassword || this.confirmDisabled) {
return;
}
this.$emit('input', this.currentPassword);
this.$emit('update:modelValue', this.currentPassword);
this.$emit('password:confirm', this.currentPassword);
},
cancel() {
this.close();
},
close() {
this.$emit('update:show', false);
}
}
+46 -21
View File
@@ -4,14 +4,15 @@
<circle class="pie-chart-background" cx="0" cy="0" :r="diameter"></circle>
<circle class="pie-chart-item"
v-for="(item, idx) in validItems" :key="idx"
fill="transparent"
cx="0" cy="0"
:r="diameter / 2"
:stroke="item.color | defaultIconColor('var(--default-icon-color)')"
:stroke="getColor(item.color)"
:stroke-width="diameter"
:stroke-dasharray="item | itemStrokeDash(circumference)"
:stroke-dashoffset="item | itemDashOffset(validItems, circumference, itemCommonDashOffset)"
:stroke-dasharray="getItemStrokeDash(item)"
:stroke-dashoffset="getItemDashOffset(item, validItems, itemCommonDashOffset)"
:key="idx"
v-for="(item, idx) in validItems"
@click="switchSelectedIndex(idx)">
</circle>
@@ -48,8 +49,8 @@
<span class="skeleton-text">Percent</span>
</f7-chip>
<f7-chip outline
:text="(selectedItem.percent) | percent(2, '&lt;0.01')"
:style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)', '--f7-chip-outline-border-color')"
:text="$utilities.formatPercent(selectedItem.percent, 2, '&lt;0.01')"
:style="getColorStyle(selectedItem ? selectedItem.color : '', '--f7-chip-outline-border-color')"
v-else-if="!skeleton"></f7-chip>
</p>
<p v-else-if="!validItems || !validItems.length">
@@ -59,7 +60,7 @@
<span class="skeleton-text" v-if="skeleton">Name</span>
<span v-else-if="!skeleton && selectedItem.name">{{ selectedItem.name }}</span>
<span class="skeleton-text" v-if="skeleton">Value</span>
<span v-else-if="!skeleton && showValue" :style="(selectedItem ? selectedItem.color : '') | iconStyle('default', 'var(--default-icon-color)')">{{ selectedItem.value | currency(selectedItem.currency || defaultCurrency) }}</span>
<span v-else-if="!skeleton && showValue" :style="getColorStyle(selectedItem ? selectedItem.color : '')">{{ $locale.getDisplayCurrency(selectedItem.value, (selectedItem.currency || defaultCurrency)) }}</span>
<f7-icon class="item-navigate-icon" f7="chevron_right" v-if="enableClickItem"></f7-icon>
</f7-link>
<f7-link :no-link-class="true" v-else-if="!validItems || !validItems.length">
@@ -107,6 +108,9 @@ export default {
'enableClickItem',
'centerTextBackground',
],
emits: [
'click'
],
data: function () {
const diameter = 100;
@@ -195,8 +199,11 @@ export default {
}
},
watch: {
'items': function () {
this.selectedIndex = 0;
'items': {
handler() {
this.selectedIndex = 0;
},
deep: true
}
},
methods: {
@@ -216,14 +223,32 @@ export default {
if (this.enableClickItem) {
this.$emit('click', item.sourceItem);
}
}
},
filters: {
itemStrokeDash(item, circumference) {
const length = item.actualPercent * circumference;
return `${length} ${circumference - length}`;
},
itemDashOffset(item, items, circumference, offset) {
getColor: function (color) {
if (color && color !== this.$constants.colors.defaultColor) {
color = '#' + color;
} else {
color = 'var(--default-icon-color)';
}
return color;
},
getColorStyle: function (color, additionalFieldName) {
const ret = {
color: this.getColor(color)
};
if (additionalFieldName) {
ret[additionalFieldName] = ret.color;
}
return ret;
},
getItemStrokeDash(item) {
const length = item.actualPercent * this.circumference;
return `${length} ${this.circumference - length}`;
},
getItemDashOffset(item, items, offset) {
let allPreviousPercent = 0;
for (let i = 0; i < items.length; i++) {
@@ -237,17 +262,17 @@ export default {
}
if (offset) {
offset += circumference / 4;
offset += this.circumference / 4;
} else {
offset = circumference / 4;
offset = this.circumference / 4;
}
if (allPreviousPercent <= 0) {
return offset;
}
const allPreviousLength = allPreviousPercent * circumference;
return circumference - allPreviousLength + offset;
const allPreviousLength = allPreviousPercent * this.circumference;
return this.circumference - allPreviousLength + offset;
}
}
}
@@ -314,7 +339,7 @@ export default {
fill: #f0f0f0;
}
.theme-dark .pie-chart-background {
.dark .pie-chart-background {
fill: #181818;
}
+248
View File
@@ -0,0 +1,248 @@
<template>
<div class="pin-code-input grid grid-gap" :class="'grid-cols-' + length">
<div class="input input-outline input-with-value"
:key="index" v-for="(code, index) in codes">
<input min="0" maxlength="1" pattern="[0-9]*"
:ref="`pin-code-input-${index}`"
:value="codes[index].value"
:type="codes[index].inputType"
@keydown="onKeydown(index, $event)"
@paste="onPaste(index, $event)"
@change="onInput(index, $event)"
/>
</div>
</div>
</template>
<script>
export default {
props: [
'modelValue',
'secure',
'length'
],
emits: [
'update:modelValue',
'pincode:confirm'
],
data() {
return {
codes: []
}
},
computed: {
finalPinCode() {
let finalPinCode = '';
for (let i = 0; i < this.codes.length; i++) {
if (this.codes[i].value) {
finalPinCode += this.codes[i].value;
} else {
break;
}
}
return finalPinCode;
}
},
watch: {
'length': function (newValue) {
this.init(newValue, this.modelValue);
},
'modelValue': function (newValue) {
if (newValue === this.finalPinCode) {
return;
}
this.init(this.length, newValue);
},
'codes': {
handler() {
this.$emit('update:modelValue', this.finalPinCode);
},
deep: true
}
},
created() {
this.init(this.length, this.modelValue);
},
methods: {
init(length, value) {
this.codes.length = 0;
for (let i = 0; i < length; i++) {
const code = {
value: '',
inputType: 'tel',
inputTimer: null
};
if (value && value[i]) {
code.value = value[i];
if (this.secure) {
code.inputType = 'password';
}
}
this.codes.push(code);
}
},
autoFillText(index, text) {
let lastIndex = index;
for (let i = index, j = 0; i < this.codes.length && j < text.length; i++, j++) {
if (text[j] < '0' || text[j] > '9') {
this.codes[i].value = '';
this.$forceUpdate();
break;
}
this.codes[i].value = text[j];
this.setInputType(i);
lastIndex = i;
}
this.setFocus(lastIndex);
if (this.finalPinCode.length === this.length) {
this.$emit('pincode:confirm', this.finalPinCode);
}
},
setInputType(index) {
const self = this;
if (!self.secure) {
return;
}
if (!self.codes[index].value) {
self.codes[index].inputType = 'tel';
return;
}
if (self.codes[index].inputTimer) {
return;
}
self.codes[index].inputTimer = setTimeout(() => {
if (self.codes[index].value) {
self.codes[index].inputType = 'password';
} else {
self.codes[index].inputType = 'tel';
}
self.codes[index].inputTimer = null;
}, 300);
},
setFocus(index) {
const refId = `pin-code-input-${index}`;
const ref = this.$refs[refId];
if (ref && ref[0]) {
ref[0].focus();
ref[0].select();
}
},
setPreviousFocus(index) {
if (index > 0) {
this.setFocus(index - 1);
}
},
setNextFocus(index) {
if (index < this.length - 1) {
this.setFocus(index + 1);
}
},
onKeydown(index, event) {
if (event.code === 'Enter' && this.finalPinCode.length === this.length) {
this.$emit('pincode:confirm', this.finalPinCode);
event.preventDefault();
return;
}
if (event.code === 'ArrowLeft' || (event.shiftKey && event.code === 'Tab')) {
this.setPreviousFocus(index);
event.preventDefault();
return;
}
if (event.code === 'ArrowRight' || (!event.shiftKey && event.code === 'Tab')) {
this.setNextFocus(index);
event.preventDefault();
return;
}
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyV') {
return;
}
if (event.code === 'Backspace' || event.code === 'Delete' || event.code === 'Del') {
for (let i = index; i < this.codes.length; i++) {
this.codes[i].value = '';
this.setInputType(i);
}
if (event.code === 'Backspace') {
this.setPreviousFocus(index);
}
event.preventDefault();
return;
}
if (event.code.indexOf('Digit') === 0 && event.code.length === 6) {
this.codes[index].value = event.key;
this.setInputType(index);
this.setNextFocus(index);
if (this.finalPinCode.length === this.length) {
this.$emit('pincode:confirm', this.finalPinCode);
}
}
event.preventDefault();
},
onPaste(index, event) {
if (!event.clipboardData) {
event.preventDefault();
return;
}
const text = event.clipboardData.getData('Text');
if (!text) {
event.preventDefault();
return;
}
this.autoFillText(index, text);
event.preventDefault();
},
onInput(index, event) {
if (!event.target.value) {
event.preventDefault();
return;
}
this.autoFillText(index, event.target.value);
event.preventDefault();
}
}
}
</script>
<style>
.pin-code-input input {
text-align: center;
padding-left: 10px;
padding-right: 10px;
height: 60px !important;
}
.pin-code-input.grid.grid-gap {
--f7-grid-gap: 4px;
}
</style>
+14 -8
View File
@@ -1,15 +1,16 @@
<template>
<f7-sheet style="height:auto" :opened="show"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-page-content>
<f7-sheet swipe-to-close swipe-handler=".swipe-handler" style="height:auto"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px"><b>{{ title }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<p class="no-margin-top margin-bottom-half">{{ hint }}</p>
<f7-list no-hairlines class="no-margin-top margin-bottom">
<p class="no-margin">{{ hint }}</p>
<f7-list no-hairlines class="no-margin">
<f7-list-item class="list-item-pincode-input">
<pincode-input secure :length="6" v-model="currentPinCode" />
<pin-code-input :secure="true" :length="6" v-model="currentPinCode"/>
</f7-list-item>
</f7-list>
<f7-button large fill
@@ -28,13 +29,18 @@
<script>
export default {
props: [
'value',
'modelValue',
'title',
'hint',
'confirmDisabled',
'cancelDisabled',
'show'
],
emits: [
'update:modelValue',
'update:show',
'pincode:confirm'
],
data() {
return {
currentPinCode: ''
@@ -57,7 +63,7 @@ export default {
return;
}
this.$emit('input', this.currentPinCode);
this.$emit('update:modelValue', this.currentPinCode);
this.$emit('pincode:confirm', this.currentPinCode);
},
cancel() {
@@ -1,6 +1,9 @@
<template>
<f7-sheet :class="{ 'tag-selection-huge-sheet': hugeListItemRows }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:opened="show" :class="{ 'tag-selection-huge-sheet': hugeListItemRows }"
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left">
<f7-link sheet-close :text="$t('Cancel')"></f7-link>
</div>
@@ -12,22 +15,25 @@
<f7-list no-hairlines class="no-margin-top no-margin-bottom" v-if="!items || !items.length || noAvailableTag">
<f7-list-item :title="$t('No available tag')"></f7-list-item>
</f7-list>
<f7-list no-hairlines class="no-margin-top no-margin-bottom" v-else-if="items && items.length && !noAvailableTag">
<f7-list-item checkbox v-for="item in items"
<f7-list dividers no-hairlines class="no-margin-top no-margin-bottom" v-else-if="items && items.length && !noAvailableTag">
<f7-list-item checkbox
v-show="!item.hidden"
:key="item.id"
:class="item.id | tagItemClass(selectedItemIds)"
:class="isChecked(item.id) ? 'list-item-selected' : ''"
:value="item.id"
:checked="item.id | isChecked(selectedItemIds)"
:checked="isChecked(item.id)"
:key="item.id"
v-for="item in items"
@change="changeItemSelection">
<f7-block slot="title" class="no-padding no-margin">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="tag-selection-list-item list-item-valign-middle padding-left-half">
{{ item.name }}
<template #title>
<f7-block class="no-padding no-margin">
<div class="display-flex">
<f7-icon f7="number"></f7-icon>
<div class="tag-selection-list-item list-item-valign-middle padding-left-half">
{{ item.name }}
</div>
</div>
</div>
</f7-block>
</f7-block>
</template>
</f7-list-item>
</f7-list>
</f7-page-content>
@@ -37,15 +43,19 @@
<script>
export default {
props: [
'value',
'modelValue',
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
selectedItemIds: self.$utilities.copyArrayTo(self.value, [])
selectedItemIds: self.$utilities.copyArrayTo(self.modelValue, [])
}
},
computed: {
@@ -64,11 +74,11 @@ export default {
},
methods: {
save() {
this.$emit('input', this.selectedItemIds);
this.$emit('update:modelValue', this.selectedItemIds);
this.$emit('update:show', false);
},
onSheetOpen(event) {
this.selectedItemIds = this.$utilities.copyArrayTo(this.value, []);
this.selectedItemIds = this.$utilities.copyArrayTo(this.modelValue, []);
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -95,9 +105,6 @@ export default {
}
},
scrollToSelectedItem(parent) {
const app = this.$f7;
const $$ = app.$;
if (!parent || !parent.length) {
return;
}
@@ -113,8 +120,8 @@ export default {
let lastSelectedItem = selectedItem;
if (selectedItem.length > 0) {
firstSelectedItem = $$(selectedItem[0]);
lastSelectedItem = $$(selectedItem[selectedItem.length - 1]);
firstSelectedItem = this.$ui.elements(selectedItem[0]);
lastSelectedItem = this.$ui.elements(selectedItem[selectedItem.length - 1]);
}
let firstSelectedItemInTop = firstSelectedItem.offset().top - container.offset().top - parseInt(container.css('padding-top'), 10);
@@ -133,26 +140,15 @@ export default {
}
container.scrollTop(targetPos);
}
},
filters: {
isChecked(itemId, selectedItemIds) {
for (let i = 0; i < selectedItemIds.length; i++) {
if (selectedItemIds[i] === itemId) {
},
isChecked(itemId) {
for (let i = 0; i < this.selectedItemIds.length; i++) {
if (this.selectedItemIds[i] === itemId) {
return true;
}
}
return false;
},
tagItemClass(itemId, selectedItemIds) {
for (let i = 0; i < selectedItemIds.length; i++) {
if (selectedItemIds[i] === itemId) {
return 'list-item-selected';
}
}
return '';
}
}
}
@@ -170,4 +166,3 @@ export default {
text-overflow: ellipsis;
}
</style>
self
@@ -1,6 +1,9 @@
<template>
<f7-sheet :class="{ 'tree-view-selection-huge-sheet': hugeTreeViewItems }" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
:class="{ 'tree-view-selection-huge-sheet': hugeTreeViewItems }"
:opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
@@ -8,26 +11,26 @@
</f7-toolbar>
<f7-page-content>
<f7-treeview>
<f7-treeview-item v-for="item in items"
item-toggle
<f7-treeview-item item-toggle
:opened="isPrimaryItemHasSecondaryValue(item)"
:key="item | itemFieldContent(primaryKeyField, item, false)"
:label="item | itemFieldContent(primaryTitleField, item, primaryTitleI18n)">
<f7-icon slot="media"
:icon="item[primaryIconField] | icon(primaryIconType)"
:style="item[primaryColorField] | iconStyle(primaryIconType, 'var(--default-icon-color)')"
v-if="primaryIconField"></f7-icon>
:label="$tIf((primaryTitleField ? item[primaryTitleField] : item), primaryTitleI18n)"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in items">
<template #media>
<ItemIcon :icon-type="primaryIconType" :icon-id="item[primaryIconField]"
:color="item[primaryColorField]" v-if="primaryIconField"></ItemIcon>
</template>
<f7-treeview-item v-for="subItem in item[primarySubItemsField]"
selectable
<f7-treeview-item selectable
:selected="isSecondarySelected(subItem)"
:key="subItem | itemFieldContent(secondaryKeyField, subItem, false)"
:label="subItem | itemFieldContent(secondaryTitleField, subItem, secondaryTitleI18n)"
:label="$tIf((secondaryTitleField ? subItem[secondaryTitleField] : subItem), secondaryTitleI18n)"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in item[primarySubItemsField]"
@click="onSecondaryItemClicked(subItem)">
<f7-icon slot="media"
:icon="subItem[secondaryIconField] | icon(secondaryIconType)"
:style="subItem[secondaryColorField] | iconStyle(secondaryIconType, 'var(--default-icon-color)')"
v-if="secondaryIconField"></f7-icon>
<template #media>
<ItemIcon :icon-type="secondaryIconType" :icon-id="subItem[secondaryIconField]"
:color="subItem[secondaryColorField]" v-if="secondaryIconField"></ItemIcon>
</template>
</f7-treeview-item>
</f7-treeview-item>
</f7-treeview>
@@ -38,7 +41,7 @@
<script>
export default {
props: [
'value',
'modelValue',
'primaryKeyField',
'primaryValueField',
'primaryTitleField',
@@ -57,11 +60,15 @@ export default {
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentValue: self.value
currentValue: self.modelValue
}
},
computed: {
@@ -85,7 +92,7 @@ export default {
},
methods: {
onSheetOpen(event) {
this.currentValue = this.value;
this.currentValue = this.modelValue;
this.scrollToSelectedItem(event.$el);
},
onSheetClosed() {
@@ -98,7 +105,7 @@ export default {
this.currentValue = subItem;
}
this.$emit('input', this.currentValue);
this.$emit('update:modelValue', this.currentValue);
this.$emit('update:show', false);
},
isPrimaryItemHasSecondaryValue(primaryItem) {
@@ -1,56 +1,60 @@
<template>
<f7-sheet style="height: auto" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
style="height: auto" :opened="show" @sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
<f7-toolbar>
<div class="swipe-handler"></div>
<div class="left"></div>
<div class="right">
<f7-link sheet-close :text="$t('Done')"></f7-link>
</div>
</f7-toolbar>
<f7-page-content>
<f7-row>
<f7-col width="50">
<div class="grid grid-cols-2 grid-gap">
<div>
<div class="primary-list-container">
<f7-list no-hairlines class="primary-list no-margin-top no-margin-bottom">
<f7-list dividers no-hairlines class="primary-list no-margin-vertical">
<f7-list-item link="#" no-chevron
v-for="item in items"
:key="item | itemFieldContent(primaryKeyField, item, false)"
:class="{ 'primary-list-item-selected': item === selectedPrimaryItem }"
:value="item | itemFieldContent(primaryValueField, item, false)"
:title="item | itemFieldContent(primaryTitleField, null, primaryTitleI18n)"
:header="item | itemFieldContent(primaryHeaderField, null, primaryHeaderI18n)"
:footer="item | itemFieldContent(primaryFooterField, null, primaryFooterI18n)"
:value="primaryValueField ? item[primaryValueField] : item"
:title="$tIf(item[primaryTitleField], primaryTitleI18n)"
:header="$tIf(item[primaryHeaderField], primaryHeaderI18n)"
:footer="$tIf(item[primaryFooterField], primaryFooterI18n)"
:key="primaryKeyField ? item[primaryKeyField] : item"
v-for="item in items"
@click="onPrimaryItemClicked(item)">
<f7-icon slot="media"
:icon="item[primaryIconField] | icon(primaryIconType)"
:style="item[primaryColorField] | iconStyle(primaryIconType, 'var(--default-icon-color)')"
v-if="primaryIconField"></f7-icon>
<f7-icon slot="after" class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon>
<template #media>
<ItemIcon :icon-type="primaryIconType" :icon-id="item[primaryIconField]" :color="item[primaryColorField]"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-showing" f7="chevron_right" v-if="item === selectedPrimaryItem"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</div>
</f7-col>
<f7-col width="50">
</div>
<div>
<div class="secondary-list-container">
<f7-list no-hairlines class="secondary-list no-margin-top no-margin-bottom" v-if="selectedPrimaryItem && primarySubItemsField && selectedPrimaryItem[primarySubItemsField]">
<f7-list dividers no-hairlines class="secondary-list no-margin-vertical" v-if="selectedPrimaryItem && primarySubItemsField && selectedPrimaryItem[primarySubItemsField]">
<f7-list-item link="#" no-chevron
v-for="subItem in selectedPrimaryItem[primarySubItemsField]"
:key="subItem | itemFieldContent(secondaryKeyField, subItem, false)"
:class="{ 'secondary-list-item-selected': isSecondarySelected(subItem) }"
:value="subItem | itemFieldContent(secondaryValueField, subItem, false)"
:title="subItem | itemFieldContent(secondaryTitleField, null, secondaryTitleI18n)"
:header="subItem | itemFieldContent(secondaryHeaderField, null, secondaryHeaderI18n)"
:footer="subItem | itemFieldContent(secondaryFooterField, null, secondaryFooterI18n)"
:value="secondaryValueField ? subItem[secondaryValueField] : subItem"
:title="$tIf(subItem[secondaryTitleField], secondaryTitleI18n)"
:header="$tIf(subItem[secondaryHeaderField], secondaryHeaderI18n)"
:footer="$tIf(subItem[secondaryFooterField], secondaryFooterI18n)"
:key="secondaryKeyField ? subItem[secondaryKeyField] : subItem"
v-for="subItem in selectedPrimaryItem[primarySubItemsField]"
@click="onSecondaryItemClicked(subItem)">
<f7-icon slot="media"
:icon="subItem[secondaryIconField] | icon(secondaryIconType)"
:style="subItem[secondaryColorField] | iconStyle(secondaryIconType, 'var(--default-icon-color)')"
v-if="secondaryIconField"></f7-icon>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="isSecondarySelected(subItem)"></f7-icon>
<template #media>
<ItemIcon :icon-type="secondaryIconType" :icon-id="subItem[secondaryIconField]" :color="subItem[secondaryColorField]"></ItemIcon>
</template>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="isSecondarySelected(subItem)"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</div>
</f7-col>
</f7-row>
</div>
</div>
</f7-page-content>
</f7-sheet>
</template>
@@ -58,7 +62,7 @@
<script>
export default {
props: [
'value',
'modelValue',
'primaryKeyField',
'primaryValueField',
'primaryTitleField',
@@ -85,12 +89,16 @@ export default {
'items',
'show'
],
emits: [
'update:modelValue',
'update:show'
],
data() {
const self = this;
return {
currentPrimaryValue: self.getPrimaryValueBySecondaryValue(self.value),
currentSecondaryValue: self.value
currentPrimaryValue: self.getPrimaryValueBySecondaryValue(self.modelValue),
currentSecondaryValue: self.modelValue
}
},
computed: {
@@ -126,13 +134,13 @@ export default {
},
methods: {
onSheetOpen(event) {
this.currentPrimaryValue = this.getPrimaryValueBySecondaryValue(this.value);
this.currentSecondaryValue = this.value;
this.currentPrimaryValue = this.getPrimaryValueBySecondaryValue(this.modelValue);
this.currentSecondaryValue = this.modelValue;
this.scrollToSelectedItem(event.$el, '.primary-list-container', 'li.primary-list-item-selected');
this.scrollToSelectedItem(event.$el, '.secondary-list-container', 'li.secondary-list-item-selected');
},
onSheetClosed() {
this.$emit('update:show', false);
this.close();
},
onPrimaryItemClicked(item) {
if (this.primaryValueField) {
@@ -148,8 +156,8 @@ export default {
this.currentSecondaryValue = subItem;
}
this.$emit('input', this.currentSecondaryValue);
this.$emit('update:show', false);
this.$emit('update:modelValue', this.currentSecondaryValue);
this.close();
},
isSecondarySelected(subItem) {
if (this.secondaryValueField) {
@@ -226,6 +234,9 @@ export default {
}
container.scrollTop(targetPos);
},
close() {
this.$emit('update:show', false);
}
}
}
+3 -5
View File
@@ -1,8 +1,6 @@
import Vue from 'vue';
import { createApp } from 'vue'
import App from './Desktop.vue';
new Vue({
el: '#app',
render: h => h(App),
})
const app = createApp(App);
app.mount('#app');
+6 -4
View File
@@ -10,9 +10,11 @@
<title>ezBookkeeping</title>
</head>
<body>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="./desktop-main.js"></script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import { autoChangeTextareaSize } from '../../lib/mobile/ui.js';
export default {
mounted(el) {
autoChangeTextareaSize(el);
},
updated(el) {
autoChangeTextareaSize(el);
}
}
-14
View File
@@ -1,14 +0,0 @@
import icons from '../consts/icon.js';
import utils from '../lib/utils.js';
export default function (iconId) {
if (utils.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!icons.allAccountIcons[iconId]) {
return icons.defaultAccountIcon.icon;
}
return icons.allAccountIcons[iconId].icon;
}
-19
View File
@@ -1,19 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor, additionalFieldName) {
if (color && color !== colorConstants.defaultAccountColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalFieldName) {
ret[additionalFieldName] = color;
}
return ret;
}
-14
View File
@@ -1,14 +0,0 @@
import icons from '../consts/icon.js';
import utils from '../lib/utils.js';
export default function (iconId) {
if (utils.isNumber(iconId)) {
iconId = iconId.toString();
}
if (!icons.allCategoryIcons[iconId]) {
return icons.defaultCategoryIcon.icon;
}
return icons.allCategoryIcons[iconId].icon;
}
-19
View File
@@ -1,19 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor, additionalFieldName) {
if (color && color !== colorConstants.defaultCategoryColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalFieldName) {
ret[additionalFieldName] = color;
}
return ret;
}
-52
View File
@@ -1,52 +0,0 @@
import currency from '../consts/currency.js';
import settings from '../lib/settings.js';
import utils from '../lib/utils.js';
export default function ({i18n}, value, currencyCode, notConvertValue) {
if (!utils.isNumber(value) && !utils.isString(value)) {
return value;
}
if (utils.isNumber(value)) {
value = value.toString();
}
if (!notConvertValue) {
const hasIncompleteFlag = utils.isString(value) && value.charAt(value.length - 1) === '+';
if (hasIncompleteFlag) {
value = value.substr(0, value.length - 1);
}
value = utils.numericCurrencyToString(value);
if (hasIncompleteFlag) {
value = value + '+';
}
}
const currencyDisplayMode = settings.getCurrencyDisplayMode();
if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Symbol) {
const currencyInfo = currency.all[currencyCode];
let currencySymbol = currency.defaultCurrencySymbol;
if (currencyInfo && currencyInfo.symbol) {
currencySymbol = currencyInfo.symbol;
} else if (currencyInfo && currencyInfo.code) {
currencySymbol = currencyInfo.code;
}
return i18n.t('format.currency.symbol', {
amount: value,
symbol: currencySymbol
});
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Code) {
return `${value} ${currencyCode}`;
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Name) {
const currencyName = i18n.t(`currency.${currencyCode}`);
return `${value} ${currencyName}`;
} else {
return value;
}
}
-11
View File
@@ -1,11 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor) {
if (color && color !== colorConstants.defaultColor) {
color = '#' + color;
} else {
color = defaultColor;
}
return color;
}
-19
View File
@@ -1,19 +0,0 @@
import colorConstants from '../consts/color.js';
export default function (color, defaultColor, additionalFieldName) {
if (color && color !== colorConstants.defaultColor) {
color = '#' + color;
} else {
color = defaultColor;
}
const ret = {
color: color
};
if (additionalFieldName) {
ret[additionalFieldName] = color;
}
return ret;
}
-18
View File
@@ -1,18 +0,0 @@
export default function (rate) {
const rateStr = rate.toString();
if (rateStr.indexOf('.') < 0) {
return rateStr;
} else {
let firstNonZeroPos = 0;
for (let i = 0; i < rateStr.length; i++) {
if (rateStr.charAt(i) !== '.' && rateStr.charAt(i) !== '0') {
firstNonZeroPos = Math.min(i + 4, rateStr.length);
break;
}
}
return rateStr.substring(0, Math.max(6, Math.max(firstNonZeroPos, rateStr.indexOf('.') + 2)));
}
}
-3
View File
@@ -1,3 +0,0 @@
export default function (value, format) {
return format.replaceAll(/#{value}/g, value);
}
-12
View File
@@ -1,12 +0,0 @@
import accountIcon from './accountIcon.js';
import categoryIcon from './categoryIcon.js';
export default function (iconId, iconType) {
if (iconType === 'account') {
return accountIcon(iconId);
} else if (iconType === 'category') {
return categoryIcon(iconId);
} else {
return '';
}
}
-13
View File
@@ -1,13 +0,0 @@
import defaultIconStyle from './defaultIconStyle.js';
import accountIconStyle from './accountIconStyle.js';
import categoryIconStyle from './categoryIconStyle.js';
export default function (color, iconType, defaultColor, additionalFieldName) {
if (iconType === 'account') {
return accountIconStyle(color, defaultColor, additionalFieldName);
} else if (iconType === 'category') {
return categoryIconStyle(color, defaultColor, additionalFieldName);
} else {
return defaultIconStyle(color, defaultColor, additionalFieldName);
}
}
-13
View File
@@ -1,13 +0,0 @@
export default function ({i18n}, value, fieldName, defaultValue, translate) {
let content = defaultValue;
if (fieldName) {
content = value[fieldName];
}
if (translate && content) {
content = i18n.t(content);
}
return content;
}
-11
View File
@@ -1,11 +0,0 @@
import { allLanguages } from '../locales/index.js';
export default function (languageCode) {
const lang = allLanguages[languageCode];
if (!lang) {
return '';
}
return lang.displayName;
}
-3
View File
@@ -1,3 +0,0 @@
export default function ({i18n}, text, options) {
return i18n.t(text, options || {});
}
-20
View File
@@ -1,20 +0,0 @@
import utils from '../lib/utils.js';
export default function (value, format, options) {
if (!utils.isNumber(value)) {
value = utils.getUnixTime(value);
}
let utcOffset = null;
let currentUtcOffset = null;
if (utils.isObject(options) && utils.isNumber(options.utcOffset)) {
utcOffset = options.utcOffset;
}
if (utils.isObject(options) && utils.isNumber(options.currentUtcOffset)) {
currentUtcOffset = options.currentUtcOffset;
}
return utils.formatUnixTime(value, format, utcOffset, currentUtcOffset);
}
-43
View File
@@ -1,43 +0,0 @@
import utils from '../lib/utils.js';
export default function (value, options, keyField, nameField, defaultName) {
if (utils.isArray(options)) {
if (keyField) {
for (let i = 0; i < options.length; i++) {
const option = options[i];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (options[value]) {
const option = options[value];
return option[nameField];
}
}
} else if (utils.isObject(options)) {
if (keyField) {
for (let key in options) {
if (!Object.prototype.hasOwnProperty.call(options, key)) {
continue;
}
const option = options[key];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (options[value]) {
const option = options[value];
return option[nameField];
}
}
}
return defaultName;
}
-11
View File
@@ -1,11 +0,0 @@
export default function (value, precision, lowPrecisionValue) {
const ratio = Math.pow(10, precision);
const normalizedValue = Math.floor(value * ratio);
if (value > 0 && normalizedValue < 1 && lowPrecisionValue) {
return lowPrecisionValue + '%';
}
const result = normalizedValue / ratio;
return result + '%';
}
-19
View File
@@ -1,19 +0,0 @@
export default function (value, maxLength) {
let length = 0;
for (let i = 0; i < value.length; i++) {
const c = value.charCodeAt(i);
if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
length++;
} else {
length += 2;
}
}
if (length <= maxLength || maxLength <= 3) {
return value;
}
return value.substring(0, maxLength - 3) + '...';
}
-36
View File
@@ -1,36 +0,0 @@
import utils from '../lib/utils.js';
export default function (token) {
const ua = utils.parseUserAgent(token.userAgent);
let result = '';
if (ua.device.model) {
result = ua.device.model;
} else if (ua.os.name) {
result = ua.os.name;
if (ua.os.version) {
result += ' ' + ua.os.version;
}
}
if (ua.browser.name) {
let browserInfo = ua.browser.name;
if (ua.browser.version) {
browserInfo += ' ' + ua.browser.version;
}
if (result) {
result += ' (' + browserInfo + ')';
} else {
result = browserInfo;
}
}
if (!result) {
return 'Unknown Device';
}
return result;
}
-22
View File
@@ -1,22 +0,0 @@
import icons from '../consts/icon.js';
import utils from '../lib/utils.js';
export default function (token) {
const ua = utils.parseUserAgent(token.userAgent);
if (!ua || !ua.device) {
return icons.deviceIcons.desktop.f7Icon;
}
if (ua.device.type === 'mobile') {
return icons.deviceIcons.mobile.f7Icon;
} else if (ua.device.type === 'wearable') {
return icons.deviceIcons.wearable.f7Icon;
} else if (ua.device.type === 'tablet') {
return icons.deviceIcons.tablet.f7Icon;
} else if (ua.device.type === 'smarttv') {
return icons.deviceIcons.tv.f7Icon;
} else {
return icons.deviceIcons.desktop.f7Icon;
}
}
-6
View File
@@ -1,6 +0,0 @@
import utils from '../lib/utils.js';
export default function (utcOffsetMinutes) {
const utcOffset = utils.getUtcOffsetByUtcOffsetMinutes(utcOffsetMinutes);
return `(UTC${utcOffset})`;
}
+6 -4
View File
@@ -52,9 +52,11 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="./index-main.js"></script>
</body>
</html>
+194 -2
View File
@@ -1,4 +1,8 @@
import { defaultLanguage, allLanguages } from '../locales/index.js';
import timezone from "../consts/timezone.js";
import currency from "../consts/currency.js";
import settings from "./settings";
import utils from './utils.js';
const apiNotFoundErrorCode = 100001;
const specifiedApiNotFoundErrors = {
@@ -121,11 +125,11 @@ const parameterizedErrors = [
}
];
export function getAllLanguages() {
export function getAllLanguageInfos() {
return allLanguages;
}
export function getLanguage(locale) {
export function getLanguageInfo(locale) {
return allLanguages[locale];
}
@@ -168,8 +172,196 @@ export function getDefaultLanguage() {
return browserLocale;
}
export function transateIf(text, isTranslate, translateFn) {
if (isTranslate) {
return translateFn(text);
}
return text;
}
export function getAllLongMonthNames(translateFn) {
return [
translateFn('datetime.January.long'),
translateFn('datetime.February.long'),
translateFn('datetime.March.long'),
translateFn('datetime.April.long'),
translateFn('datetime.May.long'),
translateFn('datetime.June.long'),
translateFn('datetime.July.long'),
translateFn('datetime.August.long'),
translateFn('datetime.September.long'),
translateFn('datetime.October.long'),
translateFn('datetime.November.long'),
translateFn('datetime.December.long')
];
}
export function getAllShortMonthNames(translateFn) {
return [
translateFn('datetime.January.short'),
translateFn('datetime.February.short'),
translateFn('datetime.March.short'),
translateFn('datetime.April.short'),
translateFn('datetime.May.short'),
translateFn('datetime.June.short'),
translateFn('datetime.July.short'),
translateFn('datetime.August.short'),
translateFn('datetime.September.short'),
translateFn('datetime.October.short'),
translateFn('datetime.November.short'),
translateFn('datetime.December.short')
];
}
export function getAllLongWeekdayNames(translateFn) {
return [
translateFn('datetime.Sunday.long'),
translateFn('datetime.Monday.long'),
translateFn('datetime.Tuesday.long'),
translateFn('datetime.Wednesday.long'),
translateFn('datetime.Thursday.long'),
translateFn('datetime.Friday.long'),
translateFn('datetime.Saturday.long')
];
}
export function getAllShortWeekdayNames(translateFn) {
return [
translateFn('datetime.Sunday.short'),
translateFn('datetime.Monday.short'),
translateFn('datetime.Tuesday.short'),
translateFn('datetime.Wednesday.short'),
translateFn('datetime.Thursday.short'),
translateFn('datetime.Friday.short'),
translateFn('datetime.Saturday.short')
];
}
export function getAllMinWeekdayNames(translateFn) {
return [
translateFn('datetime.Sunday.min'),
translateFn('datetime.Monday.min'),
translateFn('datetime.Tuesday.min'),
translateFn('datetime.Wednesday.min'),
translateFn('datetime.Thursday.min'),
translateFn('datetime.Friday.min'),
translateFn('datetime.Saturday.min')
];
}
export function getAllTimezones(includeSystemDefault, translateFn) {
const defaultTimezoneOffset = utils.getTimezoneOffset();
const defaultTimezoneOffsetMinutes = utils.getTimezoneOffsetMinutes();
const allTimezones = timezone.all;
const allTimezoneInfos = [];
for (let i = 0; i < allTimezones.length; i++) {
allTimezoneInfos.push({
name: allTimezones[i].timezoneName,
utcOffset: (allTimezones[i].timezoneName !== 'Etc/GMT' ? utils.getTimezoneOffset(allTimezones[i].timezoneName) : ''),
utcOffsetMinutes: utils.getTimezoneOffsetMinutes(allTimezones[i].timezoneName),
displayName: translateFn(`timezone.${allTimezones[i].displayName}`)
});
}
if (includeSystemDefault) {
allTimezoneInfos.push({
name: '',
utcOffset: defaultTimezoneOffset,
utcOffsetMinutes: defaultTimezoneOffsetMinutes,
displayName: translateFn('System Default')
});
}
allTimezoneInfos.sort(function(c1, c2){
const utcOffset1 = parseInt(c1.utcOffset.replace(':', ''));
const utcOffset2 = parseInt(c2.utcOffset.replace(':', ''));
if (utcOffset1 !== utcOffset2) {
return utcOffset1 - utcOffset2;
}
return c1.displayName.localeCompare(c2.displayName);
})
return allTimezoneInfos;
}
export function getAllCurrencies(translateFn) {
const allCurrencyCodes = currency.all;
const allCurrencies = [];
for (let currencyCode in allCurrencyCodes) {
if (!Object.prototype.hasOwnProperty.call(allCurrencyCodes, currencyCode)) {
return;
}
allCurrencies.push({
code: currencyCode,
displayName: translateFn(`currency.${currencyCode}`)
});
}
allCurrencies.sort(function(c1, c2){
return c1.displayName.localeCompare(c2.displayName);
})
return allCurrencies;
}
export function getDisplayCurrency(value, currencyCode, notConvertValue, translateFn) {
if (!utils.isNumber(value) && !utils.isString(value)) {
return value;
}
if (utils.isNumber(value)) {
value = value.toString();
}
if (!notConvertValue) {
const hasIncompleteFlag = utils.isString(value) && value.charAt(value.length - 1) === '+';
if (hasIncompleteFlag) {
value = value.substring(0, value.length - 1);
}
value = utils.numericCurrencyToString(value);
if (hasIncompleteFlag) {
value = value + '+';
}
}
const currencyDisplayMode = settings.getCurrencyDisplayMode();
if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Symbol) {
const currencyInfo = currency.all[currencyCode];
let currencySymbol = currency.defaultCurrencySymbol;
if (currencyInfo && currencyInfo.symbol) {
currencySymbol = currencyInfo.symbol;
} else if (currencyInfo && currencyInfo.code) {
currencySymbol = currencyInfo.code;
}
return translateFn('format.currency.symbol', {
amount: value,
symbol: currencySymbol
});
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Code) {
return `${value} ${currencyCode}`;
} else if (currencyCode && currencyDisplayMode === currency.allCurrencyDisplayModes.Name) {
const currencyName = translateFn(`currency.${currencyCode}`);
return `${value} ${currencyName}`;
} else {
return value;
}
}
export function getI18nOptions() {
return {
legacy: false,
locale: defaultLanguage,
fallbackLocale: defaultLanguage,
formatFallbackMessages: true,
+2 -2
View File
@@ -1,8 +1,8 @@
export default {
getLicense: () => {
return process.env.LICENSE;
return __EZBOOKKEEPING_LICENSE__; // eslint-disable-line
},
getThirdPartyLicenses: () => {
return process.env.THIRD_PARTY_LICENSES || [];
return __EZBOOKKEEPING_THIRD_PARTY_LICENSES__ || []; // eslint-disable-line
}
};
+130
View File
@@ -0,0 +1,130 @@
import { f7, f7ready } from 'framework7-vue';
import settings from "../settings.js";
import {
getLocalizedError,
getLocalizedErrorParameters
} from "../i18n.js";
export function showAlert(message, confirmCallback, translateFn) {
let parameters = {};
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => translateFn(s));
}
f7ready((f7) => {
f7.dialog.create({
title: translateFn('global.app.title'),
text: translateFn(message, parameters),
animate: settings.isEnableAnimate(),
buttons: [
{
text: translateFn('OK'),
onClick: confirmCallback
}
]
}).open();
});
}
export function showConfirm(message, confirmCallback, cancelCallback, translateFn) {
f7ready((f7) => {
f7.dialog.create({
title: translateFn('global.app.title'),
text: translateFn(message),
animate: settings.isEnableAnimate(),
buttons: [
{
text: translateFn('Cancel'),
onClick: cancelCallback
},
{
text: translateFn('OK'),
onClick: confirmCallback
}
]
}).open();
});
}
export function showToast(message, timeout, translateFn) {
let parameters = {};
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => translateFn(s));
}
f7ready((f7) => {
f7.toast.create({
text: translateFn(message, parameters),
position: 'center',
closeTimeout: timeout || 1500
}).open();
});
}
export function showLoading(delayConditionFunc, delayMills) {
if (!delayConditionFunc) {
f7ready((f7) => {
return f7.preloader.show();
});
}
f7ready((f7) => {
setTimeout(() => {
if (delayConditionFunc()) {
f7.preloader.show();
}
}, delayMills || 200);
});
}
export function hideLoading() {
f7ready((f7) => {
return f7.preloader.hide();
});
}
export function routeBackOnError(f7router, errorPropertyName) {
const self = this;
const router = f7router;
const unwatch = self.$watch(errorPropertyName, () => {
if (self[errorPropertyName]) {
setTimeout(() => {
if (unwatch) {
unwatch();
}
router.back();
}, 200);
}
}, {
immediate: true
});
}
export function elements(selector) {
return f7.$(selector);
}
export function isModalShowing() {
return f7.$('.modal-in').length;
}
export function onSwipeoutDeleted(domId, callback) {
f7.swipeout.delete(f7.$('#' + domId), callback);
}
export function autoChangeTextareaSize(el) {
f7.$(el).find('textarea').each(el => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
}
+164
View File
@@ -1,5 +1,6 @@
import CryptoJS from 'crypto-js';
import moment from 'moment';
import Clipboard from 'clipboard';
import uaParser from 'ua-parser-js';
import dateTimeConstants from '../consts/datetime.js';
@@ -157,6 +158,10 @@ function getCurrentUnixTime() {
return moment().unix();
}
function getCurrentDateTime() {
return moment();
}
function parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset) {
if (isNumber(utcOffset)) {
if (!isNumber(currentUtcOffset)) {
@@ -169,6 +174,16 @@ function parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset) {
return moment.unix(unixTime);
}
function is24HourFormat(format) {
if (format.indexOf('HH') >= 0 && format.indexOf('hh') < 0) {
return true;
} else if (format.indexOf('HH') < 0 && format.indexOf('hh') >= 0) {
return false;
}
return true;
}
function formatUnixTime(unixTime, format, utcOffset, currentUtcOffset) {
return parseDateFromUnixTime(unixTime, utcOffset, currentUtcOffset).format(format);
}
@@ -571,6 +586,38 @@ function getExchangedAmount(amount, fromRate, toRate) {
return amount * exchangeRate;
}
function formatPercent(value, precision, lowPrecisionValue) {
const ratio = Math.pow(10, precision);
const normalizedValue = Math.floor(value * ratio);
if (value > 0 && normalizedValue < 1 && lowPrecisionValue) {
return lowPrecisionValue + '%';
}
const result = normalizedValue / ratio;
return result + '%';
}
function limitText(value, maxLength) {
let length = 0;
for (let i = 0; i < value.length; i++) {
const c = value.charCodeAt(i);
if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
length++;
} else {
length += 2;
}
}
if (length <= maxLength || maxLength <= 3) {
return value;
}
return value.substring(0, maxLength - 3) + '...';
}
function base64encode(arrayBuffer) {
if (!arrayBuffer || arrayBuffer.length === 0) {
return null;
@@ -587,6 +634,48 @@ function stringToArrayBuffer(str){
return Uint8Array.from(str, c => c.charCodeAt(0)).buffer;
}
function getNameByKeyValue(src, value, keyField, nameField, defaultName) {
if (isArray(src)) {
if (keyField) {
for (let i = 0; i < src.length; i++) {
const option = src[i];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (src[value]) {
const option = src[value];
return option[nameField];
}
}
} else if (isObject(src)) {
if (keyField) {
for (let key in src) {
if (!Object.prototype.hasOwnProperty.call(src, key)) {
continue;
}
const option = src[key];
if (option[keyField] === value) {
return option[nameField];
}
}
} else {
if (src[value]) {
const option = src[value];
return option[nameField];
}
}
}
return defaultName;
}
function generateRandomString() {
const baseString = 'ebk_' + Math.round(new Date().getTime() / 1000) + '_' + Math.random();
return CryptoJS.SHA256(baseString).toString();
@@ -612,6 +701,41 @@ function parseUserAgent(ua) {
};
}
function parseDeviceInfo(ua) {
const uaInfo = parseUserAgent(ua);
let result = '';
if (uaInfo.device.model) {
result = uaInfo.device.model;
} else if (uaInfo.os.name) {
result = uaInfo.os.name;
if (uaInfo.os.version) {
result += ' ' + uaInfo.os.version;
}
}
if (uaInfo.browser.name) {
let browserInfo = uaInfo.browser.name;
if (uaInfo.browser.version) {
browserInfo += ' ' + uaInfo.browser.version;
}
if (result) {
result += ' (' + browserInfo + ')';
} else {
result = browserInfo;
}
}
if (!result) {
return 'Unknown Device';
}
return result;
}
function transactionTypeToCategroyType(transactionType) {
if (transactionType === transactionConstants.allTransactionTypes.Income) {
return categoryConstants.allCategoryTypes.Income;
@@ -721,6 +845,38 @@ function getAllFilteredAccountsBalance(categorizedAccounts, accountFilter) {
return ret;
}
function makeButtonCopyToClipboard({ text, el, successCallback, errorCallback }) {
const clipboard = new Clipboard(el, {
text: function () {
return text;
}
});
clipboard.on('success', (e) => {
if (successCallback) {
successCallback(e);
}
});
clipboard.on('error', (e) => {
if (errorCallback) {
errorCallback(e);
}
});
return clipboard;
}
function changeClipboardObjectText(clipboard, text) {
if (!clipboard) {
return;
}
clipboard.text = function () {
return text;
};
}
export default {
isFunction,
isObject,
@@ -739,7 +895,9 @@ export default {
getActualUnixTimeForStore,
getDummyUnixTimeForLocalUsage,
getCurrentUnixTime,
getCurrentDateTime,
parseDateFromUnixTime,
is24HourFormat,
formatUnixTime,
formatTime,
getUnixTime,
@@ -773,14 +931,20 @@ export default {
numericCurrencyToString,
stringCurrencyToNumeric,
getExchangedAmount,
formatPercent,
limitText,
base64encode,
arrayBufferToString,
stringToArrayBuffer,
getNameByKeyValue,
generateRandomString,
parseUserAgent,
parseDeviceInfo,
transactionTypeToCategroyType,
categroyTypeToTransactionType,
getCategoryInfo,
getCategorizedAccounts,
getAllFilteredAccountsBalance,
makeButtonCopyToClipboard,
changeClipboardObjectText,
};
+4 -4
View File
@@ -1,15 +1,15 @@
export default {
getVersion: () => {
let version = process.env.VERSION || 'unknown';
let commitHash = process.env.COMMIT_HASH;
let version = __EZBOOKKEEPING_VERSION__ || 'unknown'; // eslint-disable-line
let commitHash = __EZBOOKKEEPING_BUILD_COMMIT_HASH__; // eslint-disable-line
if (commitHash) {
return `${version} (${commitHash.substr(0, Math.min(7, commitHash.length))})`
return `${version} (${commitHash.substring(0, Math.min(7, commitHash.length))})`
} else {
return version;
}
},
getBuildTime: () => {
return process.env.BUILD_UNIXTIME;
return __EZBOOKKEEPING_BUILD_UNIX_TIME__; // eslint-disable-line
}
};
+7 -5
View File
@@ -36,11 +36,6 @@ export default {
'symbol': '{symbol} {amount}'
}
},
'input-format': { // The type of date or time format is framework7 format, ref: https://v5.framework7.io/docs/calendar.html#calendar-parameters
'datetime': {
'long': 'm/d/yyyy hh::mm A',
},
},
'dataExport': {
'defaultExportFilename': 'ezBookkeeping_export_data',
'exportFilename': 'ezBookkeeping_{nickname}_export_data'
@@ -658,6 +653,7 @@ export default {
'Update': 'Update',
'None': 'None',
'Not Specified': 'Not Specified',
'No results': 'No results',
'Done': 'Done',
'Continue': 'Continue',
'Status': 'Status',
@@ -690,6 +686,7 @@ export default {
'End Time': 'End Time',
'Select Date': 'Select Date',
'Select Time': 'Select Time',
'Current Time': 'Current Time',
'Custom': 'Custom',
'Pie Chart': 'Pie Chart',
'Bar Chart': 'Bar Chart',
@@ -865,8 +862,11 @@ export default {
'Account Total Assets': 'Account Total Assets',
'Account Total Liabilities': 'Account Total Liabilities',
'Statistics Settings': 'Statistics Settings',
'Chart Type': 'Chart Type',
'Default Chart Type': 'Default Chart Type',
'Chart Data Type': 'Chart Data Type',
'Default Chart Data Type': 'Default Chart Data Type',
'Date Range': 'Date Range',
'Default Date Range': 'Default Date Range',
'Default Account Filter': 'Default Account Filter',
'Default Transaction Category Filter': 'Default Transaction Category Filter',
@@ -975,6 +975,8 @@ export default {
'You have logged out all other sessions': 'You have logged out all other sessions',
'Unable to logout all other sessions': 'Unable to logout all other sessions',
'Regenerate Backup Codes': 'Regenerate Backup Codes',
'Enable Two-Factor Authentication': 'Enable Two-Factor Authentication',
'Disable Two-Factor Authentication': 'Disable Two-Factor Authentication',
'Please use two factor authentication app scan the below qrcode and input current passcode': 'Please use two factor authentication app scan the below qrcode and input current passcode',
'Please enter your current password when disable two factor authentication': 'Please enter your current password when disable two factor authentication',
'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.',
+2
View File
@@ -1,3 +1,5 @@
import 'moment/dist/locale/zh-cn.js';
import en from './en.js'
import zhHans from './zh_Hans.js'
+7 -5
View File
@@ -36,11 +36,6 @@ export default {
'symbol': '{symbol} {amount}'
}
},
'input-format': {
'datetime': {
'long': 'yyyy年m月d日 HH::mm',
},
},
'dataExport': {
'defaultExportFilename': 'ezBookkeeping_导出数据',
'exportFilename': 'ezBookkeeping_{nickname}_导出数据'
@@ -658,6 +653,7 @@ export default {
'Update': '更新',
'None': '无',
'Not Specified': '未指定',
'No results': '无结果',
'Done': '完成',
'Continue': '继续',
'Status': '状态',
@@ -690,6 +686,7 @@ export default {
'End Time': '结束时间',
'Select Date': '选择日期',
'Select Time': '选择时间',
'Current Time': '当前时间',
'Custom': '自定义',
'Pie Chart': '饼图',
'Bar Chart': '条形图',
@@ -865,8 +862,11 @@ export default {
'Account Total Assets': '账户总资产',
'Account Total Liabilities': '账户总负债',
'Statistics Settings': '统计设置',
'Chart Type': '图表类型',
'Default Chart Type': '默认图表类型',
'Chart Data Type': '图表数据类型',
'Default Chart Data Type': '默认图表数据类型',
'Date Range': '时间范围',
'Default Date Range': '默认时间范围',
'Default Account Filter': '默认账号过滤',
'Default Transaction Category Filter': '默认交易分类过滤',
@@ -975,6 +975,8 @@ export default {
'You have logged out all other sessions': '您已经退出其他所有会话',
'Unable to logout all other sessions': '无法退出其他所有会话',
'Regenerate Backup Codes': '重新生成备用码',
'Enable Two-Factor Authentication': '启用两步验证',
'Disable Two-Factor Authentication': '禁用两步验证',
'Please use two factor authentication app scan the below qrcode and input current passcode': '请使用两步验证应用扫描下方的二维码并输入当前的验证码',
'Please enter your current password when disable two factor authentication': '禁用两步验证时需要输入您的当前密码',
'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': '重新生成两步验证备用码时需要输入您的当前密码。如果您重新生成备用码,之前的备用码将失效。',
+250 -445
View File
@@ -1,54 +1,80 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { createApp } from 'vue';
import { createStore } from 'vuex';
import { createI18n } from 'vue-i18n';
import VueI18n from 'vue-i18n';
import PincodeInput from 'vue-pincode-input';
import VueClipboard from 'vue-clipboard2';
import moment from "moment-timezone";
import moment from 'moment-timezone';
import Framework7 from 'framework7/lite';
import Framework7Dialog from 'framework7/components/dialog';
import Framework7Popup from 'framework7/components/popup';
import Framework7LoginScreen from 'framework7/components/login-screen';
import Framework7Popover from 'framework7/components/popover';
import Framework7Actions from 'framework7/components/actions';
import Framework7Sheet from 'framework7/components/sheet';
import Framework7Toast from 'framework7/components/toast';
import Framework7Preloader from 'framework7/components/preloader';
import Framework7Progressbar from 'framework7/components/progressbar';
import Framework7Sortable from 'framework7/components/sortable';
import Framework7Swipeout from 'framework7/components/swipeout';
import Framework7Accordion from 'framework7/components/accordion';
import Framework7Card from 'framework7/components/card';
import Framework7Chip from 'framework7/components/chip';
import Framework7Form from 'framework7/components/form';
import Framework7Input from 'framework7/components/input';
import Framework7Checkbox from 'framework7/components/checkbox';
import Framework7Radio from 'framework7/components/radio';
import Framework7Toggle from 'framework7/components/toggle';
import Framework7SmartSelect from 'framework7/components/smart-select';
import Framework7Grid from 'framework7/components/grid';
import Framework7Picker from 'framework7/components/picker';
import Framework7InfiniteScroll from 'framework7/components/infinite-scroll';
import Framework7PullToRefresh from 'framework7/components/pull-to-refresh';
import Framework7Searchbar from 'framework7/components/searchbar';
import Framework7Tooltip from 'framework7/components/tooltip';
import Framework7Skeleton from 'framework7/components/skeleton';
import Framework7Treeview from 'framework7/components/treeview';
import Framework7Typography from 'framework7/components/typography';
import Framework7Vue, { registerComponents } from 'framework7-vue/bundle';
import Framework7 from 'framework7/framework7-lite.esm.js';
import Framework7Dialog from 'framework7/components/dialog/dialog';
import Framework7Popup from 'framework7/components/popup/popup';
import Framework7LoginScreen from 'framework7/components/login-screen/login-screen';
import Framework7Popover from 'framework7/components/popover/popover';
import Framework7Actions from 'framework7/components/actions/actions';
import Framework7Sheet from 'framework7/components/sheet/sheet';
import Framework7Toast from 'framework7/components/toast/toast';
import Framework7Preloader from 'framework7/components/preloader/preloader';
import Framework7Progressbar from 'framework7/components/progressbar/progressbar';
import Framework7Sortable from 'framework7/components/sortable/sortable';
import Framework7Swipeout from 'framework7/components/swipeout/swipeout';
import Framework7Accordion from 'framework7/components/accordion/accordion';
import Framework7Card from 'framework7/components/card/card';
import Framework7Chip from 'framework7/components/chip/chip';
import Framework7Form from 'framework7/components/form/form';
import Framework7Input from 'framework7/components/input/input';
import Framework7Checkbox from 'framework7/components/checkbox/checkbox';
import Framework7Radio from 'framework7/components/radio/radio';
import Framework7Toggle from 'framework7/components/toggle/toggle';
import Framework7SmartSelect from 'framework7/components/smart-select/smart-select';
import Framework7Grid from 'framework7/components/grid/grid';
import Framework7Calendar from 'framework7/components/calendar/calendar';
import Framework7Picker from 'framework7/components/picker/picker';
import Framework7InfiniteScroll from 'framework7/components/infinite-scroll/infinite-scroll';
import Framework7PullToRefresh from 'framework7/components/pull-to-refresh/pull-to-refresh';
import Framework7Searchbar from 'framework7/components/searchbar/searchbar';
import Framework7Tooltip from 'framework7/components/tooltip/tooltip';
import Framework7Skeleton from 'framework7/components/skeleton/skeleton';
import Framework7Menu from 'framework7/components/menu/menu';
import Framework7Treeview from 'framework7/components/treeview/treeview';
import Framework7Typography from 'framework7/components/typography/typography';
import Framework7Vue from 'framework7-vue/framework7-vue.esm.bundle.js';
import 'framework7/css';
import 'framework7/components/dialog/css';
import 'framework7/components/popup/css';
import 'framework7/components/login-screen/css';
import 'framework7/components/popover/css';
import 'framework7/components/actions/css';
import 'framework7/components/sheet/css';
import 'framework7/components/toast/css';
import 'framework7/components/preloader/css';
import 'framework7/components/progressbar/css';
import 'framework7/components/sortable/css';
import 'framework7/components/swipeout/css';
import 'framework7/components/accordion/css';
import 'framework7/components/card/css';
import 'framework7/components/chip/css';
import 'framework7/components/form/css';
import 'framework7/components/input/css';
import 'framework7/components/checkbox/css';
import 'framework7/components/radio/css';
import 'framework7/components/toggle/css';
import 'framework7/components/smart-select/css';
import 'framework7/components/grid/css';
import 'framework7/components/picker/css';
import 'framework7/components/infinite-scroll/css';
import 'framework7/components/pull-to-refresh/css';
import 'framework7/components/searchbar/css';
import 'framework7/components/tooltip/css';
import 'framework7/components/skeleton/css';
import 'framework7/components/treeview/css';
import 'framework7/components/typography/css';
import 'framework7/css/framework7.bundle.css';
import 'framework7-icons';
import 'line-awesome/dist/line-awesome/css/line-awesome.css';
import VueDatePicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
import api from './consts/api.js';
import datetime from './consts/datetime.js';
import timezone from './consts/timezone.js';
import currency from './consts/currency.js';
import colors from './consts/color.js';
import icons from './consts/icon.js';
@@ -65,35 +91,42 @@ import services from './lib/services.js';
import userstate from './lib/userstate.js';
import webauthn from './lib/webauthn.js';
import utils from './lib/utils.js';
import { getAllLanguages, getLanguage, getDefaultLanguage, getI18nOptions, getLocalizedError, getLocalizedErrorParameters } from './lib/i18n.js';
import {
getAllLanguageInfos,
getLanguageInfo,
getDefaultLanguage,
transateIf,
getAllLongMonthNames,
getAllShortMonthNames,
getAllLongWeekdayNames,
getAllShortWeekdayNames,
getAllMinWeekdayNames,
getAllTimezones,
getAllCurrencies,
getDisplayCurrency,
getI18nOptions,
} from './lib/i18n.js';
import {
showAlert,
showConfirm,
showToast,
showLoading,
hideLoading,
routeBackOnError,
elements,
isModalShowing,
onSwipeoutDeleted
} from './lib/mobile/ui.js';
import stores from './store/index.js';
import localizedFilter from './filters/localized.js';
import momentFilter from './filters/moment.js';
import percentFilter from './filters/percent.js';
import formatFilter from './filters/format.js';
import optionNameFilter from './filters/optionName.js';
import itemFieldContentFilter from './filters/itemFieldContent.js';
import languageNameFilter from './filters/languageName.js';
import currencyFilter from './filters/currency.js';
import exchangeRateFilter from './filters/exchangeRate.js';
import utcOffsetFilter from './filters/utcOffset.js';
import textLimitFilter from './filters/textLimit.js';
import iconFilter from './filters/icon.js';
import iconStyleFilter from './filters/iconStyle.js';
import defaultIconColorFilter from './filters/defaultIconColor.js';
import accountIconFilter from './filters/accountIcon.js';
import accountIconStyleFilter from './filters/accountIconStyle.js';
import categoryIconFilter from './filters/categoryIcon.js';
import categoryIconStyleFilter from './filters/categoryIconStyle.js';
import tokenDeviceFilter from './filters/tokenDevice.js';
import tokenIconFilter from './filters/tokenIcon.js';
import ItemIcon from './components/mobile/ItemIcon.vue';
import PieChart from './components/mobile/PieChart.vue';
import PinCodeInput from './components/mobile/PinCodeInput.vue';
import PinCodeInputSheet from './components/mobile/PinCodeInputSheet.vue';
import PasswordInputSheet from './components/mobile/PasswordInputSheet.vue';
import PasscodeInputSheet from './components/mobile/PasscodeInputSheet.vue';
import PinCodeInputSheet from './components/mobile/PinCodeInputSheet.vue';
import DateTimeSelectionSheet from './components/mobile/DateTimeSelectionSheet.vue';
import DateRangeSelectionSheet from './components/mobile/DateRangeSelectionSheet.vue';
import ListItemSelectionSheet from './components/mobile/ListItemSelectionSheet.vue';
import TwoColumnListItemSelectionSheet from './components/mobile/TwoColumnListItemSelectionSheet.vue';
@@ -104,93 +137,136 @@ import InformationSheet from './components/mobile/InformationSheet.vue';
import NumberPadSheet from './components/mobile/NumberPadSheet.vue';
import TransactionTagSelectionSheet from './components/mobile/TransactionTagSelectionSheet.vue';
import TextareaAutoSize from "./directives/mobile/textareaAutoSize.js";
import App from './Mobile.vue';
Framework7.use(Framework7Dialog);
Framework7.use(Framework7Popup);
Framework7.use(Framework7LoginScreen);
Framework7.use(Framework7Popover);
Framework7.use(Framework7Actions);
Framework7.use(Framework7Sheet);
Framework7.use(Framework7Toast);
Framework7.use(Framework7Preloader);
Framework7.use(Framework7Progressbar);
Framework7.use(Framework7Sortable);
Framework7.use(Framework7Swipeout);
Framework7.use(Framework7Accordion);
Framework7.use(Framework7Card);
Framework7.use(Framework7Chip);
Framework7.use(Framework7Form);
Framework7.use(Framework7Input);
Framework7.use(Framework7Checkbox);
Framework7.use(Framework7Radio);
Framework7.use(Framework7Toggle);
Framework7.use(Framework7SmartSelect);
Framework7.use(Framework7Grid);
Framework7.use(Framework7Calendar);
Framework7.use(Framework7Picker);
Framework7.use(Framework7InfiniteScroll);
Framework7.use(Framework7PullToRefresh);
Framework7.use(Framework7Searchbar);
Framework7.use(Framework7Tooltip);
Framework7.use(Framework7Skeleton);
Framework7.use(Framework7Menu);
Framework7.use(Framework7Treeview);
Framework7.use(Framework7Typography);
Framework7.use(Framework7Vue);
Framework7.use([
Framework7Dialog,
Framework7Popup,
Framework7LoginScreen,
Framework7Popover,
Framework7Actions,
Framework7Sheet,
Framework7Toast,
Framework7Preloader,
Framework7Progressbar,
Framework7Sortable,
Framework7Swipeout,
Framework7Accordion,
Framework7Card,
Framework7Chip,
Framework7Form,
Framework7Input,
Framework7Checkbox,
Framework7Radio,
Framework7Toggle,
Framework7SmartSelect,
Framework7Grid,
Framework7Picker,
Framework7InfiniteScroll,
Framework7PullToRefresh,
Framework7Searchbar,
Framework7Tooltip,
Framework7Skeleton,
Framework7Treeview,
Framework7Typography,
Framework7Vue
]);
Vue.use(Vuex);
Vue.use(VueI18n);
Vue.use(VueClipboard);
const app = createApp(App);
const store = createStore(stores);
const i18n = createI18n(getI18nOptions());
registerComponents(app);
app.use(store);
app.use(i18n);
Vue.component('PincodeInput', PincodeInput);
Vue.component('PieChart', PieChart);
Vue.component('PasswordInputSheet', PasswordInputSheet);
Vue.component('PasscodeInputSheet', PasscodeInputSheet);
Vue.component('PinCodeInputSheet', PinCodeInputSheet);
Vue.component('DateRangeSelectionSheet', DateRangeSelectionSheet);
Vue.component('ListItemSelectionSheet', ListItemSelectionSheet);
Vue.component('TwoColumnListItemSelectionSheet', TwoColumnListItemSelectionSheet);
Vue.component('TreeViewSelectionSheet', TreeViewSelectionSheet);
Vue.component('IconSelectionSheet', IconSelectionSheet);
Vue.component('ColorSelectionSheet', ColorSelectionSheet);
Vue.component('InformationSheet', InformationSheet);
Vue.component('NumberPadSheet', NumberPadSheet);
Vue.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
function setLanguage(locale) {
if (settings.getLanguage() !== locale) {
settings.setLanguage(locale);
}
Vue.filter('localized', (value, options) => localizedFilter({ i18n }, value, options));
Vue.filter('moment', (value, format, options) => momentFilter(value, format, options));
Vue.filter('percent', (value, precision, lowPrecisionValue) => percentFilter(value, precision, lowPrecisionValue));
Vue.filter('format', (value, format) => formatFilter(value, format));
Vue.filter('optionName', (value, options, keyField, nameField, defaultName) => optionNameFilter(value, options, keyField, nameField, defaultName));
Vue.filter('itemFieldContent', (value, fieldName, defaultValue, translate) => itemFieldContentFilter({ i18n }, value, fieldName, defaultValue, translate));
Vue.filter('languageName', (languageCode) => languageNameFilter(languageCode));
Vue.filter('currency', (value, currencyCode, notConvertValue) => currencyFilter({ i18n }, value, currencyCode, notConvertValue));
Vue.filter('exchangeRate', (value, currentCurrency, allExchangeRates) => exchangeRateFilter(value, currentCurrency, allExchangeRates));
Vue.filter('utcOffset', (value) => utcOffsetFilter(value));
Vue.filter('textLimit', (value, maxLength) => textLimitFilter(value, maxLength));
Vue.filter('icon', (value, iconType) => iconFilter(value, iconType));
Vue.filter('iconStyle', (value, iconType, defaultColor, additionalFieldName) => iconStyleFilter(value, iconType, defaultColor, additionalFieldName));
Vue.filter('defaultIconColor', (value, defaultColor) => defaultIconColorFilter(value, defaultColor));
Vue.filter('accountIcon', (value) => accountIconFilter(value));
Vue.filter('accountIconStyle', (value, defaultColor, additionalFieldName) => accountIconStyleFilter(value, defaultColor, additionalFieldName));
Vue.filter('categoryIcon', (value) => categoryIconFilter(value));
Vue.filter('categoryIconStyle', (value, defaultColor, additionalFieldName) => categoryIconStyleFilter(value, defaultColor, additionalFieldName));
Vue.filter('tokenDevice', (value) => tokenDeviceFilter(value));
Vue.filter('tokenIcon', (value) => tokenIconFilter(value));
i18n.global.locale.value = locale;
moment.locale(locale, {
months : app.config.globalProperties.$locale.getAllLongMonthNames(),
monthsShort : app.config.globalProperties.$locale.getAllShortMonthNames(),
weekdays : app.config.globalProperties.$locale.getAllLongWeekdayNames(),
weekdaysShort : app.config.globalProperties.$locale.getAllShortWeekdayNames(),
weekdaysMin : app.config.globalProperties.$locale.getAllMinWeekdayNames(),
});
services.setLocale(locale);
document.querySelector('html').setAttribute('lang', locale);
const store = new Vuex.Store(stores);
const i18n = new VueI18n(getI18nOptions());
const defaultCurrency = i18n.global.t('default.currency');
const defaultFirstDayOfWeekName = i18n.global.t('default.firstDayOfWeek');
let defaultFirstDayOfWeek = datetime.defaultFirstDayOfWeek;
Vue.prototype.$version = version.getVersion();
Vue.prototype.$buildTime = version.getBuildTime();
if (datetime.allWeekDays[defaultFirstDayOfWeekName]) {
defaultFirstDayOfWeek = datetime.allWeekDays[defaultFirstDayOfWeekName].type;
}
Vue.prototype.$licenses = {
store.dispatch('updateLocalizedDefaultSettings', { defaultCurrency, defaultFirstDayOfWeek });
return locale;
}
function setTimezone(timezone) {
if (timezone) {
settings.setTimezone(timezone);
moment.tz.setDefault(timezone);
} else {
settings.setTimezone('');
moment.tz.setDefault();
}
}
function initLocale() {
if (settings.getLanguage()) {
logger.info(`Current language is ${settings.getLanguage()}`);
setLanguage(settings.getLanguage());
} else {
logger.info(`No language is set, use browser default ${getDefaultLanguage()}`);
setLanguage(getDefaultLanguage());
}
if (settings.getTimezone()) {
logger.info(`Current timezone is ${settings.getTimezone()}`);
setTimezone(settings.getTimezone());
} else {
logger.info(`No timezone is set, use browser default ${utils.getTimezoneOffset()} (maybe ${moment.tz.guess(true)})`);
}
}
app.component('VueDatePicker', VueDatePicker);
app.component('ItemIcon', ItemIcon);
app.component('PieChart', PieChart);
app.component('PinCodeInput', PinCodeInput);
app.component('PinCodeInputSheet', PinCodeInputSheet);
app.component('PasswordInputSheet', PasswordInputSheet);
app.component('PasscodeInputSheet', PasscodeInputSheet);
app.component('DateTimeSelectionSheet', DateTimeSelectionSheet);
app.component('DateRangeSelectionSheet', DateRangeSelectionSheet);
app.component('ListItemSelectionSheet', ListItemSelectionSheet);
app.component('TwoColumnListItemSelectionSheet', TwoColumnListItemSelectionSheet);
app.component('TreeViewSelectionSheet', TreeViewSelectionSheet);
app.component('IconSelectionSheet', IconSelectionSheet);
app.component('ColorSelectionSheet', ColorSelectionSheet);
app.component('InformationSheet', InformationSheet);
app.component('NumberPadSheet', NumberPadSheet);
app.component('TransactionTagSelectionSheet', TransactionTagSelectionSheet);
app.directive('TextareaAutoSize', TextareaAutoSize);
app.config.globalProperties.$version = version.getVersion();
app.config.globalProperties.$buildTime = version.getBuildTime();
app.config.globalProperties.$licenses = {
license: licenses.getLicense(),
thirdPartyLicenses: licenses.getThirdPartyLicenses()
};
Vue.prototype.$constants = {
app.config.globalProperties.$constants = {
api: api,
datetime: datetime,
currency: currency,
@@ -202,314 +278,43 @@ Vue.prototype.$constants = {
statistics: statistics,
};
Vue.prototype.$utilities = utils;
Vue.prototype.$logger = logger;
Vue.prototype.$webauthn = webauthn;
Vue.prototype.$settings = settings;
Vue.prototype.$locale = {
defaultTimezoneOffset: utils.getTimezoneOffset(),
defaultTimezoneOffsetMinutes: utils.getTimezoneOffsetMinutes(),
app.config.globalProperties.$utilities = utils;
app.config.globalProperties.$logger = logger;
app.config.globalProperties.$webauthn = webauthn;
app.config.globalProperties.$settings = settings;
app.config.globalProperties.$locale = {
getDefaultLanguage: getDefaultLanguage,
getAllLanguages: getAllLanguages,
getAllLongMonthNames: function () {
return [
i18n.t('datetime.January.long'),
i18n.t('datetime.February.long'),
i18n.t('datetime.March.long'),
i18n.t('datetime.April.long'),
i18n.t('datetime.May.long'),
i18n.t('datetime.June.long'),
i18n.t('datetime.July.long'),
i18n.t('datetime.August.long'),
i18n.t('datetime.September.long'),
i18n.t('datetime.October.long'),
i18n.t('datetime.November.long'),
i18n.t('datetime.December.long')
];
},
getAllShortMonthNames: function () {
return [
i18n.t('datetime.January.short'),
i18n.t('datetime.February.short'),
i18n.t('datetime.March.short'),
i18n.t('datetime.April.short'),
i18n.t('datetime.May.short'),
i18n.t('datetime.June.short'),
i18n.t('datetime.July.short'),
i18n.t('datetime.August.short'),
i18n.t('datetime.September.short'),
i18n.t('datetime.October.short'),
i18n.t('datetime.November.short'),
i18n.t('datetime.December.short')
];
},
getAllLongWeekdayNames: function () {
return [
i18n.t('datetime.Sunday.long'),
i18n.t('datetime.Monday.long'),
i18n.t('datetime.Tuesday.long'),
i18n.t('datetime.Wednesday.long'),
i18n.t('datetime.Thursday.long'),
i18n.t('datetime.Friday.long'),
i18n.t('datetime.Saturday.long')
];
},
getAllShortWeekdayNames: function () {
return [
i18n.t('datetime.Sunday.short'),
i18n.t('datetime.Monday.short'),
i18n.t('datetime.Tuesday.short'),
i18n.t('datetime.Wednesday.short'),
i18n.t('datetime.Thursday.short'),
i18n.t('datetime.Friday.short'),
i18n.t('datetime.Saturday.short')
];
},
getAllMinWeekdayNames: function () {
return [
i18n.t('datetime.Sunday.min'),
i18n.t('datetime.Monday.min'),
i18n.t('datetime.Tuesday.min'),
i18n.t('datetime.Wednesday.min'),
i18n.t('datetime.Thursday.min'),
i18n.t('datetime.Friday.min'),
i18n.t('datetime.Saturday.min')
];
},
getInputTimeIntlDateTimeFormatOptions: function () {
const hourMinuteFormat = i18n.t('input-format.datetime.long');
const is24HourFormat = hourMinuteFormat.indexOf('H') > 0;
const hour2Digits = (hourMinuteFormat.indexOf('HH') > 0) || (hourMinuteFormat.indexOf('hh') > 0);
const minute2Digits = hourMinuteFormat.indexOf(':mm') > 0;
getAllLanguageInfos: getAllLanguageInfos,
getLanguageInfo: getLanguageInfo,
getAllLongMonthNames: () => getAllLongMonthNames(i18n.global.t),
getAllShortMonthNames: () => getAllShortMonthNames(i18n.global.t),
getAllLongWeekdayNames: () => getAllLongWeekdayNames(i18n.global.t),
getAllShortWeekdayNames: () => getAllShortWeekdayNames(i18n.global.t),
getAllMinWeekdayNames: () => getAllMinWeekdayNames(i18n.global.t),
setLanguage: setLanguage,
getTimezone: settings.getTimezone,
setTimezone: setTimezone,
getAllTimezones: (includeSystemDefault) => getAllTimezones(includeSystemDefault, i18n.global.t),
getAllCurrencies: () => getAllCurrencies(i18n.global.t),
getDisplayCurrency: (value, currencyCode, notConvertValue) => getDisplayCurrency(value, currencyCode, notConvertValue, i18n.global.t),
initLocale: initLocale
};
app.config.globalProperties.$tIf = (text, isTranslate) => transateIf(text, isTranslate, i18n.global.t);
return {
hour12: !is24HourFormat,
hour: hour2Digits ? '2-digit' : 'numeric',
minute: minute2Digits ? '2-digit' : 'numeric'
}
},
getLanguage: getLanguage,
setLanguage: function (locale) {
if (settings.getLanguage() !== locale) {
settings.setLanguage(locale);
}
i18n.locale = locale;
moment.locale(locale, {
months : this.getAllLongMonthNames(),
monthsShort : this.getAllShortMonthNames(),
weekdays : this.getAllLongWeekdayNames(),
weekdaysShort : this.getAllShortWeekdayNames(),
weekdaysMin : this.getAllMinWeekdayNames(),
});
services.setLocale(locale);
document.querySelector('html').setAttribute('lang', locale);
const defaultCurrency = i18n.t('default.currency');
const defaultFirstDayOfWeekName = i18n.t('default.firstDayOfWeek');
let defaultFirstDayOfWeek = datetime.defaultFirstDayOfWeek;
if (datetime.allWeekDays[defaultFirstDayOfWeekName]) {
defaultFirstDayOfWeek = datetime.allWeekDays[defaultFirstDayOfWeekName].type;
}
store.dispatch('updateLocalizedDefaultSettings', { defaultCurrency, defaultFirstDayOfWeek });
return locale;
},
getTimezone: function () {
return settings.getTimezone();
},
setTimezone: function (timezone) {
if (timezone) {
settings.setTimezone(timezone);
moment.tz.setDefault(timezone);
} else {
settings.setTimezone('');
moment.tz.setDefault();
}
},
getAllTimezones: function (includeSystemDefault) {
const allTimezones = timezone.all;
const allTimezoneInfos = [];
for (let i = 0; i < allTimezones.length; i++) {
allTimezoneInfos.push({
name: allTimezones[i].timezoneName,
utcOffset: (allTimezones[i].timezoneName !== 'Etc/GMT' ? utils.getTimezoneOffset(allTimezones[i].timezoneName) : ''),
utcOffsetMinutes: utils.getTimezoneOffsetMinutes(allTimezones[i].timezoneName),
displayName: i18n.t(`timezone.${allTimezones[i].displayName}`)
});
}
if (includeSystemDefault) {
allTimezoneInfos.push({
name: '',
utcOffset: this.defaultTimezoneOffset,
utcOffsetMinutes: this.defaultTimezoneOffsetMinutes,
displayName: i18n.t('System Default')
});
}
allTimezoneInfos.sort(function(c1, c2){
const utcOffset1 = parseInt(c1.utcOffset.replace(':', ''));
const utcOffset2 = parseInt(c2.utcOffset.replace(':', ''));
if (utcOffset1 !== utcOffset2) {
return utcOffset1 - utcOffset2;
}
return c1.displayName.localeCompare(c2.displayName);
})
return allTimezoneInfos;
},
getAllCurrencies: function () {
const allCurrencyCodes = currency.all;
const allCurrencies = [];
for (let currencyCode in allCurrencyCodes) {
if (!Object.prototype.hasOwnProperty.call(allCurrencyCodes, currencyCode)) {
return;
}
allCurrencies.push({
code: currencyCode,
displayName: i18n.t(`currency.${currencyCode}`)
});
}
allCurrencies.sort(function(c1, c2){
return c1.displayName.localeCompare(c2.displayName);
})
return allCurrencies;
},
init: function () {
if (settings.getLanguage()) {
logger.info(`Current language is ${settings.getLanguage()}`);
this.setLanguage(settings.getLanguage());
} else {
logger.info(`No language is set, use browser default ${getDefaultLanguage()}`);
this.setLanguage(getDefaultLanguage());
}
if (settings.getTimezone()) {
logger.info(`Current timezone is ${settings.getTimezone()}`);
this.setTimezone(settings.getTimezone());
} else {
logger.info(`No timezone is set, use browser default ${utils.getTimezoneOffset()} (maybe ${moment.tz.guess(true)})`);
}
}
app.config.globalProperties.$alert = (message, confirmCallback) => showAlert(message, confirmCallback, i18n.global.t);
app.config.globalProperties.$confirm = (message, confirmCallback, cancelCallback) => showConfirm(message, confirmCallback, cancelCallback, i18n.global.t);
app.config.globalProperties.$toast = (message, timeout) => showToast(message, timeout, i18n.global.t);
app.config.globalProperties.$showLoading = showLoading;
app.config.globalProperties.$hideLoading = hideLoading;
app.config.globalProperties.$routeBackOnError = routeBackOnError;
app.config.globalProperties.$ui = {
elements: elements,
isModalShowing: isModalShowing,
onSwipeoutDeleted: onSwipeoutDeleted
};
Vue.prototype.$alert = function (message, confirmCallback) {
let parameters = {};
app.config.globalProperties.$user = userstate;
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => i18n.t(s));
}
app.config.globalProperties.$locale.initLocale();
this.$f7.dialog.create({
title: i18n.t('global.app.title'),
text: i18n.t(message, parameters),
animate: settings.isEnableAnimate(),
buttons: [
{
text: i18n.t('OK'),
onClick: confirmCallback
}
]
}).open();
};
Vue.prototype.$confirm = function (message, confirmCallback, cancelCallback) {
this.$f7.dialog.create({
title: i18n.t('global.app.title'),
text: i18n.t(message),
animate: settings.isEnableAnimate(),
buttons: [
{
text: i18n.t('Cancel'),
onClick: cancelCallback
},
{
text: i18n.t('OK'),
onClick: confirmCallback
}
]
}).open();
};
Vue.prototype.$toast = function (message, timeout) {
let parameters = {};
if (message && message.error) {
const localizedError = getLocalizedError(message.error);
message = localizedError.message;
parameters = getLocalizedErrorParameters(localizedError.parameters, s => i18n.t(s));
}
this.$f7.toast.create({
text: i18n.t(message, parameters),
position: 'center',
closeTimeout: timeout || 1500
}).open();
};
Vue.prototype.$showLoading = function (delayConditionFunc, delayMills) {
if (!delayConditionFunc) {
return this.$f7.preloader.show();
}
setTimeout(() => {
if (delayConditionFunc()) {
this.$f7.preloader.show();
}
}, delayMills || 200);
};
Vue.prototype.$hideLoading = function () {
return this.$f7.preloader.hide();
};
Vue.prototype.$routeBackOnError = function (errorPropertyName) {
const self = this;
const router = self.$f7router;
const unwatch = self.$watch(errorPropertyName, () => {
if (self[errorPropertyName]) {
setTimeout(() => {
if (unwatch) {
unwatch();
}
router.back();
}, 200);
}
}, {
immediate: true
});
};
Vue.prototype.$user = userstate;
Vue.prototype.$locale.init();
new Vue({
el: '#app',
i18n: i18n,
store: store,
render: h => h(App),
mounted: function () {
const app = this.$f7;
const $$ = app.$;
app.on('pageBeforeOut', () => {
if ($$('.modal-in').length) {
app.actions.close('.actions-modal.modal-in', false);
app.dialog.close('.dialog.modal-in', false);
app.popover.close('.popover.modal-in', false);
app.popup.close('.popup.modal-in', false);
app.sheet.close('.sheet-modal.modal-in', false);
}
});
}
});
app.mount('#app');
+7 -5
View File
@@ -9,7 +9,7 @@
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-title" content="ezBookkeeping"/>
<meta name="apple-mobile-web-app-status-bar-style" content="default"/>
<meta name="theme-color" content="#c67e48">
<meta name="theme-color" content="#f6f6f8">
<meta name="format-detection" content="telephone=no"/>
<meta name="description" content="ezBookkeeping is a lightweight personal bookkeeping app hosted by yourself.">
<title>ezBookkeeping</title>
@@ -52,9 +52,11 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<noscript>
<strong>We're sorry but ezBookkeeping doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="./mobile-main.js"></script>
</body>
</html>
+65 -71
View File
@@ -33,14 +33,12 @@ import CategoryPresetPage from '../views/mobile/categories/Preset.vue';
import TagListPage from '../views/mobile/tags/List.vue';
function checkLogin(to, from, resolve, reject) {
const router = this;
function checkLogin({ router, resolve, reject }) {
if (!userState.isUserLogined()) {
reject();
router.navigate('/login', {
clearPreviousHistory: true,
pushState: false
browserHistory: false
});
return;
}
@@ -49,7 +47,7 @@ function checkLogin(to, from, resolve, reject) {
reject();
router.navigate('/unlock', {
clearPreviousHistory: true,
pushState: false
browserHistory: false
});
return;
}
@@ -57,14 +55,12 @@ function checkLogin(to, from, resolve, reject) {
resolve();
}
function checkLocked(to, from, resolve, reject) {
const router = this;
function checkLocked({ router, resolve, reject }) {
if (!userState.isUserLogined()) {
reject();
router.navigate('/login', {
clearPreviousHistory: true,
pushState: false
browserHistory: false
});
return;
}
@@ -73,7 +69,7 @@ function checkLocked(to, from, resolve, reject) {
reject();
router.navigate('/', {
clearPreviousHistory: true,
pushState: false
browserHistory: false
});
return;
}
@@ -81,9 +77,7 @@ function checkLocked(to, from, resolve, reject) {
resolve();
}
function checkNotLogin(to, from, resolve, reject) {
const router = this;
function checkNotLogin({ router, resolve, reject }) {
if (userState.isUserLogined() && !userState.isUserUnlocked()) {
reject();
router.navigate('/unlock', {
@@ -108,160 +102,160 @@ function checkNotLogin(to, from, resolve, reject) {
const routes = [
{
path: '/',
component: HomePage,
beforeEnter: checkLogin,
async: ({resolve}) => resolve({component: HomePage}),
beforeEnter: [checkLogin],
options: {
animate: false,
}
},
{
path: '/login',
component: LoginPage,
beforeEnter: checkNotLogin,
async: ({resolve}) => resolve({component: LoginPage}),
beforeEnter: [checkNotLogin],
options: {
animate: false,
}
},
{
path: '/signup',
component: SignUpPage,
beforeEnter: checkNotLogin,
async: ({resolve}) => resolve({component: SignUpPage}),
beforeEnter: [checkNotLogin],
options: {
animate: false,
}
},
{
path: '/unlock',
component: UnlockPage,
beforeEnter: checkLocked,
async: ({resolve}) => resolve({component: UnlockPage}),
beforeEnter: [checkLocked],
options: {
animate: false,
}
},
{
path: '/transaction/list',
component: TransactionListPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: TransactionListPage}),
beforeEnter: [checkLogin]
},
{
path: '/transaction/add',
component: TransactionEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: TransactionEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/transaction/edit',
component: TransactionEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: TransactionEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/transaction/detail',
component: TransactionEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: TransactionEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/account/list',
component: AccountListPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: AccountListPage}),
beforeEnter: [checkLogin]
},
{
path: '/account/add',
component: AccountEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: AccountEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/account/edit',
component: AccountEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: AccountEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/statistic/transaction',
component: StatisticsTransactionPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: StatisticsTransactionPage}),
beforeEnter: [checkLogin]
},
{
path: '/statistic/settings',
component: StatisticsSettingsPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: StatisticsSettingsPage}),
beforeEnter: [checkLogin]
},
{
path: '/statistic/filter/account',
component: StatisticsAccountFilterSettingsPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: StatisticsAccountFilterSettingsPage}),
beforeEnter: [checkLogin]
},
{
path: '/statistic/filter/category',
component: StatisticsCategoryFilterSettingsPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: StatisticsCategoryFilterSettingsPage}),
beforeEnter: [checkLogin]
},
{
path: '/settings',
component: SettingsPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: SettingsPage}),
beforeEnter: [checkLogin]
},
{
path: '/app_lock',
component: ApplicationLockPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: ApplicationLockPage}),
beforeEnter: [checkLogin]
},
{
path: '/exchange_rates',
component: ExchangeRatesPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: ExchangeRatesPage}),
beforeEnter: [checkLogin]
},
{
path: '/about',
component: AboutPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: AboutPage}),
beforeEnter: [checkLogin]
},
{
path: '/user/profile',
component: UserProfilePage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: UserProfilePage}),
beforeEnter: [checkLogin]
},
{
path: '/user/data/management',
component: DataManagementPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: DataManagementPage}),
beforeEnter: [checkLogin]
},
{
path: '/user/2fa',
component: TwoFactorAuthPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: TwoFactorAuthPage}),
beforeEnter: [checkLogin]
},
{
path: '/user/sessions',
component: SessionListPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: SessionListPage}),
beforeEnter: [checkLogin]
},
{
path: '/category/all',
component: CategoryAllPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: CategoryAllPage}),
beforeEnter: [checkLogin]
},
{
path: '/category/list',
component: CategoryListPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: CategoryListPage}),
beforeEnter: [checkLogin]
},
{
path: '/category/add',
component: CategoryEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: CategoryEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/category/edit',
component: CategoryEditPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: CategoryEditPage}),
beforeEnter: [checkLogin]
},
{
path: '/category/preset',
component: CategoryPresetPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: CategoryPresetPage}),
beforeEnter: [checkLogin]
},
{
path: '/tag/list',
component: TagListPage,
beforeEnter: checkLogin
async: ({resolve}) => resolve({component: TagListPage}),
beforeEnter: [checkLogin]
},
{
path: '(.*)',
+31 -19
View File
@@ -2,28 +2,22 @@
<f7-page>
<f7-navbar :title="$t('About')" :back-link="$t('Back')"></f7-navbar>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Version')" :after="version"></f7-list-item>
<f7-list-item :title="$t('Build Time')" :after="buildTime | moment($t('format.datetime.long'))" v-if="buildTime"></f7-list-item>
<f7-list-item external :title="$t('Official Website')" link="https://github.com/mayswind/ezbookkeeping" target="_blank"></f7-list-item>
<f7-list-item :title="$t('License')" link="#" popup-open=".license-popup"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top">
<f7-list-item :title="$t('Version')" :after="version"></f7-list-item>
<f7-list-item :title="$t('Build Time')" :after="buildTime" v-if="buildTime"></f7-list-item>
<f7-list-item external :title="$t('Official Website')" link="https://github.com/mayswind/ezbookkeeping" target="_blank"></f7-list-item>
<f7-list-item :title="$t('License')" link="#" popup-open=".license-popup"></f7-list-item>
</f7-list>
<f7-popup class="license-popup">
<f7-popup push with-subnavbar swipe-to-close swipe-handler=".swipe-handler" class="license-popup">
<f7-page>
<f7-navbar>
<f7-nav-title :title="$t('License')"></f7-nav-title>
<f7-nav-right>
<f7-link popup-close :text="$t('Done')"></f7-link>
</f7-nav-right>
<div class="swipe-handler"></div>
<f7-subnavbar :title="$t('License') "></f7-subnavbar>
</f7-navbar>
<f7-block>
<f7-block strong outline>
<p>
<span v-for="(line, num) in licenseLines" :key="num"
<span :key="num" v-for="(line, num) in licenseLines"
:style="{ 'display': line ? 'initial' : 'block', 'padding' : line ? '0' : '0 0 1em 0' }">
{{ line }}
</span>
@@ -34,7 +28,7 @@
<span>All the third party software included or linked is redistributed under the terms and conditions of their original licenses.</span>
</p>
<p></p>
<p v-for="license in thirdPartyLicenses" :key="license.name">
<p :key="license.name" v-for="license in thirdPartyLicenses">
<strong>{{ license.name }}</strong>
<br v-if="license.copyright"/><span v-if="license.copyright">{{ license.copyright }}</span>
<br v-if="license.url"/><span class="work-break-all" v-if="license.url">{{ license.url }}</span>
@@ -53,7 +47,11 @@ export default {
return 'v' + this.$version;
},
buildTime() {
return this.$buildTime;
if (!this.$buildTime) {
return this.$buildTime;
}
return this.$utilities.formatUnixTime(this.$buildTime, this.$t('format.datetime.long'));
},
licenseLines() {
return this.$licenses.license.replaceAll(/\r/g, '').split('\n');
@@ -64,3 +62,17 @@ export default {
}
}
</script>
<style>
.license-popup .navbar-bg {
background-color: rgb(var(--f7-navbar-bg-color-rgb, var(--f7-bars-bg-color-rgb)));
}
.license-popup .subnavbar {
background-color: rgb(var(--f7-subnavbar-bg-color-rgb, var(--f7-bars-bg-color-rgb)));
}
.license-popup .subnavbar-title {
--f7-subnavbar-title-font-size: 30px;
}
</style>
+11 -23
View File
@@ -5,38 +5,26 @@
<f7-nav-title :title="$t('Application Lock')"></f7-nav-title>
</f7-navbar>
<f7-card v-if="isEnableApplicationLock">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Status')" :after="$t('Enabled')"></f7-list-item>
<f7-list-item v-if="isSupportedWebAuthn">
<span>{{ $t('Face ID / Touch ID') }}</span>
<f7-toggle :checked="isEnableApplicationLockWebAuthn" @toggle:change="isEnableApplicationLockWebAuthn = $event"></f7-toggle>
</f7-list-item>
<f7-list-button @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!isEnableApplicationLock">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Status')" :after="$t('Disabled')"></f7-list-item>
<f7-list-button @click="enable(null)">{{ $t('Enable') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top">
<f7-list-item :title="$t('Status')" :after="$t(isEnableApplicationLock ? 'Enabled' : 'Disabled')"></f7-list-item>
<f7-list-item v-if="isEnableApplicationLock && isSupportedWebAuthn">
<span>{{ $t('Face ID / Touch ID') }}</span>
<f7-toggle :checked="isEnableApplicationLockWebAuthn" @toggle:change="isEnableApplicationLockWebAuthn = $event"></f7-toggle>
</f7-list-item>
<f7-list-button v-if="isEnableApplicationLock" @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
<f7-list-button v-if="!isEnableApplicationLock" @click="enable(null)">{{ $t('Enable') }}</f7-list-button>
</f7-list>
<pin-code-input-sheet :title="$t('PIN Code')"
:hint="$t('Please input a new PIN code. PIN code would encrypt your local data, so you need input this PIN code when you launch this app. If this PIN code is lost, you should re-login.')"
:show.sync="showInputPinCodeSheetForEnable"
v-model:show="showInputPinCodeSheetForEnable"
v-model="currentPinCodeForEnable"
@pincode:confirm="enable">
</pin-code-input-sheet>
<pin-code-input-sheet :title="$t('PIN Code')"
:hint="$t('Please enter your current PIN code when disable application lock')"
:show.sync="showInputPinCodeSheetForDisable"
v-model:show="showInputPinCodeSheetForDisable"
v-model="currentPinCodeForDisable"
@pincode:confirm="disable">
</pin-code-input-sheet>
+95 -66
View File
@@ -8,73 +8,73 @@
</f7-nav-right>
</f7-navbar>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Base Currency')"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Base Currency'), searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<f7-block slot="title" class="no-padding no-margin">
<span>{{ $t(`currency.${baseCurrency}`) }}&nbsp;</span>
<small class="smaller">{{ baseCurrency }}</small>
</f7-block>
<select v-model="baseCurrency">
<option v-for="exchangeRate in availableExchangeRates"
:key="exchangeRate.currencyCode"
:value="exchangeRate.currencyCode">{{ exchangeRate.currencyDisplayName }}</option>
</select>
</f7-list-item>
<f7-list-item
class="currency-base-amount"
link="#" no-chevron
:style="{ fontSize: baseAmountFontSize + 'px' }"
:header="$t('Base Amount')"
:title="baseAmount | currency"
@click="showBaseAmountSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
:show.sync="showBaseAmountSheet"
v-model="baseAmount"
></number-pad-sheet>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Base Currency')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Base Currency'), popupCloseLinkText: $t('Done') }"
>
<template #title>
<div class="no-padding no-margin">
<span>{{ $t(`currency.${baseCurrency}`) }}&nbsp;</span>
<small class="smaller">{{ baseCurrency }}</small>
</div>
</template>
<select v-model="baseCurrency">
<option :value="exchangeRate.currencyCode"
:key="exchangeRate.currencyCode"
v-for="exchangeRate in availableExchangeRates">{{ exchangeRate.currencyDisplayName }}</option>
</select>
</f7-list-item>
<f7-list-item
class="currency-base-amount"
link="#" no-chevron
:style="{ fontSize: baseAmountFontSize + 'px' }"
:header="$t('Base Amount')"
:title="displayBaseAmount"
@click="showBaseAmountSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
v-model:show="showBaseAmountSheet"
v-model="baseAmount"
></number-pad-sheet>
</f7-list-item>
</f7-list>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false" v-if="!exchangeRatesData || !exchangeRatesData.exchangeRates || !exchangeRatesData.exchangeRates.length">
<f7-list>
<f7-list-item :title="$t('No exchange rates data')"></f7-list-item>
</f7-list>
</f7-card-content>
<f7-card-content class="no-safe-areas" :padding="false" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list>
<f7-list-item v-for="exchangeRate in availableExchangeRates" :key="exchangeRate.currencyCode"
:after="getConvertedAmount(exchangeRate) | exchangeRate"
swipeout>
<f7-block slot="title" class="no-padding no-margin">
<span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span>
<small class="smaller">{{ exchangeRate.currencyCode }}</small>
</f7-block>
<f7-swipeout-actions right>
<f7-swipeout-button color="primary" close :text="$t('Set As Baseline')" @click="setAsBaseline(exchangeRate.currencyCode, getConvertedAmount(exchangeRate))"></f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-card-content>
<f7-card-footer v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<span>{{ $t('Last Updated') }}</span>
<span>{{ exchangeRatesData.updateTime | moment($t('format.date.long')) }}</span>
</f7-card-footer>
<f7-card-footer v-if="exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<span>{{ $t('Data source') }}</span>
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
<span v-else-if="!exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
</f7-card-footer>
</f7-card>
<f7-list strong inset dividers class="margin-vertical" v-if="!exchangeRatesData || !exchangeRatesData.exchangeRates || !exchangeRatesData.exchangeRates.length">
<f7-list-item :title="$t('No exchange rates data')"></f7-list-item>
</f7-list>
<f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list-item swipeout
:after="getDisplayConvertedAmount(exchangeRate)"
:key="exchangeRate.currencyCode" v-for="exchangeRate in availableExchangeRates">
<template #title>
<div class="no-padding no-margin">
<span style="margin-right: 5px">{{ exchangeRate.currencyDisplayName }}</span>
<small class="smaller">{{ exchangeRate.currencyCode }}</small>
</div>
</template>
<f7-swipeout-actions right>
<f7-swipeout-button color="primary" close :text="$t('Set As Baseline')" @click="setAsBaseline(exchangeRate.currencyCode, getConvertedAmount(exchangeRate))"></f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
<f7-list strong inset dividers class="margin-vertical" v-if="exchangeRatesData && exchangeRatesData.exchangeRates && exchangeRatesData.exchangeRates.length">
<f7-list-item v-if="exchangeRatesDataUpdateTime">
<small>{{ $t('Last Updated') }}</small>
<small>{{ exchangeRatesDataUpdateTime }}</small>
</f7-list-item>
<f7-list-item>
<small>{{ $t('Data source') }}</small>
<small>
<f7-link external target="_blank" :href="exchangeRatesData.referenceUrl" v-if="exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</f7-link>
<span v-else-if="!exchangeRatesData.referenceUrl">{{ exchangeRatesData.dataSource }}</span>
</small>
</f7-list-item>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
@@ -106,6 +106,13 @@ export default {
exchangeRatesData() {
return this.$store.state.latestExchangeRates.data;
},
exchangeRatesDataUpdateTime() {
if (!this.exchangeRatesData) {
return '';
}
return this.$utilities.formatUnixTime(this.exchangeRatesData.updateTime, this.$t('format.date.long'));
},
exchangeRateMap() {
const exchangeRateMap = {};
@@ -143,6 +150,9 @@ export default {
return availableExchangeRates;
},
displayBaseAmount() {
return this.$locale.getDisplayCurrency(this.baseAmount);
},
baseAmountFontSize() {
return this.getFontSizeByAmount(this.baseAmount);
}
@@ -213,6 +223,25 @@ export default {
return this.$utilities.getExchangedAmount(this.baseAmount / 100, fromExchangeRate.rate, toExchangeRate.rate);
},
getDisplayConvertedAmount(toExchangeRate) {
const rateStr = this.getConvertedAmount(toExchangeRate).toString();
if (rateStr.indexOf('.') < 0) {
return this.$utilities.appendThousandsSeparator(rateStr);
} else {
let firstNonZeroPos = 0;
for (let i = 0; i < rateStr.length; i++) {
if (rateStr.charAt(i) !== '.' && rateStr.charAt(i) !== '0') {
firstNonZeroPos = Math.min(i + 4, rateStr.length);
break;
}
}
const trimmedRateStr = rateStr.substring(0, Math.max(6, Math.max(firstNonZeroPos, rateStr.indexOf('.') + 2)));
return this.$utilities.appendThousandsSeparator(trimmedRateStr);
}
},
setAsBaseline(currency, amount) {
if (!this.$utilities.isNumber(amount)) {
amount = '';
+186 -124
View File
@@ -13,14 +13,14 @@
<small>Expense</small>
</span>
<span class="card-header-content" v-else-if="!loading">
<span class="home-summary-month">{{ dateRange.thisMonth.startTime | moment('MMMM') }}</span>
<span class="home-summary-month">{{ displayDateRange.thisMonth.displayTime }}</span>
<span>·</span>
<small>{{ $t('Expense') }}</small>
</span>
</p>
<p class="no-margin">
<span class="month-expense" v-if="loading">0.00 USD</span>
<span class="month-expense" v-else-if="!loading">{{ thisMonthAmount.expenseAmount | amount(thisMonthAmount.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</span>
<span class="month-expense" v-else-if="!loading">{{ transactionOverview.thisMonth.expenseAmount }}</span>
<f7-link class="margin-left-half" @click="toggleShowAmountInHomePage()">
<f7-icon :f7="showAmountInHomePage ? 'eye_slash_fill' : 'eye_fill'" size="18px"></f7-icon>
</f7-link>
@@ -29,137 +29,157 @@
<small class="home-summary-misc" v-if="loading">Monthly income 0.00 USD</small>
<small class="home-summary-misc" v-else-if="!loading">
<span>{{ $t('Monthly income') }}</span>
<span>{{ thisMonthAmount.incomeAmount | amount(thisMonthAmount.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</span>
<span>{{ transactionOverview.thisMonth.incomeAmount }}</span>
</small>
</p>
</f7-card-header>
</f7-card>
<f7-card :class="{ 'skeleton-text': loading }">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.Today.type" chevron-center>
<div slot="media">
<f7-icon f7="calendar_today"></f7-icon>
<f7-list strong inset dividers class="margin-top" :class="{ 'skeleton-text': loading }">
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.Today.type" chevron-center>
<template #media>
<f7-icon f7="calendar_today"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">Today</span>
<span v-else-if="!loading">{{ $t('Today') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD/YYYY</span>
<span v-else-if="!loading">{{ displayDateRange.today.displayTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today && transactionOverview.today.valid">{{ transactionOverview.today.incomeAmount }}</small>
</div>
<div slot="title" class="padding-top-half">
<span v-if="loading">Today</span>
<span v-else-if="!loading">{{ $t('Today') }}</span>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today && transactionOverview.today.valid">{{ transactionOverview.today.expenseAmount }}</small>
</div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD/YYYY</span>
<span v-else-if="!loading">{{ dateRange.today.startTime | moment($t('format.date.long')) }}</span>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today">{{ transactionOverview.today.incomeAmount | amount(transactionOverview.today.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.today">{{ transactionOverview.today.expenseAmount | amount(transactionOverview.today.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
</div>
</template>
</f7-list-item>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisWeek.type" chevron-center>
<div slot="media">
<f7-icon f7="calendar"></f7-icon>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisWeek.type" chevron-center>
<template #media>
<f7-icon f7="calendar"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">This Week</span>
<span v-else-if="!loading">{{ $t('This Week') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisWeek.startTime }}</span>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisWeek.endTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek && transactionOverview.thisWeek.valid">{{ transactionOverview.thisWeek.incomeAmount }}</small>
</div>
<div slot="title" class="padding-top-half">
<span v-if="loading">This Week</span>
<span v-else-if="!loading">{{ $t('This Week') }}</span>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek && transactionOverview.thisWeek.valid">{{ transactionOverview.thisWeek.expenseAmount }}</small>
</div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ dateRange.thisWeek.startTime | moment($t('format.monthDay.long')) }}</span>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ dateRange.thisWeek.endTime | moment($t('format.monthDay.long')) }}</span>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek">{{ transactionOverview.thisWeek.incomeAmount | amount(transactionOverview.thisWeek.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisWeek">{{ transactionOverview.thisWeek.expenseAmount | amount(transactionOverview.thisWeek.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
</div>
</template>
</f7-list-item>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisMonth.type" chevron-center>
<div slot="media">
<f7-icon f7="calendar"></f7-icon>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisMonth.type" chevron-center>
<template #media>
<f7-icon f7="calendar"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">This Month</span>
<span v-else-if="!loading">{{ $t('This Month') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisMonth.startTime }}</span>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ displayDateRange.thisMonth.endTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth && transactionOverview.thisMonth.valid">{{ transactionOverview.thisMonth.incomeAmount }}</small>
</div>
<div slot="title" class="padding-top-half">
<span v-if="loading">This Month</span>
<span v-else-if="!loading">{{ $t('This Month') }}</span>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth && transactionOverview.thisMonth.valid">{{ transactionOverview.thisMonth.expenseAmount }}</small>
</div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ dateRange.thisMonth.startTime | moment($t('format.monthDay.long')) }}</span>
<span>-</span>
<span v-if="loading">MM/DD</span>
<span v-else-if="!loading">{{ dateRange.thisMonth.endTime | moment($t('format.monthDay.long')) }}</span>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth">{{ transactionOverview.thisMonth.incomeAmount | amount(transactionOverview.thisMonth.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisMonth">{{ transactionOverview.thisMonth.expenseAmount | amount(transactionOverview.thisMonth.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
</div>
</template>
</f7-list-item>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisYear.type" chevron-center>
<div slot="media">
<f7-icon f7="square_stack_3d_up"></f7-icon>
<f7-list-item :link="'/transaction/list?dateType=' + $constants.datetime.allDateRanges.ThisYear.type" chevron-center>
<template #media>
<f7-icon f7="square_stack_3d_up"></f7-icon>
</template>
<template #title>
<div class="padding-top-half">
<span v-if="loading">This Year</span>
<span v-else-if="!loading">{{ $t('This Year') }}</span>
</div>
</template>
<template #footer>
<div class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">YYYY</span>
<span v-else-if="!loading">{{ displayDateRange.thisYear.displayTime }}</span>
</div>
</template>
<template #after>
<div>
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear && transactionOverview.thisYear.valid">{{ transactionOverview.thisYear.incomeAmount }}</small>
</div>
<div slot="title" class="padding-top-half">
<span v-if="loading">This Year</span>
<span v-else-if="!loading">{{ $t('This Year') }}</span>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear && transactionOverview.thisYear.valid">{{ transactionOverview.thisYear.expenseAmount }}</small>
</div>
<div slot="footer" class="overview-transaction-footer padding-bottom-half">
<span v-if="loading">YYYY</span>
<span v-else-if="!loading">{{ dateRange.thisYear.startTime | moment($t('format.year.long')) }}</span>
</div>
<div slot="after">
<div class="text-color-red">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear">{{ transactionOverview.thisYear.incomeAmount | amount(transactionOverview.thisYear.incompleteIncomeAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
<div class="text-color-teal">
<small v-if="loading">0.00 USD</small>
<small v-else-if="!loading && transactionOverview.thisYear">{{ transactionOverview.thisYear.expenseAmount | amount(transactionOverview.thisYear.incompleteExpenseAmount, showAmountInHomePage) | currency(defaultCurrency) }}</small>
</div>
</div>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
</div>
</template>
</f7-list-item>
</f7-list>
<f7-toolbar tabbar labels bottom>
<f7-link href="/transaction/list">
<f7-toolbar tabbar icons bottom class="main-tabbar">
<f7-link class="link" href="/transaction/list">
<f7-icon f7="square_list"></f7-icon>
<span class="tabbar-label">{{ $t('Details') }}</span>
</f7-link>
<f7-link href="/account/list">
<f7-link class="link" href="/account/list">
<f7-icon f7="creditcard"></f7-icon>
<span class="tabbar-label">{{ $t('Accounts') }}</span>
</f7-link>
<f7-link href="/transaction/add">
<f7-link class="link" href="/transaction/add">
<f7-icon f7="plus_square" class="ebk-tarbar-big-icon"></f7-icon>
</f7-link>
<f7-link href="/statistic/transaction">
<f7-link class="link" href="/statistic/transaction">
<f7-icon f7="chart_pie"></f7-icon>
<span class="tabbar-label">{{ $t('Statistics') }}</span>
</f7-link>
<f7-link href="/settings">
<f7-link class="link" href="/settings">
<f7-icon f7="gear_alt"></f7-icon>
<span class="tabbar-label">{{ $t('Settings') }}</span>
</f7-link>
@@ -182,9 +202,6 @@ export default {
};
},
computed: {
transactionOverview() {
return this.$store.state.transactionOverview;
},
defaultCurrency() {
return this.$store.getters.currentUserDefaultCurrency;
},
@@ -213,17 +230,60 @@ export default {
}
};
},
thisMonthAmount() {
displayDateRange() {
const self = this;
return {
today: {
displayTime: self.$utilities.formatUnixTime(self.dateRange.today.startTime, self.$t('format.date.long')),
},
thisWeek: {
startTime: self.$utilities.formatUnixTime(self.dateRange.thisWeek.startTime, self.$t('format.monthDay.long')),
endTime: self.$utilities.formatUnixTime(self.dateRange.thisWeek.endTime, self.$t('format.monthDay.long'))
},
thisMonth: {
displayTime: self.$utilities.formatUnixTime(self.dateRange.thisMonth.startTime, 'MMMM'),
startTime: self.$utilities.formatUnixTime(self.dateRange.thisMonth.startTime, self.$t('format.monthDay.long')),
endTime: self.$utilities.formatUnixTime(self.dateRange.thisMonth.endTime, self.$t('format.monthDay.long'))
},
thisYear: {
displayTime: self.$utilities.formatUnixTime(self.dateRange.thisYear.startTime, self.$t('format.year.long'))
}
};
},
transactionOverview() {
// make sure this computed property refers these property, so these property can trigger this computed property to update
const isEnableThousandsSeparator = this.isEnableThousandsSeparator; // eslint-disable-line
const currencyDisplayMode = this.currencyDisplayMode; // eslint-disable-line
if (!this.$store.state.transactionOverview || !this.$store.state.transactionOverview.thisMonth) {
return {
incomeAmount: 0,
expenseAmount: 0,
incompleteIncomeAmount: false,
incompleteExpenseAmount: false
thisMonth: {
valid: false,
incomeAmount: this.getDisplayAmount(0, false),
expenseAmount: this.getDisplayAmount(0, false)
}
};
}
return this.$store.state.transactionOverview.thisMonth;
const originalOverview = this.$store.state.transactionOverview;
const displayOverview = {};
[ 'today', 'thisWeek', 'thisMonth', 'thisYear' ].forEach(key => {
if (!originalOverview[key]) {
return;
}
const item = originalOverview[key];
displayOverview[key] = {
valid: true,
incomeAmount: this.getDisplayAmount(item.incomeAmount, item.incompleteIncomeAmount),
expenseAmount: this.getDisplayAmount(item.expenseAmount, item.incompleteExpenseAmount)
};
});
return displayOverview;
}
},
created() {
@@ -292,15 +352,13 @@ export default {
toggleShowAmountInHomePage() {
this.showAmountInHomePage = !this.showAmountInHomePage;
this.$settings.setShowAmountInHomePage(this.showAmountInHomePage);
}
},
filters: {
amount(amount, incomplete, showAmount) {
if (!showAmount) {
return '***';
},
getDisplayAmount(amount, incomplete) {
if (!this.showAmountInHomePage) {
return this.$locale.getDisplayCurrency('***', this.defaultCurrency);
}
return amount + (incomplete ? '+' : '');
return this.$locale.getDisplayCurrency(amount, this.defaultCurrency) + (incomplete ? '+' : '');
}
}
}
@@ -331,11 +389,11 @@ export default {
margin-right: 0;
}
.theme-dark .home-summary-card {
.dark .home-summary-card {
background-color: var(--f7-theme-color);
}
.theme-dark .home-summary-card a {
.dark .home-summary-card a {
color: var(--f7-text-color);
opacity: 0.6;
}
@@ -349,7 +407,11 @@ export default {
margin-right: 4px;
}
.tabbar-labels i.ebk-tarbar-big-icon {
.tabbar.main-tabbar .link i + span.tabbar-label {
margin-top: 2px;
}
.tabbar.main-tabbar .link i.ebk-tarbar-big-icon {
font-size: 42px;
width: 42px;
height: 42px;
+31 -27
View File
@@ -1,19 +1,19 @@
<template>
<f7-page no-toolbar no-navbar no-swipeback login-screen>
<f7-login-screen-title>
<img class="login-page-logo" src="img/ezbookkeeping-192.png" />
<img alt="logo" class="login-page-logo" src="/img/ezbookkeeping-192.png" />
<f7-block class="margin-vertical-half">{{ $t('global.app.title') }}</f7-block>
</f7-login-screen-title>
<f7-list form>
<f7-list form dividers>
<f7-list-input
type="text"
autocomplete="username"
clear-button
:label="$t('Username')"
:placeholder="$t('Your username or email')"
:value="username"
@input="username = $event.target.value; tempToken = ''"
v-model:value="username"
@input="tempToken = ''"
></f7-list-input>
<f7-list-input
type="password"
@@ -21,9 +21,9 @@
clear-button
:label="$t('Password')"
:placeholder="$t('Your password')"
:value="password"
@input="password = $event.target.value; tempToken = ''"
@keyup.enter.native="loginByPressEnter"
v-model:value="password"
@input="tempToken = ''"
@keyup.enter="loginByPressEnter"
></f7-list-input>
</f7-list>
@@ -50,15 +50,17 @@
</f7-list>
<f7-popover class="lang-popover-menu">
<f7-list>
<f7-list dividers>
<f7-list-item
link="#" no-chevron popover-close
v-for="(lang, locale) in allLanguages"
:key="locale"
:title="lang.displayName"
:key="locale"
v-for="(lang, locale) in allLanguages"
@click="changeLanguage(locale)"
>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
@@ -72,26 +74,28 @@
<div style="font-size: 18px"><b>{{ $t('Two-Factor Authentication') }}</b></div>
</div>
<div class="padding-horizontal padding-bottom">
<f7-list no-hairlines class="no-margin-top margin-bottom">
<f7-list no-hairlines strong class="no-margin">
<f7-list-input
type="number"
autocomplete="one-time-code"
outline
floating-label
clear-button
v-if="twoFAVerifyType === 'passcode'"
:label="$t('Passcode')"
:placeholder="$t('Passcode')"
:value="passcode"
@input="passcode = $event.target.value"
@keyup.enter.native="verify"
v-model:value="passcode"
@keyup.enter="verify"
></f7-list-input>
<f7-list-input
outline
floating-label
clear-button
v-if="twoFAVerifyType === 'backupcode'"
:label="$t('Backup Code')"
:placeholder="$t('Backup Code')"
:value="backupCode"
@input="backupCode = $event.target.value"
@keyup.enter.native="verify"
v-model:value="backupCode"
@keyup.enter="verify"
></f7-list-input>
</f7-list>
<f7-button large fill :class="{ 'disabled': twoFAInputIsEmpty || verifying }" :text="$t('Verify')" @click="verify"></f7-button>
@@ -106,6 +110,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
username: '',
@@ -124,7 +131,7 @@ export default {
return 'v' + this.$version;
},
allLanguages() {
return this.$locale.getAllLanguages();
return this.$locale.getAllLanguageInfos();
},
isUserRegistrationEnabled() {
return this.$settings.isUserRegistrationEnabled();
@@ -148,10 +155,10 @@ export default {
},
currentLanguageName() {
const currentLocale = this.$i18n.locale;
let lang = this.$locale.getLanguage(currentLocale);
let lang = this.$locale.getLanguageInfo(currentLocale);
if (!lang) {
lang = this.$locale.getLanguage(this.$locale.getDefaultLanguage());
lang = this.$locale.getLanguageInfo(this.$locale.getDefaultLanguage());
}
return lang.displayName;
@@ -160,7 +167,7 @@ export default {
methods: {
login() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
if (!this.username) {
self.$alert('Username cannot be empty');
@@ -208,10 +215,7 @@ export default {
});
},
loginByPressEnter() {
const app = this.$f7;
const $$ = app.$;
if ($$('.modal-in').length) {
if (this.$ui.isModalShowing()) {
return;
}
@@ -219,7 +223,7 @@ export default {
},
verify() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
if (self.twoFAInputIsEmpty || self.verifying) {
return;
+80 -85
View File
@@ -3,108 +3,103 @@
<f7-navbar :title="$t('Settings')" :back-link="$t('Back')"></f7-navbar>
<f7-block-title class="margin-top">{{ currentNickName }}</f7-block-title>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('User Profile')" link="/user/profile"></f7-list-item>
<f7-list-item :title="$t('Transaction Categories')" link="/category/all"></f7-list-item>
<f7-list-item :title="$t('Transaction Tags')" link="/tag/list"></f7-list-item>
<f7-list-item :title="$t('Data Management')" link="/user/data/management"></f7-list-item>
<f7-list-item :title="$t('Two-Factor Authentication')" link="/user/2fa"></f7-list-item>
<f7-list-item :title="$t('Device & Sessions')" link="/user/sessions"></f7-list-item>
<f7-list-button :class="{ 'disabled': logouting }" @click="logout">{{ $t('Log Out') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers>
<f7-list-item :title="$t('User Profile')" link="/user/profile"></f7-list-item>
<f7-list-item :title="$t('Transaction Categories')" link="/category/all"></f7-list-item>
<f7-list-item :title="$t('Transaction Tags')" link="/tag/list"></f7-list-item>
<f7-list-item :title="$t('Data Management')" link="/user/data/management"></f7-list-item>
<f7-list-item :title="$t('Two-Factor Authentication')" link="/user/2fa"></f7-list-item>
<f7-list-item :title="$t('Device & Sessions')" link="/user/sessions"></f7-list-item>
<f7-list-button :class="{ 'disabled': logouting }" @click="logout">{{ $t('Log Out') }}</f7-list-button>
</f7-list>
<f7-block-title>{{ $t('Application') }}</f7-block-title>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item
:key="currentLocale + '_lang'"
:title="$t('Language')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="currentLocale">
<option v-for="(lang, locale) in allLanguages"
:key="locale"
:value="locale">{{ lang.displayName }}</option>
</select>
</f7-list-item>
<f7-list strong inset dividers>
<f7-list-item
:key="currentLocale + '_lang'"
:title="$t('Language')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="currentLocale">
<option :value="locale"
:key="locale"
v-for="(lang, locale) in allLanguages">{{ lang.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item
:key="currentLocale + '_timezone'"
:title="$t('Timezone')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="currentTimezone">
<option v-for="timezone in allTimezones"
:key="timezone.name"
:value="timezone.name">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option>
</select>
</f7-list-item>
<f7-list-item
:key="currentLocale + '_timezone'"
:title="$t('Timezone')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="currentTimezone">
<option :value="timezone.name"
:key="timezone.name"
v-for="timezone in allTimezones">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option>
</select>
</f7-list-item>
<f7-list-item :title="$t('Application Lock')" :after="isEnableApplicationLock ? $t('Enabled') : $t('Disabled')" link="/app_lock"></f7-list-item>
<f7-list-item :title="$t('Application Lock')" :after="isEnableApplicationLock ? $t('Enabled') : $t('Disabled')" link="/app_lock"></f7-list-item>
<f7-list-item :title="$t('Exchange Rates Data')" :after="exchangeRatesLastUpdateDate" link="/exchange_rates"></f7-list-item>
<f7-list-item :title="$t('Exchange Rates Data')" :after="exchangeRatesLastUpdateDate" link="/exchange_rates"></f7-list-item>
<f7-list-item>
<span>{{ $t('Auto Update Exchange Rates Data') }}</span>
<f7-toggle :checked="isAutoUpdateExchangeRatesData" @toggle:change="isAutoUpdateExchangeRatesData = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Auto Update Exchange Rates Data') }}</span>
<f7-toggle :checked="isAutoUpdateExchangeRatesData" @toggle:change="isAutoUpdateExchangeRatesData = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Thousands Separator') }}</span>
<f7-toggle :checked="isEnableThousandsSeparator" @toggle:change="isEnableThousandsSeparator = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Thousands Separator') }}</span>
<f7-toggle :checked="isEnableThousandsSeparator" @toggle:change="isEnableThousandsSeparator = $event"></f7-toggle>
</f7-list-item>
<f7-list-item
:key="currentLocale + '_currency_display'"
:title="$t('Currency Display Mode')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Currency Display Mode'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="currencyDisplayMode">
<option :value="$constants.currency.allCurrencyDisplayModes.None">{{ $t('None') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Symbol">{{ $t('Currency Symbol') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Code">{{ $t('Currency Code') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Name">{{ $t('Currency Name') }}</option>
</select>
</f7-list-item>
<f7-list-item
:key="currentLocale + '_currency_display'"
:title="$t('Currency Display Mode')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Display Mode'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="currencyDisplayMode">
<option :value="$constants.currency.allCurrencyDisplayModes.None">{{ $t('None') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Symbol">{{ $t('Currency Symbol') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Code">{{ $t('Currency Code') }}</option>
<option :value="$constants.currency.allCurrencyDisplayModes.Name">{{ $t('Currency Name') }}</option>
</select>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Show Amount In Home Page') }}</span>
<f7-toggle :checked="showAmountInHomePage" @toggle:change="showAmountInHomePage = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Show Amount In Home Page') }}</span>
<f7-toggle :checked="showAmountInHomePage" @toggle:change="showAmountInHomePage = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Show Account Balance') }}</span>
<f7-toggle :checked="showAccountBalance" @toggle:change="showAccountBalance = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Show Account Balance') }}</span>
<f7-toggle :checked="showAccountBalance" @toggle:change="showAccountBalance = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Show Total Amount In Transaction List Page') }}</span>
<f7-toggle :checked="showTotalAmountInTransactionListPage" @toggle:change="showTotalAmountInTransactionListPage = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Show Total Amount In Transaction List Page') }}</span>
<f7-toggle :checked="showTotalAmountInTransactionListPage" @toggle:change="showTotalAmountInTransactionListPage = $event"></f7-toggle>
</f7-list-item>
<f7-list-item :title="$t('Statistics Settings')" link="/statistic/settings"></f7-list-item>
<f7-list-item :title="$t('Statistics Settings')" link="/statistic/settings"></f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Animate') }}</span>
<f7-toggle :checked="isEnableAnimate" @toggle:change="isEnableAnimate = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Animate') }}</span>
<f7-toggle :checked="isEnableAnimate" @toggle:change="isEnableAnimate = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Auto Dark Mode') }}</span>
<f7-toggle :checked="isEnableAutoDarkMode" @toggle:change="isEnableAutoDarkMode = $event"></f7-toggle>
</f7-list-item>
<f7-list-item>
<span>{{ $t('Enable Auto Dark Mode') }}</span>
<f7-toggle :checked="isEnableAutoDarkMode" @toggle:change="isEnableAutoDarkMode = $event"></f7-toggle>
</f7-list-item>
<f7-list-item :title="$t('About')" link="/about" :after="version"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item :title="$t('About')" link="/about" :after="version"></f7-list-item>
</f7-list>
</f7-page>
</template>
<script>
export default {
props: [
'f7router'
],
data() {
const self = this;
@@ -118,7 +113,7 @@ export default {
return 'v' + this.$version;
},
allLanguages() {
return this.$locale.getAllLanguages();
return this.$locale.getAllLanguageInfos();
},
allTimezones() {
return this.$locale.getAllTimezones(true);
@@ -223,7 +218,7 @@ export default {
},
logout() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
self.$confirm('Are you sure you want to log out?', () => {
self.logouting = true;
@@ -234,7 +229,7 @@ export default {
self.$hideLoading();
self.$settings.clearSettings();
self.$locale.init();
self.$locale.initLocale();
router.navigate('/');
}).catch(error => {
+151 -156
View File
@@ -8,126 +8,112 @@
</f7-nav-right>
</f7-navbar>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-input
type="text"
autocomplete="username"
clear-button
:label="$t('Username')"
:placeholder="$t('Your username')"
:value="user.username"
@input="user.username = $event.target.value"
></f7-list-input>
<f7-list form strong inset dividers class="margin-top">
<f7-list-input
type="text"
autocomplete="username"
clear-button
:label="$t('Username')"
:placeholder="$t('Your username')"
v-model:value="user.username"
></f7-list-input>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Password')"
:placeholder="$t('Your password, at least 6 characters')"
:value="user.password"
@input="user.password = $event.target.value"
></f7-list-input>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Password')"
:placeholder="$t('Your password, at least 6 characters')"
v-model:value="user.password"
></f7-list-input>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Confirmation Password')"
:placeholder="$t('Re-enter the password')"
:value="user.confirmPassword"
@input="user.confirmPassword = $event.target.value"
></f7-list-input>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Confirmation Password')"
:placeholder="$t('Re-enter the password')"
v-model:value="user.confirmPassword"
></f7-list-input>
<f7-list-input
type="email"
autocomplete="email"
clear-button
:label="$t('E-mail')"
:placeholder="$t('Your email address')"
:value="user.email"
@input="user.email = $event.target.value"
></f7-list-input>
<f7-list-input
type="email"
autocomplete="email"
clear-button
:label="$t('E-mail')"
:placeholder="$t('Your email address')"
v-model:value="user.email"
></f7-list-input>
<f7-list-input
type="text"
autocomplete="nickname"
clear-button
:label="$t('Nickname')"
:placeholder="$t('Your nickname')"
:value="user.nickname"
@input="user.nickname = $event.target.value"
></f7-list-input>
<f7-list-input
type="text"
autocomplete="nickname"
clear-button
:label="$t('Nickname')"
:placeholder="$t('Your nickname')"
v-model:value="user.nickname"
></f7-list-input>
<f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
</f7-list>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_lang'"
:header="$t('Language')"
:title="currentLocale | languageName"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Language'), searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<select v-model="currentLocale">
<option v-for="(lang, locale) in allLanguages"
:key="locale"
:value="locale">{{ lang.displayName }}</option>
</select>
</f7-list-item>
<f7-list strong inset dividers>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_lang'"
:header="$t('Language')"
:title="currentLanguageName"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Language'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Language'), popupCloseLinkText: $t('Done') }"
>
<select v-model="currentLocale">
<option :value="locale"
:key="locale"
v-for="(lang, locale) in allLanguages">{{ lang.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_currency'"
:header="$t('Default Currency')"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Default Currency'), searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<f7-block slot="title" class="no-padding no-margin">
<span>{{ $t(`currency.${user.defaultCurrency}`) }}&nbsp;</span>
<small class="smaller">{{ user.defaultCurrency }}</small>
</f7-block>
<select autocomplete="transaction-currency" v-model="user.defaultCurrency">
<option v-for="currency in allCurrencies"
:key="currency.code"
:value="currency.code">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_currency'"
:header="$t('Default Currency')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Default Currency'), popupCloseLinkText: $t('Done') }"
>
<template #title>
<f7-block class="no-padding no-margin">
<span>{{ $t(`currency.${user.defaultCurrency}`) }}&nbsp;</span>
<small class="smaller">{{ user.defaultCurrency }}</small>
</f7-block>
</template>
<select autocomplete="transaction-currency" v-model="user.defaultCurrency">
<option :value="currency.code"
:key="currency.code"
v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_firstDayOfWeek'"
:header="$t('First Day of Week')"
:title="user.firstDayOfWeek | optionName(allWeekDays, 'type', 'name') | format('datetime.#{value}.long') | localized"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('First Day of Week'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<select v-model="user.firstDayOfWeek">
<option v-for="weekDay in allWeekDays"
:key="weekDay.type"
:value="weekDay.type">{{ $t(`datetime.${weekDay.name}.long`) }}</option>
</select>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:key="currentLocale + '_firstDayOfWeek'"
:header="$t('First Day of Week')"
:title="getDayOfWeekName(user.firstDayOfWeek)"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('First Day of Week'), popupCloseLinkText: $t('Done') }"
>
<select v-model="user.firstDayOfWeek">
<option :value="weekDay.type"
:key="weekDay.type"
v-for="weekDay in allWeekDays">{{ $t(`datetime.${weekDay.name}.long`) }}</option>
</select>
</f7-list-item>
</f7-list>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-item :title="$t('Use preset transaction categories')" link="#" @click="showPresetCategories = true">
<f7-toggle :checked="usePresetCategories" @toggle:change="usePresetCategories = $event"></f7-toggle>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers>
<f7-list-item :title="$t('Use preset transaction categories')" link="#" @click="showPresetCategories = true">
<f7-toggle :checked="usePresetCategories" @toggle:change="usePresetCategories = $event"></f7-toggle>
</f7-list-item>
</f7-list>
<f7-popup :opened="showPresetCategories" @popup:closed="showPresetCategories = false">
<f7-popup push :close-on-escape="false" :opened="showPresetCategories"
@popup:closed="showPresetCategories = false">
<f7-page>
<f7-navbar>
<f7-nav-left>
@@ -140,39 +126,32 @@
<f7-link close @click="usePresetCategories = false; showPresetCategories = false" v-if="usePresetCategories">{{ $t('Disable') }}</f7-link>
</f7-nav-right>
</f7-navbar>
<f7-card v-for="(categories, categoryType) in presetCategories" :key="categoryType">
<f7-card-header>
<small class="card-header-content">
<span>{{ categoryType | categoryTypeName($constants.category.allCategoryTypes) | localized }}</span>
</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list v-if="showPresetCategories">
<f7-list-item v-for="(category, idx) in categories"
:key="idx"
:accordion-item="!!category.subCategories.length"
:title="$t('category.' + category.name, currentLocale)">
<f7-icon slot="media"
:icon="category.categoryIconId | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-block class="no-padding no-margin"
:key="categoryType" v-for="(categories, categoryType) in presetCategories">
<f7-block-title class="margin-top margin-horizontal">{{ getCategoryTypeName(categoryType) }}</f7-block-title>
<f7-list strong inset dividers v-if="showPresetCategories">
<f7-list-item :title="$t('category.' + category.name, currentLocale)"
:accordion-item="!!category.subCategories.length"
:key="idx"
v-for="(category, idx) in categories">
<template #media>
<ItemIcon icon-type="category" :icon-id="category.categoryIconId" :color="category.color"></ItemIcon>
</template>
<f7-accordion-content v-if="category.subCategories.length" class="padding-left">
<f7-list>
<f7-list-item v-for="(subCategory, subIdx) in category.subCategories"
:key="subIdx"
:title="$t('category.' + subCategory.name, currentLocale)">
<f7-icon slot="media"
:icon="subCategory.categoryIconId | categoryIcon"
:style="subCategory.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-accordion-content v-if="category.subCategories.length" class="padding-left">
<f7-list>
<f7-list-item :title="$t('category.' + subCategory.name, currentLocale)"
:key="subIdx"
v-for="(subCategory, subIdx) in category.subCategories">
<template #media>
<ItemIcon icon-type="category" :icon-id="subCategory.categoryIconId" :color="subCategory.color"></ItemIcon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-block>
</f7-page>
<f7-actions close-by-outside-click close-on-escape :opened="showPresetCategoriesMoreActionSheet" @actions:closed="showPresetCategoriesMoreActionSheet = false">
@@ -187,7 +166,7 @@
<list-item-selection-sheet value-type="index"
title-field="displayName"
:items="allLanguages"
:show.sync="showPresetCategoriesChangeLocaleSheet"
v-model:show="showPresetCategoriesChangeLocaleSheet"
v-model="currentLocale">
</list-item-selection-sheet>
</f7-popup>
@@ -196,6 +175,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
const self = this;
@@ -223,7 +205,7 @@ export default {
},
computed: {
allLanguages() {
return this.$locale.getAllLanguages();
return this.$locale.getAllLanguageInfos();
},
allCurrencies() {
return this.$locale.getAllCurrencies();
@@ -250,6 +232,15 @@ export default {
}
}
},
currentLanguageName() {
const languageInfo = this.$locale.getLanguageInfo(this.currentLocale);
if (!languageInfo) {
return '';
}
return languageInfo.displayName;
},
inputIsEmpty() {
return !!this.inputEmptyProblemMessage;
},
@@ -284,7 +275,7 @@ export default {
methods: {
submit() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
let problemMessage = self.inputEmptyProblemMessage || self.inputInvalidProblemMessage;
@@ -384,21 +375,25 @@ export default {
self.$toast(error.message || error);
}
});
}
},
filters: {
categoryTypeName(categoryType, allCategoryTypes) {
},
getDayOfWeekName(dayOfWeek) {
const weekName = this.$utilities.getNameByKeyValue(this.$constants.datetime.allWeekDays, dayOfWeek, 'type', 'name');
const i18nWeekNameKey = `datetime.${weekName}.long`;
return this.$t(i18nWeekNameKey);
},
getCategoryTypeName(categoryType) {
switch (categoryType) {
case allCategoryTypes.Income.toString():
return 'Income Categories';
case allCategoryTypes.Expense.toString():
return 'Expense Categories';
case allCategoryTypes.Transfer.toString():
return 'Transfer Categories';
case this.$constants.category.allCategoryTypes.Income.toString():
return this.$t('Income Categories');
case this.$constants.category.allCategoryTypes.Expense.toString():
return this.$t('Expense Categories');
case this.$constants.category.allCategoryTypes.Transfer.toString():
return this.$t('Transfer Categories');
default:
return 'Transaction Categories';
return this.$t('Transaction Categories');
}
}
}
};
</script>
+32 -28
View File
@@ -1,21 +1,23 @@
<template>
<f7-page no-toolbar no-navbar no-swipeback login-screen>
<f7-login-screen-title>
<img class="login-page-logo" src="img/ezbookkeeping-192.png" />
<img alt="logo" class="login-page-logo" src="/img/ezbookkeeping-192.png" />
<f7-block class="margin-vertical-half">{{ $t('global.app.title') }}</f7-block>
</f7-login-screen-title>
<f7-list form>
<f7-list-item-row class="justify-content-center padding-vertical-half">
{{ $t('Unlock Application') }}
</f7-list-item-row>
<f7-list-item class="list-item-pincode-input">
<pincode-input secure :length="6" v-model="pinCode" @keyup.native="unlockByPin" />
<f7-list-item class="no-padding no-margin">
<template #inner>
<div class="display-flex justify-content-center full-line">{{ $t('Unlock Application') }}</div>
</template>
</f7-list-item>
<f7-list-item class="list-item-pincode-input padding-horizontal margin-horizontal">
<pin-code-input :secure="true" :length="6" v-model="pinCode" @pincode:confirm="unlockByPin" />
</f7-list-item>
</f7-list>
<f7-list>
<f7-list-button :class="{ 'disabled': !pinCodeValid }" :text="$t('Unlock By PIN Code')" @click="unlockByPin"></f7-list-button>
<f7-list-button :class="{ 'disabled': !isPinCodeValid(pinCode) }" :text="$t('Unlock By PIN Code')" @click="unlockByPin"></f7-list-button>
<f7-list-button v-if="isWebAuthnAvailable" :text="$t('Unlock By Face ID/Touch ID')" @click="unlockByWebAuthn"></f7-list-button>
<f7-block-footer>
<f7-link :text="$t('Re-login')" @click="relogin"></f7-link>
@@ -37,15 +39,17 @@
</f7-list>
<f7-popover class="lang-popover-menu">
<f7-list>
<f7-list dividers>
<f7-list-item
link="#" no-chevron popover-close
v-for="(lang, locale) in allLanguages"
:key="locale"
:title="lang.displayName"
:key="locale"
v-for="(lang, locale) in allLanguages"
@click="changeLanguage(locale)"
>
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="$i18n.locale === locale"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
@@ -54,6 +58,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
pinCode: ''
@@ -64,22 +71,19 @@ export default {
return 'v' + this.$version;
},
allLanguages() {
return this.$locale.getAllLanguages();
return this.$locale.getAllLanguageInfos();
},
isWebAuthnAvailable() {
return this.$settings.isEnableApplicationLockWebAuthn()
&& this.$user.getWebAuthnCredentialId()
&& this.$webauthn.isSupported();
},
pinCodeValid() {
return this.pinCode && this.pinCode.length === 6;
},
currentLanguageName() {
const currentLocale = this.$i18n.locale;
let lang = this.$locale.getLanguage(currentLocale);
let lang = this.$locale.getLanguageInfo(currentLocale);
if (!lang) {
lang = this.$locale.getLanguage(this.$locale.getDefaultLanguage());
lang = this.$locale.getLanguageInfo(this.$locale.getDefaultLanguage());
}
return lang.displayName;
@@ -88,7 +92,7 @@ export default {
methods: {
unlockByWebAuthn() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
if (!self.$settings.isEnableApplicationLockWebAuthn() || !self.$user.getWebAuthnCredentialId()) {
self.$toast('Face ID/Touch ID authentication is not enabled');
@@ -131,19 +135,16 @@ export default {
}
});
},
unlockByPin() {
const app = this.$f7;
const $$ = app.$;
if (!this.pinCodeValid) {
unlockByPin(pinCode) {
if (!this.isPinCodeValid(pinCode)) {
return;
}
if ($$('.modal-in').length) {
if (this.$ui.isModalShowing()) {
return;
}
const router = this.$f7router;
const router = this.f7router;
const user = this.$store.state.currentUserInfo;
if (!user || !user.username) {
@@ -152,7 +153,7 @@ export default {
}
try {
this.$user.unlockTokenByPinCode(user.username, this.pinCode);
this.$user.unlockTokenByPinCode(user.username, pinCode);
this.$store.dispatch('refreshTokenAndRevokeOldToken');
if (this.$settings.isAutoUpdateExchangeRatesData()) {
@@ -167,7 +168,7 @@ export default {
},
relogin() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
self.$confirm('Are you sure you want to re-login?', () => {
self.$user.clearTokenAndUserInfo(true);
@@ -175,13 +176,16 @@ export default {
self.$store.dispatch('clearUserInfoState');
self.$store.dispatch('resetState');
self.$settings.clearSettings();
self.$locale.init();
self.$locale.initLocale();
router.navigate('/login', {
clearPreviousHistory: true
});
});
},
isPinCodeValid(pinCode) {
return pinCode && pinCode.length === 6;
},
changeLanguage(locale) {
this.$locale.setLanguage(locale);
}
+380 -304
View File
@@ -9,320 +9,393 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item class="list-item-with-header-and-title" link="#" header="Account Category" title="Category"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" link="#" header="Account Type" title="Account Type"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-list-item class="list-item-with-header-and-title" header="Account Category" title="Category"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Account Type" title="Account Type"></f7-list-item>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-item
class="list-item-with-header-and-title"
link="#"
:header="$t('Account Category')"
:title="account.category | optionName(allAccountCategories, 'id', 'name') | localized"
@click="showAccountCategorySheet = true"
>
<list-item-selection-sheet value-type="item"
key-field="id" value-field="id" title-field="name"
icon-field="defaultAccountIconId" icon-type="account"
:title-i18n="true"
:items="allAccountCategories"
:show.sync="showAccountCategorySheet"
v-model="account.category">
</list-item-selection-sheet>
</f7-list-item>
<f7-list form strong inset dividers class="margin-vertical" v-else-if="!loading">
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:header="$t('Account Category')"
:title="getAccountCategoryName(account.category)"
@click="showAccountCategorySheet = true"
>
<list-item-selection-sheet value-type="item"
key-field="id" value-field="id" title-field="name"
icon-field="defaultAccountIconId" icon-type="account"
:title-i18n="true"
:items="allAccountCategories"
v-model:show="showAccountCategorySheet"
v-model="account.category">
</list-item-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#"
:class="{ 'disabled': editAccountId }"
:header="$t('Account Type')"
:title="account.type | optionName(allAccountTypes, 'id', 'name') | localized"
:no-chevron="!!editAccountId"
@click="showAccountTypeSheet = true"
>
<list-item-selection-sheet value-type="item"
key-field="id" value-field="id" title-field="name"
:items="allAccountTypes"
:title-i18n="true"
:show.sync="showAccountTypeSheet"
v-model="account.type">
</list-item-selection-sheet>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:class="{ 'disabled': editAccountId }"
:header="$t('Account Type')"
:title="this.getAccountTypeName(account.type)"
@click="showAccountTypeSheet = true"
>
<list-item-selection-sheet value-type="item"
key-field="id" value-field="id" title-field="name"
:items="allAccountTypes"
:title-i18n="true"
v-model:show="showAccountTypeSheet"
v-model="account.type">
</list-item-selection-sheet>
</f7-list-item>
</f7-list>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-input label="Account Name" placeholder="Your account name"></f7-list-input>
<f7-list-item class="list-item-with-header-and-title" header="Account Icon" link="#">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</f7-block>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Account Color" link="#">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</f7-block>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Currency" title="Currency" link="#"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Account Balance" title="Balance" link="#"></f7-list-item>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item>
<f7-list-input label="Description" type="textarea" placeholder="Your account description (optional)"></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-list-input label="Account Name" placeholder="Your account name"></f7-list-input>
<f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<template #default>
<div class="grid grid-cols-2">
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>Account Icon</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>Account Color</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
</div>
</a>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Currency" title="Currency" :link="editAccountId ? null : '#'"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Account Balance" title="Balance"></f7-list-item>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item>
<f7-list-input label="Description" type="textarea" placeholder="Your account description (optional)"></f7-list-input>
</f7-list>
<f7-card v-else-if="!loading && account.type === $constants.account.allAccountTypes.SingleAccount">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-input
type="text"
clear-button
:label="$t('Account Name')"
:placeholder="$t('Your account name')"
:value="account.name"
@input="account.name = $event.target.value"
></f7-list-input>
<f7-list form strong inset dividers class="margin-vertical" v-else-if="!loading && account.type === $constants.account.allAccountTypes.SingleAccount">
<f7-list-input
type="text"
clear-button
:label="$t('Account Name')"
:placeholder="$t('Your account name')"
v-model:value="account.name"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title"
:header="$t('Account Icon')" link="#"
@click="account.showIconSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon :icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:show.sync="account.showIconSelectionSheet"
:color="account.color"
v-model="account.icon"
></icon-selection-sheet>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<template #default>
<div class="grid grid-cols-2">
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="account.showIconSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Account Icon') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item class="list-item-with-header-and-title"
:header="$t('Account Color')" link="#"
@click="account.showColorSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<color-selection-sheet :all-color-infos="allAccountColors"
:show.sync="account.showColorSelectionSheet"
v-model="account.color"
></color-selection-sheet>
</f7-list-item>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:color="account.color"
v-model:show="account.showIconSelectionSheet"
v-model="account.icon"
></icon-selection-sheet>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="account.showColorSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Account Color') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:class="{ 'disabled': editAccountId }"
:header="$t('Currency')"
:no-chevron="!!editAccountId"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<f7-block slot="title" class="no-padding no-margin">
<span>{{ $t(`currency.${account.currency}`) }}&nbsp;</span>
<small class="smaller">{{ account.currency }}</small>
</f7-block>
<select autocomplete="transaction-currency" v-model="account.currency">
<option v-for="currency in allCurrencies"
:key="currency.code"
:value="currency.code">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<color-selection-sheet :all-color-infos="allAccountColors"
v-model:show="account.showColorSelectionSheet"
v-model="account.color"
></color-selection-sheet>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
:link="editAccountId ? null : '#'"
:class="{ 'disabled': editAccountId }"
:header="$t('Account Balance')"
:title="account.balance | currency(account.currency)"
@click="account.showBalanceSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
:show.sync="account.showBalanceSheet"
v-model="account.balance"
></number-pad-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:class="{ 'disabled': editAccountId }"
:header="$t('Currency')"
:no-chevron="!!editAccountId"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Currency Name'), popupCloseLinkText: $t('Done') }"
>
<template #title>
<div class="no-padding no-margin">
<span>{{ $t(`currency.${account.currency}`) }}&nbsp;</span>
<small class="smaller">{{ account.currency }}</small>
</div>
</template>
<select autocomplete="transaction-currency" v-model="account.currency">
<option :value="currency.code"
:key="currency.code"
v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
</f7-list-item>
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:class="{ 'disabled': editAccountId }"
:header="$t('Account Balance')"
:title="$locale.getDisplayCurrency(account.balance, account.currency)"
@click="account.showBalanceSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
v-model:show="account.showBalanceSheet"
v-model="account.balance"
></number-pad-sheet>
</f7-list-item>
<f7-list-input
type="textarea"
class="textarea-auto-size"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your account description (optional)')"
:value="account.comment"
@input="account.comment = $event.target.value"
></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
</f7-list-item>
<f7-card v-else-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-input
type="text"
clear-button
:label="$t('Account Name')"
:placeholder="$t('Your account name')"
:value="account.name"
@input="account.name = $event.target.value"
></f7-list-input>
<f7-list-input
type="textarea"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your account description (optional)')"
v-textarea-auto-size
v-model:value="account.comment"
></f7-list-input>
</f7-list>
<f7-list-item class="list-item-with-header-and-title"
:header="$t('Account Icon')" link="#"
@click="account.showIconSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon :icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:show.sync="account.showIconSelectionSheet"
:color="account.color"
v-model="account.icon"
></icon-selection-sheet>
</f7-list-item>
<f7-list form strong inset dividers class="margin-vertical" v-else-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<f7-list-input
type="text"
clear-button
:label="$t('Account Name')"
:placeholder="$t('Your account name')"
v-model:value="account.name"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title"
:header="$t('Account Color')" link="#"
@click="account.showColorSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"
:style="account.color | accountIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<color-selection-sheet :all-color-infos="allAccountColors"
:show.sync="account.showColorSelectionSheet"
v-model="account.color"
></color-selection-sheet>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<template #default>
<div class="grid grid-cols-2">
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="account.showIconSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Account Icon') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
</f7-list-item>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:color="account.color"
v-model:show="account.showIconSelectionSheet"
v-model="account.icon"
></icon-selection-sheet>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="account.showColorSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Account Color') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="account.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-input
type="textarea"
class="textarea-auto-size"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your account description (optional)')"
:value="account.comment"
@input="account.comment = $event.target.value"
></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<color-selection-sheet :all-color-infos="allAccountColors"
v-model:show="account.showColorSelectionSheet"
v-model="account.color"
></color-selection-sheet>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="account.visible" @toggle:change="account.visible = $event"></f7-toggle>
</f7-list-item>
<f7-list-input
type="textarea"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your account description (optional)')"
v-textarea-auto-size
v-model:value="account.comment"
></f7-list-input>
</f7-list>
<f7-block class="no-padding no-margin" v-if="!loading && account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<f7-card v-for="(subAccount, idx) in subAccounts" :key="idx">
<f7-card-header>
<small class="subaccount-header-content">{{ $t('Sub Account') + ' #' + (idx + 1) }}</small>
<f7-button rasied fill color="red" icon-f7="trash" icon-size="16px"
<f7-list strong inset dividers class="subaccount-edit-list margin-vertical"
:key="idx"
v-for="(subAccount, idx) in subAccounts">
<f7-list-item group-title>
<small>{{ $t('Sub Account') + ' #' + (idx + 1) }}</small>
<f7-button rasied fill class="subaccount-delete-button" color="red" icon-f7="trash" icon-size="16px"
:tooltip="$t('Remove Sub Account')"
v-if="!editAccountId"
@click="removeSubAccount(subAccount, false)">
</f7-button>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-input
type="text"
clear-button
:label="$t('Sub Account Name')"
:placeholder="$t('Your sub account name')"
:value="subAccount.name"
@input="subAccount.name = $event.target.value"
></f7-list-input>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title"
:header="$t('Sub Account Icon')" link="#"
@click="subAccount.showIconSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon :icon="subAccount.icon | accountIcon"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:show.sync="subAccount.showIconSelectionSheet"
:color="subAccount.color"
v-model="subAccount.icon"
></icon-selection-sheet>
</f7-list-item>
<f7-list-input
type="text"
clear-button
:label="$t('Sub Account Name')"
:placeholder="$t('Your sub account name')"
v-model:value="subAccount.name"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title"
:header="$t('Sub Account Color')" link="#"
@click="subAccount.showColorSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<color-selection-sheet :all-color-infos="allAccountColors"
:show.sync="subAccount.showColorSelectionSheet"
v-model="subAccount.color"
></color-selection-sheet>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<template #default>
<div class="grid grid-cols-2">
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="subAccount.showIconSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Sub Account Icon') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="account" :icon-id="subAccount.icon" :color="subAccount.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:class="{ 'disabled': editAccountId }"
:header="$t('Currency')"
:no-chevron="!!editAccountId"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<f7-block slot="title" class="no-padding no-margin">
<span>{{ $t(`currency.${subAccount.currency}`) }}&nbsp;</span>
<small class="smaller">{{ subAccount.currency }}</small>
</f7-block>
<select autocomplete="transaction-currency" v-model="subAccount.currency">
<option v-for="currency in allCurrencies"
:key="currency.code"
:value="currency.code">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<icon-selection-sheet :all-icon-infos="allAccountIcons"
:color="subAccount.color"
v-model:show="subAccount.showIconSelectionSheet"
v-model="subAccount.icon"
></icon-selection-sheet>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="subAccount.showColorSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Sub Account Color') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="subAccount.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item
class="list-item-with-header-and-title"
:link="editAccountId ? null : '#'"
:class="{ 'disabled': editAccountId }"
:header="$t('Sub Account Balance')"
:title="subAccount.balance | currency(subAccount.currency)"
@click="subAccount.showBalanceSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
:show.sync="subAccount.showBalanceSheet"
v-model="subAccount.balance"
></number-pad-sheet>
</f7-list-item>
<color-selection-sheet :all-color-infos="allAccountColors"
v-model:show="subAccount.showColorSelectionSheet"
v-model="subAccount.color"
></color-selection-sheet>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="subAccount.visible" @toggle:change="subAccount.visible = $event"></f7-toggle>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:class="{ 'disabled': editAccountId }"
:header="$t('Currency')"
:no-chevron="!!editAccountId"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Currency Name'), popupCloseLinkText: $t('Done') }"
>
<template #title>
<div class="no-padding no-margin">
<span>{{ $t(`currency.${subAccount.currency}`) }}&nbsp;</span>
<small class="smaller">{{ subAccount.currency }}</small>
</div>
</template>
<select autocomplete="transaction-currency" v-model="subAccount.currency">
<option :value="currency.code"
:key="currency.code"
v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-input
type="textarea"
class="textarea-auto-size"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your sub account description (optional)')"
:value="subAccount.comment"
@input="subAccount.comment = $event.target.value"
></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item
link="#" no-chevron
class="list-item-with-header-and-title"
:class="{ 'disabled': editAccountId }"
:header="$t('Sub Account Balance')"
:title="$locale.getDisplayCurrency(subAccount.balance, subAccount.currency)"
@click="subAccount.showBalanceSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
v-model:show="subAccount.showBalanceSheet"
v-model="subAccount.balance"
></number-pad-sheet>
</f7-list-item>
<f7-list-item :header="$t('Visible')" v-if="editAccountId">
<f7-toggle :checked="subAccount.visible" @toggle:change="subAccount.visible = $event"></f7-toggle>
</f7-list-item>
<f7-list-input
type="textarea"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your sub account description (optional)')"
v-textarea-auto-size
v-model:value="subAccount.comment"
></f7-list-input>
</f7-list>
</f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
@@ -348,6 +421,10 @@
<script>
export default {
props: [
'f7route',
'f7router'
],
data() {
const self = this;
@@ -427,7 +504,7 @@ export default {
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
if (query.id) {
self.loading = true;
@@ -484,12 +561,9 @@ export default {
self.loading = false;
}
},
updated: function () {
this.autoChangeCommentTextareaSize();
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
addSubAccount() {
const self = this;
@@ -536,7 +610,7 @@ export default {
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
let problemMessage = self.getInputEmptyProblemMessage(self.account, false);
@@ -622,15 +696,13 @@ export default {
}
});
},
autoChangeCommentTextareaSize() {
const app = this.$f7;
const $$ = app.$;
$$('.textarea-auto-size textarea').each((idx, el) => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
getAccountTypeName(accountType) {
const typeName = this.$utilities.getNameByKeyValue(this.allAccountTypes, accountType, 'id', 'name');
return this.$t(typeName);
},
getAccountCategoryName(accountCategory) {
const categoryName = this.$utilities.getNameByKeyValue(this.allAccountCategories, accountCategory, 'id', 'name');
return this.$t(categoryName);
},
chooseSuitableIcon(oldCategory, newCategory) {
const allCategories = this.$constants.account.allCategories;
@@ -688,7 +760,11 @@ export default {
</script>
<style>
.subaccount-header-content {
opacity: 0.6;
.subaccount-edit-list {
--f7-list-group-title-height: 40px;
}
.subaccount-delete-button {
margin-left: auto;
}
</style>
+115 -154
View File
@@ -18,7 +18,7 @@
</p>
<p class="no-margin">
<span class="net-assets" v-if="loading">0.00 USD</span>
<span class="net-assets" v-else-if="!loading">{{ netAssets | currency(defaultCurrency) }}</span>
<span class="net-assets" v-else-if="!loading">{{ $locale.getDisplayCurrency(netAssets, defaultCurrency) }}</span>
<f7-link class="margin-left-half" @click="toggleShowAccountBalance()">
<f7-icon :f7="showAccountBalance ? 'eye_slash_fill' : 'eye_fill'" size="18px"></f7-icon>
</f7-link>
@@ -29,155 +29,106 @@
</small>
<small class="account-overview-info" v-else-if="!loading">
<span>{{ $t('Total assets') }}</span>
<span>{{ totalAssets | currency(defaultCurrency) }}</span>
<span>{{ $locale.getDisplayCurrency(totalAssets, defaultCurrency) }}</span>
<span>|</span>
<span>{{ $t('Total liabilities') }}</span>
<span>{{ totalLiabilities | currency(defaultCurrency) }}</span>
<span>{{ $locale.getDisplayCurrency(totalLiabilities, defaultCurrency) }}</span>
</small>
</p>
</f7-card-header>
</f7-card>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<small class="card-header-content">Account Category</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name</div>
</div>
</f7-block>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="account-list margin-vertical skeleton-text"
:key="listIdx" v-for="listIdx in [ 1, 2, 3 ]" v-if="loading">
<f7-list-item group-title>
<small>Account Category</small>
</f7-list-item>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#"
:key="itemIdx" v-for="itemIdx in (listIdx === 1 ? [ 1 ] : [ 1, 2 ])">
<template #title>
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name</div>
</div>
</template>
</f7-list-item>
</f7-list>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<small class="card-header-content">Account Category 2</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name</div>
</div>
</f7-block>
</f7-list-item>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name 2</div>
</div>
</f7-block>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical" v-if="!loading && noAvailableAccount">
<f7-list-item :title="$t('No available account')"></f7-list-item>
</f7-list>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<small class="card-header-content">Account Category 3</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name</div>
<div :key="accountCategory.id"
v-for="accountCategory in allAccountCategories"
v-show="(showHidden && hasAccount(accountCategory, false)) || hasAccount(accountCategory, true)">
<f7-list strong inset dividers sortable class="account-list margin-vertical"
:sortable-enabled="sortable"
v-if="categorizedAccounts[accountCategory.id]"
@sortable:sort="onSort">
<f7-list-item group-title :sortable="false">
<small>
<span>{{ $t(accountCategory.name) }}</span>
<span style="margin-left: 10px">{{ $locale.getDisplayCurrency(accountCategoryTotalBalance(accountCategory), defaultCurrency) }}</span>
</small>
</f7-list-item>
<f7-list-item swipeout
:id="getAccountDomId(account)"
:class="{ 'nested-list-item': true, 'has-child-list-item': account.type === $constants.account.allAccountTypes.MultiSubAccounts }"
:after="$locale.getDisplayCurrency(accountBalance(account), account.currency)"
:link="!sortable ? '/transaction/list?accountId=' + account.id : null"
:key="account.id"
v-for="account in categorizedAccounts[accountCategory.id].accounts"
v-show="showHidden || !account.hidden"
@taphold="setSortable()"
>
<template #title>
<div class="display-flex padding-top-half padding-bottom-half">
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color">
<f7-badge color="gray" class="right-bottom-icon" v-if="account.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</ItemIcon>
<div class="nested-list-item-title">
<span>{{ account.name }}</span>
<div class="item-footer" v-if="account.comment">{{ account.comment }}</div>
</div>
</f7-block>
</f7-list-item>
<f7-list-item class="nested-list-item" after="0.00 USD" link="#">
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<div class="nested-list-item-title">Account Name 2</div>
</div>
</f7-block>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-if="!loading && noAvailableAccount">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('No available account')"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-for="accountCategory in allAccountCategories" :key="accountCategory.id" v-show="(showHidden && hasAccount(accountCategory, false)) || hasAccount(accountCategory, true)">
<f7-card-header>
<small class="card-header-content">
<span>{{ $t(accountCategory.name) }}</span>
<span style="margin-left: 10px">{{ accountCategoryTotalBalance(accountCategory) | currency(defaultCurrency) }}</span>
</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list class="account-list" sortable :sortable-enabled="sortable" @sortable:sort="onSort" v-if="categorizedAccounts[accountCategory.id]">
<f7-list-item v-for="account in categorizedAccounts[accountCategory.id].accounts" v-show="showHidden || !account.hidden"
:key="account.id" :id="account | accountDomId"
:class="{ 'nested-list-item': true, 'has-child-list-item': account.type === $constants.account.allAccountTypes.MultiSubAccounts }"
:after="accountBalance(account) | currency(account.currency)"
:link="!sortable ? '/transaction/list?accountId=' + account.id : null"
swipeout @taphold.native="setSortable()"
>
<f7-block slot="title" class="no-padding">
<div class="display-flex padding-top-half padding-bottom-half">
<f7-icon class="align-self-center" slot="media" :icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')">
<f7-badge color="gray" class="right-bottom-icon" v-if="account.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
<div class="nested-list-item-title">
<span>{{ account.name }}</span>
<div class="item-footer" slot="footer" v-if="account.comment">{{ account.comment }}</div>
</div>
</div>
<li v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<ul class="no-padding">
<f7-list-item class="no-sortable nested-list-item-child" v-for="subAccount in account.subAccounts" v-show="showHidden || !subAccount.hidden"
:key="subAccount.id" :id="subAccount | accountDomId"
:title="subAccount.name" :footer="subAccount.comment" :after="accountBalance(subAccount) | currency(subAccount.currency)"
:link="!sortable ? '/transaction/list?accountId=' + subAccount.id : null"
>
<f7-icon slot="media" :icon="subAccount.icon | accountIcon"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')">
</div>
<li v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts">
<ul class="no-padding">
<f7-list-item class="no-sortable nested-list-item-child"
:id="getAccountDomId(subAccount)"
:title="subAccount.name" :footer="subAccount.comment" :after="$locale.getDisplayCurrency(accountBalance(subAccount), subAccount.currency)"
:link="!sortable ? '/transaction/list?accountId=' + subAccount.id : null"
:key="subAccount.id"
v-for="subAccount in account.subAccounts"
v-show="showHidden || !subAccount.hidden"
>
<template #media>
<ItemIcon icon-type="account" :icon-id="subAccount.icon" :color="subAccount.color">
<f7-badge color="gray" class="right-bottom-icon" v-if="subAccount.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
</f7-list-item>
</ul>
</li>
</f7-block>
<f7-swipeout-actions left v-if="sortable">
<f7-swipeout-button :color="account.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(account, !account.hidden)">
<f7-icon :f7="account.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(account)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(account, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
</ItemIcon>
</template>
</f7-list-item>
</ul>
</li>
</template>
<f7-swipeout-actions left v-if="sortable">
<f7-swipeout-button :color="account.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(account, !account.hidden)">
<f7-icon :f7="account.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(account)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(account, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</div>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
@@ -204,6 +155,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
loading: true,
@@ -353,7 +307,7 @@ export default {
this.reload(null);
}
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
reload(done) {
if (this.sortable) {
@@ -473,17 +427,22 @@ export default {
onSort(event) {
const self = this;
if (!event || !event.el || !event.el.id || event.el.id.indexOf('account_') !== 0) {
if (!event || !event.el || !event.el.id) {
self.$toast('Unable to move account');
return;
}
const id = event.el.id.substring(8); // account_
const id = self.parseAccountIdFromDomId(event.el.id);
if (!id) {
self.$toast('Unable to move account');
return;
}
self.$store.dispatch('changeAccountDisplayOrder', {
accountId: id,
from: event.from,
to: event.to
from: event.from - 1, // first item in the list is title, so the index need minus one
to: event.to - 1
}).then(() => {
self.displayOrderModified = true;
}).catch(error => {
@@ -519,7 +478,7 @@ export default {
});
},
edit(account) {
this.$f7router.navigate('/account/edit?id=' + account.id);
this.f7router.navigate('/account/edit?id=' + account.id);
},
hide(account, hidden) {
const self = this;
@@ -541,8 +500,6 @@ export default {
},
remove(account, confirm) {
const self = this;
const app = self.$f7;
const $$ = app.$;
if (!account) {
self.$alert('An error has occurred');
@@ -562,9 +519,7 @@ export default {
self.$store.dispatch('deleteAccount', {
account: account,
beforeResolve: (done) => {
app.swipeout.delete($$(`#${self.$options.filters.accountDomId(account)}`), () => {
done();
});
self.$ui.onSwipeoutDeleted(self.getAccountDomId(account), done);
}
}).then(() => {
self.$hideLoading();
@@ -575,11 +530,16 @@ export default {
self.$toast(error.message || error);
}
});
}
},
filters: {
accountDomId(account) {
},
getAccountDomId(account) {
return 'account_' + account.id;
},
parseAccountIdFromDomId(domId) {
if (!domId || domId.indexOf('account_') !== 0) {
return null;
}
return domId.substring(8); // account_
}
}
};
@@ -590,11 +550,11 @@ export default {
background-color: var(--f7-color-yellow);
}
.theme-dark .account-overview-card {
.dark .account-overview-card {
background-color: var(--f7-theme-color);
}
.theme-dark .account-overview-card a {
.dark .account-overview-card a {
color: var(--f7-text-color);
opacity: 0.6;
}
@@ -616,6 +576,7 @@ export default {
}
.account-list {
--f7-list-group-title-height: 36px;
--f7-list-item-footer-font-size: 13px;
}
+14 -19
View File
@@ -2,31 +2,26 @@
<f7-page ptr @ptr:refresh="reload" @page:afterin="onPageAfterIn">
<f7-navbar :title="$t('Transaction Categories')" :back-link="$t('Back')"></f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item title="Expense" link="#"></f7-list-item>
<f7-list-item title="Income" link="#"></f7-list-item>
<f7-list-item title="Transfer" link="#"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-list-item title="Expense" link="#"></f7-list-item>
<f7-list-item title="Income" link="#"></f7-list-item>
<f7-list-item title="Transfer" link="#"></f7-list-item>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Expense')" link="/category/list?type=2"></f7-list-item>
<f7-list-item :title="$t('Income')" link="/category/list?type=1"></f7-list-item>
<f7-list-item :title="$t('Transfer')" link="/category/list?type=3"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top" v-else-if="!loading">
<f7-list-item :title="$t('Expense')" link="/category/list?type=2"></f7-list-item>
<f7-list-item :title="$t('Income')" link="/category/list?type=1"></f7-list-item>
<f7-list-item :title="$t('Transfer')" link="/category/list?type=3"></f7-list-item>
</f7-list>
</f7-page>
</template>
<script>
export default {
props: [
'f7router'
],
data() {
return {
loading: true,
@@ -53,7 +48,7 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
reload(done) {
const self = this;
+119 -89
View File
@@ -8,91 +8,134 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-input label="Category Name" placeholder="Your category name"></f7-list-input>
<f7-list-item class="list-item-with-header-and-title" header="Category Icon" link="#">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</f7-block>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Category Color" link="#">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</f7-block>
</f7-list-item>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item>
<f7-list-input label="Description" type="textarea" placeholder="Your category description (optional)"></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-list-input label="Category Name" placeholder="Your category name"></f7-list-input>
<f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<template #default>
<div class="grid grid-cols-2">
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>Category Icon</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>Category Color</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
</div>
</a>
</div>
</div>
</template>
</f7-list-item>
<f7-list-item class="list-item-toggle" header="Visible" after="True"></f7-list-item>
<f7-list-input label="Description" type="textarea" placeholder="Your category description (optional)"></f7-list-input>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-input
type="text"
clear-button
:label="$t('Category Name')"
:placeholder="$t('Your category name')"
:value="category.name"
@input="category.name = $event.target.value"
></f7-list-input>
<f7-list form strong inset dividers class="margin-top" v-else-if="!loading">
<f7-list-input
type="text"
clear-button
:label="$t('Category Name')"
:placeholder="$t('Your category name')"
v-model:value="category.name"
></f7-list-input>
<f7-list-item class="list-item-with-header-and-title"
key="singleTypeCategoryIconSelection" link="#"
:header="$t('Category Icon')"
@click="category.showIconSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon :icon="category.icon | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<icon-selection-sheet :all-icon-infos="allCategoryIcons"
:show.sync="category.showIconSelectionSheet"
:color="category.color"
v-model="category.icon"
></icon-selection-sheet>
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-with-multi-item">
<template #default>
<div class="grid grid-cols-2">
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="category.showIconSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Category Icon') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item class="list-item-with-header-and-title"
key="singleTypeCategoryColorSelection" link="#"
:header="$t('Category Color')"
@click="category.showColorSelectionSheet = true">
<f7-block slot="title" class="list-item-custom-title no-padding">
<f7-icon f7="app_fill"
:style="category.color | categoryIconStyle('var(--default-icon-color)')"></f7-icon>
</f7-block>
<color-selection-sheet :all-color-infos="allCategoryColors"
:show.sync="category.showColorSelectionSheet"
v-model="category.color"
></color-selection-sheet>
</f7-list-item>
<icon-selection-sheet :all-icon-infos="allCategoryIcons"
:color="category.color"
v-model:show="category.showIconSelectionSheet"
v-model="category.icon"
></icon-selection-sheet>
</div>
<div class="list-item-subitem no-chevron">
<a class="item-link" href="#" @click="category.showColorSelectionSheet = true">
<div class="item-content">
<div class="item-inner">
<div class="item-header">
<span>{{ $t('Category Color') }}</span>
</div>
<div class="item-title">
<div class="list-item-custom-title no-padding">
<ItemIcon icon-type="fixed-f7" icon-id="app_fill" :color="category.color"></ItemIcon>
</div>
</div>
</div>
</div>
</a>
<f7-list-item :header="$t('Visible')" v-if="editCategoryId">
<f7-toggle :checked="category.visible" @toggle:change="category.visible = $event"></f7-toggle>
</f7-list-item>
<color-selection-sheet :all-color-infos="allCategoryColors"
v-model:show="category.showColorSelectionSheet"
v-model="category.color"
></color-selection-sheet>
</div>
</div>
</template>
</f7-list-item>
<f7-list-input
type="textarea"
class="textarea-auto-size"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your category description (optional)')"
:value="category.comment"
@input="category.comment = $event.target.value"
></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item :header="$t('Visible')" v-if="editCategoryId">
<f7-toggle :checked="category.visible" @toggle:change="category.visible = $event"></f7-toggle>
</f7-list-item>
<f7-list-input
type="textarea"
style="height: auto"
:label="$t('Description')"
:placeholder="$t('Your category description (optional)')"
v-textarea-auto-size
v-model:value="category.comment"
></f7-list-input>
</f7-list>
</f7-page>
</template>
<script>
export default {
props: [
'f7route',
'f7router'
],
data() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
return {
editCategoryId: null,
@@ -150,7 +193,7 @@ export default {
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
if (!query.id && !query.parentId) {
self.$toast('Parameter Invalid');
@@ -197,16 +240,13 @@ export default {
self.loading = false;
}
},
updated: function () {
this.autoChangeCommentTextareaSize();
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
const problemMessage = self.inputEmptyProblemMessage;
@@ -253,16 +293,6 @@ export default {
self.$toast(error.message || error);
}
});
},
autoChangeCommentTextareaSize() {
const app = this.$f7;
const $$ = app.$;
$$('.textarea-auto-size textarea').each((idx, el) => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
}
}
}
+74 -70
View File
@@ -10,63 +10,57 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item title="Category Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
<f7-list-item title="Category Name 2">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
<f7-list-item title="Category Name 3">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-list-item title="Category Name"
:link="hasSubCategories ? '#' : null"
:key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<template #media>
<f7-icon f7="app_fill"></f7-icon>
</template>
</f7-list-item>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list v-if="noAvailableCategory">
<f7-list-item :title="$t('No available category')"></f7-list-item>
<f7-list-button v-if="hasSubCategories"
:title="$t('Add Default Categories')"
:href="'/category/preset?type=' + categoryType"></f7-list-button>
</f7-list>
<f7-list strong inset dividers class="margin-top" v-if="!loading && noAvailableCategory">
<f7-list-item :title="$t('No available category')"></f7-list-item>
<f7-list-button v-if="hasSubCategories"
:title="$t('Add Default Categories')"
:href="'/category/preset?type=' + categoryType"></f7-list-button>
</f7-list>
<f7-list class="category-list" sortable :sortable-enabled="sortable" @sortable:sort="onSort">
<f7-list-item v-for="category in categories"
:key="category.id"
:id="category | categoryDomId"
:title="category.name"
:footer="category.comment"
:link="hasSubCategories ? '/category/list?type=' + categoryType + '&id=' + category.id : null"
v-show="showHidden || !category.hidden"
swipeout @taphold.native="setSortable()">
<f7-icon slot="media"
:icon="category.icon | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
<f7-badge color="gray" class="right-bottom-icon" v-if="category.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
<f7-swipeout-actions left v-if="sortable">
<f7-swipeout-button :color="category.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(category, !category.hidden)">
<f7-icon :f7="category.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(category)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(category, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers sortable class="margin-top category-list"
:sortable-enabled="sortable"
v-if="!loading"
@sortable:sort="onSort">
<f7-list-item swipeout
:id="getCategoryDomId(category)"
:title="category.name"
:footer="category.comment"
:link="hasSubCategories ? '/category/list?type=' + categoryType + '&id=' + category.id : null"
:key="category.id"
v-for="category in categories"
v-show="showHidden || !category.hidden"
@taphold="setSortable()">
<template #media>
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color">
<f7-badge color="gray" class="right-bottom-icon" v-if="category.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</ItemIcon>
</template>
<f7-swipeout-actions left v-if="sortable">
<f7-swipeout-button :color="category.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(category, !category.hidden)">
<f7-icon :f7="category.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(category)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(category, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
@@ -91,6 +85,10 @@
<script>
export default {
props: [
'f7route',
'f7router'
],
data() {
return {
hasSubCategories: false,
@@ -166,7 +164,7 @@ export default {
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
self.categoryType = parseInt(query.type);
@@ -207,7 +205,7 @@ export default {
this.reload(null);
}
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
reload(done) {
if (this.sortable) {
@@ -245,12 +243,17 @@ export default {
onSort(event) {
const self = this;
if (!event || !event.el || !event.el.id || event.el.id.indexOf('category_') !== 0) {
this.$toast('Unable to move category');
if (!event || !event.el || !event.el.id) {
self.$toast('Unable to move category');
return;
}
const id = event.el.id.substring(9); // category_
const id = self.parseCategoryIdFromDomId(event.el.id);
if (!id) {
self.$toast('Unable to move category');
return;
}
self.$store.dispatch('changeCategoryDisplayOrder', {
categoryId: id,
@@ -294,7 +297,7 @@ export default {
});
},
edit(category) {
this.$f7router.navigate('/category/edit?id=' + category.id);
this.f7router.navigate('/category/edit?id=' + category.id);
},
hide(category, hidden) {
const self = this;
@@ -316,8 +319,6 @@ export default {
},
remove(category, confirm) {
const self = this;
const app = self.$f7;
const $$ = app.$;
if (!category) {
self.$alert('An error has occurred');
@@ -337,9 +338,7 @@ export default {
self.$store.dispatch('deleteCategory', {
category: category,
beforeResolve: (done) => {
app.swipeout.delete($$(`#${self.$options.filters.categoryDomId(category)}`), () => {
done();
});
self.$ui.onSwipeoutDeleted(self.getCategoryDomId(category), done);
}
}).then(() => {
self.$hideLoading();
@@ -350,11 +349,16 @@ export default {
self.$toast(error.message || error);
}
});
}
},
filters: {
categoryDomId(category) {
},
getCategoryDomId(category) {
return 'category_' + category.id;
},
parseCategoryIdFromDomId(domId) {
if (!domId || domId.indexOf('category_') !== 0) {
return null;
}
return domId.substring(9); // category_
}
}
};
+45 -50
View File
@@ -4,44 +4,37 @@
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Default Categories')"></f7-nav-title>
<f7-nav-right>
<f7-link icon-f7="ellipsis" @click="showMoreActionSheet = true"></f7-link>
<f7-link :text="$t('Save')" :class="{ 'disabled': submitting }" @click="save"></f7-link>
<f7-link icon-f7="ellipsis" v-if="allCategories && allCategories.length" @click="showMoreActionSheet = true"></f7-link>
<f7-link :text="$t('Save')" :class="{ 'disabled': submitting }" v-if="allCategories && allCategories.length" @click="save"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-card v-for="categoryInfo in allCategories" :key="categoryInfo.type">
<f7-card-header>
<small class="card-header-content">
<span>{{ categoryInfo.type | categoryTypeName($constants.category.allCategoryTypes) | localized }}</span>
</small>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item v-for="(category, idx) in categoryInfo.categories"
:key="idx"
:accordion-item="!!category.subCategories.length"
:title="$t('category.' + category.name, currentLocale)">
<f7-icon slot="media"
:icon="category.categoryIconId | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-block class="no-padding no-margin" :key="categoryInfo.type" v-for="categoryInfo in allCategories">
<f7-block-title class="margin-top margin-horizontal">{{ getCategoryTypeName(categoryInfo.type) }}</f7-block-title>
<f7-accordion-content v-if="category.subCategories.length" class="padding-left">
<f7-list>
<f7-list-item v-for="(subCategory, subIdx) in category.subCategories"
:key="subIdx"
:title="$t('category.' + subCategory.name, currentLocale)">
<f7-icon slot="media"
:icon="subCategory.categoryIconId | categoryIcon"
:style="subCategory.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top">
<f7-list-item :title="$t('category.' + category.name, currentLocale)"
:accordion-item="!!category.subCategories.length"
:key="idx"
v-for="(category, idx) in categoryInfo.categories">
<template #media>
<ItemIcon icon-type="category" :icon-id="category.categoryIconId" :color="category.color"></ItemIcon>
</template>
<f7-accordion-content v-if="category.subCategories.length" class="padding-left">
<f7-list>
<f7-list-item :title="$t('category.' + subCategory.name, currentLocale)"
:key="subIdx"
v-for="(subCategory, subIdx) in category.subCategories">
<template #media>
<ItemIcon icon-type="category" :icon-id="subCategory.categoryIconId" :color="subCategory.color"></ItemIcon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-list-item>
</f7-list>
</f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
@@ -55,7 +48,7 @@
<list-item-selection-sheet value-type="index"
title-field="displayName"
:items="allLanguages"
:show.sync="showChangeLocaleSheet"
v-model:show="showChangeLocaleSheet"
v-model="currentLocale">
</list-item-selection-sheet>
</f7-page>
@@ -63,6 +56,10 @@
<script>
export default {
props: [
'f7route',
'f7router'
],
data() {
const self = this;
@@ -78,12 +75,12 @@ export default {
},
computed: {
allLanguages() {
return this.$locale.getAllLanguages();
return this.$locale.getAllLanguageInfos();
}
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
self.categoryType = parseInt(query.type);
@@ -112,7 +109,7 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
getDefaultCategories(categoryType) {
switch (categoryType) {
@@ -128,7 +125,7 @@ export default {
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
self.submitting = true;
self.$showLoading(() => self.submitting);
@@ -178,19 +175,17 @@ export default {
self.$toast(error.message || error);
}
});
}
},
filters: {
categoryTypeName(categoryType, allCategoryTypes) {
},
getCategoryTypeName(categoryType) {
switch (categoryType) {
case allCategoryTypes.Income:
return 'Income Categories';
case allCategoryTypes.Expense:
return 'Expense Categories';
case allCategoryTypes.Transfer:
return 'Transfer Categories';
case this.$constants.category.allCategoryTypes.Income:
return this.$t('Income Categories');
case this.$constants.category.allCategoryTypes.Expense:
return this.$t('Expense Categories');
case this.$constants.category.allCategoryTypes.Transfer:
return this.$t('Transfer Categories');
default:
return 'Transaction Categories';
return this.$t('Transaction Categories');
}
}
}
@@ -9,96 +9,93 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small class="card-header-content">
<span>Account Category</span>
</small>
<f7-icon class="card-chevron-icon float-right" f7="chevron_up"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-block class="combination-list-wrapper margin-vertical skeleton-text"
:key="blockIdx" v-for="blockIdx in [ 1, 2, 3 ]" v-if="loading">
<f7-accordion-item>
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="combination-list-header combination-list-opened">
<f7-list-item>
<template #title>
<span>Account Category</span>
<f7-icon class="combination-list-chevron-icon" f7="chevron_up"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content style="height: auto">
<f7-list strong inset dividers accordion-list class="combination-list-content">
<f7-list-item checkbox class="disabled" title="Account Name"
:key="itemIdx" v-for="itemIdx in (blockIdx === 1 ? [ 1 ] : [ 1, 2 ])">
<template #media>
<f7-icon f7="app_fill"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item checkbox class="disabled" title="Account Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-block class="combination-list-wrapper margin-vertical"
:key="accountCategory.id"
v-for="accountCategory in allAccountCategories"
v-else-if="!loading">
<f7-accordion-item :opened="collapseStates[accountCategory.id].opened"
v-show="hasShownAccount(accountCategory)"
@accordion:open="collapseStates[accountCategory.id].opened = true"
@accordion:close="collapseStates[accountCategory.id].opened = false">
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="combination-list-header"
:class="collapseStates[accountCategory.id].opened ? 'combination-list-opened' : 'combination-list-closed'">
<f7-list-item>
<template #title>
<span>{{ $t(accountCategory.name) }}</span>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[accountCategory.id].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content :style="{ height: collapseStates[accountCategory.id].opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content"
v-if="categorizedAccounts[accountCategory.id]">
<f7-list-item checkbox
:title="account.name"
:value="account.id"
:checked="isAccountOrSubAccountsAllChecked(account, filterAccountIds)"
:indeterminate="isAccountOrSubAccountsHasButNotAllChecked(account, filterAccountIds)"
:key="account.id"
v-for="account in categorizedAccounts[accountCategory.id].accounts"
v-show="!account.hidden"
@change="selectAccountOrSubAccounts">
<template #media>
<ItemIcon icon-type="account" :icon-id="account.icon" :color="account.color"></ItemIcon>
</template>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small class="card-header-content">
<span>Account Category 2</span>
</small>
<f7-icon class="card-chevron-icon float-right" f7="chevron_up"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item checkbox class="disabled" title="Account Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
<f7-list-item checkbox class="disabled" title="Account Name 2">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-block class="no-padding no-margin" v-if="!loading">
<f7-card v-for="accountCategory in allAccountCategories" :key="accountCategory.id" v-show="hasShownAccount(accountCategory)">
<f7-accordion-item :opened="collapseStates[accountCategory.id].opened"
@accordion:open="collapseStates[accountCategory.id].opened = true"
@accordion:close="collapseStates[accountCategory.id].opened = false">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small class="card-header-content">
<span>{{ $t(accountCategory.name) }}</span>
</small>
<f7-icon class="card-chevron-icon float-right" :f7="collapseStates[accountCategory.id].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false" accordion-list>
<f7-accordion-content :style="{ height: collapseStates[accountCategory.id].opened ? 'auto' : '' }">
<f7-list v-if="categorizedAccounts[accountCategory.id]">
<f7-list-item checkbox v-for="account in categorizedAccounts[accountCategory.id].accounts"
v-show="!account.hidden"
:key="account.id"
:title="account.name"
:value="account.id"
:checked="account | accountOrSubAccountsAllChecked(filterAccountIds)"
:indeterminate="account | accountOrSubAccountsHasButNotAllChecked(filterAccountIds)"
@change="selectAccountOrSubAccounts">
<f7-icon slot="media"
:icon="account.icon | accountIcon"
:style="account.color | accountIconStyle('var(--default-icon-color)')">
</f7-icon>
<ul slot="root" v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts" class="padding-left">
<f7-list-item checkbox v-for="subAccount in account.subAccounts"
v-show="!subAccount.hidden"
:key="subAccount.id"
:title="subAccount.name"
:value="subAccount.id"
:checked="subAccount | accountChecked(filterAccountIds) "
@change="selectAccount">
<f7-icon slot="media"
:icon="subAccount.icon | accountIcon"
:style="subAccount.color | accountIconStyle('var(--default-icon-color)')">
</f7-icon>
</f7-list-item>
</ul>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-card-content>
</f7-accordion-item>
</f7-card>
<template #root>
<ul v-if="account.type === $constants.account.allAccountTypes.MultiSubAccounts" class="padding-left">
<f7-list-item checkbox
:title="subAccount.name"
:value="subAccount.id"
:checked="isAccountChecked(subAccount, filterAccountIds)"
:key="subAccount.id"
v-for="subAccount in account.subAccounts"
v-show="!subAccount.hidden"
@change="selectAccount">
<template #media>
<ItemIcon icon-type="account" :icon-id="subAccount.icon" :color="subAccount.color"></ItemIcon>
</template>
</f7-list-item>
</ul>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
@@ -116,6 +113,10 @@
<script>
export default {
props: [
'f7route',
'f7router'
],
data: function () {
const self = this;
@@ -152,7 +153,7 @@ export default {
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
self.modifyDefault = !!query.modifyDefault;
@@ -188,11 +189,11 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
const filteredAccountIds = {};
@@ -286,6 +287,39 @@ export default {
}
}
},
isAccountChecked(account, filterAccountIds) {
return !filterAccountIds[account.id];
},
isAccountOrSubAccountsAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return !filterAccountIds[account.id];
}
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (filterAccountIds[subAccount.id]) {
return false;
}
}
return true;
},
isAccountOrSubAccountsHasButNotAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return false;
}
let checkedCount = 0;
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (!filterAccountIds[subAccount.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < account.subAccounts.length;
},
hasShownAccount(accountCategory) {
if (!this.categorizedAccounts[accountCategory.id] ||
!this.categorizedAccounts[accountCategory.id].accounts ||
@@ -320,41 +354,6 @@ export default {
return collapseStates;
}
},
filters: {
accountChecked(account, filterAccountIds) {
return !filterAccountIds[account.id];
},
accountOrSubAccountsAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return !filterAccountIds[account.id];
}
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (filterAccountIds[subAccount.id]) {
return false;
}
}
return true;
},
accountOrSubAccountsHasButNotAllChecked(account, filterAccountIds) {
if (!account.subAccounts) {
return false;
}
let checkedCount = 0;
for (let i = 0; i < account.subAccounts.length; i++) {
const subAccount = account.subAccounts[i];
if (!filterAccountIds[subAccount.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < account.subAccounts.length;
}
}
}
</script>
@@ -9,98 +9,101 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small class="card-header-content">
<span>Transaction Category</span>
</small>
<f7-icon class="card-chevron-icon float-right" f7="chevron_up"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item checkbox class="disabled" title="Category Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<ul slot="root" class="padding-left">
<f7-list-item checkbox class="disabled" title="Sub Category Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<f7-block class="combination-list-wrapper margin-vertical skeleton-text"
:key="blockIdx" v-for="blockIdx in [ 1, 2 ]" v-if="loading">
<f7-accordion-item>
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="combination-list-header combination-list-opened">
<f7-list-item>
<template #title>
<span>Transaction Category</span>
<f7-icon class="combination-list-chevron-icon" f7="chevron_up"></f7-icon>
</template>
</f7-list-item>
<f7-list-item checkbox class="disabled" title="Sub Category Name 2">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
<f7-list-item checkbox class="disabled" title="Sub Category Name 3">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
</ul>
</f7-list-item>
<f7-list-item checkbox class="disabled" title="Category Name 2">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<ul slot="root" class="padding-left">
<f7-list-item checkbox class="disabled" title="Sub Category Name">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
<f7-list-item checkbox class="disabled" title="Sub Category Name 2">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
<f7-list-item checkbox class="disabled" title="Sub Category Name 3">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</f7-list-item>
</ul>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content style="height: auto">
<f7-list strong inset dividers accordion-list class="combination-list-content">
<f7-list-item checkbox class="disabled" title="Category Name"
:key="itemIdx" v-for="itemIdx in [ 1, 2 ]">
<template #media>
<f7-icon f7="app_fill"></f7-icon>
</template>
<template #root>
<ul class="padding-left">
<f7-list-item checkbox class="disabled" title="Sub Category Name"
:key="subItemIdx" v-for="subItemIdx in [ 1, 2, 3 ]">
<template #media>
<f7-icon f7="app_fill"></f7-icon>
</template>
</f7-list-item>
</ul>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-block class="no-padding no-margin" v-if="!loading">
<f7-card v-for="(categories, categoryType) in allTransactionCategories" :key="categoryType">
<f7-accordion-item :opened="collapseStates[categoryType].opened"
@accordion:open="collapseStates[categoryType].opened = true"
@accordion:close="collapseStates[categoryType].opened = false">
<f7-card-header>
<f7-accordion-toggle class="full-line">
<small class="card-header-content">
<span>{{ categoryType | categoryTypeName($constants.category.allCategoryTypes) | localized }}</span>
</small>
<f7-icon class="card-chevron-icon float-right" :f7="collapseStates[categoryType].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</f7-accordion-toggle>
</f7-card-header>
<f7-card-content class="no-safe-areas" :padding="false" accordion-list>
<f7-accordion-content :style="{ height: collapseStates[categoryType].opened ? 'auto' : '' }">
<f7-list>
<f7-list-item checkbox v-for="category in categories"
v-show="!category.hidden"
:key="category.id"
:title="category.name"
:value="category.id"
:checked="category | subCategoriesAllChecked(filterCategoryIds)"
:indeterminate="category | subCategoriesHasButNotAllChecked(filterCategoryIds)"
@change="selectSubCategories">
<f7-icon slot="media"
:icon="category.icon | categoryIcon"
:style="category.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
<f7-block class="combination-list-wrapper margin-vertical"
:key="categoryType"
v-for="(categories, categoryType) in allTransactionCategories"
v-else-if="!loading">
<f7-accordion-item :opened="collapseStates[categoryType].opened"
@accordion:open="collapseStates[categoryType].opened = true"
@accordion:close="collapseStates[categoryType].opened = false">
<f7-block-title>
<f7-accordion-toggle>
<f7-list strong inset dividers media-list
class="combination-list-header"
:class="collapseStates[categoryType].opened ? 'combination-list-opened' : 'combination-list-closed'">
<f7-list-item>
<template #title>
<span>{{ getCategoryTypeName(categoryType) }}</span>
<f7-icon class="combination-list-chevron-icon" :f7="collapseStates[categoryType].opened ? 'chevron_up' : 'chevron_down'"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-toggle>
</f7-block-title>
<f7-accordion-content :style="{ height: collapseStates[categoryType].opened ? 'auto' : '' }">
<f7-list strong inset dividers accordion-list class="combination-list-content">
<f7-list-item checkbox
:title="category.name"
:value="category.id"
:checked="isSubCategoriesAllChecked(category, filterCategoryIds)"
:indeterminate="isSubCategoriesHasButNotAllChecked(category, filterCategoryIds)"
:key="category.id"
v-for="category in categories"
v-show="!category.hidden"
@change="selectSubCategories">
<template #media>
<ItemIcon icon-type="category" :icon-id="category.icon" :color="category.color"></ItemIcon>
</template>
<ul slot="root" v-if="category.subCategories.length" class="padding-left">
<f7-list-item checkbox v-for="subCategory in category.subCategories"
v-show="!subCategory.hidden"
:key="subCategory.id"
:title="subCategory.name"
:value="subCategory.id"
:checked="subCategory | categoryChecked(filterCategoryIds) "
@change="selectCategory">
<f7-icon slot="media"
:icon="subCategory.icon | categoryIcon"
:style="subCategory.color | categoryIconStyle('var(--default-icon-color)')">
</f7-icon>
</f7-list-item>
</ul>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-card-content>
</f7-accordion-item>
</f7-card>
<template #root>
<ul v-if="category.subCategories.length" class="padding-left">
<f7-list-item checkbox
:title="subCategory.name"
:value="subCategory.id"
:checked="isCategoryChecked(subCategory, filterCategoryIds)"
:key="subCategory.id"
v-for="subCategory in category.subCategories"
v-show="!subCategory.hidden"
@change="selectCategory">
<template #media>
<ItemIcon icon-type="category" :icon-id="subCategory.icon" :color="subCategory.color"></ItemIcon>
</template>
</f7-list-item>
</ul>
</template>
</f7-list-item>
</f7-list>
</f7-accordion-content>
</f7-accordion-item>
</f7-block>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
@@ -118,6 +121,10 @@
<script>
export default {
props: [
'f7route',
'f7router'
],
data: function () {
const self = this;
@@ -151,7 +158,7 @@ export default {
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
self.modifyDefault = !!query.modifyDefault;
@@ -187,11 +194,11 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
const filteredCategoryIds = {};
@@ -277,6 +284,43 @@ export default {
}
}
},
getCategoryTypeName(categoryType) {
switch (categoryType) {
case this.$constants.category.allCategoryTypes.Income.toString():
return this.$t('Income Categories');
case this.$constants.category.allCategoryTypes.Expense.toString():
return this.$t('Expense Categories');
case this.$constants.category.allCategoryTypes.Transfer.toString():
return this.$t('Transfer Categories');
default:
return this.$t('Transaction Categories');
}
},
isCategoryChecked(category, filterCategoryIds) {
return !filterCategoryIds[category.id];
},
isSubCategoriesAllChecked(category, filterCategoryIds) {
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (filterCategoryIds[subCategory.id]) {
return false;
}
}
return true;
},
isSubCategoriesHasButNotAllChecked(category, filterCategoryIds) {
let checkedCount = 0;
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (!filterCategoryIds[subCategory.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < category.subCategories.length;
},
getCollapseStates() {
const collapseStates = {};
@@ -294,45 +338,6 @@ export default {
return collapseStates;
}
},
filters: {
categoryTypeName(categoryType, allCategoryTypes) {
switch (categoryType) {
case allCategoryTypes.Income.toString():
return 'Income Categories';
case allCategoryTypes.Expense.toString():
return 'Expense Categories';
case allCategoryTypes.Transfer.toString():
return 'Transfer Categories';
default:
return 'Transaction Categories';
}
},
categoryChecked(category, filterCategoryIds) {
return !filterCategoryIds[category.id];
},
subCategoriesAllChecked(category, filterCategoryIds) {
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (filterCategoryIds[subCategory.id]) {
return false;
}
}
return true;
},
subCategoriesHasButNotAllChecked(category, filterCategoryIds) {
let checkedCount = 0;
for (let i = 0; i < category.subCategories.length; i++) {
const subCategory = category.subCategories[i];
if (!filterCategoryIds[subCategory.id]) {
checkedCount++;
}
}
return checkedCount > 0 && checkedCount < category.subCategories.length;
}
}
}
</script>
+39 -43
View File
@@ -2,54 +2,50 @@
<f7-page>
<f7-navbar :title="$t('Statistics Settings')" :back-link="$t('Back')"></f7-navbar>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item
:title="$t('Default Chart Type')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Chart Type'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="defaultChartType">
<option :value="$constants.statistics.allChartTypes.Pie">{{ $t('Pie Chart') }}</option>
<option :value="$constants.statistics.allChartTypes.Bar">{{ $t('Bar Chart') }}</option>
</select>
</f7-list-item>
<f7-list strong inset dividers class="margin-top">
<f7-list-item
:title="$t('Default Chart Type')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Chart Type'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultChartType">
<option :value="$constants.statistics.allChartTypes.Pie">{{ $t('Pie Chart') }}</option>
<option :value="$constants.statistics.allChartTypes.Bar">{{ $t('Bar Chart') }}</option>
</select>
</f7-list-item>
<f7-list-item
:title="$t('Default Chart Data Type')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Chart Data Type'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="defaultChartDataType">
<option v-for="chartDataType in allChartDataTypes"
:key="chartDataType.type"
:value="chartDataType.type">{{ $t(chartDataType.name) }}</option>
</select>
</f7-list-item>
<f7-list-item
:title="$t('Default Chart Data Type')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Chart Data Type'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultChartDataType">
<option :value="chartDataType.type"
:key="chartDataType.type"
v-for="chartDataType in allChartDataTypes">{{ $t(chartDataType.name) }}</option>
</select>
</f7-list-item>
<f7-list-item
:title="$t('Default Date Range')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Date Range'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="defaultDateRange">
<option v-for="dateRange in allDateRanges"
:key="dateRange.type"
:value="dateRange.type">{{ $t(dateRange.name) }}</option>
</select>
</f7-list-item>
<f7-list-item
:title="$t('Default Date Range')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date Range'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultDateRange">
<option :value="dateRange.type"
:key="dateRange.type"
v-for="dateRange in allDateRanges">{{ $t(dateRange.name) }}</option>
</select>
</f7-list-item>
<f7-list-item :title="$t('Default Account Filter')" link="/statistic/filter/account?modifyDefault=1"></f7-list-item>
<f7-list-item :title="$t('Default Account Filter')" link="/statistic/filter/account?modifyDefault=1"></f7-list-item>
<f7-list-item :title="$t('Default Transaction Category Filter')" link="/statistic/filter/category?modifyDefault=1"></f7-list-item>
<f7-list-item :title="$t('Default Transaction Category Filter')" link="/statistic/filter/category?modifyDefault=1"></f7-list-item>
<f7-list-item
:title="$t('Default Sort By')"
smart-select :smart-select-params="{ openIn: 'popup', searchbar: true, searchbarPlaceholder: $t('Default Sort By'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="defaultSortingType">
<option v-for="sortingType in allSortingTypes"
:key="sortingType.type"
:value="sortingType.type">{{ $t(sortingType.name) }}</option>
</select>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item
:title="$t('Default Sort By')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Sort By'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), popupCloseLinkText: $t('Done') }">
<select v-model="defaultSortingType">
<option :value="sortingType.type"
:key="sortingType.type"
v-for="sortingType in allSortingTypes">{{ $t(sortingType.name) }}</option>
</select>
</f7-list-item>
</f7-list>
</f7-page>
</template>
+158 -154
View File
@@ -4,7 +4,7 @@
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title>
<f7-link popover-open=".chart-data-type-popover-menu">
<span>{{ query.chartDataType | optionName(allChartDataTypes, 'type', 'name', 'Statistics') | localized }}</span>
<span>{{ queryChartDataTypeName }}</span>
<f7-icon size="14px" :f7="showChartDataTypePopover ? 'arrowtriangle_up_fill' : 'arrowtriangle_down_fill'"></f7-icon>
</f7-link>
</f7-nav-title>
@@ -15,11 +15,14 @@
<f7-popover class="chart-data-type-popover-menu" :opened="showChartDataTypePopover"
@popover:open="showChartDataTypePopover = true" @popover:close="showChartDataTypePopover = false">
<f7-list>
<f7-list-item
v-for="dataType in allChartDataTypes" :key="dataType.type"
:title="$t(dataType.name)" @click="setChartDataType(dataType.type)">
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="query.chartDataType === dataType.type"></f7-icon>
<f7-list dividers>
<f7-list-item :title="$t(dataType.name)"
:key="dataType.type"
v-for="dataType in allChartDataTypes"
@click="setChartDataType(dataType.type)">
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.chartDataType === dataType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
@@ -28,7 +31,7 @@
<f7-card-header class="no-border display-block">
<div class="statistics-chart-header full-line text-align-right">
<span style="margin-right: 4px;">{{ $t('Sort By') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ query.sortingType | optionName(allSortingTypes, 'type', 'name', 'System Default') | localized }}</f7-link>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div>
</f7-card-header>
<f7-card-content class="pie-chart-container" style="margin-top: -6px" :padding="false">
@@ -61,10 +64,10 @@
@click="clickPieChartItem"
>
<text class="statistics-pie-chart-total-amount-title" v-if="statisticsData.items && statisticsData.items.length">
{{ query.chartDataType | totalAmountName(allChartDataTypes) | localized }}
{{ totalAmountName }}
</text>
<text class="statistics-pie-chart-total-amount-value" v-if="statisticsData.items && statisticsData.items.length">
{{ statisticsData.totalAmount | currency(defaultCurrency) | finalAmount(showAccountBalance, query.chartDataType, allChartDataTypes) | textLimit(16) }}
{{ getDisplayAmount(statisticsData.totalAmount, defaultCurrency, 16) }}
</text>
<text class="statistics-pie-chart-total-no-data" cy="50%" v-if="!statisticsData.items || !statisticsData.items.length">
{{ $t('No data') }}
@@ -77,128 +80,91 @@
<f7-card-header class="no-border display-block">
<div class="statistics-chart-header display-flex full-line justify-content-space-between">
<div>
{{ query.chartDataType | totalAmountName(allChartDataTypes) | localized }}
{{ totalAmountName }}
</div>
<div class="align-self-flex-end">
<span style="margin-right: 4px;">{{ $t('Sort By') }}</span>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ query.sortingType | optionName(allSortingTypes, 'type', 'name', 'System Default') | localized }}</f7-link>
<f7-link href="#" popover-open=".sorting-type-popover-menu">{{ querySortingTypeName }}</f7-link>
</div>
</div>
<div class="display-flex full-line">
<div :class="{ 'statistics-list-item-overview-amount': true, 'text-color-teal': query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type, 'text-color-red': query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type }">
<span v-if="!loading && statisticsData && statisticsData.items && statisticsData.items.length">
{{ statisticsData.totalAmount | currency(defaultCurrency) | finalAmount(showAccountBalance, query.chartDataType, allChartDataTypes) }}
{{ getDisplayAmount(statisticsData.totalAmount, defaultCurrency) }}
</span>
<span v-else-if="loading || !statisticsData || !statisticsData.items || !statisticsData.items.length">
{{ '---' | currency(defaultCurrency, true) }}
<span :class="{ 'skeleton-text': loading }" v-else-if="loading || !statisticsData || !statisticsData.items || !statisticsData.items.length">
***.**
</span>
</div>
</div>
</f7-card-header>
<f7-card-content class="no-safe-areas" style="margin-top: -14px" :padding="false">
<f7-card-content style="margin-top: -14px" :padding="false">
<f7-list class="statistics-list-item skeleton-text" v-if="loading">
<f7-list-item link="#">
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon slot="media" f7="app_fill"></f7-icon>
<f7-list-item link="#" :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<template #media>
<div class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon f7="app_fill"></f7-icon>
</div>
</div>
</div>
<div slot="title" class="statistics-list-item-text">
<span>Category Name 1</span>
<small class="statistics-percent">33.33</small>
</div>
<div slot="after">
</template>
<template #title>
<div class="statistics-list-item-text">
<span>Category Name</span>
<small class="statistics-percent">33.33</small>
</div>
</template>
<template #after>
<span>0.00 USD</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar></f7-progressbar>
</template>
<template #inner-end>
<div class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar></f7-progressbar>
</div>
</div>
</div>
</f7-list-item>
<f7-list-item link="#">
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="statistics-list-item-text">
<span>Category Name 2</span>
<small class="statistics-percent">33.33</small>
</div>
<div slot="after">
<span>0.00 USD</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar></f7-progressbar>
</div>
</div>
</f7-list-item>
<f7-list-item link="#">
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon slot="media" f7="app_fill"></f7-icon>
</div>
</div>
<div slot="title" class="statistics-list-item-text">
<span>Category Name 3</span>
<small class="statistics-percent">33.33</small>
</div>
<div slot="after">
<span>0.00 USD</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar></f7-progressbar>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
<f7-list v-else-if="!loading && (!statisticsData || !statisticsData.items || !statisticsData.items.length)">
<f7-list-item :title="$t('No transaction data')"></f7-list-item>
</f7-list>
<f7-list v-else-if="!loading && statisticsData && statisticsData.items && statisticsData.items.length">
<f7-list-item v-for="(item, idx) in statisticsData.items" :key="idx"
class="statistics-list-item"
:link="item | itemLinkUrl(query, allChartDataTypes)"
<f7-list-item class="statistics-list-item"
:link="getItemLinkUrl(item)"
:key="idx"
v-for="(item, idx) in statisticsData.items"
v-show="!item.hidden"
>
<div slot="media" class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<f7-icon v-if="item.icon"
:icon="item.icon | icon(item.type)"
:style="item.color | iconStyle(item.type, 'var(--category-icon-color)')">
</f7-icon>
<f7-icon v-else-if="!item.icon"
f7="pencil_ellipsis_rectangle">
</f7-icon>
<template #media>
<div class="display-flex no-padding-horizontal">
<div class="display-flex align-items-center statistics-icon">
<ItemIcon icon-type="category" :icon-id="item.icon" :color="item.color" v-if="item.icon"></ItemIcon>
<f7-icon f7="pencil_ellipsis_rectangle" v-else-if="!item.icon"></f7-icon>
</div>
</div>
</div>
</template>
<div slot="title" class="statistics-list-item-text">
<span>{{ item.name }}</span>
<small class="statistics-percent" v-if="item.percent >= 0">{{ item.percent | percent(2, '&lt;0.01') }}</small>
</div>
<div slot="after">
<span>{{ item.totalAmount | currency(item.currency || defaultCurrency) | finalAmount(showAccountBalance, query.chartDataType, allChartDataTypes) }}</span>
</div>
<div slot="inner-end" class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar :progress="item.percent >= 0 ? item.percent : 0" :style="{ '--f7-progressbar-progress-color': (item.color ? '#' + item.color : '') } "></f7-progressbar>
<template #title>
<div class="statistics-list-item-text">
<span>{{ item.name }}</span>
<small class="statistics-percent" v-if="item.percent >= 0">{{ $utilities.formatPercent(item.percent, 2, '&lt;0.01') }}</small>
</div>
</div>
</template>
<template #after>
<span>{{ getDisplayAmount(item.totalAmount, (item.currency || defaultCurrency)) }}</span>
</template>
<template #inner-end>
<div class="statistics-item-end">
<div class="statistics-percent-line">
<f7-progressbar :progress="item.percent >= 0 ? item.percent : 0" :style="{ '--f7-progressbar-progress-color': (item.color ? '#' + item.color : '') } "></f7-progressbar>
</div>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-card-content>
@@ -207,13 +173,15 @@
<f7-popover class="sorting-type-popover-menu" :opened="showSortingTypePopover"
@popover:open="scrollPopoverToSelectedItem"
@popover:opened="showSortingTypePopover = true" @popover:closed="showSortingTypePopover = false">
<f7-list>
<f7-list-item v-for="sortingType in allSortingTypes"
:key="sortingType.type"
<f7-list dividers>
<f7-list-item :title="$t(sortingType.name)"
:class="{ 'list-item-selected': query.sortingType === sortingType.type }"
:title="$t(sortingType.name)"
:key="sortingType.type"
v-for="sortingType in allSortingTypes"
@click="setSortingType(sortingType.type)">
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="query.sortingType === sortingType.type"></f7-icon>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.sortingType === sortingType.type"></f7-icon>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
@@ -239,28 +207,31 @@
<f7-popover class="date-popover-menu" :opened="showDatePopover"
@popover:open="scrollPopoverToSelectedItem"
@popover:opened="showDatePopover = true" @popover:closed="showDatePopover = false">
<f7-list>
<f7-list-item v-for="dateRange in allDateRanges"
:key="dateRange.type"
<f7-list dividers>
<f7-list-item :title="$t(dateRange.name)"
:class="{ 'list-item-selected': query.dateType === dateRange.type }"
:title="$t(dateRange.name)"
:key="dateRange.type"
v-for="dateRange in allDateRanges"
@click="setDateFilter(dateRange.type)">
<f7-icon slot="after" class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon>
<div slot="footer"
v-if="dateRange.type === allDateRanges.Custom.type && query.dateType === allDateRanges.Custom.type && query.startTime && query.endTime">
<span>{{ query.startTime | moment($t('format.datetime.long-without-second')) }}</span>
<span>&nbsp;-&nbsp;</span>
<br/>
<span>{{ query.endTime | moment($t('format.datetime.long-without-second')) }}</span>
</div>
<template #after>
<f7-icon class="list-item-checked-icon" f7="checkmark_alt" v-if="query.dateType === dateRange.type"></f7-icon>
</template>
<template #footer>
<div v-if="dateRange.type === allDateRanges.Custom.type && query.dateType === allDateRanges.Custom.type && query.startTime && query.endTime">
<span>{{ $utilities.formatUnixTime(query.startTime, $t('format.datetime.long-without-second')) }}</span>
<span>&nbsp;-&nbsp;</span>
<br/>
<span>{{ $utilities.formatUnixTime(query.endTime, $t('format.datetime.long-without-second')) }}</span>
</div>
</template>
</f7-list-item>
</f7-list>
</f7-popover>
<date-range-selection-sheet :title="$t('Custom Date Range')"
:show.sync="showCustomDateRangeSheet"
:min-time="query.startTime"
:max-time="query.endTime"
v-model:show="showCustomDateRangeSheet"
@dateRange:change="setCustomDateFilter">
</date-range-selection-sheet>
@@ -281,6 +252,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
const self = this;
@@ -305,6 +279,14 @@ export default {
query() {
return this.$store.state.transactionStatisticsFilter;
},
queryChartDataTypeName() {
const queryChartDataTypeName = this.$utilities.getNameByKeyValue(this.allChartDataTypes, this.query.chartDataType, 'type', 'name', 'Statistics');
return this.$t(queryChartDataTypeName);
},
querySortingTypeName() {
const querySortingTypeName = this.$utilities.getNameByKeyValue(this.allSortingTypes, this.query.sortingType, 'type', 'name', 'System Default');
return this.$t(querySortingTypeName);
},
allChartDataTypes() {
return this.$constants.statistics.allChartDataTypes;
},
@@ -314,6 +296,23 @@ export default {
allDateRanges() {
return this.$constants.datetime.allDateRanges;
},
totalAmountName() {
if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type) {
return this.$t('Total Income');
} else if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
return this.$t('Total Expense');
} else if (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type) {
return this.$t('Total Assets');
} else if (this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type) {
return this.$t('Total Liabilities');
}
return this.$t('Total Amount');
},
statisticsData() {
const self = this;
let combinedData = {
@@ -466,7 +465,7 @@ export default {
this.reload(null);
}
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
reload(done) {
const self = this;
@@ -649,16 +648,16 @@ export default {
return `${displayStartTime} ~ ${displayEndTime}`;
},
clickPieChartItem(item) {
this.$f7router.navigate(this.$options.filters.itemLinkUrl(item, this.query, this.allChartDataTypes));
this.f7router.navigate(this.getItemLinkUrl(item));
},
filterAccounts() {
this.$f7router.navigate('/statistic/filter/account');
this.f7router.navigate('/statistic/filter/account');
},
filterCategories() {
this.$f7router.navigate('/statistic/filter/category');
this.f7router.navigate('/statistic/filter/category');
},
settings() {
this.$f7router.navigate('/statistic/settings');
this.f7router.navigate('/statistic/settings');
},
scrollPopoverToSelectedItem(event) {
if (!event || !event.$el || !event.$el.length) {
@@ -680,48 +679,53 @@ export default {
}
container.scrollTop(targetPos);
}
},
filters: {
finalAmount(amount, isShowAccountBalance, dataType, allChartDataTypes) {
if (!isShowAccountBalance && (dataType === allChartDataTypes.AccountTotalAssets.type || dataType === allChartDataTypes.AccountTotalLiabilities.type)) {
},
getDisplayAmount(amount, currency, textLimit) {
amount = this.$locale.getDisplayCurrency(amount, currency);
if (!this.showAccountBalance
&& (this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type)
) {
return '***';
}
if (textLimit) {
this.$utilities.limitText(amount, textLimit);
}
return amount;
},
totalAmountName(dataType, allChartDataTypes) {
if (dataType === allChartDataTypes.IncomeByAccount.type || dataType === allChartDataTypes.IncomeByPrimaryCategory.type || dataType === allChartDataTypes.IncomeBySecondaryCategory.type) {
return 'Total Income';
} else if (dataType === allChartDataTypes.ExpenseByAccount.type || dataType === allChartDataTypes.ExpenseByPrimaryCategory.type || dataType === allChartDataTypes.ExpenseBySecondaryCategory.type) {
return 'Total Expense';
} else if (dataType === allChartDataTypes.AccountTotalAssets.type) {
return 'Total Assets';
} else if (dataType === allChartDataTypes.AccountTotalLiabilities.type) {
return 'Total Liabilities';
}
return 'Total Amount';
},
itemLinkUrl(item, query, allChartDataTypes) {
getItemLinkUrl(item) {
const querys = [];
if (query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type) {
if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type) {
querys.push('type=2');
} else if (query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type) {
} else if (this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
querys.push('type=3');
}
if (query.chartDataType === allChartDataTypes.IncomeByAccount.type || query.chartDataType === allChartDataTypes.ExpenseByAccount.type || query.chartDataType === allChartDataTypes.AccountTotalAssets.type || query.chartDataType === allChartDataTypes.AccountTotalLiabilities.type) {
if (this.query.chartDataType === this.allChartDataTypes.IncomeByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByAccount.type
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalAssets.type
|| this.query.chartDataType === this.allChartDataTypes.AccountTotalLiabilities.type) {
querys.push('accountId=' + item.id);
} else if (query.chartDataType === allChartDataTypes.IncomeByPrimaryCategory.type || query.chartDataType === allChartDataTypes.IncomeBySecondaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseByPrimaryCategory.type || query.chartDataType === allChartDataTypes.ExpenseBySecondaryCategory.type) {
} else if (this.query.chartDataType === this.allChartDataTypes.IncomeByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.IncomeBySecondaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseByPrimaryCategory.type
|| this.query.chartDataType === this.allChartDataTypes.ExpenseBySecondaryCategory.type) {
querys.push('categoryId=' + item.id);
}
if (query.chartDataType !== allChartDataTypes.AccountTotalAssets.type && query.chartDataType !== allChartDataTypes.AccountTotalLiabilities.type) {
querys.push('dateType=' + query.dateType);
querys.push('minTime=' + query.startTime);
querys.push('maxTime=' + query.endTime);
if (this.query.chartDataType !== this.allChartDataTypes.AccountTotalAssets.type
&& this.query.chartDataType !== this.allChartDataTypes.AccountTotalLiabilities.type) {
querys.push('dateType=' + this.query.dateType);
querys.push('minTime=' + this.query.startTime);
querys.push('maxTime=' + this.query.endTime);
}
return '/transaction/list?' + querys.join('&');
@@ -808,7 +812,7 @@ export default {
--f7-progressbar-bg-color: #f8f8f8;
}
.theme-dark .statistics-percent-line .progressbar {
.dark .statistics-percent-line .progressbar {
--f7-progressbar-bg-color: #161616;
}
</style>
+130 -134
View File
@@ -10,132 +10,115 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item>
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name</div>
</div>
</f7-block>
</f7-list-item>
<f7-list-item>
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name 2</div>
</div>
</f7-block>
</f7-list-item>
<f7-list-item>
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name 3</div>
</div>
</f7-block>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="tag-item-list margin-top skeleton-text" v-if="loading">
<f7-list-item :key="itemIdx" v-for="itemIdx in [ 1, 2, 3 ]">
<template #media>
<f7-icon f7="number"></f7-icon>
</template>
<template #title>
<div class="display-flex">
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half">Tag Name</div>
</div>
</template>
</f7-list-item>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list v-if="noAvailableTag && !newTag">
<f7-list-item :title="$t('No available tag')"></f7-list-item>
</f7-list>
<f7-list strong inset dividers class="tag-item-list margin-top" v-if="!loading && noAvailableTag && !newTag">
<f7-list-item :title="$t('No available tag')"></f7-list-item>
</f7-list>
<f7-list sortable :sortable-enabled="sortable" @sortable:sort="onSort">
<f7-list-item v-for="tag in tags"
:key="tag.id"
:id="tag | tagDomId"
v-show="showHidden || !tag.hidden"
swipeout @taphold.native="setSortable()">
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number">
<f7-badge color="gray" class="right-bottom-icon" v-if="tag.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
<f7-list strong inset dividers sortable class="tag-item-list margin-top"
:sortable-enabled="sortable" @sortable:sort="onSort"
v-if="!loading">
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half"
v-if="editingTag.id !== tag.id">
{{ tag.name }}
</div>
<f7-input class="list-title-input padding-left-half"
type="text"
:placeholder="$t('Tag Title')"
:value="editingTag.name"
v-else-if="editingTag.id === tag.id"
@input="editingTag.name = $event.target.value"
@keyup.enter.native="save(tag)">
</f7-input>
</div>
</f7-block>
<f7-button slot="after"
:class="{ 'no-padding': true, 'disabled': !isTagModified(tag) }"
raised fill
icon-f7="checkmark_alt"
color="blue"
v-if="editingTag.id === tag.id"
@click="save(editingTag)">
</f7-button>
<f7-button slot="after"
class="no-padding margin-left-half"
raised fill
icon-f7="xmark"
color="gray"
v-if="editingTag.id === tag.id"
@click="cancelSave(editingTag)">
</f7-button>
<f7-swipeout-actions left v-if="sortable && editingTag.id !== tag.id">
<f7-swipeout-button :color="tag.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(tag, !tag.hidden)">
<f7-icon :f7="tag.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable && editingTag.id !== tag.id">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(tag)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(tag, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
<f7-list-item swipeout
:id="getTagDomId(tag)"
:key="tag.id"
v-for="tag in tags"
v-show="showHidden || !tag.hidden"
@taphold="setSortable()">
<template #media>
<f7-icon f7="number">
<f7-badge color="gray" class="right-bottom-icon" v-if="tag.hidden">
<f7-icon f7="eye_slash_fill"></f7-icon>
</f7-badge>
</f7-icon>
</template>
<template #title>
<div class="display-flex">
<div class="transaction-tag-list-item-content list-item-valign-middle padding-left-half"
v-if="editingTag.id !== tag.id">
{{ tag.name }}
</div>
<f7-input class="list-title-input padding-left-half"
type="text"
:placeholder="$t('Tag Title')"
v-else-if="editingTag.id === tag.id"
v-model:value="editingTag.name"
@keyup.enter="save(tag)">
</f7-input>
</div>
</template>
<template #after>
<f7-button :class="{ 'no-padding': true, 'disabled': !isTagModified(tag) }"
raised fill
icon-f7="checkmark_alt"
color="blue"
v-if="editingTag.id === tag.id"
@click="save(editingTag)">
</f7-button>
<f7-button class="no-padding margin-left-half"
raised fill
icon-f7="xmark"
color="gray"
v-if="editingTag.id === tag.id"
@click="cancelSave(editingTag)">
</f7-button>
</template>
<f7-swipeout-actions left v-if="sortable && editingTag.id !== tag.id">
<f7-swipeout-button :color="tag.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe close @click="hide(tag, !tag.hidden)">
<f7-icon :f7="tag.hidden ? 'eye' : 'eye_slash'"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
<f7-swipeout-actions right v-if="!sortable && editingTag.id !== tag.id">
<f7-swipeout-button color="orange" close :text="$t('Edit')" @click="edit(tag)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(tag, false)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
<f7-list-item v-if="newTag">
<f7-block slot="title" class="no-padding">
<div class="display-flex">
<f7-icon slot="media" f7="number"></f7-icon>
<f7-input class="list-title-input padding-left-half"
type="text"
:placeholder="$t('Tag Title')"
:value="newTag.name"
@input="newTag.name = $event.target.value"
@keyup.enter.native="save(newTag)">
</f7-input>
</div>
</f7-block>
<f7-button slot="after"
:class="{ 'no-padding': true, 'disabled': !isTagModified(newTag) }"
raised fill
icon-f7="checkmark_alt"
color="blue"
@click="save(newTag)">
</f7-button>
<f7-button slot="after"
class="no-padding margin-left-half"
raised fill
icon-f7="xmark"
color="gray"
@click="cancelSave(newTag)">
</f7-button>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item v-if="newTag">
<template #media>
<f7-icon f7="number"></f7-icon>
</template>
<template #title>
<div class="display-flex">
<f7-input class="list-title-input padding-left-half"
type="text"
:placeholder="$t('Tag Title')"
v-model:value="newTag.name"
@keyup.enter="save(newTag)">
</f7-input>
</div>
</template>
<template #after>
<f7-button :class="{ 'no-padding': true, 'disabled': !isTagModified(newTag) }"
raised fill
icon-f7="checkmark_alt"
color="blue"
@click="save(newTag)">
</f7-button>
<f7-button class="no-padding margin-left-half"
raised fill
icon-f7="xmark"
color="gray"
@click="cancelSave(newTag)">
</f7-button>
</template>
</f7-list-item>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
@@ -160,6 +143,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
newTag: null,
@@ -216,7 +202,7 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
reload(done) {
if (this.sortable || this.hasEditingTag) {
@@ -254,12 +240,17 @@ export default {
onSort(event) {
const self = this;
if (!event || !event.el || !event.el.id || event.el.id.indexOf('tag_') !== 0) {
if (!event || !event.el || !event.el.id) {
self.$toast('Unable to move tag');
return;
}
const id = event.el.id.substring(4); // tag_
const id = self.parseTagIdFromDomId(event.el.id);
if (!id) {
self.$toast('Unable to move tag');
return;
}
self.$store.dispatch('changeTagDisplayOrder', {
tagId: id,
@@ -367,8 +358,6 @@ export default {
},
remove(tag, confirm) {
const self = this;
const app = self.$f7;
const $$ = app.$;
if (!tag) {
self.$alert('An error has occurred');
@@ -388,9 +377,7 @@ export default {
self.$store.dispatch('deleteTag', {
tag: tag,
beforeResolve: (done) => {
app.swipeout.delete($$(`#${self.$options.filters.tagDomId(tag)}`), () => {
done();
});
self.$ui.onSwipeoutDeleted(self.getTagDomId(tag), done);
}
}).then(() => {
self.$hideLoading();
@@ -401,17 +388,26 @@ export default {
self.$toast(error.message || error);
}
});
}
},
filters: {
tagDomId(category) {
},
getTagDomId(category) {
return 'tag_' + category.id;
},
parseTagIdFromDomId(domId) {
if (!domId || domId.indexOf('tag_') !== 0) {
return null;
}
return domId.substring(4); // tag_
}
}
}
</script>
<style>
.tag-item-list.list .item-media + .item-inner {
margin-left: 5px;
}
.transaction-tag-list-item-content {
overflow: hidden;
text-overflow: ellipsis;
+261 -287
View File
@@ -1,5 +1,5 @@
<template>
<f7-page @page:afterin="onPageAfterIn">
<f7-page with-subnavbar @page:afterin="onPageAfterIn">
<f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t(title)"></f7-nav-title>
@@ -17,255 +17,255 @@
</f7-subnavbar>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item
class="transaction-edit-amount"
style="font-size: 40px;"
header="Expense Amount" title="0.00">
</f7-list-item>
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title list-item-title-hide-overflow" header="Category" title="Category Names" link="#"></f7-list-item>
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title" header="Account" title="Account Name" link="#"></f7-list-item>
<f7-list-input label="Transaction Time" placeholder="YYYY/MM/DD HH:mm"></f7-list-input>
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title list-item-title-hide-overflow" header="Transaction Time Zone" title="(UTC XX:XX) System Default" link="#"></f7-list-item>
<f7-list-item :no-chevron="mode === 'view'" header="Tags" link="#">
<f7-block class="margin-top-half no-padding" slot="footer">
<f7-chip class="transaction-edit-tag" text="None"></f7-chip>
</f7-block>
</f7-list-item>
<f7-list-input type="textarea" label="Description" placeholder="Your transaction description (optional)"></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-list-item
class="transaction-edit-amount"
style="font-size: 40px;"
header="Expense Amount" title="0.00">
</f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-title-hide-overflow" header="Category" title="Category Names"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title" header="Account" title="Account Name"></f7-list-item>
<f7-list-input label="Transaction Time" placeholder="YYYY/MM/DD HH:mm"></f7-list-input>
<f7-list-item :no-chevron="mode === 'view'" class="list-item-with-header-and-title list-item-title-hide-overflow" header="Transaction Time Zone" title="(UTC XX:XX) System Default" link="#"></f7-list-item>
<f7-list-item header="Tags">
<template #footer>
<f7-block class="margin-top-half no-padding no-margin">
<f7-chip class="transaction-edit-tag" text="None"></f7-chip>
</f7-block>
</template>
</f7-list-item>
<f7-list-input type="textarea" label="Description" placeholder="Your transaction description (optional)"></f7-list-input>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form :class="{ 'readonly': mode === 'view' }">
<f7-list-item
class="transaction-edit-amount"
link="#" no-chevron
:class="{ 'color-theme-teal': transaction.type === $constants.transaction.allTransactionTypes.Expense, 'color-theme-red': transaction.type === $constants.transaction.allTransactionTypes.Income }"
:style="{ fontSize: sourceAmountFontSize + 'px' }"
:header="$t(sourceAmountName)"
:title="transaction.sourceAmount | finalAmount(transaction.hideAmount) | currency"
@click="showSourceAmountSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
:show.sync="showSourceAmountSheet"
v-model="transaction.sourceAmount"
></number-pad-sheet>
</f7-list-item>
<f7-list form strong inset dividers
class="margin-vertical" :class="{ 'readonly': mode === 'view' }"
v-else-if="!loading">
<f7-list-item
class="transaction-edit-amount"
link="#" no-chevron
:class="{ 'color-teal': transaction.type === $constants.transaction.allTransactionTypes.Expense, 'color-red': transaction.type === $constants.transaction.allTransactionTypes.Income }"
:style="{ fontSize: sourceAmountFontSize + 'px' }"
:header="$t(sourceAmountName)"
:title="getDisplayAmount(transaction.sourceAmount, transaction.hideAmount)"
@click="showSourceAmountSheet = true"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
v-model:show="showSourceAmountSheet"
v-model="transaction.sourceAmount"
></number-pad-sheet>
</f7-list-item>
<f7-list-item
class="transaction-edit-amount"
link="#" no-chevron
:style="{ fontSize: destinationAmountFontSize + 'px' }"
:header="$t('Transfer In Amount')"
:title="transaction.destinationAmount | finalAmount(transaction.hideAmount) | currency"
@click="showDestinationAmountSheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
:show.sync="showDestinationAmountSheet"
v-model="transaction.destinationAmount"
></number-pad-sheet>
</f7-list-item>
<f7-list-item
class="transaction-edit-amount"
link="#" no-chevron
:style="{ fontSize: destinationAmountFontSize + 'px' }"
:header="$t('Transfer In Amount')"
:title="getDisplayAmount(transaction.destinationAmount, transaction.hideAmount)"
@click="showDestinationAmountSheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
>
<number-pad-sheet :min-value="$constants.transaction.minAmount"
:max-value="$constants.transaction.maxAmount"
v-model:show="showDestinationAmountSheet"
v-model="transaction.destinationAmount"
></number-pad-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="expenseCategorySelection"
link="#" :no-chevron="mode === 'view'"
:class="{ 'disabled': !hasAvailableExpenseCategories }"
:header="$t('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Expense"
>
<div slot="title" class="list-item-custom-title">
<span>{{ transaction.expenseCategory | primaryCategoryName(allCategories[$constants.category.allCategoryTypes.Expense]) }}</span>
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
<span>{{ transaction.expenseCategory | secondaryCategoryName(allCategories[$constants.category.allCategoryTypes.Expense]) }}</span>
</div>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
:items="allCategories[$constants.category.allCategoryTypes.Expense]"
:show.sync="showCategorySheet"
v-model="transaction.expenseCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="expenseCategorySelection"
link="#" no-chevron
:class="{ 'disabled': !hasAvailableExpenseCategories }"
:header="$t('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Expense"
>
<template #title>
<div class="list-item-custom-title">
<span>{{ getPrimaryCategoryName(transaction.expenseCategory, allCategories[$constants.category.allCategoryTypes.Expense]) }}</span>
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
<span>{{ getSecondaryCategoryName(transaction.expenseCategory, allCategories[$constants.category.allCategoryTypes.Expense]) }}</span>
</div>
</template>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
:items="allCategories[$constants.category.allCategoryTypes.Expense]"
v-model:show="showCategorySheet"
v-model="transaction.expenseCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="incomeCategorySelection"
link="#" :no-chevron="mode === 'view'"
:class="{ 'disabled': !hasAvailableIncomeCategories }"
:header="$t('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Income"
>
<div slot="title" class="list-item-custom-title">
<span>{{ transaction.incomeCategory | primaryCategoryName(allCategories[$constants.category.allCategoryTypes.Income]) }}</span>
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
<span>{{ transaction.incomeCategory | secondaryCategoryName(allCategories[$constants.category.allCategoryTypes.Income]) }}</span>
</div>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
:items="allCategories[$constants.category.allCategoryTypes.Income]"
:show.sync="showCategorySheet"
v-model="transaction.incomeCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="incomeCategorySelection"
link="#" no-chevron
:class="{ 'disabled': !hasAvailableIncomeCategories }"
:header="$t('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Income"
>
<template #title>
<div class="list-item-custom-title">
<span>{{ getPrimaryCategoryName(transaction.incomeCategory, allCategories[$constants.category.allCategoryTypes.Income]) }}</span>
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
<span>{{ getSecondaryCategoryName(transaction.incomeCategory, allCategories[$constants.category.allCategoryTypes.Income]) }}</span>
</div>
</template>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
:items="allCategories[$constants.category.allCategoryTypes.Income]"
v-model:show="showCategorySheet"
v-model="transaction.incomeCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="transferCategorySelection"
link="#" :no-chevron="mode === 'view'"
:class="{ 'disabled': !hasAvailableTransferCategories }"
:header="$t('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
>
<div slot="title" class="list-item-custom-title">
<span>{{ transaction.transferCategory | primaryCategoryName(allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span>
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
<span>{{ transaction.transferCategory | secondaryCategoryName(allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span>
</div>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
:items="allCategories[$constants.category.allCategoryTypes.Transfer]"
:show.sync="showCategorySheet"
v-model="transaction.transferCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-title-hide-overflow"
key="transferCategorySelection"
link="#" no-chevron
:class="{ 'disabled': !hasAvailableTransferCategories }"
:header="$t('Category')"
@click="showCategorySheet = true"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
>
<template #title>
<div class="list-item-custom-title">
<span>{{ getPrimaryCategoryName(transaction.transferCategory, allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span>
<f7-icon class="category-separate-icon" f7="chevron_right"></f7-icon>
<span>{{ getSecondaryCategoryName(transaction.transferCategory, allCategories[$constants.category.allCategoryTypes.Transfer]) }}</span>
</div>
</template>
<tree-view-selection-sheet primary-key-field="id" primary-value-field="id" primary-title-field="name"
primary-icon-field="icon" primary-icon-type="category" primary-color-field="color"
primary-sub-items-field="subCategories"
secondary-key-field="id" secondary-value-field="id" secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="category" secondary-color-field="color"
:items="allCategories[$constants.category.allCategoryTypes.Transfer]"
v-model:show="showCategorySheet"
v-model="transaction.transferCategory">
</tree-view-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#" :no-chevron="mode === 'view'"
:class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t(sourceAccountName)"
:title="transaction.sourceAccountId | optionName(allAccounts, 'id', 'name')"
@click="showSourceAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="categorizedAccounts"
:show.sync="showSourceAccountSheet"
v-model="transaction.sourceAccountId">
</two-column-list-item-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#" no-chevron
:class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t(sourceAccountName)"
:title="$utilities.getNameByKeyValue(allAccounts, transaction.sourceAccountId, 'id', 'name')"
@click="showSourceAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="categorizedAccounts"
v-model:show="showSourceAccountSheet"
v-model="transaction.sourceAccountId">
</two-column-list-item-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#" :no-chevron="mode === 'view'"
:class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t('Destination Account')"
:title="transaction.destinationAccountId | optionName(allAccounts, 'id', 'name')"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
@click="showDestinationAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="categorizedAccounts"
:show.sync="showDestinationAccountSheet"
v-model="transaction.destinationAccountId">
</two-column-list-item-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#" no-chevron
:class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t('Destination Account')"
:title="$utilities.getNameByKeyValue(allAccounts, transaction.destinationAccountId, 'id', 'name')"
v-if="transaction.type === $constants.transaction.allTransactionTypes.Transfer"
@click="showDestinationAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name" primary-footer-field="displayBalance"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name" secondary-footer-field="displayBalance"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="categorizedAccounts"
v-model:show="showDestinationAccountSheet"
v-model="transaction.destinationAccountId">
</two-column-list-item-selection-sheet>
</f7-list-item>
<f7-list-input
:label="$t('Transaction Time')"
type="datepicker"
class="transaction-edit-time"
:calendar-params="{
timePicker: true,
dateFormat: $t('input-format.datetime.long'),
firstDay: defaultFirstDayOfWeek,
toolbarCloseText: $t('Done'),
timePickerPlaceholder: $t('Select Time'),
timePickerFormat: $locale.getInputTimeIntlDateTimeFormatOptions(),
monthNames: $locale.getAllLongMonthNames(),
monthNamesShort: $locale.getAllShortMonthNames(),
dayNames: $locale.getAllLongWeekdayNames(),
dayNamesShort: $locale.getAllShortWeekdayNames()}"
:value="transaction.time"
@calendar:change="transaction.time = $event"
>
</f7-list-input>
<f7-list-item
class="list-item-with-header-and-title"
link="#" no-chevron
:header="$t('Transaction Time')"
:title="$utilities.formatUnixTime($utilities.getActualUnixTimeForStore(transaction.time, $utilities.getTimezoneOffsetMinutes(), $utilities.getBrowserTimezoneOffsetMinutes()), this.$t('format.datetime.long'))"
@click="showTransactionDateTimeSheet = true"
>
<date-time-selection-sheet v-model:show="showTransactionDateTimeSheet"
v-model="transaction.time">
</date-time-selection-sheet>
</f7-list-item>
<f7-list-item
:no-chevron="mode === 'view'"
class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after"
:header="$t('Transaction Time Zone')"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Transaction Time Zone'), searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }">
<select v-model="transaction.timeZone">
<option v-for="timezone in allTimezones"
:key="timezone.name"
:value="timezone.name">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option>
</select>
<f7-block slot="title" class="list-item-custom-title no-padding">
<span>{{ transaction.utcOffset | utcOffset }}</span>
<span class="transaction-edit-timezone-name" v-if="transaction.timeZone || transaction.timeZone === ''">{{ transaction.timeZone | optionName(allTimezones, 'name', 'displayName') }}</span>
</f7-block>
</f7-list-item>
<f7-list-item
:no-chevron="mode === 'view'"
class="list-item-with-header-and-title list-item-title-hide-overflow list-item-no-item-after"
:header="$t('Transaction Time Zone')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Timezone'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Transaction Time Zone'), popupCloseLinkText: $t('Done') }">
<select v-model="transaction.timeZone">
<option :value="timezone.name"
:key="timezone.name"
v-for="timezone in allTimezones">{{ `(UTC${timezone.utcOffset}) ${timezone.displayName}` }}</option>
</select>
<template #title>
<f7-block class="list-item-custom-title no-padding no-margin">
<span>{{ `(UTC${$utilities.getUtcOffsetByUtcOffsetMinutes(transaction.utcOffset)})` }}</span>
<span class="transaction-edit-timezone-name" v-if="transaction.timeZone || transaction.timeZone === ''">{{ $utilities.getNameByKeyValue(allTimezones, transaction.timeZone, 'name', 'displayName') }}</span>
</f7-block>
</template>
</f7-list-item>
<f7-list-item
link="#" :no-chevron="mode === 'view'"
:header="$t('Tags')"
@click="showTransactionTagSheet = true"
>
<transaction-tag-selection-sheet :items="allTags"
:show.sync="showTransactionTagSheet"
v-model="transaction.tagIds">
</transaction-tag-selection-sheet>
<f7-list-item
link="#" no-chevron
:header="$t('Tags')"
@click="showTransactionTagSheet = true"
>
<transaction-tag-selection-sheet :items="allTags"
v-model:show="showTransactionTagSheet"
v-model="transaction.tagIds">
</transaction-tag-selection-sheet>
<f7-block class="margin-top-half no-padding" slot="footer" v-if="transaction.tagIds && transaction.tagIds.length">
<f7-chip class="transaction-edit-tag" media-bg-color="black"
v-for="tagId in transaction.tagIds"
:key="tagId"
:text="tagId | optionName(allTags, 'id', 'name')">
<f7-icon slot="media" f7="number"></f7-icon>
</f7-chip>
</f7-block>
<f7-block class="margin-top-half no-padding" slot="footer" v-else-if="!transaction.tagIds || !transaction.tagIds.length">
<f7-chip class="transaction-edit-tag" :text="$t('None')">
</f7-chip>
</f7-block>
</f7-list-item>
<template #footer>
<f7-block class="margin-top-half no-padding no-margin" v-if="transaction.tagIds && transaction.tagIds.length">
<f7-chip media-bg-color="black" class="transaction-edit-tag"
:text="$utilities.getNameByKeyValue(allTags, tagId, 'id', 'name')"
:key="tagId"
v-for="tagId in transaction.tagIds">
<template #media>
<f7-icon f7="number"></f7-icon>
</template>
</f7-chip>
</f7-block>
<f7-block class="margin-top-half no-padding no-margin" v-else-if="!transaction.tagIds || !transaction.tagIds.length">
<f7-chip class="transaction-edit-tag" :text="$t('None')">
</f7-chip>
</f7-block>
</template>
</f7-list-item>
<f7-list-input
type="textarea"
class="transaction-edit-comment textarea-auto-size"
style="height: auto"
:label="$t('Description')"
:placeholder="mode !== 'view' ? $t('Your transaction description (optional)') : ''"
:value="transaction.comment"
@input="transaction.comment = $event.target.value"
></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-input
type="textarea"
class="transaction-edit-comment"
style="height: auto"
:label="$t('Description')"
:placeholder="mode !== 'view' ? $t('Your transaction description (optional)') : ''"
v-textarea-auto-size
v-model:value="transaction.comment"
></f7-list-input>
</f7-list>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
@@ -287,9 +287,13 @@
<script>
export default {
props: [
'f7route',
'f7router'
],
data() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
const now = self.$utilities.getCurrentUnixTime();
const currentTimezone = self.$locale.getTimezone();
@@ -306,8 +310,7 @@ export default {
editTransactionId: null,
transaction: {
type: defaultType,
unixTime: now,
time: self.$utilities.getLocalDatetimeFromUnixTime(now),
time: now,
timeZone: currentTimezone,
utcOffset: self.$utilities.getTimezoneOffsetMinutes(currentTimezone),
expenseCategory: '',
@@ -331,6 +334,7 @@ export default {
showCategorySheet: false,
showSourceAccountSheet: false,
showDestinationAccountSheet: false,
showTransactionDateTimeSheet: false,
showTransactionTagSheet: false
};
},
@@ -407,9 +411,9 @@ export default {
const account = accountCategory.accounts[i];
if (this.showAccountBalance && account.isAsset) {
account.displayBalance = this.$options.filters.currency(account.balance, account.currency);
account.displayBalance = this.$locale.getDisplayCurrency(account.balance, account.currency);
} else if (this.showAccountBalance && account.isLiability) {
account.displayBalance = this.$options.filters.currency(-account.balance, account.currency);
account.displayBalance = this.$locale.getDisplayCurrency(-account.balance, account.currency);
} else {
account.displayBalance = '***';
}
@@ -448,7 +452,7 @@ export default {
totalBalance = totalBalance + '+';
}
accountCategory.displayBalance = this.$options.filters.currency(totalBalance, this.defaultCurrency);
accountCategory.displayBalance = this.$locale.getDisplayCurrency(totalBalance, this.defaultCurrency);
} else {
accountCategory.displayBalance = '***';
}
@@ -543,20 +547,6 @@ export default {
this.transaction.sourceAmount = newValue;
}
},
'transaction.time': function (newValue) {
if (this.$utilities.isArray(newValue)) {
newValue = newValue[0];
}
if (!newValue) {
newValue = this.$utilities.getLocalDatetimeFromUnixTime(this.$utilities.getCurrentUnixTime());
this.transaction.time = [newValue];
}
if (this.$utilities.getUnixTimeFromLocalDatetime(newValue) !== this.transaction.unixTime) {
this.transaction.unixTime = this.$utilities.getUnixTimeFromLocalDatetime(newValue);
}
},
'transaction.timeZone': function (newValue) {
for (let i = 0; i < this.allTimezones.length; i++) {
if (this.allTimezones[i].name === newValue) {
@@ -568,11 +558,11 @@ export default {
},
created() {
const self = this;
const query = self.$f7route.query;
const query = self.f7route.query;
if (self.$f7route.path === '/transaction/edit') {
if (self.f7route.path === '/transaction/edit') {
self.mode = 'edit';
} else if (self.$f7route.path === '/transaction/detail') {
} else if (self.f7route.path === '/transaction/detail') {
self.mode = 'view';
}
@@ -695,8 +685,7 @@ export default {
if (self.mode === 'edit' || self.mode === 'view') {
self.transaction.utcOffset = transaction.utcOffset;
self.transaction.timeZone = null;
self.transaction.unixTime = self.$utilities.getDummyUnixTimeForLocalUsage(transaction.time, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes());
self.transaction.time = [self.$utilities.getLocalDatetimeFromUnixTime(self.transaction.unixTime)];
self.transaction.time = self.$utilities.getDummyUnixTimeForLocalUsage(transaction.time, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes());
}
self.transaction.sourceAccountId = transaction.sourceAccountId;
@@ -728,16 +717,13 @@ export default {
}
});
},
updated: function () {
this.autoChangeCommentTextareaSize();
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
if (self.mode === 'view') {
return;
@@ -745,7 +731,7 @@ export default {
const submitTransaction = {
type: self.transaction.type,
time: self.$utilities.getActualUnixTimeForStore(self.transaction.unixTime, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes()),
time: self.$utilities.getActualUnixTimeForStore(self.transaction.time, self.transaction.utcOffset, self.$utilities.getBrowserTimezoneOffsetMinutes()),
sourceAccountId: self.transaction.sourceAccountId,
sourceAmount: self.transaction.sourceAmount,
destinationAccountId: '0',
@@ -809,16 +795,6 @@ export default {
doSubmit();
}
},
autoChangeCommentTextareaSize() {
const app = this.$f7;
const $$ = app.$;
$$('.textarea-auto-size textarea').each((idx, el) => {
el.scrollTop = 0;
el.style.height = '';
el.style.height = el.scrollHeight + 'px';
});
},
isCategoryIdAvailable(categories, categoryId) {
if (!categories || !categories.length) {
return false;
@@ -853,17 +829,15 @@ export default {
} else {
return 40;
}
}
},
filters: {
finalAmount(amount, hideAmount) {
},
getDisplayAmount(amount, hideAmount) {
if (hideAmount) {
return '***';
return this.$locale.getDisplayCurrency('***');
}
return amount;
return this.$locale.getDisplayCurrency(amount);
},
primaryCategoryName(categoryId, allCategories) {
getPrimaryCategoryName(categoryId, allCategories) {
for (let i = 0; i < allCategories.length; i++) {
for (let j = 0; j < allCategories[i].subCategories.length; j++) {
const subCategory = allCategories[i].subCategories[j];
@@ -875,7 +849,7 @@ export default {
return '';
},
secondaryCategoryName(categoryId, allCategories) {
getSecondaryCategoryName(categoryId, allCategories) {
for (let i = 0; i < allCategories.length; i++) {
for (let j = 0; j < allCategories[i].subCategories.length; j++) {
const subCategory = allCategories[i].subCategories[j];
File diff suppressed because it is too large Load Diff
+43 -35
View File
@@ -2,39 +2,33 @@
<f7-page @page:afterin="onPageAfterIn">
<f7-navbar :title="$t('Data Management')" :back-link="$t('Back')"></f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item title="Accounts" after="Count"></f7-list-item>
<f7-list-item title="Transaction Categories" after="Count"></f7-list-item>
<f7-list-item title="Transaction Tags" after="Count"></f7-list-item>
<f7-list-item title="Transactions" after="Count"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-list-item title="Accounts" after="Count"></f7-list-item>
<f7-list-item title="Transaction Categories" after="Count"></f7-list-item>
<f7-list-item title="Transaction Tags" after="Count"></f7-list-item>
<f7-list-item title="Transactions" after="Count"></f7-list-item>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Accounts')" :after="dataStatistics.totalAccountCount"></f7-list-item>
<f7-list-item :title="$t('Transaction Categories')" :after="dataStatistics.totalTransactionCategoryCount"></f7-list-item>
<f7-list-item :title="$t('Transaction Tags')" :after="dataStatistics.totalTransactionTagCount"></f7-list-item>
<f7-list-item :title="$t('Transactions')" :after="dataStatistics.totalTransactionCount"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical" v-else-if="!loading">
<f7-list-item :title="$t('Accounts')" :after="dataStatistics.totalAccountCount"></f7-list-item>
<f7-list-item :title="$t('Transaction Categories')" :after="dataStatistics.totalTransactionCategoryCount"></f7-list-item>
<f7-list-item :title="$t('Transaction Tags')" :after="dataStatistics.totalTransactionTagCount"></f7-list-item>
<f7-list-item :title="$t('Transactions')" :after="dataStatistics.totalTransactionCount"></f7-list-item>
</f7-list>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-button @click="exportedData = null; showExportDataSheet = true" v-if="isDataExportingEnabled">{{ $t('Export Data') }}</f7-list-button>
<f7-list-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical" :class="{ 'disabled': loading }">
<f7-list-button :class="{ 'disabled': !dataStatistics || !dataStatistics.totalTransactionCount || dataStatistics.totalTransactionCount === '0' }"
v-if="isDataExportingEnabled"
@click="exportedData = null; showExportDataSheet = true">{{ $t('Export Data') }}</f7-list-button>
<f7-list-button color="red" @click="clearData(null)">{{ $t('Clear User Data') }}</f7-list-button>
</f7-list>
<f7-sheet style="height:auto" :opened="showExportDataSheet" @sheet:closed="showExportDataSheet = false; exportedData = null;">
<f7-page-content>
<f7-sheet swipe-handler=".swipe-handler" style="height:auto"
:swipe-to-close="!exportingData" :close-on-escape="!exportingData"
:close-by-backdrop-click="!exportingData" :close-by-outside-click="!exportingData"
:opened="showExportDataSheet" @sheet:closed="showExportDataSheet = false; exportedData = null;">
<div class="swipe-handler"></div>
<f7-page-content class="margin-top no-padding-top">
<div class="display-flex padding justify-content-space-between align-items-center">
<div style="font-size: 18px"><b>{{ $t('Are you sure you want to export all data to csv file?') }}</b></div>
</div>
@@ -51,9 +45,9 @@
<password-input-sheet :title="$t('Are you sure you want to clear all data?')"
:hint="$t('You CANNOT undo this action. This will clear your accounts, categories, tags and transactions data. Please input your current password to confirm.')"
:show.sync="showInputPasswordSheetForClearData"
:confirm-disabled="clearingData"
:cancel-disabled="clearingData"
v-model:show="showInputPasswordSheetForClearData"
v-model="currentPasswordForClearData"
@password:confirm="clearData">
</password-input-sheet>
@@ -62,6 +56,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
loading: true,
@@ -76,9 +73,6 @@ export default {
};
},
computed: {
currentTimezoneOffsetMinutes() {
return this.$utilities.getTimezoneOffsetMinutes();
},
isDataExportingEnabled() {
return this.$settings.isDataExportingEnabled();
},
@@ -113,7 +107,7 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
exportData() {
const self = this;
@@ -155,6 +149,20 @@ export default {
self.showInputPasswordSheetForClearData = false;
self.$toast('All user data has been cleared');
self.loading = true;
self.$store.dispatch('getUserDataStatistics').then(dataStatistics => {
self.dataStatistics = dataStatistics;
self.loading = false;
}).catch(error => {
if (error.processed) {
self.loading = false;
} else {
self.loadingError = error;
self.$toast(error.message || error);
}
});
}).catch(error => {
self.clearingData = false;
self.$hideLoading();
+83 -47
View File
@@ -4,46 +4,49 @@
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t('Device & Sessions')"></f7-nav-title>
<f7-nav-right>
<f7-link :class="{ 'disabled': tokens.length < 2 }" :text="$t('Logout All')" @click="revokeAll"></f7-link>
<f7-link :class="{ 'disabled': sessions.length < 2 }" :text="$t('Logout All')" @click="revokeAll"></f7-link>
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list media-list>
<f7-list-item class="list-item-media-valign-middle"
title="Current"
text="Device Name (Browser xx.x.xxxx.xx)">
<f7-icon slot="media" f7="device_phone_portrait"></f7-icon>
<small slot="after">MM/DD/YYYY HH:mm:ss</small>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list media-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-list-item class="list-item-media-valign-middle"
title="Current"
text="Device Name (Browser xx.x.xxxx.xx)">
<template #media>
<f7-icon f7="device_phone_portrait"></f7-icon>
</template>
<template #after>
<small>MM/DD/YYYY HH:mm:ss</small>
</template>
</f7-list-item>
</f7-list>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list media-list>
<f7-list-item class="list-item-media-valign-middle" swipeout
v-for="token in tokens"
:key="token.tokenId"
:id="token | tokenDomId"
:title="token | tokenTitle | localized"
:text="token | tokenDevice | localized">
<f7-icon slot="media" :f7="token | tokenIcon"></f7-icon>
<small slot="after">{{ token.createdAt | moment($t('format.datetime.long')) }}</small>
<f7-swipeout-actions right v-if="!token.isCurrent">
<f7-swipeout-button color="red" :text="$t('Log Out')" @click="revoke(token)"></f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list media-list strong inset dividers class="margin-top" v-else-if="!loading">
<f7-list-item class="list-item-media-valign-middle" swipeout
:id="session.domId"
:title="session.deviceType"
:text="session.deviceInfo"
:key="session.tokenId"
v-for="session in sessions">
<template #media>
<f7-icon :f7="session.icon"></f7-icon>
</template>
<template #after>
<small>{{ session.createdAt }}</small>
</template>
<f7-swipeout-actions right v-if="!session.isCurrent">
<f7-swipeout-button color="red" :text="$t('Log Out')" @click="revoke(session)"></f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-page>
</template>
<script>
export default {
props: [
'f7router'
],
data() {
return {
tokens: [],
@@ -51,6 +54,31 @@ export default {
loadingError: null
};
},
computed: {
sessions() {
if (!this.tokens) {
return this.tokens;
}
const sessions = [];
for (let i = 0; i < this.tokens.length; i++) {
const token = this.tokens[i];
sessions.push({
tokenId: token.tokenId,
domId: this.getTokenDomId(token.tokenId),
isCurrent: token.isCurrent,
deviceType: this.$t(token.isCurrent ? 'Current' : 'Other Device'),
deviceInfo: this.$utilities.parseDeviceInfo(token.userAgent),
icon: this.getTokenIcon(token),
createdAt: this.$utilities.formatUnixTime(token.createdAt, this.$t('format.datetime.long'))
});
}
return sessions;
}
},
created() {
const self = this;
@@ -70,7 +98,7 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
reload(done) {
const self = this;
@@ -91,22 +119,20 @@ export default {
}
});
},
revoke(token) {
revoke(session) {
const self = this;
const app = self.$f7;
const $$ = app.$;
self.$confirm('Are you sure you want to logout from this session?', () => {
self.$showLoading();
self.$store.dispatch('revokeToken', {
tokenId: token.tokenId
tokenId: session.tokenId
}).then(() => {
self.$hideLoading();
app.swipeout.delete($$(`#${self.$options.filters.tokenDomId(token)}`), () => {
self.$ui.onSwipeoutDeleted(self.getTokenDomId(session.tokenId), () => {
for (let i = 0; i < self.tokens.length; i++) {
if (self.tokens[i].tokenId === token.tokenId) {
if (self.tokens[i].tokenId === session.tokenId) {
self.tokens.splice(i, 1);
}
}
@@ -148,18 +174,28 @@ export default {
}
});
});
}
},
filters: {
tokenTitle(token) {
if (token.isCurrent) {
return 'Current';
},
getTokenIcon(token) {
const ua = this.$utilities.parseUserAgent(token.userAgent);
if (!ua || !ua.device) {
return this.$constants.icons.deviceIcons.desktop.f7Icon;
}
return 'Other Device';
if (ua.device.type === 'mobile') {
return this.$constants.icons.deviceIcons.mobile.f7Icon;
} else if (ua.device.type === 'wearable') {
return this.$constants.icons.deviceIcons.wearable.f7Icon;
} else if (ua.device.type === 'tablet') {
return this.$constants.icons.deviceIcons.tablet.f7Icon;
} else if (ua.device.type === 'smarttv') {
return this.$constants.icons.deviceIcons.tv.f7Icon;
} else {
return this.$constants.icons.deviceIcons.desktop.f7Icon;
}
},
tokenDomId(token) {
return 'token_' + token.tokenId.replace(/:/g, '_');
getTokenDomId(tokenId) {
return 'token_' + tokenId.replace(/:/g, '_');
}
}
};
+28 -38
View File
@@ -2,62 +2,44 @@
<f7-page @page:afterin="onPageAfterIn">
<f7-navbar :title="$t('Two-Factor Authentication')" :back-link="$t('Back')"></f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item title="Status" after="Unknown"></f7-list-item>
<f7-list-button class="disabled">Operate</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top skeleton-text" v-if="loading">
<f7-list-item title="Status" after="Unknown"></f7-list-item>
<f7-list-button class="disabled">Operate</f7-list-button>
</f7-list>
<f7-card v-else-if="!loading && status === true">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Status')" :after="$t('Enabled')"></f7-list-item>
<f7-list-button :class="{ 'disabled': regenerating }" @click="regenerateBackupCode(null)">{{ $t('Regenerate Backup Codes') }}</f7-list-button>
<f7-list-button :class="{ 'disabled': disabling }" @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-top" v-else-if="!loading">
<f7-list-item :title="$t('Status')" :after="$t(status ? 'Enabled' : 'Disabled')"></f7-list-item>
<f7-list-button :class="{ 'disabled': regenerating }" v-if="status === true" @click="regenerateBackupCode(null)">{{ $t('Regenerate Backup Codes') }}</f7-list-button>
<f7-list-button :class="{ 'disabled': disabling }" v-if="status === true" @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
<f7-list-button :class="{ 'disabled': enabling }" v-if="status === false" @click="enable">{{ $t('Enable') }}</f7-list-button>
</f7-list>
<f7-card v-else-if="!loading && status === false">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Status')" :after="$t('Disabled')"></f7-list-item>
<f7-list-button :class="{ 'disabled': enabling }" @click="enable">{{ $t('Enable') }}</f7-list-button>
</f7-list>
</f7-card-content>
</f7-card>
<passcode-input-sheet :title="$t('Passcode')"
<passcode-input-sheet :title="$t('Enable Two-Factor Authentication')"
:hint="$t('Please use two factor authentication app scan the below qrcode and input current passcode')"
:show.sync="showInputPasscodeSheetForEnable"
:confirm-disabled="enableConfirming"
:cancel-disabled="enableConfirming"
v-model:show="showInputPasscodeSheetForEnable"
v-model="currentPasscodeForEnable"
@passcode:confirm="enableConfirm">
<div class="row">
<div class="col-100 text-align-center">
<img alt="qrcode" width="240px" height="240px" :src="new2FAQRCode" />
</div>
<div class="col-100 text-align-center">
<img alt="qrcode" class="img-qrcode" :src="new2FAQRCode" />
</div>
</passcode-input-sheet>
<password-input-sheet :title="$t('Current Password')"
<password-input-sheet :title="$t('Disable Two-Factor Authentication')"
:hint="$t('Please enter your current password when disable two factor authentication')"
:show.sync="showInputPasswordSheetForDisable"
:confirm-disabled="disabling"
:cancel-disabled="disabling"
v-model:show="showInputPasswordSheetForDisable"
v-model="currentPasswordForDisable"
@password:confirm="disable">
</password-input-sheet>
<password-input-sheet :title="$t('Current Password')"
<password-input-sheet :title="$t('Regenerate Backup Codes')"
:hint="$t('Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.')"
:show.sync="showInputPasswordSheetForRegenerate"
:confirm-disabled="regenerating"
:cancel-disabled="regenerating"
v-model:show="showInputPasswordSheetForRegenerate"
v-model="currentPasswordForRegenerate"
@password:confirm="regenerateBackupCode">
</password-input-sheet>
@@ -68,7 +50,7 @@
:information="currentBackupCode"
:row-count="10"
:enable-copy="true"
:show.sync="showBackupCodeSheet"
v-model:show="showBackupCodeSheet"
@info:copied="onBackupCodeCopied">
</information-sheet>
</f7-page>
@@ -76,6 +58,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
status: null,
@@ -116,7 +101,7 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
enable() {
const self = this;
@@ -245,6 +230,11 @@ export default {
</script>
<style>
.img-qrcode {
width: 240px;
height: 240px
}
.backup-code-sheet .information-content {
font-family: monospace;
}
+123 -132
View File
@@ -8,152 +8,135 @@
</f7-nav-right>
</f7-navbar>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-input label="Password" placeholder="Your password"></f7-list-input>
<f7-list-input label="Confirmation Password" placeholder="Re-enter the password"></f7-list-input>
<f7-list-input label="E-mail" placeholder="Your email address"></f7-list-input>
<f7-list-input label="Nickname" placeholder="Your nickname"></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-list-input label="Password" placeholder="Your password"></f7-list-input>
<f7-list-input label="Confirmation Password" placeholder="Re-enter the password"></f7-list-input>
<f7-list-input label="E-mail" placeholder="Your email address"></f7-list-input>
<f7-list-input label="Nickname" placeholder="Your nickname"></f7-list-input>
</f7-list>
<f7-card class="skeleton-text" v-if="loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Default Currency" title="Currency"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="First Day of Week" title="Week Day"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Editable Transaction Scope" title="All"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list strong inset dividers class="margin-vertical skeleton-text" v-if="loading">
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Default Currency" title="Currency" link="#"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Default Account" title="Not Specified"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="First Day of Week" title="Week Day" link="#"></f7-list-item>
<f7-list-item class="list-item-with-header-and-title list-item-no-item-after" header="Editable Transaction Scope" title="All" link="#"></f7-list-item>
</f7-list>
<f7-card v-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Password')"
:placeholder="$t('Your password')"
:value="newProfile.password"
@input="newProfile.password = $event.target.value"
></f7-list-input>
<f7-list form strong inset dividers class="margin-vertical" v-if="!loading">
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Password')"
:placeholder="$t('Your password')"
v-model:value="newProfile.password"
></f7-list-input>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Confirmation Password')"
:placeholder="$t('Re-enter the password')"
:value="newProfile.confirmPassword"
@input="newProfile.confirmPassword = $event.target.value"
></f7-list-input>
<f7-list-input
type="password"
autocomplete="new-password"
clear-button
:label="$t('Confirmation Password')"
:placeholder="$t('Re-enter the password')"
v-model:value="newProfile.confirmPassword"
></f7-list-input>
<f7-list-input
type="email"
autocomplete="email"
clear-button
:label="$t('E-mail')"
:placeholder="$t('Your email address')"
:value="newProfile.email"
@input="newProfile.email = $event.target.value"
></f7-list-input>
<f7-list-input
type="email"
autocomplete="email"
clear-button
:label="$t('E-mail')"
:placeholder="$t('Your email address')"
v-model:value="newProfile.email"
></f7-list-input>
<f7-list-input
type="text"
autocomplete="nickname"
clear-button
:label="$t('Nickname')"
:placeholder="$t('Your nickname')"
:value="newProfile.nickname"
@input="newProfile.nickname = $event.target.value"
></f7-list-input>
<f7-list-input
type="text"
autocomplete="nickname"
clear-button
:label="$t('Nickname')"
:placeholder="$t('Your nickname')"
v-model:value="newProfile.nickname"
></f7-list-input>
<f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item class="ebk-list-item-error-info" v-if="inputIsInvalid" :footer="$t(inputInvalidProblemMessage)"></f7-list-item>
</f7-list>
<f7-card v-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list form>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Default Currency')"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Default Currency'), searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<f7-block slot="title" class="no-padding no-margin">
<span>{{ $t(`currency.${newProfile.defaultCurrency}`) }}&nbsp;</span>
<small class="smaller">{{ newProfile.defaultCurrency }}</small>
</f7-block>
<select autocomplete="transaction-currency" v-model="newProfile.defaultCurrency">
<option v-for="currency in allCurrencies"
:key="currency.code"
:value="currency.code">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list form strong inset dividers class="margin-vertical" v-if="!loading">
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Default Currency')"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Currency Name'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Default Currency'), popupCloseLinkText: $t('Done') }"
>
<template #title>
<f7-block class="no-padding no-margin">
<span>{{ $t(`currency.${newProfile.defaultCurrency}`) }}&nbsp;</span>
<small class="smaller">{{ newProfile.defaultCurrency }}</small>
</f7-block>
</template>
<select autocomplete="transaction-currency" v-model="newProfile.defaultCurrency">
<option :value="currency.code"
:key="currency.code"
v-for="currency in allCurrencies">{{ currency.displayName }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#"
:class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t('Default Account')"
:title="newProfile.defaultAccountId | optionName(allAccounts, 'id', 'name', $t('Not Specified'))"
@click="showAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="allCategorizedAccounts"
:show.sync="showAccountSheet"
v-model="newProfile.defaultAccountId">
</two-column-list-item-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title"
link="#" no-chevron
:class="{ 'disabled': !allVisibleAccounts.length }"
:header="$t('Default Account')"
:title="$utilities.getNameByKeyValue(allAccounts, newProfile.defaultAccountId, 'id', 'name', $t('Not Specified'))"
@click="showAccountSheet = true"
>
<two-column-list-item-selection-sheet primary-key-field="id" primary-value-field="category"
primary-title-field="name"
primary-icon-field="icon" primary-icon-type="account"
primary-sub-items-field="accounts"
:primary-title-i18n="true"
secondary-key-field="id" secondary-value-field="id"
secondary-title-field="name"
secondary-icon-field="icon" secondary-icon-type="account" secondary-color-field="color"
:items="allCategorizedAccounts"
v-model:show="showAccountSheet"
v-model="newProfile.defaultAccountId">
</two-column-list-item-selection-sheet>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('First Day of Week')"
:title="newProfile.firstDayOfWeek | optionName(allWeekDays, 'type', 'name') | format('datetime.#{value}.long') | localized"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('First Day of Week'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<select v-model="newProfile.firstDayOfWeek">
<option v-for="weekDay in allWeekDays"
:key="weekDay.type"
:value="weekDay.type">{{ $t(`datetime.${weekDay.name}.long`) }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('First Day of Week')"
:title="getDayOfWeekName(newProfile.firstDayOfWeek)"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('First Day of Week'), popupCloseLinkText: $t('Done') }"
>
<select v-model="newProfile.firstDayOfWeek">
<option :value="weekDay.type"
:key="weekDay.type"
v-for="weekDay in allWeekDays">{{ $t(`datetime.${weekDay.name}.long`) }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Editable Transaction Scope')"
:title="newProfile.transactionEditScope | optionName(allTransactionEditScopeTypes, 'value', 'name') | localized"
smart-select :smart-select-params="{ openIn: 'popup', pageTitle: $t('Editable Transaction Scope'), closeOnSelect: true, popupCloseLinkText: $t('Done'), scrollToSelectedItem: true }"
>
<select v-model="newProfile.transactionEditScope">
<option v-for="option in allTransactionEditScopeTypes"
:key="option.value"
:value="option.value">{{ $t(option.name) }}</option>
</select>
</f7-list-item>
<f7-list-item
class="list-item-with-header-and-title list-item-no-item-after"
:header="$t('Editable Transaction Scope')"
:title="$t($utilities.getNameByKeyValue(allTransactionEditScopeTypes, newProfile.transactionEditScope, 'value', 'name'))"
smart-select :smart-select-params="{ openIn: 'popup', popupPush: true, closeOnSelect: true, scrollToSelectedItem: true, searchbar: true, searchbarPlaceholder: $t('Date Range'), searchbarDisableText: $t('Cancel'), appendSearchbarNotFound: $t('No results'), pageTitle: $t('Editable Transaction Scope'), popupCloseLinkText: $t('Done') }"
>
<select v-model="newProfile.transactionEditScope">
<option :value="option.value"
:key="option.value"
v-for="option in allTransactionEditScopeTypes">{{ $t(option.name) }}</option>
</select>
</f7-list-item>
<f7-list-item class="ebk-list-item-error-info" v-if="extendInputIsInvalid" :footer="$t(extendInputInvalidProblemMessage)"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-list-item class="ebk-list-item-error-info" v-if="extendInputIsInvalid" :footer="$t(extendInputInvalidProblemMessage)"></f7-list-item>
</f7-list>
<password-input-sheet :title="$t('Current Password')"
:hint="$t('Please enter your current password when modifying your password')"
:show.sync="showInputPasswordSheet"
:confirm-disabled="saving"
:cancel-disabled="saving"
v-model:show="showInputPasswordSheet"
v-model="currentPassword"
@password:confirm="save()">
</password-input-sheet>
@@ -162,6 +145,9 @@
<script>
export default {
props: [
'f7router'
],
data() {
return {
newProfile: {
@@ -318,11 +304,11 @@ export default {
},
methods: {
onPageAfterIn() {
this.$routeBackOnError('loadingError');
this.$routeBackOnError(this.f7router, 'loadingError');
},
save() {
const self = this;
const router = self.$f7router;
const router = self.f7router;
self.showInputPasswordSheet = false;
@@ -360,6 +346,11 @@ export default {
self.$toast(error.message || error);
}
});
},
getDayOfWeekName(dayOfWeek) {
const weekName = this.$utilities.getNameByKeyValue(this.$constants.datetime.allWeekDays, dayOfWeek, 'type', 'name');
const i18nWeekNameKey = `datetime.${weekName}.long`;
return this.$t(i18nWeekNameKey);
}
}
};
+41 -29
View File
@@ -82,34 +82,22 @@
"licenseUrl": "https://github.com/stretchr/testify/blob/v1.8.2/LICENSE"
},
{
"name": "vue",
"copyright": "Copyright (c) 2013-present, Yuxi (Evan) You",
"url": "https://github.com/vuejs/vue",
"licenseUrl": "https://github.com/vuejs/vue/blob/v2.7.14/LICENSE"
"name": "vuejs/core",
"copyright": "Copyright (c) 2018-present, Yuxi (Evan) You",
"url": "https://github.com/vuejs/core",
"licenseUrl": "https://github.com/vuejs/core/blob/v3.2.47/LICENSE"
},
{
"name": "vuex",
"copyright": "Copyright (c) 2015-present Evan You",
"url": "https://github.com/vuejs/vuex",
"licenseUrl": "https://github.com/vuejs/vuex/blob/v3.6.2/LICENSE"
"licenseUrl": "https://github.com/vuejs/vuex/blob/v4.1.0/LICENSE"
},
{
"name": "vue-i18n",
"copyright": "Copyright (c) 2016 kazuya kawaguchi",
"url": "https://github.com/kazupon/vue-i18n",
"licenseUrl": "https://github.com/kazupon/vue-i18n/blob/v8.28.2/LICENSE"
},
{
"name": "vue-clipboard2",
"copyright": "Copyright (c) 2017 Inndy <inndy \\dot tw \\at gmail \\dot com>",
"url": "https://github.com/Inndy/vue-clipboard2",
"licenseUrl": "https://github.com/Inndy/vue-clipboard2/blob/v0.3.3/LICENSE"
},
{
"name": "vue-pincode-input",
"copyright": "Copyright (c) 2019 Maxim Noverin",
"url": "https://github.com/Seokky/vue-pincode-input",
"licenseUrl": "https://github.com/Seokky/vue-pincode-input/blob/master/LICENSE"
"url": "https://github.com/intlify/vue-i18n-next",
"licenseUrl": "https://github.com/intlify/vue-i18n-next/blob/v9.2.2/LICENSE"
},
{
"name": "register-service-worker",
@@ -117,23 +105,17 @@
"url": "https://github.com/yyx990803/register-service-worker",
"licenseUrl": "https://github.com/yyx990803/register-service-worker/blob/v1.7.2/LICENSE"
},
{
"name": "core-js",
"copyright": "Copyright (c) 2014-2023 Denis Pushkarev",
"url": "https://github.com/zloirock/core-js",
"licenseUrl": "https://github.com/zloirock/core-js/blob/v3.30.0/LICENSE"
},
{
"name": "Framework7",
"copyright": "Copyright (c) 2014 Vladimir Kharlampidi",
"url": "https://framework7.io/",
"licenseUrl": "https://github.com/framework7io/framework7/blob/v5.7.14/LICENSE"
"licenseUrl": "https://github.com/framework7io/framework7/blob/v8.0.3/LICENSE"
},
{
"name": "Framework7-vue",
"copyright": "Copyright (c) 2014 Vladimir Kharlampidi",
"url": "https://framework7.io/vue/",
"licenseUrl": "https://github.com/framework7io/framework7/blob/v5.7.14/LICENSE"
"licenseUrl": "https://github.com/framework7io/framework7/blob/v8.0.3/LICENSE"
},
{
"name": "Framework7-icons",
@@ -141,11 +123,35 @@
"url": "https://framework7.io/icons/",
"licenseUrl": "https://github.com/framework7io/framework7-icons/blob/v5.0.5/LICENSE"
},
{
"name": "Dom7",
"copyright": "Copyright (c) 2017 Vladimir Kharlampidi",
"url": "https://framework7.io/docs/dom7.html",
"licenseUrl": "https://github.com/nolimits4web/dom7/blob/v4.0.6/LICENSE"
},
{
"name": "Swiper",
"copyright": "Copyright (c) 2019 Vladimir Kharlampidi",
"url": "https://swiperjs.com",
"licenseUrl": "https://github.com/nolimits4web/swiper/blob/v9.2.3/LICENSE"
},
{
"name": "Skeleton Elements",
"copyright": "Copyright (c) 2020 Vladimir Kharlampidi",
"url": "https://github.com/nolimits4web/skeleton-elements",
"licenseUrl": "https://github.com/nolimits4web/skeleton-elements/blob/v4.0.1/LICENSE"
},
{
"name": "@vuepic/vue-datepicker",
"copyright": "Copyright (c) 2021-present Vuepic",
"url": "https://vue3datepicker.com/",
"licenseUrl": "https://github.com/Vuepic/vue-datepicker/blob/v4.4.0/LICENSE"
},
{
"name": "axios",
"copyright": "Copyright (c) 2014-present Matt Zabriskie",
"url": "https://github.com/axios/axios",
"licenseUrl": "https://github.com/axios/axios/blob/v1.3.5/LICENSE"
"url": "https://axios-http.com",
"licenseUrl": "https://github.com/axios/axios/blob/v1.3.6/LICENSE"
},
{
"name": "Moment.js",
@@ -171,6 +177,12 @@
"url": "https://github.com/paroga/cbor-js",
"licenseUrl": "https://github.com/paroga/cbor-js/blob/v0.1.0/LICENSE"
},
{
"name": "clipboard.js",
"copyright": "Copyright (c) Zeno Rocha",
"url": "https://clipboardjs.com",
"licenseUrl": "https://github.com/zenorocha/clipboard.js/blob/v2.0.11/LICENSE"
},
{
"name": "js-cookie",
"copyright": "Copyright (c) 2018 Copyright 2018 Klaus Hartl, Fagner Brack, GitHub Contributors",
+148
View File
@@ -0,0 +1,148 @@
import fs from 'fs';
import { resolve } from 'path';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
import git from 'git-rev-sync';
import packageFile from './package.json';
import thirdPartyLicenseFile from './third-patry-dependencies.json';
const SRC_DIR = resolve(__dirname, './src');
const PUBLIC_DIR = resolve(__dirname, './public');
const BUILD_DIR = resolve(__dirname, './dist',);
export default defineConfig(async () => {
const licenseContent = fs.readFileSync('./LICENSE', 'UTF-8');
let buildUnixTime = '';
for (let i = 0; i < process.argv.length; i++) {
if (process.argv[i].indexOf('--') !== 0) {
continue;
}
const pairs = process.argv[i].split('=');
if (pairs[0] === '--buildUnixTime') {
buildUnixTime = pairs[1];
}
}
return {
root: SRC_DIR,
publicDir: PUBLIC_DIR,
base: './',
define: {
__EZBOOKKEEPING_VERSION__: JSON.stringify(packageFile.version),
__EZBOOKKEEPING_BUILD_UNIX_TIME__: JSON.stringify(buildUnixTime),
__EZBOOKKEEPING_BUILD_COMMIT_HASH__: JSON.stringify(git.short()),
__EZBOOKKEEPING_LICENSE__: JSON.stringify(licenseContent),
__EZBOOKKEEPING_THIRD_PARTY_LICENSES__: JSON.stringify(thirdPartyLicenseFile)
},
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.includes('swiper-')
}
}
}),
VitePWA({
filename: 'sw.js',
manifestFilename: 'manifest.json',
strategies: 'generateSW',
injectRegister: 'null',
manifest: {
name: 'ezBookkeeping',
short_name: 'ezBookkeeping',
description: 'A lightweight personal bookkeeping app hosted by yourself.',
theme_color: '#C67E48',
background_color: '#F6F7F8',
start_url: './',
scope: './',
display: 'standalone',
related_applications: [],
prefer_related_applications: false,
icons: [
{
src: 'img/ezbookkeeping-192.png',
sizes: '192x192',
type: 'image/png'
},
{
src: 'img/ezbookkeeping-512.png',
sizes: '512x512',
type: 'image/png'
}
]
},
workbox: {
globDirectory: 'dist/',
globPatterns: ['**/*.{js,css,html,ico,png,jpg,jpeg,gif,tiff,bmp,ttf,woff,woff2,svg,eot}'],
globIgnores: [
'index.html',
'mobile.html',
'desktop.html',
'robots.txt',
'css/desktop-*.js',
'js/desktop-*.js'
],
navigateFallback: '',
skipWaiting: true,
clientsClaim: true
}
})
],
build: {
outDir: BUILD_DIR,
sourcemap: false,
assetsInlineLimit: 0,
emptyOutDir: true,
rollupOptions: {
input: {
index: resolve(SRC_DIR, 'index.html'),
desktop: resolve(SRC_DIR, 'desktop.html'),
mobile: resolve(SRC_DIR, 'mobile.html')
},
output: {
assetFileNames: (assetInfo) => {
const fileExt = assetInfo.name.split('.').at(1);
let assetType = fileExt;
if (/png|jpe?g|gif|tiff|bmp|ico/i.test(fileExt)) {
assetType = 'img';
} else if (/ttf|woff|woff2|svg|eot/i.test(fileExt)) {
assetType = 'fonts';
}
return `${assetType}/[name]-[hash][extname]`;
},
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
manualChunks: function (id) {
if (/[\\/]node_modules[\\/]/i.test(id)) {
return 'vendor';
}
}
},
treeshake: false
},
},
resolve: {
alias: {
'@': SRC_DIR,
},
},
server: {
host: '0.0.0.0',
port: 8081,
strictPort: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true
}
}
},
};
})
-161
View File
@@ -1,161 +0,0 @@
const fs = require('fs');
const GitRevisionPlugin = require('git-revision-webpack-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const pkgFile = require('./package.json');
const thirdPartyLicenseFile = require('./third-patry-dependencies.json');
const licenseFile = fs.readFileSync('./LICENSE', 'UTF-8');
module.exports = {
pages: {
index: {
entry: 'src/index-main.js',
template: 'src/public/index.html',
filename: 'index.html',
chunks: ['vendors-common-bundle', 'vendors-index-bundle', 'common-bundle', 'index']
},
desktop: {
entry: 'src/desktop-main.js',
template: 'src/public/desktop.html',
filename: 'desktop.html',
chunks: ['vendors-common-bundle', 'vendors-desktop-bundle', 'common-bundle', 'desktop']
},
mobile: {
entry: 'src/mobile-main.js',
template: 'src/public/mobile.html',
filename: 'mobile.html',
chunks: ['vendors-common-bundle', 'vendors-mobile-bundle', 'common-bundle', 'mobile']
}
},
publicPath: '',
productionSourceMap: false,
configureWebpack: {
plugins: [
new MomentLocalesPlugin()
]
},
chainWebpack: config => {
config.optimization.splitChunks({
cacheGroups: {
'vendors-common-bundle': {
name: 'vendors-common-bundle',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 10,
minChunks: 2
},
'vendors-bundle': {
name: (module, chunks) => {
const allChunksNames = chunks.map((item) => item.name).join('-');
return `vendors-${allChunksNames}-bundle`;
},
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 5,
minChunks: 1
},
'common-bundle': {
name: 'common-bundle',
chunks: 'initial',
priority: 1,
minChunks: 2
}
}
});
config.plugin('define').tap(definitions => {
let buildUnixTime = '';
for (let i = 0; i < process.argv.length; i++) {
if (process.argv[i].indexOf('--') !== 0) {
continue;
}
const pairs = process.argv[i].split('=');
if (pairs[0] === '--buildUnixTime') {
buildUnixTime = pairs[1];
}
}
const gitRevisionPlugin = new GitRevisionPlugin();
definitions[0]['process.env']['VERSION'] = JSON.stringify(pkgFile.version);
definitions[0]['process.env']['COMMIT_HASH'] = JSON.stringify(gitRevisionPlugin.commithash());
definitions[0]['process.env']['BUILD_UNIXTIME'] = buildUnixTime;
definitions[0]['process.env']['LICENSE'] = JSON.stringify(licenseFile.trim());
definitions[0]['process.env']['THIRD_PARTY_LICENSES'] = JSON.stringify(thirdPartyLicenseFile);
return definitions;
});
},
pwa: {
name: 'ezBookkeeping',
themeColor: '#C67E48',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'default',
workboxPluginMode: 'GenerateSW',
manifestPath: 'manifest.json',
manifestOptions: {
short_name: 'ezBookkeeping',
icons: [
{
src: "img/ezbookkeeping-192.png",
sizes: "192x192",
type: "image/png"
},
{
src: "img/ezbookkeeping-512.png",
sizes: "512x512",
type: "image/png"
}
],
start_url: './',
scope: "./",
display: 'standalone',
background_color: "#F6F7F8",
related_applications: [],
prefer_related_applications: false
},
iconPaths: {
favicon32: 'favicon.png',
favicon16: 'favicon.ico',
appleTouchIcon: 'touchicon.png',
},
workboxOptions: {
importWorkboxFrom: 'local',
importsDirectory: 'sw',
precacheManifestFilename: 'precache-manifest.[manifestHash].js',
directoryIndex: '/',
globDirectory: '.',
templatedURLs: {
'/': [
'src/public/mobile.html'
]
},
exclude: [
/^index\.html$/,
/^mobile\.html$/,
/^desktop\.html$/,
/^robots\.txt$/,
/^(css|js)\/desktop\.[a-z0-9]+\.js$/,
/^(css|js)\/vendors-desktop-bundle\.[a-z0-9]+\.js$/,
],
skipWaiting: true,
clientsClaim: true,
swDest: 'sw.js'
}
},
devServer: {
host: '0.0.0.0',
port: 8081,
disableHostCheck: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true
}
}
}
}