Перед вами перевод пятой части руководства по разработке веб-решений на базе Node.js, Vue.js и MongoDB. В первой, второй, третьей и четвёртой частях мы рассказывали о поэтапном создании клиентской и серверной частей приложения Budget Manager. Те, кому не терпится увидеть в действии то, что в итоге получилось у автора этого материала, могут заглянуть сюда. Кроме того, вот GitHub-репозиторий проекта. Если вы — из тех, кто ценит строгую типизацию, то здесь и здесь находятся результаты переноса Budget Manager на TypeScript.



Сегодня работа над этим учебным проектом завершится. А именно, в данном материале пойдёт речь о разработке страниц по добавлению в систему записей о новых клиентах и финансовых документах, а также о создании механизмов для редактирования этих данных. Здесь же мы рассмотрим некоторые улучшения 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 завершена. Вот как оно выглядит.


Напомним, что опробовать его в действии можно здесь, а посмотреть полный код — здесь.
Надеемся, то, чему вы научились, осваивая это руководство, пригодится вам при разработке ваших собственных проектов.

Уважаемые читатели! Пригодилось ли вам на практике то, что вы узнали из этой серии материалов?

Комментарии (1)


  1. x893
    17.01.2018 11:51

    Красивая природа. Пожалуй и всё.