javascript

Веб-приложение на Node и Vue, часть 5: завершение работы над проектом

  • четверг, 18 января 2018 г. в 03:13:25
https://habrahabr.ru/company/ruvds/blog/346784/
  • Разработка веб-сайтов
  • VueJS
  • Node.JS
  • JavaScript
  • Блог компании RUVDS.com


Перед вами перевод пятой части руководства по разработке веб-решений на базе 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 завершена. Вот как оно выглядит.


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

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