Перед вами перевод пятой части руководства по разработке веб-решений на базе Node.js, Vue.js и MongoDB. В первой, второй, третьей и четвёртой частях мы рассказывали о поэтапном создании клиентской и серверной частей приложения Budget Manager. Те, кому не терпится увидеть в действии то, что в итоге получилось у автора этого материала, могут заглянуть сюда. Кроме того, вот GitHub-репозиторий проекта. Если вы — из тех, кто ценит строгую типизацию, то здесь и здесь находятся результаты переноса Budget Manager на TypeScript.
Сегодня работа над этим учебным проектом завершится. А именно, в данном материале пойдёт речь о разработке страниц по добавлению в систему записей о новых клиентах и финансовых документах, а также о создании механизмов для редактирования этих данных. Здесь же мы рассмотрим некоторые улучшения API и доведём Budget Manager до рабочего состояния.
Для начала перейдём в папку
Теперь перейдём в папку
Похожие изменения внесём в файл
И, наконец, добавим в систему новые маршруты. Для этого перейдём в папку
Внесём похожие изменения в файл
Вот и все изменения, которые нужно внести в API.
Теперь добавим новые компоненты в маршруты. Для этого откроем файл
Здесь мы импортировали и определили компонент
Начнём с компонента
Первый именованный слот —
Второй именованный слот —
Третий именованный слот —
И, наконец здесь имеется, последний именованный слот, который используется для редактирования информации о клиентах. Он будет видим тогда, когда свойство
Разработаем компонент, который используется для создания новых финансовых документов. Перейдём в папку
Компонент это довольно большой, разберём его поэтапно, начиная с шаблона.
Шаблон компонента BudgetCreation
Тут мы сначала добавляем в шаблон элемент
Затем мы перебираем элементы
Далее, здесь есть три поля
В конце ряда имеется простой элемент
Ниже списка товаров имеется ещё три элемента. Это — синяя кнопка, которая используется для добавления новых элементов путём вызова функции
Скрипт компонента BudgetCreation
В этом коде мы сначала получаем два свойства —
Затем мы определяем объект и массив, играющие роль данных. Объект имеет имя
Здесь же определён массив состояний документа,
Ниже, после описания структур данных, имеется пара методов:
Каждый раз, когда мы щёлкаем по синей кнопке, вызывается метод
Метод
Стили компонента BudgetCreation
Теперь рассмотрим следующий компонент.
Этот компонент, по сути, является упрощённой версией только что рассмотренного компонента
Теперь пришла очередь компонента
Этот компонент, по сути, является модифицированной версией уже рассмотренного компонента
Единственное различие шаблонов компонентов
Здесь всё начинается с получения трёх свойств. Это —
Далее, здесь можно видеть обработчик события жизненного цикла компонента
Этот компонент, по аналогии с только что рассмотренным, похож на соответствующий компонент, используемый для создания клиентов —
На этом мы завершаем создание новых компонентов и переходим к работе с компонентами, которые уже были в системе.
Теперь осталось лишь внести некоторые изменения в существующие компоненты и приложение будет готово к работе.
Начнём с компонента
Шаблон компонента ListBody
Напомним, что код этого компонента хранится в файле
В этом компоненте надо выполнить буквально пару изменений и дополнений. Так, сначала добавим новое условие в конструкцию
Кроме того, мы уберём первую кнопку, которую использовали для вывода документа, так как она нам больше не нужна из-за того, что увидеть документ можно, нажав на кнопку редактирования.
Тут мы добавили метод
Ниже всего этого имеется блок
В этом компоненте мы получаем множество свойств. Опишем их:
В компоненте есть всего один метод,
Правку кода компонента
Здесь, в первую очередь, мы меняем свойство
Кроме того, мы модифицируем элемент
Тут добавлены два новых свойства —
С компонентом
Пожалуй, этот компонент претерпел наибольшие изменения. Теперь мы передаём ему
В тег
Сюда добавлен тег
В
В этот компонент добавлено множество новых данных. Опишем их.
Тут, так же, как в одном из примеров выше, назначен обработчик события жизненного цикла
В этот компонент, к полю списка, добавлена функция
Здесь мы улучшили все методы и добавили множество новых.
На этом работа над веб-приложением Budget Manager завершена. Вот как оно выглядит.
Напомним, что опробовать его в действии можно здесь, а посмотреть полный код — здесь.
Надеемся, то, чему вы научились, осваивая это руководство, пригодится вам при разработке ваших собственных проектов.
Уважаемые читатели! Пригодилось ли вам на практике то, что вы узнали из этой серии материалов?
Сегодня работа над этим учебным проектом завершится. А именно, в данном материале пойдёт речь о разработке страниц по добавлению в систему записей о новых клиентах и финансовых документах, а также о создании механизмов для редактирования этих данных. Здесь же мы рассмотрим некоторые улучшения API и доведём Budget Manager до рабочего состояния.
Доработка API
Для начала перейдём в папку
models
и откроем файл budget.js
. Добавим в него поле description
для модели:description: {
type: String,
required: true
},
Теперь перейдём в папку
app/api
и откроем файл budget.js
, который находится в ней. Тут мы собираемся отредактировать функцию сохранения данных, store
, для того, чтобы новые документы обрабатывались правильно, добавить функцию edit
, которая позволит редактировать документы, добавить функцию remove
, которая нужна для удаления документов, и добавить функцию getByState
, которая позволит фильтровать документы. Здесь приведён полный код файла. Для того, чтобы его просмотреть, разверните соответствующий блок. В дальнейшем большие фрагменты кода будут оформлены так же.Исходный код
const mongoose = require('mongoose');
const api = {};
api.store = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Client.findOne({ _id: req.body.client }, (error, client) => {
if (error) res.status(400).json(error);
if (client) {
const budget = new Budget({
client_id: req.body.client,
user_id: req.query.user_id,
client: client.name,
state: req.body.state,
description: req.body.description,
title: req.body.title,
total_price: req.body.total_price,
items: req.body.items
});
budget.save(error => {
if (error) return res.status(400).json(error)
res.status(200).json({ success: true, message: "Budget registered successfully" })
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.getAll = (User, Budget, Token) => (req, res) => {
if (Token) {
Budget.find({ user_id: req.query.user_id }, (error, budget) => {
if (error) return res.status(400).json(error);
res.status(200).json(budget);
return true;
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.getAllFromClient = (User, Budget, Token) => (req, res) => {
if (Token) {
Budget.find({ client_id: req.query.client_id }, (error, budget) => {
if (error) return res.status(400).json(error);
res.status(200).json(budget);
return true;
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.index = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.findOne({ _id: req.query._id }, (error, budget) => {
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.edit = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.findOneAndUpdate({ _id: req.body._id }, req.body, (error, budget) => {
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.getByState = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.find({ state: req.query.state }, (error, budget) => {
console.log(budget)
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.remove = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Budget.remove({ _id: req.query._id }, (error, removed) => {
if (error) res.status(400).json(error);
res.status(200).json({ success: true, message: 'Removed successfully' });
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
Похожие изменения внесём в файл
client.js
из папки api
:Исходный код
const mongoose = require('mongoose');
const api = {};
api.store = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
const client = new Client({
user_id: req.query.user_id,
name: req.body.name,
email: req.body.email,
phone: req.body.phone,
});
client.save(error => {
if (error) return res.status(400).json(error);
res.status(200).json({ success: true, message: "Client registration successful" });
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.getAll = (User, Client, Token) => (req, res) => {
if (Token) {
Client.find({ user_id: req.query.user_id }, (error, client) => {
if (error) return res.status(400).json(error);
res.status(200).json(client);
return true;
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.index = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.findOne({ _id: req.query._id }, (error, client) => {
if (error) res.status(400).json(error);
res.status(200).json(client);
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.edit = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.findOneAndUpdate({ _id: req.body._id }, req.body, (error, client) => {
if (error) res.status(400).json(error);
res.status(200).json(client);
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.remove = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.remove({ _id: req.query._id }, (error, removed) => {
if (error) res.status(400).json(error);
res.status(200).json({ success: true, message: 'Removed successfully' });
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
И, наконец, добавим в систему новые маршруты. Для этого перейдём в папку
routes
и откроем файл budget.js
:Исходный код
const passport = require('passport'),
config = require('@config'),
models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.budget;
app.route('/api/v1/budget')
.post(passport.authenticate('jwt', config.session), api.store(models.User, models.Budget, models.Client, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Budget, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAllFromClient(models.User, models.Budget, app.get('budgetsecret')))
.delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Budget, models.Client, app.get('budgetsecret')))
app.route('/api/v1/budget/single')
.get(passport.authenticate('jwt', config.session), api.index(models.User, models.Budget, models.Client, app.get('budgetsecret')))
.put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Budget, models.Client, app.get('budgetsecret')))
app.route('/api/v1/budget/state')
.get(passport.authenticate('jwt', config.session), api.getByState(models.User, models.Budget, models.Client, app.get('budgetsecret')))
}
Внесём похожие изменения в файл
client.js
, который находится в той же папке:Исходный код
const passport = require('passport'),
config = require('@config'),
models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.client;
app.route('/api/v1/client')
.post(passport.authenticate('jwt', config.session), api.store(models.User, models.Client, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Client, app.get('budgetsecret')))
.delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Client, app.get('budgetsecret')))
app.route('/api/v1/client/single')
.get(passport.authenticate('jwt', config.session), api.index(models.User, models.Client, app.get('budgetsecret')))
.put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Client, app.get('budgetsecret')))
}
Вот и все изменения, которые нужно внести в API.
Доработка маршрутизатора
Теперь добавим новые компоненты в маршруты. Для этого откроем файл
index.js
, находящийся внутри папки router
.Исходный код
...
// Global components
import Header from '@/components/Header'
import List from '@/components/List/List'
import Create from '@/components/pages/Create'
// Register components
Vue.component('app-header', Header)
Vue.component('list', List)
Vue.component('create', Create)
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header,
list: List,
create: Create
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
…
Здесь мы импортировали и определили компонент
Create
и назначили его компонентом маршрута Home
(сам компонент создадим ниже).Создание новых компонентов
▍Компонент Create
Начнём с компонента
Create
. Перейдём в папку components/pages
и создадим там новый файл Create.vue
.Исходный код
<template>
<div class="l-create-page">
<budget-creation v-if="budgetCreation && !editPage" slot="budget-creation" :clients="clients" :saveBudget="saveBudget"></budget-creation>
<client-creation v-if="!budgetCreation && !editPage" slot="client-creation" :saveClient="saveClient"></client-creation>
<budget-edit v-else-if="budgetEdit && editPage"
slot="budget-creation"
:clients="clients"
:selectedBudget="budget"
:fixClientNameAndUpdate="fixClientNameAndUpdate">
</budget-edit>
<client-edit v-else-if="!budgetEdit && editPage"
slot="client-creation"
:selectedClient="client"
:updateClient="updateClient">
</client-edit>
</div>
</template>
<script>
import BudgetCreation from './../Creation/BudgetCreation'
import ClientCreation from './../Creation/ClientCreation'
import BudgetEdit from './../Creation/BudgetEdit'
import ClientEdit from './../Creation/ClientEdit'
export default {
props: [
'budgetCreation', 'clients', 'saveBudget',
'saveClient', 'budget', 'client', 'updateClient',
'fixClientNameAndUpdate', 'editPage', 'budgetEdit'
],
components: {
'budget-creation': BudgetCreation,
'client-creation': ClientCreation,
'budget-edit': BudgetEdit,
'client-edit': ClientEdit
}
}
</script>
Первый именованный слот —
budget-creation
. Он представляет компонент, который мы будем использовать для создания новых финансовых документов. Он будет виден только в том случае, когда свойство budgetCreation
установлено в значение true
, а editPage
— в значение false
, мы передаём ему всех наших клиентов и метод saveBudget
.Второй именованный слот —
client-creation
. Это — компонент, используемый для создания новых клиентов. Он будет видимым лишь в том случае, когда свойство budgetCreation
установлено в false
, и editPage
так же имеет значение false
. Сюда мы передаём метод saveClient
.Третий именованный слот —
budget-edit
. Это — компонент, который применяется для редактирования выбранного документа. Видим он только тогда, когда свойства budgetEdit
и editPage
установлены в true
. Сюда мы передаём всех клиентов, выбранный финансовый документ и метод fixClientNameAndUpdate
.И, наконец здесь имеется, последний именованный слот, который используется для редактирования информации о клиентах. Он будет видим тогда, когда свойство
budgetEdit
установлено в false
, а editPage
— в true
. Ему мы передаём выбранного клиента и метод updateClient
.▍Компонент BudgetCreation
Разработаем компонент, который используется для создания новых финансовых документов. Перейдём в папку
components
и создадим в ней новую папку, дав ей имя Creation
. В этой папке создадим файл компонента BudgetCreation.vue
.Компонент это довольно большой, разберём его поэтапно, начиная с шаблона.
Шаблон компонента BudgetCreation
Вот код шаблона компонента
<template>
<div class="l-budget-creation">
<v-layout row wrap>
<span class="md-budget-state-hint uppercased white--text">status</span>
<v-flex xs12 md2>
<v-select
label="Status"
:items="states"
v-model="budget.state"
>
</v-select>
</v-flex>
<v-flex xs12 md9 offset-md1>
<v-select
label="Client"
:items="clients"
v-model="budget.client"
item-text="name"
item-value="_id"
>
</v-select>
</v-flex>
<v-flex xs12 md12>
<v-text-field label="Title"
v-model="budget.title"
required
color="light-blue lighten-1">
</v-text-field>
<v-text-field label="Description"
v-model="budget.description"
textarea
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-layout row wrap v-for="item in budget.items" class="l-budget-item" :key="item.id">
<v-flex xs12 md1>
<v-btn block dark color="red lighten-1" @click.native="removeItem(item)">Remove</v-btn>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Title"
box dark
v-model="item.title"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md1 offset-md1>
<v-text-field label="Price"
box dark
prefix="$"
v-model="item.price"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md1>
<v-text-field label="Quantity"
box dark
min="0"
v-model="item.quantity"
type="number"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2>
<span class="md-budget-item-subtotal white--text">ITEM PRICE $ {{ item.subtotal }}</span>
</v-flex>
</v-layout>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn light-blue lighten-1" @click.native="addItem()">Add item</v-btn>
</v-flex>
<v-flex xs12 md2 offset-md10>
<span class="md-budget-item-total white--text">TOTAL $ {{ budget.total_price }}</span>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="saveBudget(budget)">Save</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Тут мы сначала добавляем в шаблон элемент
v-select
для установки состояния документа, затем — v-select
для выбора клиента, который нам нужен. Далее, у нас имеется поле v-text-field
для ввода заголовка документа и v-text-field
для вывода описания.Затем мы перебираем элементы
budget.items
, что даёт нам возможность добавлять элементы в документ и удалять их из него. Здесь же имеется красная кнопка, которая позволяет вызывать функцию removeItem
, передавая ей элемент, который нужно удалить.Далее, здесь есть три поля
v-text-fields
, предназначенные, соответственно, для названия товара, цены за единицу и количества.В конце ряда имеется простой элемент
span
, в котором выводится промежуточный итог по строке, subtotal
, представляющий собой произведение количества и цены товара.Ниже списка товаров имеется ещё три элемента. Это — синяя кнопка, которая используется для добавления новых элементов путём вызова функции
addItem
, элемент span
, который показывает общую стоимость всех товаров, которые имеются в документе (сумма показателей subtotal
всех элементов), и зелёная кнопка, которая используется для сохранения документа в базу данных путём вызова функции saveBudget
с передачей ей, в качестве параметра, документа, который мы хотим сохранить.Скрипт компонента BudgetCreation
Вот код, который приводит компонент BudgetCreation в действие
<script>
export default {
props: ['clients', 'saveBudget'],
data () {
return {
budget: {
title: null,
description: null,
state: 'writing',
client: null,
get total_price () {
let value = 0
this.items.forEach(({ subtotal }) => {
value += parseInt(subtotal)
})
return value
},
items: [
{
title: null,
quantity: 0,
price: 0,
get subtotal () {
return this.quantity * this.price
}
}
]
},
states: [
'writing', 'editing', 'pending', 'approved', 'denied', 'waiting'
]
}
},
methods: {
addItem () {
const items = this.budget.items
const item = {
title: '',
quantity: 0,
price: 0,
get subtotal () {
return this.quantity * this.price
}
}
items.push(item)
},
removeItem (selected) {
const items = this.budget.items
items.forEach((item, index) => {
if (item === selected) {
items.splice(index, 1)
}
})
}
}
}
</script>
В этом коде мы сначала получаем два свойства —
clients
и saveBudget
. Источник этих свойств — компонент Home
.Затем мы определяем объект и массив, играющие роль данных. Объект имеет имя
budget
. Он используется для создания документа, мы можем добавлять в него значения и сохранять его в базе данных. У этого объекта есть свойства title
(заголовок), description
(описание), state
(состояние, по умолчанию установленное в значение writing
), client
(клиент), total_price
(общая стоимость по документу), и массив товаров items
. У товаров имеются свойства title
(название), quantity
(количество), price
(цена) и subtotal
(промежуточный итог).Здесь же определён массив состояний документа,
states
. Его значения используют для установки состояния документа. Вот эти состояния: writing
, editing
, pending
, approved
, denied
и waiting
.Ниже, после описания структур данных, имеется пара методов:
addItem
(для добавления товаров) и removeItem
(для их удаления).Каждый раз, когда мы щёлкаем по синей кнопке, вызывается метод
addItem
, который добавляет элементы в массив items
, находящийся внутри объекта budget
.Метод
removeItem
выполняет обратное действие. А именно — при щелчке по красной кнопке заданный элемент удаляется из массива items
.Стили компонента BudgetCreation
Вот стили для рассматриваемого компонента
<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-budget-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #29b6f6!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #29b6f6!important;
}
}
}
.md-budget-state-hint {
margin: 10px 0;
display: block;
width: 100%;
}
.md-budget-state {
background-color: rgba(41, 182, 246, .6);
display: flex;
height: 35px;
width: 100%;
font-size: 14px;
align-items: center;
justify-content: center;
border-radius: 2px;
margin: 10px 0 15px;
}
.l-budget-item {
align-items: center;
}
.md-budget-item-subtotal {
font-size: 16px;
text-align: center;
display: block;
}
.md-budget-item-total {
font-size: 22px;
text-align: center;
display: block;
width: 100%;
margin: 30px 0 10px;
}
.md-add-item-btn {
margin-top: 30px !important;
display: block;
}
.list__tile__title, .input-group__selections {
text-transform: uppercase !important;
}
</style>
Теперь рассмотрим следующий компонент.
▍Компонент ClientCreation
Этот компонент, по сути, является упрощённой версией только что рассмотренного компонента
BudgetCreation
. Мы так же, как сделано выше, рассмотрим его по частям. Если вы разобрались с устройством компонента BudgetCreation
, вы без труда поймёте и принципы работы компонента ClientCreation
.Шаблон компонента ClientCreation
<template>
<div class="l-client-creation">
<v-layout row wrap>
<v-flex xs12 md4>
<v-text-field label="Name"
v-model="client.name"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Email"
v-model="client.email"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Phone"
v-model="client.phone"
required
mask="phone"
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="saveClient(client)">Save</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Скрипт компонента ClientCreation
<script>
export default {
props: ['saveClient'],
data () {
return {
client: {
name: null,
email: null,
phone: null
}
}
}
}
</script>
Стили компонента ClientCreation
<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-client-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #66bb6a!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #66bb6a!important;
}
}
}
</style>
Теперь пришла очередь компонента
BudgetEdit
.▍Компонент BudgetEdit
Этот компонент, по сути, является модифицированной версией уже рассмотренного компонента
BudgetCreation
. Рассмотрим его составные части.Шаблон компонента BudgetEdit
<template>
<div class="l-budget-creation">
<v-layout row wrap>
<span class="md-budget-state-hint uppercased white--text">status</span>
<v-flex xs12 md2>
<v-select
label="Status"
:items="states"
v-model="budget.state"
>
</v-select>
</v-flex>
<v-flex xs12 md9 offset-md1>
<v-select
label="Client"
:items="clients"
v-model="budget.client_id"
item-text="name"
item-value="_id"
>
</v-select>
</v-flex>
<v-flex xs12 md12>
<v-text-field label="Title"
v-model="budget.title"
required
color="light-blue lighten-1">
</v-text-field>
<v-text-field label="Description"
v-model="budget.description"
textarea
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-layout row wrap v-for="item in budget.items" class="l-budget-item" :key="item.id">
<v-flex xs12 md1>
<v-btn block dark color="red lighten-1" @click.native="removeItem(item)">Remove</v-btn>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Title"
box dark
v-model="item.title"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md1 offset-md1>
<v-text-field label="Price"
box dark
prefix="$"
v-model="item.price"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md1>
<v-text-field label="Quantity"
box dark
min="0"
v-model="item.quantity"
type="number"
required
color="light-blue lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2>
<span class="md-budget-item-subtotal white--text">ITEM PRICE $ {{ item.subtotal }}</span>
</v-flex>
</v-layout>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn light-blue lighten-1" @click.native="addItem()">Add item</v-btn>
</v-flex>
<v-flex xs12 md2 offset-md10>
<span class="md-budget-item-total white--text">TOTAL $ {{ budget.total_price }}</span>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="fixClientNameAndUpdate(budget)">Update</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Единственное различие шаблонов компонентов
BudgetEdit
и BudgetCreation
заключается в кнопке сохранения изменений и в связанной с ней логике. А именно, в BudgetCreation
на ней написано Save
, она вызывает метод saveBudget
. В BudgetEdit
эта кнопка несёт на себе надпись Update
и вызывает метод fixClientNameAndUpdate
.Скрипт компонента BudgetCreation
<script>
export default {
props: ['clients', 'fixClientNameAndUpdate', 'selectedBudget'],
data () {
return {
budget: {
title: null,
description: null,
state: 'pending',
client: null,
get total_price () {
let value = 0
this.items.forEach(({ subtotal }) => {
value += parseInt(subtotal)
})
return value
},
items: [
{
title: null,
quantity: 0,
price: null,
get subtotal () {
return this.quantity * this.price
}
}
]
},
states: [
'writing', 'editing', 'pending', 'approved', 'denied', 'waiting'
]
}
},
mounted () {
this.parseBudget()
},
methods: {
addItem () {
const items = this.budget.items
const item = {
title: '',
quantity: 0,
price: 0,
get subtotal () {
return this.quantity * this.price
}
}
items.push(item)
},
removeItem (selected) {
const items = this.budget.items
items.forEach((item, index) => {
if (item === selected) {
items.splice(index, 1)
}
})
},
parseBudget () {
for (let key in this.selectedBudget) {
if (key !== 'total' && key !== 'items') {
this.budget[key] = this.selectedBudget[key]
}
if (key === 'items') {
const items = this.selectedBudget.items
const buildItems = item => ({
title: item.title,
quantity: item.quantity,
price: item.price,
get subtotal () {
return this.quantity * this.price
}
})
const parseItems = items => items.map(buildItems)
this.budget.items = parseItems(items)
}
}
}
}
}
</script>
Здесь всё начинается с получения трёх свойств. Это —
clients
, fixClientNameAndUpdate
и selectedBudget
. Данные тут те же самые, что и в компоненте BudgetCreation
. А именно, тут имеется объект Budget
и массив states
.Далее, здесь можно видеть обработчик события жизненного цикла компонента
mounted
, в котором мы вызываем метод parseBudget
, о котором поговорим ниже. И, наконец, здесь есть объект methods
, в котором присутствуют уже знакомые вам по компоненту BudgetCreation
методы addItem
и removeItem
, а также новый метод parseBudget
. Этот метод используется для того, чтобы установить значение объекта budget
в то, которое передано в свойстве selectedBudget
, но мы, кроме того, используем его для подсчёта промежуточных итогов по товарам документа и общей суммы по документу.Стиль компонента BudgetCreation
<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-budget-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #29b6f6!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #29b6f6!important;
}
}
}
.md-budget-state-hint {
margin: 10px 0;
display: block;
width: 100%;
}
.md-budget-state {
background-color: rgba(41, 182, 246, .6);
display: flex;
height: 35px;
width: 100%;
font-size: 14px;
align-items: center;
justify-content: center;
border-radius: 2px;
margin: 10px 0 15px;
}
.l-budget-item {
align-items: center;
}
.md-budget-item-subtotal {
font-size: 16px;
text-align: center;
display: block;
}
.md-budget-item-total {
font-size: 22px;
text-align: center;
display: block;
width: 100%;
margin: 30px 0 10px;
}
.md-add-item-btn {
margin-top: 30px !important;
display: block;
}
.list__tile__title, .input-group__selections {
text-transform: uppercase !important;
}
</style>
▍Компонент ClientEdit
Этот компонент, по аналогии с только что рассмотренным, похож на соответствующий компонент, используемый для создания клиентов —
ClientCreation
. Главное отличие заключается в том, что тут вместо метода saveClient
используется метод updateClient
. Рассмотрим устройство компонента ClientEdit
.Шаблон компонента ClientEdit
<template>
<div class="l-client-creation">
<v-layout row wrap>
<v-flex xs12 md4>
<v-text-field label="Name"
v-model="client.name"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Email"
v-model="client.email"
required
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md3 offset-md1>
<v-text-field label="Phone"
v-model="client.phone"
required
mask="phone"
color="green lighten-1">
</v-text-field>
</v-flex>
<v-flex xs12 md2 offset-md10>
<v-btn block color="md-add-item-btn green lighten-1" @click.native="updateClient(client)">Update</v-btn>
</v-flex>
</v-layout>
</div>
</template>
Скрипт компонента ClientEdit
<script>
export default {
props: ['updateClient', 'selectedClient'],
data () {
return {
client: {
name: null,
email: null,
phone: null
}
}
},
mounted () {
this.client = this.selectedClient
}
}
</script>
Стиль компонента ClientEdit
<style lang="scss">
@import "./../../assets/styles";
.uppercased {
text-transform: uppercase;
}
.l-client-creation {
label, input, .icon, .input-group__selections__comma, textarea {
color: #66bb6a!important;
}
.input-group__details {
&:before {
background-color: $border-color-input !important;
}
}
.input-group__input {
border-color: $border-color-input !important;
.input-group--text-field__prefix {
margin-bottom: 3px !important;
}
}
.input-group--focused {
.input-group__input {
border-color: #66bb6a!important;
}
}
}
</style>
На этом мы завершаем создание новых компонентов и переходим к работе с компонентами, которые уже были в системе.
Доработка существующих компонентов
Теперь осталось лишь внести некоторые изменения в существующие компоненты и приложение будет готово к работе.
Начнём с компонента
ListBody
.▍Компонент ListBody
Шаблон компонента ListBody
Напомним, что код этого компонента хранится в файле
ListBody.vue
Исходный код
<template>
<section class="l-list-body">
<div class="md-list-item"
v-if="data != null && parsedBudgets === null"
v-for="item in data">
<div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"
v-for="info in item"
v-if="info != item._id && info != item.client_id">
{{ info }}
</div>
<div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">
<v-btn small flat color="yellow accent-1" @click.native="getItemAndEdit(item)">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1" @click.native="deleteItem(item, data, budgetsVisible)">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
<div class="md-list-item"
v-if="parsedBudgets !== null"
v-for="item in parsedBudgets">
<div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"
v-for="info in item"
v-if="info != item._id && info != item.client_id">
{{ info }}
</div>
<div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">
<v-btn small flat color="yellow accent-1" @click.native="getItemAndEdit(item)">
<v-icon>mode_edit</v-icon>
</v-btn>
<v-btn small flat color="red lighten-1" @click.native="deleteItem(item, data, budgetsVisible)">
<v-icon>delete_forever</v-icon>
</v-btn>
</div>
</div>
</section>
</template>
В этом компоненте надо выполнить буквально пару изменений и дополнений. Так, сначала добавим новое условие в конструкцию
v-if
блока md-list-item
:parsedBudgets === null
Кроме того, мы уберём первую кнопку, которую использовали для вывода документа, так как она нам больше не нужна из-за того, что увидеть документ можно, нажав на кнопку редактирования.
Тут мы добавили метод
getItemAndEdit
к новой первой кнопке и метод deleteItem
к последней кнопке, передавая этому методу элемент, данные и переменную budgetsVisible
в качестве параметров.Ниже всего этого имеется блок
md-item-list
, который мы используем для вывода отфильтрованного после поиска списка документов.Скрипт компонента ListBody
<script>
export default {
props: ['data', 'budgetsVisible', 'deleteItem', 'getBudget', 'getClient', 'parsedBudgets'],
methods: {
getItemAndEdit (item) {
!item.phone ? this.getBudget(item) : this.getClient(item)
}
}
}
</script>
В этом компоненте мы получаем множество свойств. Опишем их:
data
: это либо список документов, либо список клиентов, но никогда и то и другое.
budgetsVisible
: используется для проверки того, просматриваем ли мы список документов или клиентов, может принимать значенияtrue
илиfalse
.
deleteItem
: функция для удаления элемента, которая принимает, в качестве параметра, некий элемент.
getBudget
: функция, которую мы используем для загрузки отдельного документа, который планируется редактировать.
getClient
: функция, используемая для загрузки карточки отдельного клиента для последующего редактирования.
parsedBudgets
: документы, отфильтрованные после выполнения поиска.
В компоненте есть всего один метод,
getItemAndEdit
. Он принимает, в качестве параметра, элемент, при этом, на основе анализа наличия у элемента свойства, содержащего телефонный номер, принимается решение о том, является ли элемент карточкой клиента или финансовым документом.Стиль компонента ListBody
<style lang="scss">
@import "./../../assets/styles";
.l-list-body {
display: flex;
flex-direction: column;
.md-list-item {
width: 100%;
display: flex;
flex-direction: column;
margin: 15px 0;
@media (min-width: 960px) {
flex-direction: row;
margin: 0;
}
.md-budget-info {
flex-basis: 25%;
width: 100%;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
padding: 0 15px;
display: flex;
height: 35px;
align-items: center;
justify-content: center;
&:first-of-type, &:nth-of-type(2) {
text-transform: capitalize;
}
&:nth-of-type(3) {
text-transform: uppercase;
}
@media (min-width: 601px) {
justify-content: flex-start;
}
}
.md-client-info {
@extend .md-budget-info;
background-color: rgba(102, 187, 106, 0.45)!important;
&:nth-of-type(2) {
text-transform: none;
}
}
.l-budget-actions {
flex-basis: 25%;
display: flex;
background-color: rgba(0, 175, 255, 0.45);
border: 1px solid $border-color-input;
align-items: center;
justify-content: center;
.btn {
min-width: 45px !important;
margin: 0 5px !important;
}
}
.l-client-actions {
@extend .l-budget-actions;
background-color: rgba(102, 187, 106, 0.45)!important;
}
}
}
</style>
Правку кода компонента
ListBody
мы завершили, займёмся теперь компонентом Header
.▍Компонент Header
Шаблон компонента Header
<template>
<header class="l-header-container">
<v-layout row wrap :class="budgetsVisible ? 'l-budgets-header' : 'l-clients-header'">
<v-flex xs12 md5>
<v-text-field v-model="searchValue"
label="Search"
append-icon="search"
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'">
</v-text-field>
</v-flex>
<v-flex xs12 offset-md1 md1>
<v-btn block
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"
@click.native="$emit('toggleVisibleData')">
{{ budgetsVisible ? "Clients" : "Budgets" }}
</v-btn>
</v-flex>
<v-flex xs12 offset-md1 md2>
<v-select label="Status"
:color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"
v-model="status"
:items="statusItems"
single-line
@change="selectState">
</v-select>
</v-flex>
<v-flex xs12 offset-md1 md1>
<v-btn block color="red lighten-1 white--text" @click.native="submitSignout()">Sign out</v-btn>
</v-flex>
</v-layout>
</header>
</template>
Здесь, в первую очередь, мы меняем свойство
v-model
поля поиска на searchValue
.Кроме того, мы модифицируем элемент
v-select
, привязывая к его событию change
метод selectState
.Скрипт компонента Header
<script>
import Authentication from '@/components/pages/Authentication'
export default {
props: ['budgetsVisible', 'selectState', 'search'],
data () {
return {
searchValue: '',
status: '',
statusItems: [
'all', 'approved', 'denied', 'waiting', 'writing', 'editing'
]
}
},
watch: {
'searchValue': function () {
this.$emit('input', this.searchValue)
}
},
created () {
this.searchValue = this.search
},
methods: {
submitSignout () {
Authentication.signout(this, '/login')
}
}
}
</script>
Тут добавлены два новых свойства —
selectState
, представляющее собой функцию, и search
, которое является строкой. В данных search
теперь используется searchValue
и приведённый к нижнему регистру массив элементов statusItems
.Стиль компонента Header
<script>
import Authentication from '@/components/pages/Authentication'
export default {
props: ['budgetsVisible', 'selectState', 'search'],
data () {
return {
searchValue: '',
status: '',
statusItems: [
'all', 'approved', 'denied', 'waiting', 'writing', 'editing'
]
}
},
watch: {
'searchValue': function () {
this.$emit('input', this.searchValue)
}
},
created () {
this.searchValue = this.search
},
methods: {
submitSignout () {
Authentication.signout(this, '/login')
}
}
}
</script>
С компонентом
Header
мы разобрались, теперь поработаем с компонентом Home
.▍Компонент Home
Шаблон компонента Home
<template>
<main class="l-home-page">
<app-header :budgetsVisible="budgetsVisible"
@toggleVisibleData="budgetsVisible = !budgetsVisible; budgetCreation = !budgetCreation"
:selectState="selectState"
:search="search"
v-model="search">
</app-header>
<div class="l-home">
<h4 class="white--text text-xs-center my-0">
Focus Budget Manager
</h4>
<list v-if="listPage">
<list-header slot="list-header" :headers="budgetsVisible ? budgetHeaders : clientHeaders"></list-header>
<list-body slot="list-body"
:budgetsVisible="budgetsVisible"
:data="budgetsVisible ? budgets : clients"
:search="search"
:deleteItem="deleteItem"
:getBudget="getBudget"
:getClient="getClient"
:parsedBudgets="parsedBudgets">
</list-body>
</list>
<create v-else-if="createPage"
:budgetCreation="budgetCreation"
:budgetEdit="budgetEdit"
:editPage="editPage"
:clients="clients"
:budget="budget"
:client="client"
:saveBudget="saveBudget"
:saveClient="saveClient"
:fixClientNameAndUpdate="fixClientNameAndUpdate"
:updateClient="updateClient">
</create>
</div>
<v-snackbar :timeout="timeout"
bottom="bottom"
:color="snackColor"
v-model="snackbar">
{{ message }}
</v-snackbar>
<v-fab-transition>
<v-speed-dial v-model="fab"
bottom
right
fixed
direction="top"
transition="scale-transition">
<v-btn slot="activator"
color="red lighten-1"
dark
fab
v-model="fab">
<v-icon>add</v-icon>
<v-icon>close</v-icon>
</v-btn>
<v-tooltip left>
<v-btn color="light-blue lighten-1"
dark
small
fab
slot="activator"
@click.native="budgetCreation = true; listPage = false; editPage = false; createPage = true">
<v-icon>assignment</v-icon>
</v-btn>
<span>Add new Budget</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="green lighten-1"
dark
small
fab
slot="activator"
@click.native="budgetCreation = false; listPage = false; editPage = false; createPage = true">
<v-icon>account_circle</v-icon>
</v-btn>
<span>Add new Client</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="purple lighten-2"
dark
small
fab
slot="activator"
@click.native="budgetCreation = false; listPage = true; budgetsVisible = true">
<v-icon>assessment</v-icon>
</v-btn>
<span>List Budgets</span>
</v-tooltip>
<v-tooltip left>
<v-btn color="deep-orange lighten-2"
dark
small
fab
slot="activator"
@click.native="budgetCreation = false; listPage = true; budgetsVisible = false;">
<v-icon>supervisor_account</v-icon>
</v-btn>
<span>List Clients</span>
</v-tooltip>
</v-speed-dial>
</v-fab-transition>
</main>
</template>
Пожалуй, этот компонент претерпел наибольшие изменения. Теперь мы передаём ему
budgetsVisible
, selectState
, search
и toggleVisibleData
в качестве свойств, кроме того, мы работаем с другой переменной в toggleVisibleData
, и мы добавили v-model
к search
.В тег
list
добавлена конструкция v-if
, в результате он отображается только тогда, когда мы находимся на странице просмотра списков. Также, добавлено много новых свойств к list-body
.Сюда добавлен тег
create
, функционал которого похож на функционал list
, но мы выводим его лишь в том случае, если находимся на странице создания элементов. Ему мы передаём все данные клиента и документов, а также все методы загрузки и обновления элементов.В
v-fab-transition
добавлены две новые кнопки, что позволяет нам выводить документы и карточки клиентов, а так же создавать эти объекты.Скрипт компонента Home
<script>
import Axios from 'axios'
import Authentication from '@/components/pages/Authentication'
import ListHeader from './../List/ListHeader'
import ListBody from './../List/ListBody'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
export default {
components: {
'list-header': ListHeader,
'list-body': ListBody
},
data () {
return {
parsedBudgets: null,
budget: null,
client: null,
state: null,
search: null,
budgets: [],
clients: [],
budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],
clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],
budgetsVisible: true,
snackbar: false,
timeout: 6000,
message: '',
fab: false,
listPage: true,
createPage: true,
editPage: false,
budgetCreation: true,
budgetEdit: true,
snackColor: 'red lighten-1'
}
},
mounted () {
this.getAllBudgets()
this.getAllClients()
this.hidden = false
},
watch: {
'search': function () {
if (this.search !== null || this.search !== '') {
const searchTerm = this.search
const regex = new RegExp(`^(${searchTerm})`, 'g')
const results = this.budgets.filter(budget => budget.client.match(regex))
this.parsedBudgets = results
} else {
this.parsedBudgets = null
}
}
},
methods: {
getAllBudgets () {
Axios.get(`${BudgetManagerAPI}/api/v1/budget`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state', 'client_id')
}).catch(error => {
this.errorHandler(error)
})
},
getAllClients () {
Axios.get(`${BudgetManagerAPI}/api/v1/client`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
}).then(({data}) => {
this.clients = this.dataParser(data, 'name', 'email', '_id', 'phone')
}).catch(error => {
this.errorHandler(error)
})
},
getBudget (budget) {
Axios.get(`${BudgetManagerAPI}/api/v1/budget/single`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: {
user_id: this.$cookie.get('user_id'),
_id: budget._id
}
}).then(({data}) => {
this.budget = data
this.enableEdit('budget')
}).catch(error => {
this.errorHandler(error)
})
},
getClient (client) {
Axios.get(`${BudgetManagerAPI}/api/v1/client/single`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: {
user_id: this.$cookie.get('user_id'),
_id: client._id
}
}).then(({data}) => {
this.client = data
this.enableEdit('client')
}).catch(error => {
this.errorHandler(error)
})
},
enableEdit (type) {
if (type === 'budget') {
this.listPage = false
this.budgetEdit = true
this.budgetCreation = false
this.editPage = true
} else if (type === 'client') {
this.listPage = false
this.budgetEdit = false
this.budgetCreation = false
this.editPage = true
}
},
saveBudget (budget) {
Axios.post(`${BudgetManagerAPI}/api/v1/budget`, budget, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(res => {
this.resetFields(budget)
this.snackbar = true
this.message = res.data.message
this.snackColor = 'green lighten-1'
this.getAllBudgets()
})
.catch(error => {
this.errorHandler(error)
})
},
fixClientNameAndUpdate (budget) {
this.clients.find(client => {
if (client._id === budget.client_id) {
budget.client = client.name
}
})
this.updateBudget(budget)
},
updateBudget (budget) {
Axios.put(`${BudgetManagerAPI}/api/v1/budget/single`, budget, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(() => {
this.snackbar = true
this.message = 'Budget updated'
this.snackColor = 'green lighten-1'
this.listPage = true
this.budgetCreation = false
this.budgetsVisible = true
this.getAllBudgets()
})
.catch(error => {
this.errorHandler(error)
})
},
updateClient (client) {
Axios.put(`${BudgetManagerAPI}/api/v1/client/single`, client, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(() => {
this.snackbar = true
this.message = 'Client updated'
this.snackColor = 'green lighten-1'
this.listPage = true
this.budgetCreation = false
this.budgetsVisible = false
this.getAllClients()
})
.catch(error => {
this.errorHandler(error)
})
},
saveClient (client) {
Axios.post(`${BudgetManagerAPI}/api/v1/client`, client, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id') }
})
.then(res => {
this.resetFields(client)
this.snackbar = true
this.message = res.data.message
this.snackColor = 'green lighten-1'
this.getAllClients()
})
.catch(error => {
this.errorHandler(error)
})
},
deleteItem (selected, items, api) {
let targetApi = ''
api ? targetApi = 'budget' : targetApi = 'client'
Axios.delete(`${BudgetManagerAPI}/api/v1/${targetApi}`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: {
user_id: this.$cookie.get('user_id'),
_id: selected._id
}
})
.then(() => {
this.removeItem(selected, items)
})
.then(() => {
api ? this.getAllBudgets() : this.getAllClients()
})
.catch(error => {
this.errorHandler(error)
})
},
errorHandler (error) {
const status = error.response.status
this.snackbar = true
this.snackColor = 'red lighten-1'
if (status === 404) {
this.message = 'Invalid request'
} else if (status === 401 || status === 403) {
this.message = 'Unauthorized'
} else if (status === 400) {
this.message = 'Invalid or missing information'
} else {
this.message = error.message
}
},
removeItem (selected, items) {
items.forEach((item, index) => {
if (item === selected) {
items.splice(index, 1)
}
})
},
dataParser (targetedArray, ...options) {
let parsedData = []
targetedArray.forEach(item => {
let parsedItem = {}
options.forEach(option => (parsedItem[option] = item[option]))
parsedData.push(parsedItem)
})
return parsedData
},
resetFields (item) {
for (let key in item) {
item[key] = null
if (key === 'quantity' || key === 'price') {
item[key] = 0
}
item['items'] = []
}
},
selectState (state) {
this.state = state
state === 'all' ? this.getAllBudgets() : this.getBudgetsByState(state)
},
getBudgetsByState (state) {
Axios.get(`${BudgetManagerAPI}/api/v1/budget/state`, {
headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },
params: { user_id: this.$cookie.get('user_id'), state }
}).then(({data}) => {
this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state', 'client_id')
}).catch(error => {
this.errorHandler(error)
})
}
}
}
</script>
В этот компонент добавлено множество новых данных. Опишем их.
parsedBudgets
: это свойство используется как массив для хранения всех документов, отфильтрованных в ходе поиска.
budget
: выбранный финансовый документ, который можно редактировать.
client
: выбранный клиент, данные которого можно редактировать.
state
: выбранное состояние документа, что позволяет выводить только документы, которым назначено это состояние.
search
: поисковый фильтр, использованный при поиске.
budgets
: все документы, полученные из API.
clients
: все карточки клиентов, полученные из API.
budgetHeaders
: массив, используемый для вывода таблицы документов.
clientHeaders
: массив, хранящий текст, используемый для вывода таблицы клиентов.
budgetsVisible
: используется для указания того, выводится ли список документов или клиентов.
snackbar
: используется для показа панели уведомлений.
timeout
: тайм-аут панели уведомлений.
message
: сообщение, выводимое в панель уведомлений.
fab
: состояние плавающей кнопки, по умолчанию установлено вfalse
.
listPage
: используется для проверки того, находимся ли мы на странице списка, по умолчанию установлено вtrue
.
createPage
: используется для проверки того, находимся ли мы на странице создания элемента, по умолчанию установлено вfalse
.
editPage
: используется для проверки того, находимся ли мы на странице редактирования элемента, по умолчанию установлено вfalse
.
budgetCreation
: используется для проверки того, создаём ли мы запись о клиенте или новый финансовый документ, по умолчанию установлено вtrue
.
budgetEdit
: используется для проверки того, редактируем ли мы карточку клиента или финансовый документ, по умолчанию установлено вtrue
.
snackColor
: цвет панели уведомлений.
Тут, так же, как в одном из примеров выше, назначен обработчик события жизненного цикла
mounted
, в нём мы загружаем все документы и все данные по клиентам.В этот компонент, к полю списка, добавлена функция
watch
. Спасибо @mrmonkeytech за то, что предложил воспользоваться здесь регулярными выражениями (я эту часть проекта чрезмерно усложнил).Здесь мы улучшили все методы и добавили множество новых.
- В методе
getAllBudgets
добавлены новые параметры кdataParser
, теперь мы вызываемerrorHandler
в блокеcatch
. То же самое касается и методаgetAllClients
.
- В компонент добавлены методы
getBudget
иgetClient
, которые ответственны за загрузку лишь выбранных элементов из API.
- Метод
enableEdit
принимает, в качестве параметра, строку, и перенаправляет нас на страницу редактирования соответствующего элемента.
- Методы
saveBudget
иsaveClient
используются, соответственно, для сохранения документов и карточек клиентов в базе данных.
- Метод
fixClientNameAndUpdate
используется для задания правильного имени клиента, основанного на егоID
, и для обновления документа в базе данных путём вызова методаupdateBudget
.
- Метод
updateBudget
используется для обновления документов в базе данных.
- Метод
updateClient
используется для обновления карточек клиентов в базе данных.
- Метод
deleteItem
представляет собой универсальную функцию для удаления элементов из базы данных. Он принимает выбранный элемент, представленный параметромselected
, параметрitems
(список документов или клиентов), и строковой параметрapi
.
- Метод
errorHandler
применяется для обработки ошибок.
- Метод
removeItem
используется в методеdeleteItem
для удаления элемента из интерфейса приложения после того, как он удалён из базы данных.
- Метод
dataParser
остаётся таким же, как был, его мы не изменили.
- Метод
resetFields
используется для сброса всех элементов в состояние по умолчанию после создания нового элемента. В результате пользователь может добавить столько документов или записей о клиентах, сколько нужно, без необходимости самостоятельно очищать заполненные поля после каждого сохранения нового объекта.
- Метод
selectState
используется для выбора нужного состояния документа из элементаv-select
компонентаHeader
и для фильтрации списка на основе выбранного состояния.
- Метод
getBudgetsByState
используется в методеselectState
для загрузки только тех финансовых документов, состояние которых соответствует выбранному.
Стиль компонента Home
<style lang="scss">
@import "./../../assets/styles";
.l-home {
background-color: $background-color;
margin: 25px auto;
padding: 15px;
min-width: 272px;
}
.snack__content {
justify-content: center !important;
}
</style>
Итоги
На этом работа над веб-приложением Budget Manager завершена. Вот как оно выглядит.
Напомним, что опробовать его в действии можно здесь, а посмотреть полный код — здесь.
Надеемся, то, чему вы научились, осваивая это руководство, пригодится вам при разработке ваших собственных проектов.
Уважаемые читатели! Пригодилось ли вам на практике то, что вы узнали из этой серии материалов?