4
1

More than 3 years have passed since last update.

LaravelとVue.jsを使った見積作成アプリ その2

Last updated at Posted at 2020-12-29

前回の復習

前回は見積一覧ページと見積編集ページのコントローラーを作成しました。今回はVue.jsを使用し見積編集ページのテンプレートを作成します。

Vue.jsの導入

以下のサイトからnodeをインストールします。
https://nodejs.org/ja/download/
その後ライブラリを使ってnpmをインストールします。

$ npm install

これから、resoueces/js/app.jsの以下の部分を編集することで内容を追加していきます。

app.js
const app = new Vue({
    el: '#app'
});

ターミナルで以下のコマンドを実行することでコンパイルが可能です。

$ npm run dev

コントローラーの作成

Vue.jsの準備はできましたが、商品を保存するコントローラーができていないため、コントローラーを追加します。

$ php artisan make:controller ItemController
ItemController.php
<?php

namespace App\Http\Controllers;

use App\Estimate;
use App\Item;
use Illuminate\Http\Request;

class ItemController extends Controller
{
    public function create(Request $request)
    {
        $estimate = $request->input('estimate');

        $items = $request->items;

        $delete_items_id = $request->delete_items;

        for($i=0; $i<count($delete_items_id); $i++){
            $delete_item = Item::find($delete_items_id[$i]);
            $delete_item->delete();
        }

        for($i=0; $i<count($items); $i++) {
            if (!empty($items[$i]['id'])) {
                $current_item = Item::find($items[$i]['id']);

                $current_item->name = $items[$i]['name'];
                $current_item->unit = $items[$i]['unit'];
                $current_item->quantity = $items[$i]['quantity'];
                $current_item->unit_price = $items[$i]['unit_price'];
                $current_item->other = $items[$i]['other'];
                $current_item->save();
            } else {
                $new_item = new Item();

                if(!empty($items[$i]['name'])) $new_item->name = $items[$i]['name'];
                $new_item->estimate_id = $estimate;
                if(!empty($items[$i]['unit'])) $new_item->unit = $items[$i]['unit'];
                if(!empty($items[$i]['quantity'])) $new_item->quantity = $items[$i]['quantity'];
                if(!empty($items[$i]['unit_price'])) $new_item->unit_price = $items[$i]['unit_price'];
                if(!empty($items[$i]['other'])) $new_item->other = $items[$i]['other'];
                $new_item->save();
            } 
        }
        $items = Item::where('estimate_id', $estimate)->get();
        return $items;
    }

    public function get(Request $request)
    {
        $estimate = $request->input('estimate');
        $items = Item::where('estimate_id', $estimate)->get();

        if ($items->isEmpty()) {
            $item = new Item();

            $item->estimate_id = $estimate;
            $item->name = null;
            $item->unit = null;
            $item->quantity = null;
            $item->unit_price = null;
            $item->other = null;
            $item->save();

            return [$item];
        } else {
            return $items;
        }
    }
}

まずgetメソッドでは、クエリパラメータから見積IDを取得し、一致する商品を返します。もし、新規作成時など商品が1つも登録されていない場合は全て空欄の商品をひとつ作成し、データを返します。

createメソッドでは新規作成、編集、削除を一度に行えるようにします。登録するデータはJSON形式で受け取りますが、削除するデータの商品IDもdelete_itemsという名前で受け取ります。最初のfor文でdelete_itemsに追加された商品を削除し、次のfor文で、商品を保存・編集しています。

ルーティングの追加

今回はroutesディレクトリのapi.phpに設定を追加していきます。

api.php
Route::group(['middleware' => 'api'], function(){
    Route::get('get', 'ItemController@get');
    Route::post('create', 'ItemController@create');
    Route::post('edit', 'ItemController@edit');
});

見積編集ページのテンプレートを作成

計算して動的に表現したい金額や、商品の処理をVue.jsに任せています。

edit.blade.php
@extends('layout')

@section('content')
  <div id="app">
    <main>
      <div class="container">
        <form id="estimate_information" action="{{ route('estimates.edit', ['estimate' => $estimate->id]) }}" method="POST">
          @csrf
          <div class="row">
            <div class="col">
              <div class="form-group row">
                <a>宛先</a>
                <input type="text" name="customer" value="{{ $estimate->customer }}" class="form-control">
              </div>
              <p>税抜合計金額:<input type="text" class="form-control" :value="totalPrice | priceLocaleString"></p>
              <p>消費税:<input type="text" class="form-control" :value="taxPrice | priceLocaleString"></p>
              <p>御見積合計金額:<input type="text" class="form-control" :value="totalPriceWithTax | priceLocaleString"></p>
            </div>
            <div class="col">
            <div class="form-group row">
                <label class="col-md-2 col-form-label">見積日:</label>
                <div class="col-md-10">
                  <input type="text" name="estimated_at" id="estimated_at" value="{{ $estimate->estimated_at }}" class="form-control">
                </div>
              </div>
              <div class="form-group row">
                <label class="col-md-2 col-form-label">件名:</label>
                <div class="col-md-10">
                  <input type="text" name="title" value="{{ $estimate->title }}" class="form-control">
                </div>
              </div>
              <div class="form-group row">
                <label class="col-md-2 col-form-label">納入期限:</label>
                <div class="col-md-10">
                  <input type="text" list="deadline_list" name="deadline_at" value="{{ $estimate->deadline_at }}" class="form-control">
                  <datalist id="deadline_list">
                    @foreach(['御打ち合わせによる', '受注後1週間以内', '受注後1ヶ月以内'] as $value)
                      <option value="{{ $value }}">
                    @endforeach
                  </datalist>
                </div>
              </div>
              <div class="form-group row">
                <label class="col-md-2 col-form-label">納入場所:</label>
                <div class="col-md-10">
                  <input type="text" list="location_list" name="location" value="{{ $estimate->location }}" class="form-control">
                  <datalist id="location_list">
                    @foreach(['御打ち合わせによる', '貴社指定場所'] as $value)
                      <option value="{{ $value }}">
                    @endforeach
                </div>
              </div>
              <div class="form-group row">
                <label class="col-md-2 col-form-label">取引方法:</label>
                <div class="col-md-10">
                  <input type="text" list="transaction_list" name="transaction" value="{{ $estimate->transaction }}" class="form-control">
                  <datalist id="transaction_list">
                    @foreach(['御打ち合わせによる', '月末締め、翌月末にて'] as $value)
                      <option value="{{ $value }}">
                    @endforeach
                </div>
              </div>
              <div class="form-group row">
                <label class="col-md-2 col-form-label">有効期限:</label>
                <div class="col-md-10">
                  <input type="text" list="effectiveness_list" name="effectiveness" value="{{ $estimate->effectiveness }}" class="form-control">
                  <datalist id="effectiveness_list">
                    @foreach(['発行より1ヶ月以内', '発行より3ヶ月以内'] as $value)
                      <option value="{{ $value }}">
                    @endforeach
                </div>
              </div>
            </div>
          </div>
        </form>
      </div>
      <table class="table table-bordered">
        <thead  class="thead-dark">
          <tr>
            <th>商品名</th>
            <th>単位</th>
            <th>数量</th>
            <th>単価</th>
            <th>金額</th>
            <th>備考</th>
            <th>追加</th>
            <th>削除</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(item, index) in listItems" :key="item.id">
            <td>
              <input type="text" class="form-control" v-model="item.name">
            </td>
            <td>
              <input type="text" class="form-control" v-model="item.unit">
            </td>
            <td>
              <input type="text" class="form-control" v-model="item.quantity">
            </td>
            <td>
              <input type="text" class="form-control" v-model="item.unit_price" @keyup.enter="append">
            </td>
            <td>
              <input type="text" class="form-control" :value="itemPrice(item.quantity, item.unit_price, index) | priceLocaleString">
            </td>
            <td>
              <input type="text" class="form-control" v-model="item.other">
            </td>
            <td>
              <span @click="append"><i class="fas fa-plus"></i></span>
            </td>
            <td>
              <span @click="remove(item.id, index)"><i class="fas fa-trash-alt"></i></span>
            </td>
          </tr>
        </tbody>
      </table>
    </main>
    <footer class="fixed-bottom bg-dark">
      <nav class="my-navbar">
        <div class="container">
          <div class="row">
            <div class="col-md-3">
              <button type="submit" form="estimate_information" @click="saveItems">
                保存
              </button>
            </div>
            <div class="col-md-3">
              <a href="{{ route('pdf.index', ['estimate' => $estimate->id]) }}" onclick="window.open(this.href, '_blank'); return false;">
                <button>印刷</button>
              </a>
            </div>
            <div class="col-md-3">
              <a href="{{ route('estimates.index') }}">
                <button onclick="return confirm('保存されていないデータは消えますがよろしいですか?')">見積一覧に戻る</button>
              </a>
            </div>
            <div class="col-md-3">
              <form action="{{ route('estimates.delete', ['estimate' => $estimate->id]) }}" method="POST">
                @csrf
                <button type="submit" onclick="return confirm('削除します。よろしいですか?')">削除</button>
              </form>
            </div>
          </div>
        </div>
      </nav>
    </footer>
  </div>
@endsection

@section('scripts')
  <script src="{{ asset('js/app.js')}}"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
  <script src="https://npmcdn.com/flatpickr/dist/flatpickr.min.js"></script>
  <script src="https://npmcdn.com/flatpickr/dist/l10n/ja.js"></script>
  <script>
    flatpickr(document.getElementById('estimated_at'), {
      locale: 'ja',
      dateFormat: "Y/m/d"
    });
  </script>
@endsection

Vue.js以外で覚えておきたいのは以下の部分です。

edit.blade.php
<input type="text" list="deadline_list" name="deadline_at" value="{{ $estimate->deadline_at }}" class="form-control">
<datalist id="deadline_list">
   @foreach(['御打ち合わせによる', '受注後1週間以内', '受注後1ヶ月以内'] as $value)
     <option value="{{ $value }}">
   @endforeach
</datalist>

datalistタグは、フォームの入力欄などで入力候補となるデータリストを定義します。各データのリスト項目は、optionタグで定義し指定された値がユーザーに対して入力候補として提案表示されます。キーワードの入力欄はinputタグで作成しますが、 inputタグのlist属性の値と、datalistタグのid属性の値を同じにして、入力欄とデータリストを関連付けます。今回は値を直に記述していますが、後で管理しやすくするために変数に入れておくことが可能です。

次にapp.jsを作成します。

app.js
const app = new Vue({
    el: '#app',
    data: {
        items: [],
        items_price: [],
        deleted_items: []
    },
    mounted: function(){
        var query = window.location.search.slice(1); 
        Axios.get('/api/get?' + query).then(response => this.items = response.data);
    },
    computed: {
        listItems: function(){
            return this.items.sort((a, b) => {
                return (a.id < b.id) ? -1 : (a.id > b.id) ? 1 : 0;
              });
        },
        totalPrice: function(){
            return this.items_price.reduce(function(sum, element){
                return sum + element;
            }, 0);
        },
        taxPrice: function(){
            let tax = this.totalPrice * 0.1;
            return Math.floor(tax);
        },
        totalPriceWithTax: function() {
            return this.totalPrice + this.taxPrice;
        }
    },
    filters: {
        priceLocaleString: function(value) {
            if(value){
                return value.toLocaleString();
            }
        }
    },
    methods: {
        itemPrice: function(quantity, unit_price, index) {
            let calculationPrice = 0;
            if(quantity && unit_price) {
                calculationPrice = quantity * unit_price;
            }
            this.items_price.splice(index, 1, calculationPrice);
            return calculationPrice;
        },
        append: function(event) {
            this.items.push({});
        },
        remove: function(id, index) {
            this.deleted_items.push(id);
            this.items.splice(index, 1);
        },
        saveItems: function() {
            var query = window.location.search.slice(1); 
            var add_items = Object.assign({},this.items);
            var remove_items = Object.assign({},this.deleted_items);
            this.deleted_items = [];
            Axios.post('/api/create?' + query, {items: add_items, delete_items: remove_items}).then(response => this.items = response.data);

        }
    }
});

mountedは新たに作成される要素に対して、インスタンスがマウントされたちょうど後に呼ばれます。今回の場合は、マウント後すぐにクエリパラメータから見積IDを取得し、非同期通信によりItemControllerのgetメソッドを呼び出し、返ってきた値をdataのitemsに保存しています。

computedはあるデータから派生するデータをプロパティとして公開する仕組みです。データそのものになんらかの処理を与えたものをプロパティにしたい場合に使用し、今回は商品データの並び替えと金額処理関係をテンプレートに記述するために使います。
最初のlistItemsメソッドでは取得したデータをid順に並び替えています。totalPriceメソッドでは後述するitemPriceメソッドによって追加される配列のitems_priceの合計を算出します。また、computedで定義したプロパティはdetaと同様にthis経由で参照可能であることを利用し、消費税と見積合計金額も算出しています。

filtersは凡用的なテキストフォーマット処理を適用する仕組みです。今回は金額を3桁ごとにカンマ区切りをする処理を書きましたが、DateオブジェクトをYYYY/mm/ddという形式に変換する処理や、0.5といった数値を50%というテキストに変換する処理などにも使われます。

methodsは名前の通りVueインスタンスのメソッドとして機能します。定義されたメソッドはビューのイベントが発生した時に呼び出したりテンプレート内でも{{ メソッド名() }}のようにテキスト展開の式で呼び出すことが可能です。
itemPriceメソッドでは、商品の数量と単価が入力されている場合に金額を計算して表示すると同時にspliceという関数を利用しitems_priceに金額を追加しています。appendメソッドとremoveメソッドでは商品の列を追加、削除しています。削除の際、removeメソッドでは画面上の表示から消すだけで実際のデータは消えていません。removeメソッドはdeleted_itemsに削除する商品IDを保存するだけで、実際に消えるタイミングはsaveItemsメソッドの発火時になります。saveItemsメソッドはaxiosを利用し、商品のデータと削除する商品IDを同時に送ります。これにより、商品の登録、編集、削除を可能にしています。

次回

今回でVue.jsを利用した見積編集ページの作成が完了しました。次回はPDF表示ページとユーザー認証関係のページを作成していきます。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1