学習の備忘録です。タスク管理アプリを作っています。
今回は、編集ボタンを押した際に、編集チケットを消費するかどうか確認するモーダル画面を作っていきます。
最終的には、以下のような挙動になるようにします。
- 「編集チケットを消費するかどうか」の選択モーダルは、当日タスクの時だけ表示される
- 編集チケットを持っているなら、編集ボタン押下後にチケット消費するかの選択モーダルが出る
- 編集チケットを持っていないなら、編集ボタン押下時にアラートが出る
(「編集チケットを持っていません!」など) - チケットを消費するかの選択モーダルで「はい」を押したら編集モーダルへ移行、編集完了したらタスクのアップデートと共に、編集チケットを1枚減らす
- もしチケット消費選択モーダルで「はい」を押しても、編集モーダルでキャンセルしたらチケットは減らないようにする
- チケットを消費するかの選択モーダルで「いいえ」を押したら、そのまま選択モーダルを閉じる
改修するのは主にコントローラーとHTMLとJavaScriptです。
本記事は個人の学習記録です。内容の正確性を保証するものではありませんので、あらかじめご了承ください。
コントローラーの改修:編集チケット数取得のため
編集チケットの数で条件分岐させるなら、まずコントローラーで編集チケット数をデータベースから引っ張ってきて、HTML側に渡す必要があります。
ということで、TaskController.phpの画面表示のためのshowIndexメソッドに、編集チケット数取得のためのコードを追加します。
改修前と後でタブを切り替えて確認してください。改修箇所の背景を変更しています。
//トップ画面表示
public function showIndex(Request $request, $date = null) {
//指定が無ければ今日、あればその日を基準にする
$targetDate = Carbon::parse($date ?? Carbon::today())->toImmutable();
//前後の日付を取得する
$prevDate = $targetDate->subDay()->toDateString(); //前の日
$nextDate = $targetDate->addDay()->toDateString(); //次の日
//表示用にフォーマット
$displayDate = $targetDate->format('Y/m/d');
//ログインユーザー取得
$user = User::findByToken($request);
//表示中の日のタスクを全て取得
$tasks = Task::where('user_id', $user->id)
->whereDate('task_date', $targetDate)
->orderBy('priority', 'asc')
->get();
//登録している日課タスクを全て取得
$masters = DailyMasterTask::where('user_id', $user->id)
->with(['user'])
->get();
//該当日のタスクの中で、日課由来のものが何件あるか数える
$dailyTaskCount = $tasks->whereNotNull('master_id')->count();
//達成率取得
$rate = Task::calAchievementRate($tasks);
//日課タスクがあるのに今日のタスクに登録されていなければ登録処理をする
if ($dailyTaskCount === 0 && $masters->isNotEmpty()) {
try {
// トランザクション開始
DB::beginTransaction();
// 登録処理呼び出し
$tasks = Task::registerDailyTask($masters, $user, $targetDate);
DB::commit();
$tasks = Task::where('user_id', $user->id)
->whereDate('task_date', $targetDate)
->orderBy('priority', 'asc')
->get();
$rate = Task::calAchievementRate($tasks);
} catch (\Exception $e) {
DB::rollback();
}
}
return view('tasks.index', compact('tasks', 'displayDate', 'prevDate', 'nextDate', 'rate'));
}
//トップ画面表示
public function showIndex(Request $request, $date = null) {
//指定が無ければ今日、あればその日を基準にする
$targetDate = Carbon::parse($date ?? Carbon::today())->toImmutable();
//前後の日付を取得する
$prevDate = $targetDate->subDay()->toDateString(); //前の日
$nextDate = $targetDate->addDay()->toDateString(); //次の日
//表示用にフォーマット
$displayDate = $targetDate->format('Y/m/d');
//ログインユーザー取得
$user = User::findByToken($request);
//編集チケット数取得
$userStats = $user->userStat;
$editTickets = $userStats ? $userStats->edit_tickets : 0;
//表示中の日のタスクを全て取得
$tasks = Task::where('user_id', $user->id)
->whereDate('task_date', $targetDate)
->orderBy('priority', 'asc')
->get();
//登録している日課タスクを全て取得
$masters = DailyMasterTask::where('user_id', $user->id)
->with(['user'])
->get();
//該当日のタスクの中で、日課由来のものが何件あるか数える
$dailyTaskCount = $tasks->whereNotNull('master_id')->count();
//達成率取得
$rate = Task::calAchievementRate($tasks);
//日課タスクがあるのに今日のタスクに登録されていなければ登録処理をする
if ($dailyTaskCount === 0 && $masters->isNotEmpty()) {
try {
// トランザクション開始
DB::beginTransaction();
// 登録処理呼び出し
$tasks = Task::registerDailyTask($masters, $user, $targetDate);
DB::commit();
$tasks = Task::where('user_id', $user->id)
->whereDate('task_date', $targetDate)
->orderBy('priority', 'asc')
->get();
$rate = Task::calAchievementRate($tasks);
} catch (\Exception $e) {
DB::rollback();
}
}
return view('tasks.index', compact('tasks', 'displayDate', 'prevDate', 'nextDate', 'rate', 'editTickets'));
}
$userでログインユーザーを取得しているのでそれを利用して、
$userStats = $user->userStat;でそのユーザーのステータスを特定、取得します。
User.phpとUserStat.phpがあり、リレーションさせてUser.phpのuserStat()を呼び出しています。
※1対1のリレーションhasOneを定義
その特定したユーザーステータスの中から、
$editTickets = $userStats ? $userStats->edit_tickets : 0;で編集チケット数を取得します。
普通に$editTickets = $userStats->edit_tickets;じゃだめなの? となるかもしれませんが、これだと$userStatsがnullだった場合が考慮されず、万が一nullだった場合はエラーになってしまいます。
$editTickets = $userStats ? $userStats->edit_tickets : 0;のように三項演算子を使うことで、「$userStatsがnullの場合はとりあえず0を入れといて」という命令にすることができます。
最後のreturn viewの()の中に忘れずに’editTickets’を追加したら、編集チケット数の受け渡しのコード追加は完了です。
HTML側で条件分岐付きのチケット消費確認モーダルを準備する
モーダルを開くボタンがあるindex.blade.phpの該当部分に、以下のように付け足します。
<div id="add-task-modal" class="modal-overlay">
@include('tasks._create_modal', ['tasks' => $tasks, 'displayDate' => $displayDate])
</div>
<div id="edit-task-modal" class="modal-overlay">
@include('tasks._edit_modal', ['tasks' => $tasks, 'displayDate' => $displayDate])
</div>
<div id="add-task-modal" class="modal-overlay">
@include('tasks._create_modal', ['tasks' => $tasks, 'displayDate' => $displayDate])
</div>
<div id="edit-task-modal" class="modal-overlay">
@include('tasks._edit_modal', ['tasks' => $tasks, 'displayDate' => $displayDate])
</div>
<div id="edit-tickets-modal" class="modal-overlay">
@include('tasks._edit_tickets_modal', ['editTickets' => $editTickets])
</div>
@includeというのは、別ファイルとして独立させて作成したHTMLを読み込むためのBladeディレクティブです。
Bladeディレクティブ:
「Blade」というテンプレートエンジン専用の、表示を制御するための命令。あらかじめ用意されている処理を引っ張ってくるのでメソッドに近い概念(だと思います。)
HTMLファイルを新規作成
@include(‘tasks._edit_tickets_modal’, [‘editTickets’ => $editTickets])
上記のBladeディレクティブに則り、新しくHTMLファイルを作成します。
私の場合は、「resources/views」の中に「tasks」フォルダを作り、その中にタスク関連のHTMLファイルを格納しています。
今回もその中に、「_edit_tickets_modal.blade.php」を新規作成しています。
@includeでよそから読み込む前提のHTMLはアンダーバー(_)で始まるファイル名にすると、「あ、これは@include用の独立したHTMLファイルなんだな」とひと目で分かって管理しやすいです。
<div class="modal-content">
@if ($editTickets <= 0)
<div class="edit-tickets-area">
<p>チケットを持っていません!</p>
</div>
<div class="modal-btn">
<button type="button" id="close-tickets-modal" class="modal-btn-primary modal-cancel-btn">いいえ</button>
</div>
@else
<div class="edit-tickets-area">
<p>編集チケットを1枚消費します。よろしいですか?</p>
<p>{{ $editTickets }} → {{ $editTickets - 1 }}</p>
<p>※ここで「はい」を選んでも、編集が完了しない限りチケットは減りません。</p>
</div>
<div class="modal-btn">
<button type="button" id="close-tickets-modal" class="modal-btn-primary modal-cancel-btn">いいえ</button>
<button type="button" class="modal-btn-primary confirm-yes-btn">はい</button>
</div>
@endif
</div>
JavaScript側で条件分岐させる
タスクに関するモーダル表示を一括管理しているJavaScriptのクラスに、edit-tickets-btnとedit-tickets-modal、edit-tickets-modalの分岐を追加していきます。
function manageTaskModal() {
//登録ボタンや編集ボタン、閉じるボタン、モーダル背景を一括監視
$(document).on('click', '.task-create-btn, .task-edit-btn, #close-add-modal, #close-edit-modal, #add-task-modal, #edit-task-modal', function(e) {
//送信ボタンなら何もしない
if ($(e.target).closest('button[type="submit"]').length) return;
e.preventDefault();
//ターゲットとなるモーダルを特定する
//ボタンのクラス名やIDから、操作すべきモーダルを自動判別
let targetModalId = '';
if ($(this).hasClass('task-create-btn') || $(this).is('#add-task-modal, #close-add-modal')) {
targetModalId = 'add-task-modal';
}else if ($(this).hasClass('task-edit-btn') || $(this).is('#edit-task-modal, #close-edit-modal')) {
targetModalId = 'edit-task-modal';
}
const $modal = $('#' + targetModalId);
if (!$modal.length) return;
//表示・非表示の切り替え
if ($(this).hasClass('task-create-btn') || $(this).hasClass('task-edit-btn')) {
//開く処理
$modal.addClass('is-open');
} else if ($(this).is('#close-add-modal, #close-edit-modal') || e.target.id === targetModalId) {
//閉じる処理
$modal.removeClass('is-open');
//リセット処理(登録モーダルかつ閉じた時のみ実行)
if (targetModalId === 'add-task-modal') {
setTimeout(function() {
const $container = $('#add-input-container');
$container.find('.input-row').not(':first').remove();
const $FirstRow = $container.find('.input-row').first();
$FirstRow.find('input[type="text"]').val('');
$FirstRow.find('.priority-select').val('2').removeClass('p-1 p-3').addClass('p-2');
}, 300);
}
}
});
}
function manageTaskModal() {
//登録ボタンや編集ボタン、閉じるボタン、モーダル背景を一括監視
$(document).on('click',
'.task-create-btn, .task-edit-btn, .edit-tickets-btn, .confirm-yes-btn, #close-add-modal, #close-edit-modal, #close-tickets-modal, #add-task-modal, #edit-task-modal, #edit-tickets-modal', function(e) {
//送信ボタンなら何もしない
if ($(e.target).closest('button[type="submit"]').length) return;
e.preventDefault();
//ターゲットとなるモーダルを特定する
//ボタンのクラス名やIDから、操作すべきモーダルを自動判別
let targetModalId = '';
if ($(this).hasClass('task-create-btn') || $(this).is('#add-task-modal, #close-add-modal')) {
targetModalId = 'add-task-modal';
}else if ($(this).hasClass('task-edit-btn') || $(this).is('#edit-task-modal, #close-edit-modal')) {
targetModalId = 'edit-task-modal';
}else if ($(this).hasClass('edit-tickets-btn') || $(this).hasClass('confirm-yes-btn') || $(this).is('#edit-tickets-modal, #close-tickets-modal')) {
targetModalId = 'edit-tickets-modal';
}
const $modal = $('#' + targetModalId);
if (!$modal.length) return;
//表示・非表示の切り替え
if ($(this).hasClass('task-create-btn') || $(this).hasClass('task-edit-btn') || $(this).hasClass('edit-tickets-btn')) {
//開く処理
$modal.addClass('is-open');
} else if ($(this).hasClass('confirm-yes-btn')) {
$modal.removeClass('is-open'); // まずチケット確認画面を閉じる
$('#edit-task-modal').addClass('is-open'); // 次に編集モーダルを開く
}else if ($(this).is('#close-add-modal, #close-edit-modal, #close-tickets-modal') || e.target.id === targetModalId) {
//閉じる処理
$modal.removeClass('is-open');
//リセット処理(登録モーダルかつ閉じた時のみ実行)
if (targetModalId === 'add-task-modal') {
setTimeout(function() {
const $container = $('#add-input-container');
$container.find('.input-row').not(':first').remove();
const $FirstRow = $container.find('.input-row').first();
$FirstRow.find('input[type="text"]').val('');
$FirstRow.find('.priority-select').val('2').removeClass('p-1 p-3').addClass('p-2');
}, 300);
}
}
});
}
まず、イベントデリゲートでクリックを監視する要素の中に、「.edit-tickets-btn」「.confirm-yes-btn」「#edit-tickets-modal」「#close-tickets-modal」を追加します。
次に、「今操作しているのはどのモーダルか」を判別する処理に「edit-tickets-modalだと特定する場合」を追加します。
「edit-tickets-btnクラスを含む要素がクリックされた」または「confirm-yes-btnクラスを含む要素がクリックされた」または「edit-tickets-modalかclose-tickets-modalのidを含む要素がクリックされた」場合に、「今操作しているモーダルはedit-tickets-modalだ」と特定しています。
そして次に、チケット消費確認モーダルで「はい」をクリックした場合の処理を追加します。まずはチケット消費確認モーダルを閉じ、その後タスク編集モーダルを表示させる処理です。
最後に、モーダルを閉じる処理のところに「#close-tickets-modal」を追加します。
コントローラーの改修:編集完了した際にチケット消費させる
コントローラー側で、「編集チケット消費確認モーダル経由でタスクを編集した場合は、編集チケットを1減らす」処理を書いていきます。
編集チケット消費確認モーダルは、当日のタスク画面でしか表示できないようになっているので、「編集しようとしているタスクが当日のタスクなら、編集チケット消費確認モーダルを経由した」と見なします。
つまり、コントローラー側で日付を取得し、「それが当日だと判定された場合のみ編集チケットを消費する」というif分岐を作ります。
以下はTaskControllerのupdateメソッド改修前/改修後です。
//タスク編集
public function update(Request $request) {
$user = User::findByToken($request);
$targetDate = str_replace('/', '-', $request->task_date);
//編集ボタンを押した段階で、画面に残されているIDのリストを取得
$remainingIds = $request->input('task_ids', []);
try {
// トランザクション開始
DB::beginTransaction();
//画面から消されたタスクをDBから削除
Task::where('user_id', $user->id)
->whereDate('task_date', $targetDate)
->whereNotIn('id', $remainingIds)
->delete();
foreach ($request->titles as $index => $title) {
if (empty($title)) continue;
$taskId = $request->task_ids[$index] ?? null;
$priority = $request->priorities[$index] ?? 2;
if ($taskId) {
Task::updateTask($taskId, $title, $priority);
} else {
//+ボタンで新規追加した時の挙動
Task::createTask($user->id, $targetDate, $title, $priority);
}
}
DB::commit();
return redirect()->route('tasks.index', ['date' => $targetDate])
->with('success', '更新しました');
} catch (\Exception $e) {
DB::rollback();
return redirect()->route('tasks.index', ['date' => $targetDate])
->with('error', '更新に失敗しました。');
}
}
//タスク編集
public function update(Request $request) {
$user = User::findByToken($request);
$targetDate = str_replace('/', '-', $request->task_date);
//編集ボタンを押した段階で、画面に残されているIDのリストを取得
$remainingIds = $request->input('task_ids', []);
//今日の日付か判定
$today = now()->toDateString();
$isToday = ($targetDate === $today);
//編集チケットを減らす用にuserStatを引っ張ってくる
$userStats = $user->userStat;
//編集チケットを持っているかの確認用
$hasTickets = $userStats && $userStats->edit_tickets > 0;
try {
// トランザクション開始
DB::beginTransaction();
//当日、かつチケットがある場合
if ($isToday) {
if (!$hasTickets) {
throw new \Exception('チケットが足りません');
}
//ここで編集チケットを1減らす
$userStats->decrement('edit_tickets');
}
//画面から消されたタスクをDBから削除
Task::where('user_id', $user->id)
->whereDate('task_date', $targetDate)
->whereNotIn('id', $remainingIds)
->delete();
foreach ($request->titles as $index => $title) {
if (empty($title)) continue;
$taskId = $request->task_ids[$index] ?? null;
$priority = $request->priorities[$index] ?? 2;
if ($taskId) {
Task::updateTask($taskId, $title, $priority);
} else {
//+ボタンで新規追加した時の挙動
Task::createTask($user->id, $targetDate, $title, $priority);
}
}
DB::commit();
return redirect()->route('tasks.index', ['date' => $targetDate])
->with('success', '更新しました');
} catch (\Exception $e) {
DB::rollback();
return redirect()->route('tasks.index', ['date' => $targetDate])
->with('error', '更新に失敗しました。');
}
}
改修した箇所を分解して解説します。
今日の日付かどうかを判定
//今日の日付か判定
$today = now()->toDateString();
$isToday = ($targetDate === $today);
コードの中のコメントそのままですが、今日の日付かどうかを判定しています。
$today = now()->toDateString();で、その日の日付を取得、
$isToday = ($targetDate === $today);で、操作した画面が当日のものかを判定しています。
ユーザーステータス情報を取得、チケット有無判定
//編集チケットを減らす用にuserStatを引っ張ってくる
$userStats = $user->userStat;
//編集チケットを持っているかの確認用
$hasTickets = $userStats && $userStats->edit_tickets > 0;
$userStats = $user->userStat;にて、DBからuser_statsテーブルのデータを取得しています。
$hasTickets = $userStats && $userStats->edit_tickets > 0;
ここでは、「ユーザーステータスが存在し、かつ、編集チケットが1枚以上ある」場合にtrueが返り、条件に当てはまらなければfalseが返ります。
当日かつチケットがある場合のみ、チケットを1減らす
//当日、かつチケットがある場合
if ($isToday) {
if (!$hasTickets) {
throw new \Exception('チケットが足りません');
}
//ここで編集チケットを1減らす
$userStats->decrement('edit_tickets');
}
まずif ($isToday)で分岐を作り、現在操作している画面が当日のものなのか(編集チケット消費確認モーダルを経由しているか)を確認します。
なおかつ、if (!$hasTickets)にてチケットがあるかないか確認しています。
HTML側で、チケットを持っていない場合はそもそも「チケットを持っていません!」と表示させて編集モーダルには遷移させないif分岐を作っていますが、これは何らかの不備でチケット0でも編集モーダルを操作できてしまった場合のフォロー措置です。
throw new \Exception(‘チケットが足りません’);
……「ここで操作を中断してエラーを投げろ!」という命令です。try-catch文では、これで即座にcatchの処理に移行します。
上記のようにチケットの有無を判定したうえで、if ($isToday)がtrue(当日)なら$userStats->decrement(‘edit_tickets’);にて編集チケットを1減らす処理を行います。
decrement(‘edit_tickets’)のdecrement
……対象のカラムのデータを1減らしてsave()までしてくれる便利なメソッドです。
まとめ
以上、「編集ボタンを押す際に、編集チケットを消費するかの選択画面を表示したい」場合の処理についての解説でした。
途中のJavaScript、もっとすっきり書ける気がする……! とは思いつつ、まずは私が理解しやすい形で改修していきました。
今回の改修内容は大別したら以下の2点となります。
- 当日だけ編集チケット消費させたい
→HTML、PHP(コントローラー)で日付判定して表示・DB処理をif分岐させる - 編集チケット消費確認モーダルから編集モーダルに遷移させたい
→HTMLに消費確認モーダルを追加し、JavaScriptでボタンの挙動を制御する
改修することで、「タスクを変更する」ボタンを押したら編集チケット確認モーダルが表示され、「はい」を押したら編集モーダルに遷移するようになりました。
※CSS調整の説明は割愛しています。
チケット消費してタスクを編集する動作も、現状では問題ありません。

この備忘録が同じ悩みを持つ方のお役に立ったなら幸いです。


コメント