利用者管理アプリ開発:面談記録一覧表示機能の実装
はじめに
前回は、データーベースのテーブル設計と構築を行いました!
前回の記事はこちら↓
今回は、面談記録一覧表示機能を実装していきます!
フィーチャーブランチの作成
今回は、データベース構築部分の実装していきたいので、それ専用のブランチを作成します。
作成するには以下のコマンドを実行します。
ブランチ名はmeetinglog
にしています。
git flow feature start meetinglog
ブランチが作成されているか確認します。
git branch
develop
* feature/meetinglog
main
無事作成されていることがわかりました。
ブランチをGitHub上にアップロード
今のままでは、ローカル上にブランチが存在しているだけなので、GitHub上には反映していきます。
反映するには以下のコマンドを実行します。
git flow feature publish meetinglog
GitHub上で確認して、ブランチが追加さていれば成功です。
AuthenticatedLayout.jsxの編集
まずは、全ての画面で使用する予定のAuthenticatedLayout.jsx
を編集していきます。
ナビゲーションバーの編集
まずは、ナビゲーションバーの部分を修正していきます。
該当箇所を以下のように、修正していきます。
////省略////
import { HomeIcon } from '@heroicons/react/24/solid'
////省略////
<div className="shrink-0 flex items-center">
<Link href="/">
<HomeIcon className="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" />
</Link>
</div>
<div className="hidden space-x-4 sm:-my-px sm:ms-10 sm:flex">
<NavLink href={route('dashboard')} active={route().current('dashboard')}>
ダッシュボード
</NavLink>
<NavLink href={route('meetinglog.index')} active={route().current('meetinglog.index')}>
面談記録
</NavLink>
</div>
////省略////
まず、デフォルトのアイコンからHomeIcon
に変更しています。
次に、デフォルトではDashboard
のみしか記載されていませんが、面談記録
のナビゲーションを追加しています。
また、各ナビゲーション間のスペースが広すぎるので、ClassName
のspace-x-6
をspace-x-4
に変更しています。
DropdownとResponsiveNavLinkの編集
次に、Dropdown
タグとResponsiveNavLink
タグ内の英語のメニュー名を日本語に修正します。
まずは、Dropdown.Content
から
<Dropdown.Content>
<Dropdown.Link href={route('profile.edit')}>プロフィール編集</Dropdown.Link>
<Dropdown.Link href={route('logout')} method="post" as="button">
ログアウト
</Dropdown.Link>
</Dropdown.Content>
次に、ResponsiveNavLink
タグ内を編集し、面談記録ページへリンクした項目も追加します。
<div className="pt-2 pb-3 space-y-1">
<ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}>
ダッシュボード
</ResponsiveNavLink>
<ResponsiveNavLink href={route('meetinglog.index')} active={route().current('meetinglog.index')}>
面談記録
</ResponsiveNavLink>
</div>
<div className="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div className="px-4">
<div className="font-medium text-base text-gray-800 dark:text-gray-200">{user.name}</div>
<div className="font-medium text-sm text-gray-500">{user.email}</div>
</div>
<div className="mt-3 space-y-1">
<ResponsiveNavLink href={route('profile.edit')}>プロフィール編集</ResponsiveNavLink>
<ResponsiveNavLink method="post" href={route('logout')} as="button">
ログアウト
</ResponsiveNavLink>
</div>
</div>
面談記録へリンクした項目を追加し、英語表記になっているメニュー名を日本語に直しただけです。
MeegingLogコントローラーの生成
面談記録のCRUD処理を実装するために、コントローラーを生成していきます。
コントローラーの生成には以下のコマンドを実行します。
php artisan make:controller MeetingLogController --model=MeetingLog --requests
これにより、MeetingLogController.php
とStoreMeetingLogRequest.php
、UpdateMeetingLogRequest.php
が作成されます。
面談記録の閲覧機能を実装
まずは、面談記録の一覧表示させる機能を実装しましょう!
Index.jsxの作成
面談記録一覧のフロントエンド部分を作成していきます!
resources/js/Pages
内にMeetingLog
ディレクトリを作成します。
その後、resources/js/Pages/MeetingLog
内に、Index.jsx
ファイルを作成します。
作成したIndex.jsx
ファイルを以下のように記述して、面談記録一覧画面を実装していきます。
- Props
- 表示するデータである
meetinglogs
、絞り込みに必要なデータであるoffices
/users
/members
、絞り込むパラメータであるqueryParams
をPropsとして渡しています。
- 表示するデータである
- return部分
-
AuthenticatedLayout
でナビゲーションバーを表示させ、その下に面談記録の一覧を表形式で表示します。 - 各項目には
TableHead
コンポーネント(後述)を利用して、項目をクリックすることで昇順降順に並べ替えることができます。 -
TextInput
やSelectInput
コンポーネントを利用した各項目の検索ウィンドウを設けており、面談記録を絞り込むことができます。
-
- searchFieldChanged
-
searchFieldChanged
は、TextInput
などに入力された絞り込みのための検索ワードをqueryParams
に代入して、バックエンドに渡すための関数です。
-
- onKeyPress
-
onKeyPress
は、TextInput
で文字を入力してEnterキーが押されたとき、入力された文字列をsearchFieldChanged
に渡す関数です。
-
- sortChanged
- ソートしたい項目がクリックされたときに、その項目と昇順か降順を
queryParams
に代入して、バックエンドに渡すための関数です。
- ソートしたい項目がクリックされたときに、その項目と昇順か降順を
- deleteMeetingLog
- 本当に削除するかどうかの確認をするためのウィンドウを表示した後、削除する場合は削除リクエストをバックエンドに伝える関数です。
TableHeadingコンポーネントの作成
最初は、全てのコードをIndex.jsx
に記述していたのですが、一覧表のヘッダーの部分をコンポーネントとして分けました。
- Props
- どの項目かを判別するための
name
、ソート機能を表示するかどうかを判別するためのsortable
- 現在の
queryParams
に代入されている項目と昇順か降順を取得するためのsort_field
とsort_direction
- 該当項目がクリックされたときに
queryParams
に項目と昇順か降順かを伝えるためのsortChanged
関数 - 子要素である
children
- どの項目かを判別するための
- return部分
-
table
タグ内で各項目を表示するときに使われるコンポーネントなので、全体をth
タグで囲んでいます。 -
HeroIcons
ライブラリからChevronUpIcon
とChevronDownIcon
を利用して、昇順と降順のマークを表示しています。
-
Pagenationコンポーネントの作成
TableHeading.jsx
と同様にページの出力部分もコンポーネントして分けています。
- Props
- ページ遷移先のURLが格納されている
links
- 検索や絞り込みを行っていた場合、ページをまたいでもその条件を引き継ぐために
queryParams
も持ってきます。
- ページ遷移先のURLが格納されている
- return部分
-
nav
タグ内でlinks
を展開してそれぞれLink
タグで表現しています。 -
href
にはlink
のURLの末尾にgetUrlParams
を利用して適切なパラメータを付与しています。 -
link.active
がtrue
の場合、背景色を追加します。 -
link.url
の値が存在しない場合、!text-gray-500
で強制的にテキストの色を変更して、cursor-not-allowed
で、マウスオーバーしたときに禁止マークを表示するようにしています。
-
- getUrlParams
- 親要素から持ってきた
queryParams
のキーを抽出して、page
意外のキーがある場合は、URLパラメーターの形に整形して値を返す関数です。
- 親要素から持ってきた
- isPrevNext
-
links
オブジェクト内のlabel
キーがpagination.previous
だった場合は「< 前へ」、pagination.next
だった場合は「次へ >」と表示すると用にする関数です。
-
SelectInputコンポーネントの作成
- Props
- コンポーネントのCSSを利用個所に合わせて調整するための
className
- 子要素を格納した
children
- その他
select
タグに指定する要素を記述した...props
- コンポーネントのCSSを利用個所に合わせて調整するための
- return部分
-
select
タグを用いて、セレクトボックスを生成しています。
-
MeetingLogController.phpの編集
続いて、バックエンドの処理実装のためにコントローラーを編集していきます。
MeetingLogController
のindex()
メソッドに実装していきます。
ソースコード
public function index()
{
$query = MeetingLog::query();
$users = User::all();
$offices = Office::all();
$members = Member::all();
if(!Auth::user()->is_global_admin) {
$officeId = Auth::user()->office_id;
$query->select("meeting_logs.*", "offices.id as office_id")
->leftJoin('members', 'meeting_logs.member_id', '=', 'members.id')
->leftJoin('offices', 'members.office_id', '=', 'offices.id')
->where('offices.id', '=', $officeId);
$users = User::where("office_id", "=", $officeId)->get();
$members = Member::where("office_id", "=", $officeId)->get();
}
$sortField = request("sort_field", "created_at");
$sortDirection = request("sort_direction", "desc");
if (request("id")) {
$query->where("id", "=", request("id"));
}
if (request("title")) {
$query->where("title", "like", "%" . request("title") . "%");
}
if (request("user")) {
$query->where("user_id", "=", request("user"));
$officeId = User::select("office_id")->where("id", "=", request("user"));
$offices = Office::where("id", "=", $officeId)->get();
$members = Member::where("office_id", "=", $officeId)->get();
}
if (request("member")) {
$query->where("member_id", "=", request("member"));
$officeId = Member::select("office_id")->where("id", "=", request("member"));
$offices = Office::where("id", "=", $officeId)->get();
$users = User::where("office_id", "=", $officeId)->get();
}
if (request("office")) {
$query->select("meeting_logs.*", "offices.id as office_id")
->leftJoin('members', 'meeting_logs.member_id', '=', 'members.id')
->leftJoin('offices', 'members.office_id', '=', 'offices.id')
->where('offices.id', '=', request("office"));
$members = Member::where("office_id", "=", request("office"))->get();
$users = User::where("office_id", "=", request("office"))->get();
}
if (request("condition")) {
$query->where("condition", "=", request("condition"));
}
if ($sortField == "office_id") {
if (!request("office")) {
$query->select("meeting_logs.*", "offices.id as office_id")
->leftJoin('members', 'meeting_logs.member_id', '=', 'members.id')
->leftJoin('offices', 'members.office_id', '=', 'offices.id');
}
}
$meetingLogs = $query->orderBy($sortField, $sortDirection)->paginate(10);
$queryParams = request()->query();
return inertia("MeetingLog/Index", [
'meetingLogs' => MeetingLogResource::collection($meetingLogs),
'offices' => OfficeResource::collection($offices),
'users' => UserResource::collection($users),
'members' => MemberResource::collection($members),
'queryParams' => $queryParams ?: null,
]);
}
- 初期設定
$query = MeetingLog::query();
$users = User::all();
$offices = Office::all();
$members = Member::all();
まず、MeetingLog::query()
はクエリビルダーを使ってMeetingLog
モデルに対するクエリを構築します。
次に、all()
メソッドを用いて、従業員・事業所・利用者の全レコードを一旦取得しています。
- 最上位権限者によるフィルタリング
if(!Auth::user()->is_global_admin) {
$officeId = Auth::user()->office_id;
$query->select("meeting_logs.*", "offices.id as office_id")
->leftJoin('members', 'meeting_logs.member_id', '=', 'members.id')
->leftJoin('offices', 'members.office_id', '=', 'offices.id')
->where('offices.id', '=', $officeId);
$users = User::where("office_id", "=", $officeId)->get();
$members = Member::where("office_id", "=", $officeId)->get();
}
最上位権限者は、全ての面談記録を閲覧することができる。
それ以外のユーザーは、自身の所属する事業所に付随する情報のみを閲覧することができます。
- ソートの設定
$sortField = request("sort_field", "created_at");
$sortDirection = request("sort_direction", "desc");
sortField
はソートする要素を、sortDirection
はソートの方向を格納する変数です。
デフォルトで、sortField
は作成日時を、sortDirection
は降順を指定しています。
- 絞り込みの処理
if (request("title")) {
$query->where("title", "like", "%" . request("title") . "%");
}
if (request("user")) {
$query->where("user_id", "=", request("user"));
$officeId = User::select("office_id")->where("id", "=", request("user"));
$offices = Office::where("id", "=", $officeId)->get();
$members = Member::where("office_id", "=", $officeId)->get();
}
////省略////
フロントエンド側から受け取ったデータからrequest()
メソッドを用いて該当するキーに対応した値を取り出します。
値が存在する場合、where句
を用いて、フィルタリングを行います。
title
といったテキスト入力による絞り込みに対しては、like句
とワイルドカード%
を用いて部分一致検索するようにしています。
- クエリの実行とデータの送信
$meetingLogs = $query->orderBy($sortField, $sortDirection)->paginate(10);
$queryParams = request()->query();
return inertia("MeetingLog/Index", [
'meetingLogs' => MeetingLogResource::collection($meetingLogs),
'offices' => OfficeResource::collection($offices),
'users' => UserResource::collection($users),
'members' => MemberResource::collection($members),
'queryParams' => $queryParams ?: null,
]);
ここでは、orderBy
で該当するカラムに対してソートをかけた後、paginate()
を用いて結果を10件ずつのページネーション形式で取得しています。
さらに、URLパラメーターをrequest()->query()
を用いて連想配列として$queryParams
に格納しています。
そして、inertia()
メソッドを用いて、MeetingLog/Index.jsx
に対してデータを送信します。
その際、MeetingLogResource
などのリソースクラス(後述)に定義したデータの形で送信します。
Resourceの作成
バックエンドからフロントエンドにデータを流す際のデータの形を決める、Resource
を作成していきます。
Resource
の作成には以下のコマンドを実行します。
php artisan make:resource MeetingLogResource
php artisan make:resource OfficeResource
php artisan make:resource UserResource
php artisan make:resource MemberResource
生成されたリソースファイルは、app/Http/Resources
内に配置されます。
- MeetingLogResource
面談記録のリソースファイルであるMeetingLogResource.php
を以下のように書き換えます。
////省略////
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'user' => new UserResource($this->user),
'member' => new MemberResource($this->member),
'condition' => $this->condition,
'meeting_log' => $this->meeting_log,
'created_at' => (new Carbon($this->created_at))->format('Y-m-d'),
'updated_at' => (new Carbon($this->updated_at))->format('Y-m-d'),
];
}
////省略////
- OfficeResource
事業所のリソースファイルであるOfficeResource.php
を以下のように書き換えます。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'zip_code' => $this->zip_code,
'address' => $this->address,
'phone_number' => $this->phone_number,
'created_at' => (new Carbon($this->created_at))->format('Y-m-d'),
'updated_at' => (new Carbon($this->updated_at))->format('Y-m-d'),
];
}
- UserResource
職員のリソースファイルであるUserResource.php
を以下のように書き換えます。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'avatar' => $this->avatar,
'email' => $this->email,
'office' => new OfficeResource($this->office),
'is_admin' => $this->is_admin,
'is_main_office' => $this->is_main_office,
'created_at' => (new Carbon($this->created_at))->format('Y-m-d'),
'updated_at' => (new Carbon($this->updated_at))->format('Y-m-d'),
];
}
- MemberResource
利用者のリソースファイルであるMemberResource.php
を以下のように書き換えます。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'sex' => $this->sex,
'office' => new OfficeResource($this->office),
'status' => $this->status,
'characteristics' => $this->characteristics,
'notes' => $this->notes,
'created_at' => (new Carbon($this->created_at))->format('Y-m-d'),
'updated_at' => (new Carbon($this->updated_at))->format('Y-m-d'),
];
}
web.phpの編集
続いて、ルーティングの設定をしていきます。
<?php
+ use App\Http\Controllers\MeetingLogController;
////省略////
- Route::get('/', function () {
- return Inertia::render('Welcome', [
- 'canLogin' => Route::has('login'),
- 'canRegister' => Route::has('register'),
- 'laravelVersion' => Application::VERSION,
- 'phpVersion' => PHP_VERSION,
- ]);
- });
+ Route::redirect('/', '/dashboard');
- Route::get('/dashboard', function () {
- return Inertia::render('Dashboard');
- })->middleware(['auth', 'verified'])->name('dashboard');
+ Route::middleware(['auth', 'verified'])->group(function () {
+ Route::get('/dashboard', fn() => Inertia::render('Dashboard'))
+ ->name('dashboard');
+ Route::get('/meetinglog', [MeetingLogController::class, 'index'])
+ ->name('meetinglog.index');
+ Route::get('/meetinglog/show/{meetingLog}', [MeetingLogController::class, 'show'])
+ ->name('meetinglog.show');
+ Route::get('/meetinglog/create', [MeetingLogController::class, 'create'])
+ ->name('meetinglog.create');
+ Route::post('/meetinglog', [MeetingLogController::class, 'store'])
+ ->name('meetinglog.store');
+ Route::delete('/meetinglog/destroy/{meetingLog}', [MeetingLogController::class, 'destroy'])
+ ->name('meetinglog.destroy');
+ Route::get('/meetinglog/edit/{meetingLog}', [MeetingLogController::class, 'edit'])
+ ->name('meetinglog.edit');
+ Route::put('/meetinglog/update/{meetingLog}', [MeetingLogController::class, 'update'])
+ ->name('meetinglog.update');
+ Route::patch('/meetinglog/update/{meetingLog}', [MeetingLogController::class, 'update'])
+ ->name('meetinglog.update');
+ });
////省略////
表示確認
ここまでできたら実際にブラウザでhttp://localhost:8000/meetinglog
にアクセスして表示を確認してみます。
以下のようなページが表示されていればOKです。
おわりに
面談記録の一覧表示機能を実装しました!
次回は、新規作成ページとデータの保存機能を実装していきます!
ではでは!
Discussion