add category edit ui

This commit is contained in:
MaysWind
2020-11-30 01:10:36 +08:00
parent 255c9bb65f
commit 4c53cd63cb
13 changed files with 962 additions and 7 deletions
+1
View File
@@ -7,6 +7,7 @@
<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('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>
@@ -0,0 +1,15 @@
<template>
<f7-page>
<f7-navbar :title="$t('Transaction Categories')" :back-link="$t('Back')"></f7-navbar>
<f7-card>
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<f7-list-item :title="$t('Expense')" link="/category/list?type=1"></f7-list-item>
<f7-list-item :title="$t('Income')" link="/category/list?type=2"></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-page>
</template>
@@ -0,0 +1,373 @@
<template>
<f7-page>
<f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t(title)"></f7-nav-title>
<f7-nav-right>
<f7-link :class="{ 'disabled': inputIsEmpty || submitting }" :text="$t(saveButtonTitle)" @click="save"></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>
<f7-list-input label="Category Name" placeholder="Your category name"></f7-list-input>
<f7-list-item header="Category Icon" after="Icon"></f7-list-item>
<f7-list-item header="Category Color" after="Color"></f7-list-item>
<f7-list-input type="textarea" label="Description" placeholder="Your category description (optional)"></f7-list-input>
</f7-list>
</f7-card-content>
</f7-card>
<f7-card v-else-if="!loading">
<f7-card-content class="no-safe-areas" :padding="false">
<f7-list>
<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-item :header="$t('Category Icon')" key="singleTypeCategoryIconSelection" link="#"
@click="showIconSelectionSheet(category)">
<f7-icon slot="after" :icon="category.icon | categoryIcon" :style="{ color: '#' + category.color }"></f7-icon>
</f7-list-item>
<f7-list-item :header="$t('Category Color')" key="singleTypeCategoryColorSelection" link="#"
@click="showColorSelectionSheet(category)">
<f7-icon slot="after" f7="app_fill" :style="{ color: '#' + category.color }"></f7-icon>
</f7-list-item>
<f7-list-input
type="textarea"
:label="$t('Description')"
:placeholder="$t('Your category description (optional)')"
:value="category.comment"
@input="category.comment = $event.target.value"
></f7-list-input>
<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>
</f7-card-content>
</f7-card>
<f7-sheet :opened="showIconSelection" @sheet:closed="hideIconSelectionSheet">
<f7-toolbar>
<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" v-for="(row, idx) in allCategoryIconRows" :key="idx">
<f7-col v-for="categoryIcon in row" :key="categoryIcon.id">
<f7-icon :icon="categoryIcon.icon" :style="{ color: '#' + (categoryChoosingIcon ? categoryChoosingIcon.color : '000000') }" @click.native="setSelectedIcon(categoryIcon)">
<f7-badge color="default" class="right-bottom-icon" v-if="categoryChoosingIcon && categoryChoosingIcon.icon === categoryIcon.id">
<f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge>
</f7-icon>
</f7-col>
<f7-col v-for="idx in (iconCountPerRow - row.length)" :key="idx"></f7-col>
</f7-row>
</f7-block>
</f7-page-content>
</f7-sheet>
<f7-sheet :opened="showColorSelection" @sheet:closed="hideColorSelectionSheet">
<f7-toolbar>
<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" v-for="(row, idx) in allCategoryColorRows" :key="idx">
<f7-col v-for="categoryColor in row" :key="categoryColor.color">
<f7-icon f7="app_fill" :style="{ color: '#' + categoryColor.color }" @click.native="setSelectedColor(categoryColor.color)">
<f7-badge color="default" class="right-bottom-icon" v-if="categoryChoosingColor && categoryChoosingColor.color === categoryColor.color">
<f7-icon f7="checkmark_alt"></f7-icon>
</f7-badge>
</f7-icon>
</f7-col>
<f7-col v-for="idx in (iconCountPerRow - row.length)" :key="idx"></f7-col>
</f7-row>
</f7-block>
</f7-page-content>
</f7-sheet>
</f7-page>
</template>
<script>
export default {
data() {
const self = this;
const query = self.$f7route.query;
return {
editCategoryId: null,
loading: false,
category: {
type: query.type,
name: '',
parentId: query.parentId,
icon: self.$constants.icons.defaultCategoryIconId,
color: self.$constants.colors.defaultCategoryColor,
comment: '',
visible: true
},
iconCountPerRow: 7,
categoryChoosingIcon: null,
categoryChoosingColor: null,
submitting: false,
showIconSelection: false,
showColorSelection: false
};
},
computed: {
title() {
if (!this.editCategoryId) {
if (this.category.parentId === '0') {
return 'Add Primary Category';
} else {
return 'Add Secondary Category';
}
} else {
return 'Edit Category';
}
},
saveButtonTitle() {
if (!this.editCategoryId) {
return 'Add';
} else {
return 'Save';
}
},
allCategoryIconRows() {
const allCategoryIcons = this.$constants.icons.allCategoryIcons;
const ret = [];
let rowCount = 0;
for (let categoryIconId in allCategoryIcons) {
if (!Object.prototype.hasOwnProperty.call(allCategoryIcons, categoryIconId)) {
continue;
}
const categoryIcon = allCategoryIcons[categoryIconId];
if (!ret[rowCount]) {
ret[rowCount] = [];
} else if (ret[rowCount] && ret[rowCount].length >= this.iconCountPerRow) {
rowCount++;
ret[rowCount] = [];
}
ret[rowCount].push({
id: categoryIconId,
icon: categoryIcon.icon
});
}
return ret;
},
allCategoryColorRows() {
const allCategoryColors = this.$constants.colors.allCategoryColors;
const ret = [];
let rowCount = -1;
for (let i = 0; i < allCategoryColors.length; i++) {
if (i % this.iconCountPerRow === 0) {
ret[++rowCount] = [];
}
ret[rowCount].push({
color: allCategoryColors[i]
});
}
return ret;
},
inputIsEmpty() {
return !!this.inputEmptyProblemMessage;
},
inputEmptyProblemMessage() {
if (!this.category.name) {
return 'Category name cannot be empty';
} else {
return null;
}
}
},
created() {
const self = this;
const query = self.$f7route.query;
const router = self.$f7router;
if (!query.id && !query.parentId) {
self.$toast('Parameter Invalid');
router.back();
return;
}
if (query.id) {
self.loading = true;
self.editCategoryId = query.id;
self.$services.getTransactionCategory({
id: self.editCategoryId
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to get category');
router.back();
return;
}
const category = data.result;
self.category.id = category.id;
self.category.type = category.type.toString();
self.category.parentId = category.type.parentId;
self.category.name = category.name;
self.category.icon = category.icon;
self.category.color = category.color;
self.category.comment = category.comment;
self.category.visible = !category.hidden;
self.loading = false;
}).catch(error => {
self.$logger.error('failed to load category info', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
router.back();
} else if (!error.processed) {
self.$toast('Unable to get category');
router.back();
}
});
} else if (query.parentId) {
if (query.type !== '1' && query.type !== '2' && query.type !== '3') {
self.$toast('Parameter Invalid');
router.back();
return;
}
self.loading = false;
}
},
methods: {
showIconSelectionSheet(category) {
this.categoryChoosingIcon = category;
this.showIconSelection = true;
},
setSelectedIcon(categoryIcon) {
if (!this.categoryChoosingIcon) {
return;
}
this.categoryChoosingIcon.icon = categoryIcon.id;
this.categoryChoosingIcon = null;
this.showIconSelection = false;
},
hideIconSelectionSheet() {
this.categoryChoosingIcon = null;
this.showIconSelection = false;
},
showColorSelectionSheet(category) {
this.categoryChoosingColor = category;
this.showColorSelection = true;
},
setSelectedColor(color) {
if (!this.categoryChoosingColor) {
return;
}
this.categoryChoosingColor.color = color;
this.categoryChoosingColor = null;
this.showColorSelection = false;
},
hideColorSelectionSheet() {
this.categoryChoosingColor = null;
this.showColorSelection = false;
},
save() {
const self = this;
const router = self.$f7router;
const problemMessage = self.inputEmptyProblemMessage;
if (problemMessage) {
self.$alert(problemMessage);
return;
}
self.submitting = true;
self.$showLoading(() => self.submitting);
const submitCategory = {
type: parseInt(self.category.type),
name: self.category.name,
parentId: self.category.parentId,
icon: self.category.icon,
color: self.category.color,
comment: self.category.comment
};
let promise = null;
if (!self.editCategoryId) {
promise = self.$services.addTransactionCategory(submitCategory);
} else {
submitCategory.id = self.category.id;
submitCategory.hidden = !self.category.visible;
promise = self.$services.modifyTransactionCategory(submitCategory);
}
promise.then(response => {
self.submitting = false;
self.$hideLoading();
const data = response.data;
if (!data || !data.success || !data.result) {
if (!self.editCategoryId) {
self.$toast('Unable to add category');
} else {
self.$toast('Unable to save category');
}
return;
}
if (!self.editCategoryId) {
self.$toast('You have added a new category');
} else {
self.$toast('You have saved this category');
}
router.back();
}).catch(error => {
self.$logger.error('failed to save category', error);
self.submitting = false;
self.$hideLoading();
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
if (!self.editCategoryId) {
self.$toast('Unable to add category');
} else {
self.$toast('Unable to save category');
}
}
});
}
}
}
</script>
@@ -0,0 +1,424 @@
<template>
<f7-page :ptr="!sortable" @ptr:refresh="reload" @page:afterin="onPageAfterIn">
<f7-navbar>
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
<f7-nav-title :title="$t(title)"></f7-nav-title>
<f7-nav-right class="navbar-compact-icons">
<f7-link icon-f7="ellipsis" v-if="!sortable" @click="showMoreActionSheet = true"></f7-link>
<f7-link :href="'/category/add?type=' + categoryType + '&parentId=' + categoryId" icon-f7="plus" v-if="!sortable"></f7-link>
<f7-link :text="$t('Done')" :class="{ 'disabled': displayOrderSaving }" @click="saveSortResult" v-else-if="sortable"></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>
<f7-list-item title="Category Name"></f7-list-item>
<f7-list-item title="Category Name 2"></f7-list-item>
<f7-list-item title="Category Name 3"></f7-list-item>
<f7-list-item title="Category Name 4"></f7-list-item>
<f7-list-item title="Category Name 5"></f7-list-item>
<f7-list-item title="Category Name 6"></f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<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>
<f7-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"
: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="{ 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>
</f7-icon>
<f7-swipeout-actions left v-if="sortable">
<f7-swipeout-button :color="category.hidden ? 'blue' : 'gray'" class="padding-left padding-right"
overswipe @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" :text="$t('Edit')" @click="edit(category)"></f7-swipeout-button>
<f7-swipeout-button color="red" class="padding-left padding-right" @click="remove(category)">
<f7-icon f7="trash"></f7-icon>
</f7-swipeout-button>
</f7-swipeout-actions>
</f7-list-item>
</f7-list>
</f7-card-content>
</f7-card>
<f7-actions close-by-outside-click close-on-escape :opened="showMoreActionSheet" @actions:closed="showMoreActionSheet = false">
<f7-actions-group>
<f7-actions-button @click="setSortable()">{{ $t('Sort') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ $t('Cancel') }}</f7-actions-button>
</f7-actions-group>
</f7-actions>
<f7-actions close-by-outside-click close-on-escape :opened="showDeleteActionSheet" @actions:closed="showDeleteActionSheet = false">
<f7-actions-group>
<f7-actions-label>{{ $t('Are you sure you want to delete this category?') }}</f7-actions-label>
<f7-actions-button color="red" @click="remove(categoryToDelete)">{{ $t('Delete') }}</f7-actions-button>
</f7-actions-group>
<f7-actions-group>
<f7-actions-button bold close>{{ $t('Cancel') }}</f7-actions-button>
</f7-actions-group>
</f7-actions>
</f7-page>
</template>
<script>
export default {
data() {
return {
categories: {},
hasSubCategories: false,
categoryType: '',
categoryId: '',
loading: true,
showHidden: false,
sortable: false,
categoryToDelete: null,
showMoreActionSheet: false,
showDeleteActionSheet: false,
displayOrderModified: false,
displayOrderSaving: false
};
},
computed: {
title() {
let title = '';
switch (this.categoryType) {
case '1':
title = 'Expense';
break;
case '2':
title = 'Income';
break;
case '3':
title = 'Transfer';
break;
default:
title = 'Transaction';
break;
}
switch (this.hasSubCategories) {
case true:
title += ' Primary';
break;
case false:
title += ' Secondary';
break;
}
return title + ' Categories';
},
noAvailableCategory() {
for (let i = 0; i < this.categories.length; i++) {
if (this.showHidden || !this.categories[i].hidden) {
return false;
}
}
return true;
}
},
created() {
const self = this;
const query = self.$f7route.query;
const router = self.$f7router;
if (query.type !== '1' && query.type !== '2' && query.type !== '3') {
self.$toast('Parameter Invalid');
router.back();
return;
}
self.categoryType = query.type;
if (query.id && query.id !== '0') {
self.categoryId = query.id;
self.hasSubCategories = false;
} else {
self.categoryId = '0';
self.hasSubCategories = true;
}
self.loading = true;
self.$services.getAllTransactionCategories({
type: self.categoryType,
parentId: self.categoryId
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to get category list');
router.back();
return;
}
if (data.result[self.categoryType]) {
self.categories = data.result[self.categoryType];
} else {
self.categories = [];
}
self.loading = false;
}).catch(error => {
self.$logger.error('failed to load category list', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
router.back();
} else if (!error.processed) {
self.$toast('Unable to get category list');
router.back();
}
});
},
methods: {
onPageAfterIn() {
const self = this;
const previousRoute = self.$f7router.previousRoute;
if (previousRoute && (previousRoute.path === '/category/add' || previousRoute.path === '/category/edit') && !self.loading) {
self.reload(null);
}
},
reload(done) {
const self = this;
self.$services.getAllTransactionCategories({
type: self.categoryType,
parentId: self.categoryId
}).then(response => {
if (done) {
done();
}
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to get category list');
return;
}
if (data.result[self.categoryType]) {
self.categories = data.result[self.categoryType];
} else {
self.categories = [];
}
}).catch(error => {
self.$logger.error('failed to reload category list', error);
if (done) {
done();
}
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
self.$toast('Unable to get category list');
}
});
},
setSortable() {
if (this.sortable) {
return;
}
this.showHidden = true;
this.sortable = true;
this.displayOrderModified = false;
},
onSort(event) {
if (!event || !event.el || !event.el.id || event.el.id.indexOf('category_') !== 0) {
this.$toast('Unable to move category');
return;
}
const id = event.el.id.substr(9);
let category = null;
for (let i = 0; i < this.categories.length; i++) {
if (this.categories[i].id === id) {
category = this.categories[i];
break;
}
}
if (!category || !this.categories[event.to]) {
this.$toast('Unable to move category');
return;
}
this.categories.splice(event.to, 0, this.categories.splice(event.from, 1)[0]);
this.displayOrderModified = true;
},
saveSortResult() {
const self = this;
const newDisplayOrders = [];
if (!self.displayOrderModified) {
self.showHidden = false;
self.sortable = false;
return;
}
self.displayOrderSaving = true;
for (let i = 0; i < self.categories.length; i++) {
newDisplayOrders.push({
id: self.categories[i].id,
displayOrder: i + 1
});
}
self.$showLoading();
self.$services.moveTransactionCategory({
newDisplayOrders: newDisplayOrders
}).then(response => {
self.displayOrderSaving = false;
self.$hideLoading();
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to move category');
return;
}
self.showHidden = false;
self.sortable = false;
self.displayOrderModified = false;
}).catch(error => {
self.$logger.error('failed to save categories display order', error);
self.displayOrderSaving = false;
self.$hideLoading();
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
self.$toast('Unable to move category');
}
});
},
edit(category) {
this.$f7router.navigate('/category/edit?id=' + category.id);
},
hide(category, hidden) {
const self = this;
self.$showLoading();
self.$services.hideTransactionCategory({
id: category.id,
hidden: hidden
}).then(response => {
self.$hideLoading();
const data = response.data;
if (!data || !data.success || !data.result) {
if (hidden) {
self.$toast('Unable to hide this category');
} else {
self.$toast('Unable to unhide this category');
}
return;
}
category.hidden = hidden;
}).catch(error => {
self.$logger.error('failed to change category visibility', error);
self.$hideLoading();
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
if (hidden) {
self.$toast('Unable to hide this category');
} else {
self.$toast('Unable to unhide this category');
}
}
});
},
remove(category) {
const self = this;
const app = self.$f7;
const $$ = app.$;
if (!category) {
self.$alert('An error has occurred');
return;
}
if (!self.showDeleteActionSheet) {
self.categoryToDelete = category;
self.showDeleteActionSheet = true;
return;
}
self.showDeleteActionSheet = false;
self.categoryToDelete = null;
self.$showLoading();
self.$services.deleteTransactionCategory({
id: category.id
}).then(response => {
self.$hideLoading();
const data = response.data;
if (!data || !data.success || !data.result) {
self.$toast('Unable to delete this category');
return;
}
app.swipeout.delete($$(`#${self.$options.filters.categoryDomId(category)}`), () => {
for (let i = 0; i < self.categories.length; i++) {
if (self.categories[i].id === category.id) {
self.categories.splice(i, 1);
}
}
});
}).catch(error => {
self.$logger.error('failed to delete category', error);
self.$hideLoading();
if (error.response && error.response.data && error.response.data.errorMessage) {
self.$toast({ error: error.response.data });
} else if (!error.processed) {
self.$toast('Unable to delete this category');
}
});
}
},
filters: {
categoryDomId(category) {
return 'category_' + category.id;
}
}
};
</script>