📜 ⬆️ ⬇️

Web application on Node and Vue, part 5: finalizing the project

This is a translation of the fifth part of the Node.js, Vue.js, and MongoDB based web solutions development guide. In the first , second , third and fourth parts, we talked about the phased creation of the client and server parts of the Budget Manager application. Those who can not wait to see in action what the author of this material eventually obtained can look here . Also, here is the project 's GitHub repository. If you are one of those who appreciate strong typing, here and here are the results of transferring Budget Manager to TypeScript.



Today, work on this educational project is completed. Namely, this material will deal with the development of pages for adding records of new customers and financial documents to the system, as well as creating mechanisms for editing these data. Here we will look at some API improvements and bring the Budget Manager to a working state.

API refinement


First, go to the models folder and open the budget.js file. Add a description field for the model:
')
 description: {   type: String,   required: true }, 

Now go to the folder app/api and open the file budget.js , which is located in it. Here we are going to edit the data storage function, store , so that new documents are processed correctly, add the edit function, which allows editing documents, add the remove function, which is needed to remove documents, and add the getByState function, which allows filtering documents. Here is the complete code of the file. To view it, expand the corresponding block. In the future, large code snippets will be issued in the same way.

Source
 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; 

We make similar changes to the client.js file from the api folder:

Source
 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; 

And finally, add new routes to the system. To do this, go to the routes folder and open the budget.js file:

Source
 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'))) } 

Let's make similar changes to the client.js file, which is located in the same folder:

Source
 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'))) } 

That's all the changes you need to make to the API.

Router upgrade


Now add new components to the routes. To do this, open the file index.js , located inside the router folder.

Source
 ... // 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   } ] }) … 

Here we imported and defined the Create component and assigned it as the Home component of the route (we will create the component itself below).

Creating new components


CreateCreate Component


Let's start with the Create component. Go to the components/pages folder and create a new file Create.vue .

Source
 <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> 

The first named slot is budget-creation . It represents the component that we will use to create new financial documents. It will only be visible if the budgetCreation property budgetCreation set to true , and editPage is set to false , we pass it all our clients and the saveBudget method.

The second named slot is client-creation . This is the component used to create new customers. It will only be visible when the budgetCreation property budgetCreation set to false , and the editPage is also set to false . Here we pass the saveClient method.

The third named slot is budget-edit . This is the component that is used to edit the selected document. We see it only when the budgetEdit and editPage set to true . Here we transfer all clients, the selected financial document and the fixClientNameAndUpdate method.

And finally, there is the last named slot used to edit customer information. It will be visible when the budgetEdit property budgetEdit set to false and the editPage is set to true . We give him the selected client and the updateClient method.

BudgetThe BudgetCreation Component


Develop a component that is used to create new financial documents. Go to the components folder and create a new folder in it, giving it the name Creation . In this folder, create a component file BudgetCreation.vue .

The component is quite large, we will analyze it in stages, starting with the template.

BudgetCreation Component Template

Here is the code for the component template.
 <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> 

Here we first add the v-select element to the template to set the state of the document, then v-select to select the client we need. Next, we have a v-text-field for entering the title of the document and a v-text-field for displaying the description.

Then we budget.items through the elements of budget.items , which gives us the opportunity to add elements to the document and remove them from it. Here there is a red button that allows you to call the function removeItem , passing it the item that you want to remove.

Further, there are three fields v-text-fields , intended, respectively, for the name of the goods, the price per unit and quantity.

At the end of the series there is a simple span element, in which the subtotal is displayed, subtotal , which is the product of the quantity and price of the product.

Below the list of products there are three more items. This is the blue button that is used to add new items by calling the addItem function, the span element, which shows the total cost of all items that are in the document (the sum of the subtotal indicators of all items), and the green button that is used to save the document to the database. by calling the saveBudget function with passing it, as a parameter, the document we want to save.

The script component BudgetCreation

Here is the code that drives the BudgetCreation component.
 <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> 

In this code, we first get two properties - clients and saveBudget . The source of these properties is the Home component.

Then we define the object and array, which play the role of data. The object is named budget . It is used to create a document, we can add values ​​to it and save it in the database. This object has the properties title (title), description (description), state (state, the default is set to the value of writing ), client (client), total_price (total value of the document), and an array of items . Products have title (title), quantity (quantity), price (price) and subtotal (subtotal).

It also defines an array of document states , states . Its values ​​are used to set the state of the document. These states are: writing , editing , pending , approved , denied and waiting .

Below, after describing the data structures, there are a couple of methods: addItem (to add products) and removeItem (to remove them).

Each time we click on the blue button, the addItem method is addItem , which adds items to the items array inside the budget object.

The removeItem method does the opposite. Namely - when you click on the red button, the specified item is deleted from the items array.

Styles component BudgetCreation

Here are the styles for the component in question.
 <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> 

Now consider the next component.

ClientComponent ClientCreation


This component is essentially a simplified version of the BudgetCreation component BudgetCreation . We are just as done above, we consider it in parts. If you have dealt with the device component of the BudgetCreation , you can easily understand the principles of the ClientCreation component.

ClientCreation Component Template
 <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> 

The script component ClientCreation
 <script> export default {   props: ['saveClient'],   data () {     return {       client: {         name: null,         email: null,         phone: null       }     }   } } </script> 

ClientCreation styles
 <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> 

Now it’s the turn of the BudgetEdit component.

BudgetThe BudgetEdit Component


This component, in fact, is a modified version of the BudgetCreation component already considered. Consider its component parts.

Component template 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> 

The only difference in the BudgetEdit and BudgetCreation component templates is in the save button and the associated logic. Namely, in the BudgetCreation Save is written on it, it calls the saveBudget method. In BudgetEdit this button carries the Update text on it and calls the fixClientNameAndUpdate method.

The script component 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> 

Here it all starts with getting three properties. These are clients , fixClientNameAndUpdate and selectedBudget . The data here is the same as in the BudgetCreation component. Namely, there is the Budget object and the states array.

Next, here you can see the handler event of the life cycle of the component, in which we call the parseBudget method, which we will discuss below. And finally, there is a methods object, in which there are the addItem and removeItem methods already familiar to you from the BudgetCreation component, as well as the new parseBudget method. This method is used to set the value of the budget object to that passed in the selectedBudget property, but we also use it to calculate subtotals for the goods in the document and the total amount for the document.

Component style 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> 

ClientComponent ClientEdit


This component, by analogy with what we have just considered, is similar to the corresponding component used to create clients — ClientCreation . The main difference is that instead of the saveClient method, the updateClient method is updateClient . Consider the device component ClientEdit .

ClientEdit Component Template
 <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> 

The script component 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

Source
 <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> 

. We describe them:


, 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> 

. .


, , , mounted , .

, , watch . @mrmonkeytech , ( ).

.


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> 

Results


- Budget Manager . .


, , — .
, , , , .

Dear readers! , ?

Source: https://habr.com/ru/post/346784/


All Articles