STEP12：Laravel5.7 + Vue2.5 で論理削除

vueからアップロードしてLaravelで保存したデータを論理削除できるようにしてみます
検索時に「論理削除したデータも含める」かも指定可能に

論理削除する対象データは関連記事の給与明細データ
ついでのついでに明細データの公開フラグも更新できるようにしときます



環境設定他関連記事はこちら
[Laravel + Vue + Vuetify で業務サイト作ってみる](https://qiita.com/nobu-maple/items/d1e7170d62ab07890a7a)


# ＤＢテーブル

論理削除するのはアップした給与明細のCSVファイルを管理するテーブル 「csv_payslips」
テーブルにはすでに論理削除用のカラムを設定済み

```php:database/migrations/yyyy_mm_dd_hhmmss_create_csv_payslips_table.php
～～～
class CreateCsvPayslipsTable extends Migration
　～～～
        Schema::create('csv_payslips', function (Blueprint $table) {
　　　　　～～～
            $table -> softDeletes();
～～～
```




# モデル

テーブル「csv_payslips」に対応するモデルにも論理削除に必要な項目を設定済み

```php:app/CsvPayslip.php
    <?php

    namespace App;

    use Illuminate\Database\Eloquent\Model;
★  use Illuminate\Database\Eloquent\SoftDeletes;
    use Illuminate\Support\Facades\Crypt;

    class CsvPayslip extends Model
    {
        // 論理削除有効化
★      use SoftDeletes;
★      protected $dates = ['deleted_at'];

～～～

```


# 削除ボタンを追加

アップした給与明細のCSVファイルの一覧画面に「削除」と「公開」のボタンを追加します

削除済みや公開済みの時に不活性化したりなどでちょっとめんどくさい記述になってます。。
もっときれいに書きたいな。。

```html:resources/js/components/Admin/PayslipComponent.vue
～～～
      <v-data-table
        :headers="headers"
        :items="tabledata"
        :pagination.sync="pagination"
        :rows-per-page-items='[5,10,20,{"text":"All","value":-1}]'
        :loading="loading"
        :search="search"
        class="elevation-0 p-1"
      >
        <v-progress-linear slot="progress" color="blue" indeterminate></v-progress-linear>

        <template slot="items" slot-scope="props">
          <tr :class="{'pink--text': (props.item.deleted_at != null)}">
            <td class="text-xs-center" xs1>{{ (props.index + 1) + (pagination.page - 1) * pagination.rowsPerPage }}</td>
            <template v-for="n in (headers.length - 1)">

★１          <td v-if="headers[n].text != 'アクション'"
                :class="'text-xs-' + headers[n].align"
                style="white-space: nowrap;"
                v-text="props.item[headers[n].value]"
              ></td>

★１          <td v-else
                :class="'text-xs-' + headers[n].align"
                style="white-space: nowrap;"
              >
★２            <v-tooltip v-if="props.item.published_at == null" right :color="(props.item.deleted_at == null) ? 'success' : 'gray'">
                  <v-btn fab small  flat @click="dialog_open(props.item, 'pub')" slot="activator"
                    :disabled="props.item.deleted_at != null"
                  >
                    <v-icon color="success">lock</v-icon>
                  </v-btn>
                  <span>公開</span>
                </v-tooltip>
                <v-btn v-else fab small flat disabled>
                  <v-icon color="grey lighten-1">lock_open</v-icon>
                </v-btn>

★３            <v-tooltip right :color="(props.item.deleted_at == null) ? 'error': 'gray'">
                  <v-btn fab small flat @click="dialog_open(props.item, 'del')" slot="activator"
                    :disabled="props.item.deleted_at != null">
                    <v-icon color="error">delete</v-icon>
                  </v-btn>
                  <span>{{(props.item.deleted_at == null ? '削除' : '削除済')}}</span>
                </v-tooltip>
            </td>
            </template>
          </tr>
        </template>
      </v-data-table>
～～～
```

★１　headers でアクションを指定したところにアクションボタンを配置
★２　「公開」ボタン　色は「success」で指定  　ボタンを押したら dialog_open を呼び出し
★３　「削除」ボタン　色は「error」で指定　　ボタンを押したら dialog_open を呼び出し

![z20.png](https://qiita-image-store.s3.amazonaws.com/0/153259/27952d85-4e35-53c3-4d41-46f96f956d11.png)




# 確認ダイアログの追加

間違ってボタン押したときに問答無用で削除されたりすると悲しすぎるので、確認用のダイアログを追加します
ダイアログは画面の外をクリックしても消えない モーダル で表示
　(ダイアログに persistent を付けてモーダルタイプに)

削除と公開と同じダイアログを使いまわします

## ダイアログレイアウト部分

```html:resources/js/components/Admin/PayslipComponent.vue
～～～
    <!-- 確認ダイアログ -->
    <v-dialog v-model="dialog" width="500" persistent>
      <v-card>
        <v-toolbar :color="d.titlecolor" dark>
          <v-toolbar-title>{{ d.title }}</v-toolbar-title>
        </v-toolbar>
        <v-card-text class="subheading">
          <span v-html="d.body"></span>
          <br>よろしいですか？
        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
          <v-btn flat block @click="dialog_no()" > いいえ </v-btn>
          <v-spacer></v-spacer>
          <v-btn flat block @click="dialog_yes()" :color="d.titlecolor"> はい </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
  </v-flex>
</template>

<script>
～～～
    data: () => ({
　　　　～～～
      // dialog
      dialog: false,
      d: {
        title: '',
        titlecolor: '',
        icon: '',
        type: '',
        item: [],
      },

    }),

```

![z22.png](https://qiita-image-store.s3.amazonaws.com/0/153259/1aa7af60-51d8-f1ca-b80f-d0d62631a26e.png)


![z23.png](https://qiita-image-store.s3.amazonaws.com/0/153259/e465a3a0-bc85-0776-b8d5-69a1aa90bfd4.png)





## ダイアログ制御部分

削除の時と公開の時とで表示する文言を変えてます
ダイアログで「はい」を押したらLaravelへ更新要求を投げます（ this.csvUpdate ）
ダイアログで「いいえ」を押したら何もしないで閉じます（変数の初期化だけ）


```javascript:resources/js/components/Admin/PayslipComponent.vue
～～～
      dialog_open(item, type) {
        if (process.env.MIX_DEBUG) console.log('Payslip Component Dialog Open')
        this.d.type = type
        this.d.item = item
        this.d.body = 'CSV ID：' + item.id + '<br>' + '対象年月：' + item.ym + '<br>' + 'ファイル：' + item.filename + '<br><br>'
        if (type == 'pub') {
          this.d.title = '明細情報を公開します'
          this.d.titlecolor = 'success'
          this.d.body += '対象のデータを公開します。<br>公開後は非公開とすることはできません。'
        }
        else {
          this.d.title = '明細情報を削除します'
          this.d.titlecolor = 'error'
          this.d.body += '対象のデータを削除します。'
        }
        this.dialog = true
      },

      dialog_yes() {
        if (process.env.MIX_DEBUG) console.log('Payslip Component Dialog YES')
        this.dialog = false
        var url = '/api/admin/payslip/publish'
        if (this.d.type == 'del') { url = '/api/admin/payslip/delete' }
        this.csvUpdate(url, this.d.item.id)
        this.d.type = ''
        this.d.item = []
      },

      dialog_no() {
        if (process.env.MIX_DEBUG) console.log('Payslip Component Dialog NO')
        this.dialog = false
        this.d.type = ''
        this.d.item = []
      },
～～～

```




# 削除時の処理  vue側

削除の時と公開の時とで処理は共用（呼び出しURLだけ変えてます）
削除時：/api/admin/payslip/delete
公開時：/api/admin/payslip/publish

```javascript:resources/js/components/Admin/PayslipComponent.vue
～～～
      // CSVの更新
      csvUpdate(url, id) {
        if (process.env.MIX_DEBUG) console.log('Payslip Component CSV Update')

        // パラメータ設定
        var params = new URLSearchParams()
        params.append('id', id)

        // 更新要求
        axios.post(url, params)

        // 検索結果［正常］
        .then( function (response) {
          if (process.env.MIX_DEBUG) console.log(response)
          this.reload()
        }.bind(this))

        // 検索結果［異常］
        .catch(function (error) {
          console.log(error)
          if (error.response) {
            if ([401, 419].includes(error.response.status)) {
              this.$emit('axios-logout')
            }
            else {
              alert('ERROR ' + error.response.status + ' ' + error.response.statusText)
            }
          }
          else {
            alert('ERROR ' + error)
          }
        }.bind(this))
      },
～～～
```




# 削除時の処理  Laravel側

更新要求を受け取るLaravel側
router側で Adminしか触れないURLに指定するのですごく単純に書いちゃってます
公開日時の設定は Carbon です

```php:app/Http/Controllers/PayslipController.php
    public function delete(Request $request)
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());

        // 対象データ削除
        $csv_payslip = CsvPayslip::find($request -> id);
        if ($csv_payslip) {
            $csv_payslip -> delete();
        }
        return;
    }


    public function publish(Request $request)
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());

        // 更新データ生成
        $data['published_at'] = Carbon::now();
        $data['status'] = '1';

        // 対象データ更新
        $csv_payslip = CsvPayslip::find($request -> id);
        if ($csv_payslip) {
            $csv_payslip -> fill($data) -> save();
        }
        return;
    }
```

これで対象データを論理削除可能になりました
ついでに「公開」も

論理削除済みデータをさらに論理削除処理しようとしたりとかのデータチェック、エラー処理等はもろもろ省いてます
何らかのエラーが発生したらたいていは Laravelがブラウザに通知してくれるので。。
あまり使わない？機能だし。。


# Laravel ルートの追加

Admin 権限だけがアクセス可能なように設定

```php:routes/web.php
～～～

// Admin
Route::group( ['middleware' => ['auth', 'can:admin']], function() {

　～～～
  // Payslip
  Route::post('/api/admin/payslip/csvlist', 'PayslipController@csvlist')->name('admin/payslip/csvlist');
  Route::post('/api/admin/payslip/upload', 'PayslipController@upload')->name('admin/payslip/upload');
★  Route::post('/api/admin/payslip/publish', 'PayslipController@publish')->name('admin/payslip/publish');
★  Route::post('/api/admin/payslip/delete', 'PayslipController@delete')->name('admin/payslip/delete');

～～～

```



# 検索時に削除済みを含めるかを指定可能に

論理削除は可能になったけど、論理削除したデータをまた確認したい！？って要望に応えるために、削除済みデータも検索可能にしときます


```html:resources/js/components/Admin/PayslipComponent.vue
～～～
      <v-layout row wrap class="mx-3 my-2">
        <!-- 共通： 対象年月（カレンダーで月を選択）-->
        <v-flex xs2 md4 lg3>
          <v-menu
            ref="menu"
            v-model="menu"
            :return-value.sync="target.ym"
            :close-on-content-click="false"
            :nudge-right="20"
            lazy transition="scale-transition" offset-y full-width max-width="290px" min-width="290px"
            show-current="true"
          >
            <v-text-field
              readonly
              clearable
              autofocus
              slot="activator"
              v-model="target.ym"
              label="対象年月"
              placeholder="明細の対象年月を選択"
              :hint="'明細の対象年月を選択' + (searchTab?'(指定なしで全期間対象)':'')"
            ></v-text-field>
            <v-date-picker v-model="target.ym" type="month" no-title scrollable locale="ja">
              <v-spacer></v-spacer>
              <v-btn flat color="primary" @click="menu = false">Cancel</v-btn>
              <v-btn flat color="primary" @click="$refs.menu.save(target.ym)">OK</v-btn>
            </v-date-picker>
          </v-menu>
        </v-flex>

★      <!-- 検索： 削除済みデータも検索に含める -->
        <v-flex xs5 md4 lg3 v-if="searchTab">
          <v-switch
            :label="`削除済データ ${target.deleted ? '対象' : '対象外'}`"
            v-model="target.deleted"
            color="error"
            class="px-2"
            hint="削除済みデータも検索したい場合に指定"
          ></v-switch>
        </v-flex>

      </v-layout>
～～～

```

★部分を追加
年月指定の後ろにスイッチのイメージを追加して、「検索」処理時にのみ表示するようにしときます

![z21.png](https://qiita-image-store.s3.amazonaws.com/0/153259/2cd3b398-7326-6244-2d28-81917ace35f3.png)



# 検索時の処理 vue側

検索時に追加した「削除済含」フラグを渡すように改修

```javascript:resources/js/components/Admin/PayslipComponent.vue

～～～
    data: () => ({
　　　～～～
      target: {
        ym: '',
★      deleted: false,
      },
～～～
      // 登録済みCSVのリストをサーバから取得する
      getCsvPayslip() {
        if (process.env.MIX_DEBUG) console.log('Payslip Component getCsvPayslip')

        // 初期化
        this.initList()

        // 検索パラメータ設定
        var params = new URLSearchParams()
        params.append('ym', (this.target.ym ? this.target.ym : ''))
★      params.append('deleted', this.target.deleted)

～～～
```

★ 部分を追記

ついでにステータス表示も「削除」を追加しときます

```javascript:resources/js/components/Admin/PayslipComponent.vue

      setStatus() {
        var wk = '不明'
        for (var i=0; i<this.tabledata.length; i++) {
          if (this.tabledata[i].status) {
            wk = '不明'
            if (this.tabledata[i].status == 0) { wk = '非公開' }
            else if (this.tabledata[i].status == 1) { wk = '公開' }
★          if (this.tabledata[i].deleted_at != null ) { wk = '削除' }
            this.tabledata[i].status = wk
          }
        }
      },
```



# 検索時の処理  Laravel側

削除済みも含める場合は 「 withTrashed 」を指定するように改修

```php:app/Http/Controllers/PayslipController.php

    public function csvlist(Request $request)
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());

        // INIT
        $where = array();

        // 検索条件確認 - 対象年月（未指定の場合は全期間を対象とする）
        $ym = $this -> getYM($request -> all(), false);
        if (is_object($ym)) { return $ym; } // 期間エラー
        if ($ym) {
            $where['ym'] = $ym;
        }

        if ($where) {
★          if ($request -> deleted == 'true') {
                $CsvPayslips = CsvPayslip::withTrashed() -> where($where) -> get();
            }
            else {
                $CsvPayslips = CsvPayslip::where($where)->get();
            }
        }
        else {
★          if ($request -> deleted == 'true') {
                $CsvPayslips = CsvPayslip::withTrashed() -> get();
            }
            else {
                $CsvPayslips = CsvPayslip::all();
            }
        }
        return ['data' => $CsvPayslips];
    }

```

★ 部分で判定しているけど、すごく。。美しくない。。。
もっとキレイなコードにならないかな。。


### 改修 (2019/1/14)

もっときれいに書き直してみた

```php:app/Http/Controllers/PayslipController.php

    public function csvlist(Request $request)
    {
        Log::Debug(__CLASS__.':'.__FUNCTION__, $request->all());

        // 検索条件指定準備
        $query = CsvPayslip::query();

        // -- 削除済みを含む
        if (array_key_exists('deleted', $request -> all())) {
          if ($request -> deleted == 'true') {
            $query -> withTrashed();
          }
        }

        // -- 対象年月（未指定の場合は全期間を対象とする）
        $ym = $this -> getYM($request -> all(), false);
        if (is_object($ym)) { return $ym; } // 期間エラー
        if ($ym) {
          $query -> where('ym', '=', $ym);
        }

        // データ取得 orderby 年月 / id
        $CsvPayslips = $query -> orderBy('ym','desc')
                              -> orderBy('id', 'desc')
                              -> get();

        // 検索結果を戻す
        return ['data' => $CsvPayslips];
    }

```

調べてみたら 「 ::query() 」なるものが使えそうってことで書き直してみたら
すごくスッキリした




## 削除済みデータも含めた検索結果

削除済みデータは色を変えて表示するようにしときました

![z20.png](https://qiita-image-store.s3.amazonaws.com/0/153259/54218a85-61bd-0bda-69e4-8a9c88d976e4.png)


---

以上
論理削除の処理と論理削除したデータの検索ができるようになりました
記事ではソースが細切れなので github でソース全文を見てみると良いかもです


今回もソースはこちら
https://github.com/u9m31/u9m31/tree/step12