面談記録管理アプリ開発:面談記録詳細閲覧機能と新規登録機能の実装
はじめに
今回は面談記録の詳細閲覧と新規登録機能の実装を解説して行きます。
前回の記事はこちら↓
面談記録の詳細閲覧機能を実装
面談記録の詳細画面を実装していきます。
Show.jsxの作成
詳細画面のフロントエンド部分を実装します。
完成したページはこんな感じです。
まず、resources/js/Pages/MeetingLog
ディレクトリ内に、Show.jsx
ファイルを作成します。
作成したShow.jsx
を以下のように記述して、詳細画面を実装します。
チャット機能の解説は次回以降で行おうと思うので、ここではreturn
部分の解説のみ行います!
return
部分は、ナビゲーションバー・基本情報・面談記録・チャット欄にパーツが別れています。
- ナビゲーションバー
他ページ同様AuthenticatedLayout
を用いてナビゲーションバーを表示しています。
<AuthenticatedLayout
user={auth.user}
header={
<div className="flex justify-between items-center">
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{`面談記録 - "${meetingLog.title}"`}
</h2>
{meetingLog.user.office.id == auth.user.office.id || auth.user.is_global_admin?(
<Link
href={route("meetinglog.edit", meetingLog.id)}
className="bg-emerald-400 py-1 px-3 text-gray-900 rounded shadown transition-all hover:bg-emerald-500"
>
編集
</Link>
): ""}
</div>
}
>
header
部分には、h2
タグ内に面談記録のタイトルを表示しています。
さらに、面談記録作成者と同じ事業所に所属するアカウントか最上位権限を持っている人のみ編集ボタンを表示させるようにしています。
- 基本情報の表示部分
<div className="grid gap-1 grid-cols-2">
<div>
<div>
<label className="font-bold text-lg">ID</label>
<p className="mt-1">{meetingLog.id}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">利用者名</label>
<p className="mt-1">{meetingLog.member.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">体調</label>
<p className="mt-1">{meetingLog.condition}</p>
</div>
</div>
<div>
<div>
<label className="font-bold text-lg">作成者</label>
<p className="mt-1">{meetingLog.user.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">事業所</label>
<p className="mt-1">{meetingLog.member.office.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">作成日</label>
<p className="mt-1">{meetingLog.created_at}</p>
</div>
</div>
</div>
grid gird-cols-2
を指定することで、そのタグ中の要素を2分割して表示しています。
その中でさらに表示させたい要素を2つのグループに分けて、div
タグで囲っています。
さらに各々の表示したい情報をlabel
タグとp
タグで表現して、それをdiv
タグで囲うことで、ページを表現しています。
- 面談記録とチャット欄
<div className="grid gap-1 grid-cols-2">
<div>
<div>
<label className="font-bold text-lg">面談記録</label>
<div className="mt-1 whitespace-pre-wrap max-h-[400px] overflow-y-auto">
{meetingLog.meeting_log}
</div>
</div>
</div>
<div>
<label className="font-bold text-lg">チャット</label>
<div className="mt-1 whitespace-pre-wrap">
<>
<div
ref={messagesCtrRef}
className="flex-1 overflow-y-auto p-5 max-h-[400px]"
>
{/* {messages} */}
{localMessages.length === 0 && (
<div className="flex justify-center items-center h-full">
<div className="text-lg text-gray-500">
メッセージがありません
</div>
</div>
)}
{localMessages.length > 0 && (
<div className="flex-1 flex flex-col">
<div ref={loadMoreIntersect}></div>
{localMessages.map((message) => (
<MessageItem
key={message.id}
message={message}
/>
))}
</div>
)}
</div>
<MessageInput meetingLogId={meetingLog.id} />
</>
</div>
</div>
</div>
基本情報のときと同様に、grid grid-cols-2
で2分割しています。
そして、分割する要素をdiv
タグで囲い、左側に面談記録、右側にチャット欄を表示させています。
面談記録は、基本情報と同じようにlabel
タグとdiv
タグをdiv
タグで囲うことで表現している。
面談記録の内容を表示するdiv
タグには、whitespace-pre-wrap max-h-[400px] overflow-y-auto
を指定して、文章の折り返しの指定とウィンドウをはみ出したときにスクロールバーを表示する指定、そして、最大で表示する高さ(400px)を指定しています。
チャット欄に関する解説は本記事では割愛。
コントローラーの実装:詳細画面の呼び出し
app/Http/Controllers/MeetingLogController.php
のshow
メソッドを以下のように記述します。
public function show(MeetingLog $meetingLog)
{
$messages = Message::where('meeting_logs_id', $meetingLog->id)
->latest()
->paginate(10);
return inertia('MeetingLog/Show', [
'meetingLog' => new MeetingLogResource($meetingLog),
'messages' => MessageResource::collection($messages),
]);
}
inertia()
メソッドを用いて、先述したresources/js/Pages/MeetingLog/Show.jsx
を該当する面談記録の詳細情報とともに出力しています。
こちらも、メッセージ機能については、次回以降の記事で書きます!
ルーティング
そして、ルーティングの設定をして行きます。
Route::middleware(['auth', 'verified'])->group(function () {
////省略////
+ Route::get('/meetinglog/show/{meetingLog}', [MeetingLogController::class, 'show'])
+ ->name('meetinglog.show');
});
面談記録の新規登録機能を実装
続いて、面談記録の新規作成機能を実装していきます。
完成した画面がこちら
Create.jsx
まずは、新規登録機能のフロントエンド部分を実装していきます。
ソースは以下のとおりです。
ソースコード
import InputError from "@/Components/InputError";
import InputLabel from "@/Components/InputLabel";
import Pagenation from "@/Components/Pagenation";
import SelectInput from "@/Components/SelectInput";
import TextAreaInput from "@/Components/TextAreaInput";
import TextInput from "@/Components/TextInput";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, Link, router, useForm } from "@inertiajs/react";
import { useEffect, useState } from "react";
export default function Create ({ auth, members, meetingLogs, queryParams = null}) {
queryParams = queryParams || {}
const {data, setData, post, errors, reset} = useForm({
title: "",
user_id: auth.user.id ?? "",
member_id: queryParams.member ?? "",
condition: "",
meeting_log: "",
})
const memberChanged = (value) => {
if (value) {
queryParams["member"] = value;
} else {
delete queryParams["member"];
}
console.log(queryParams)
router.get(route('meetinglog.create'), queryParams);
}
const onSubmit = (e) => {
e.preventDefault();
console.log("onSubmit");
post(route('meetinglog.index'));
}
return (
<AuthenticatedLayout
user={auth.user}
header={
<div className="flex justify-between items-center">
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
新規作成
</h2>
</div>
}
>
<Head title="新規作成" />
<div className="py-12">
<div className="flex gap-4 justify-center items-start max-w-7xl mx-auto sm:px-6 lg:px-8">
{meetingLogs.data.length != 0 && (
<div className="p-6 text-gray-900 w-1/2 sm:p-8 bg-white shadow sm:rounded-lg">
{meetingLogs.data.map(meetingLog => (
<div>
<div className="grid gap-1 grid-cols-2">
<div>
<div>
<label className="font-bold text-lg">ID</label>
<p className="mt-1">{meetingLog.id}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">利用者名</label>
<p className="mt-1">{meetingLog.member.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">体調</label>
<p className="mt-1">{meetingLog.condition}</p>
</div>
</div>
<div>
<div>
<label className="font-bold text-lg">作成者</label>
<p className="mt-1">{meetingLog.user.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">事業所</label>
<p className="mt-1">{meetingLog.member.office.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">作成日</label>
<p className="mt-1">{meetingLog.created_at}</p>
</div>
</div>
</div>
<div>
<label className="font-bold text-lg">面談記録</label>
<div className="mt-1 whitespace-pre-wrap h-96 overflow-y-auto">{meetingLog.meeting_log}</div>
</div>
</div>
))}
<Pagenation links={meetingLogs.meta.links} queryParams={queryParams}/>
</div>
)}
<div className=" text-gray-900 w-1/2 bg-white shadow sm:rounded-lg">
<form
onSubmit={onSubmit}
className="p-6 sm:p-8"
>
<div className="">
<InputLabel
htmlFor="member_id"
value="利用者名"
/>
<SelectInput
id="member_id"
value={queryParams.member}
className="mt-1 block w-full"
onChange={(e) => {memberChanged(e.target.value) }}
>
<option value="">利用者名を選択してください</option>
{members.data.map(member => (
<option value={member.id}>{member.name}</option>
))}
</SelectInput>
<InputError message={errors.member_id} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel
htmlFor="title"
value="タイトル"
/>
<TextInput
id="title"
type="text"
value={data.title}
className="mt-1 block w-full"
onChange={(e) => setData("title", e.target.value)}
/>
<InputError message={errors.title} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel
htmlFor="condition"
value="体調"
/>
<SelectInput
id="condition"
value={data.condition}
className="mt-1 block w-full"
onChange={(e) => setData("condition", e.target.value)}
>
<option value="">体調を選択してください</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</SelectInput>
<InputError message={errors.condition} className="mt-2" />
</div>
<div className="mt-4 max-h-96">
<InputLabel
htmlFor="meeting_log"
value="面談記録"
/>
<TextAreaInput
id="meeting_log"
rows="14"
value={data.meeting_log}
className="mt-1 block w-full"
onChange={(e) => setData("meeting_log", e.target.value)}
/>
<InputError message={errors.meeting_log} className="mt-2" />
</div>
<div className="mt-4 text-right">
<Link
href={route("meetinglog.index")}
className="bg-gray-300 py-1 px-3 text-gray-800 rounded shadow transition-all hover:bg-gray-200 mr-2"
>
戻る
</Link>
<button
className="bg-emerald-500 py-1 px-3 text-white rounded shadow transition-all hover:bg-emerald-400"
>
保存
</button>
</div>
</form>
</div>
</div>
</div>
</AuthenticatedLayout>
);
}
以下パーツごとに解説していきます。
- useForm
const {data, setData, post, errors} = useForm({
title: "",
user_id: auth.user.id ?? "",
member_id: queryParams.member ?? "",
condition: "",
meeting_log: "",
})
useForm
とはフォームのデータを扱うためのinertiaJS
の機能の一つです。
React
に同様の名前のhook
があると思いますがそれとは別物です。
data
にはフォームに入力されたデータが入ります。setData
関数を用いて、データを入力していきます。
入力したいデータの形は、useForm({})
の中に記述していきます。
post
はデータの送信方法を表しています。
errors
はサーバー側からエラーコードやエラーメッセージを受け取ったときに、その内容が格納されます。
- memberChanged
const memberChanged = (value) => {
if (value) {
queryParams["member"] = value;
} else {
delete queryParams["member"];
}
router.get(route('meetinglog.create'), queryParams);
}
memberChanged
関数は、選択された利用者のid
を受け取り、queryParams
に代入して、バックエンドに送信する関数です。
これによりバックエンド側にどの利用者を選択しているかを伝えており、バックエンド側から該当する利用者の過去の面談記録を取得することができるようになっています。
- onSubmit
const onSubmit = (e) => {
e.preventDefault();
post(route('meetinglog.store'));
}
onSubmit
関数は、保存ボタンクリック時にフォームデータをバックエンドへpost
する関数です。
e.preventDefault()
は、イベントの発生元であるフォームのデフォルトの動作をキャンセルするメソッドです。
この処理を加えることで、ボタンのイベントであるonClick
などの処理を妨害して、2重送信や送信中にデータの変更を防ぎます。
なお、同様の処理が複数ヶ所にあるため、今後リファクタリングの際に外部化する予定です。
- return部分
ソースコード
return (
<AuthenticatedLayout
user={auth.user}
header={
<div className="flex justify-between items-center">
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
新規作成
</h2>
</div>
}
>
<Head title="新規作成" />
<div className="py-12">
<div className="flex gap-4 justify-center items-start max-w-7xl mx-auto sm:px-6 lg:px-8">
{meetingLogs.data.length != 0 && (
<div className="p-6 text-gray-900 w-1/2 sm:p-8 bg-white shadow sm:rounded-lg">
{meetingLogs.data.map(meetingLog => (
<div>
<div className="grid gap-1 grid-cols-2">
<div>
<div>
<label className="font-bold text-lg">ID</label>
<p className="mt-1">{meetingLog.id}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">利用者名</label>
<p className="mt-1">{meetingLog.member.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">体調</label>
<p className="mt-1">{meetingLog.condition}</p>
</div>
</div>
<div>
<div>
<label className="font-bold text-lg">作成者</label>
<p className="mt-1">{meetingLog.user.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">事業所</label>
<p className="mt-1">{meetingLog.member.office.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">作成日</label>
<p className="mt-1">{meetingLog.created_at}</p>
</div>
</div>
</div>
<div>
<label className="font-bold text-lg">面談記録</label>
<div className="mt-1 whitespace-pre-wrap h-96 overflow-y-auto">{meetingLog.meeting_log}</div>
</div>
</div>
))}
<Pagenation links={meetingLogs.meta.links} queryParams={queryParams}/>
</div>
)}
<div className=" text-gray-900 w-1/2 bg-white shadow sm:rounded-lg">
<form
onSubmit={onSubmit}
className="p-6 sm:p-8"
>
<div className="">
<InputLabel
htmlFor="member_id"
value="利用者名"
/>
<SelectInput
id="member_id"
value={queryParams.member}
className="mt-1 block w-full"
onChange={(e) => {memberChanged(e.target.value) }}
>
<option value="">利用者名を選択してください</option>
{members.data.map(member => (
<option value={member.id}>{member.name}</option>
))}
</SelectInput>
<InputError message={errors.member_id} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel
htmlFor="title"
value="タイトル"
/>
<TextInput
id="title"
type="text"
value={data.title}
className="mt-1 block w-full"
onChange={(e) => setData("title", e.target.value)}
/>
<InputError message={errors.title} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel
htmlFor="condition"
value="体調"
/>
<SelectInput
id="condition"
value={data.condition}
className="mt-1 block w-full"
onChange={(e) => setData("condition", e.target.value)}
>
<option value="">体調を選択してください</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</SelectInput>
<InputError message={errors.condition} className="mt-2" />
</div>
<div className="mt-4 max-h-96">
<InputLabel
htmlFor="meeting_log"
value="面談記録"
/>
<TextAreaInput
id="meeting_log"
rows="14"
value={data.meeting_log}
className="mt-1 block w-full"
onChange={(e) => setData("meeting_log", e.target.value)}
/>
<InputError message={errors.meeting_log} className="mt-2" />
</div>
<div className="mt-4 text-right">
<Link
href={route("meetinglog.index")}
className="bg-gray-300 py-1 px-3 text-gray-800 rounded shadow transition-all hover:bg-gray-200 mr-2"
>
戻る
</Link>
<button
className="bg-emerald-500 py-1 px-3 text-white rounded shadow transition-all hover:bg-emerald-400"
>
保存
</button>
</div>
</form>
</div>
</div>
</div>
</AuthenticatedLayout>
);
まず、flex
を用いて、直下のタグ要素を横並びにします。
次に、子要素であるdiv
タグにw-1/2
を用いて、画面と2分割するようにします。
左側に過去の面談記録を表示し、右側に新登録用のフォームを表示します。
利用者が未選択などの理由で面談記録のデータがない場合は、左側の要素が表示されないようになっています。
ソースコード
<div className="py-12">
<div className="flex gap-4 justify-center items-start max-w-7xl mx-auto sm:px-6 lg:px-8">
{meetingLogs.data.length != 0 && (
<div className="p-6 text-gray-900 w-1/2 sm:p-8 bg-white shadow sm:rounded-lg">
//// 省略 ////
</div>
)}
<div className=" text-gray-900 w-1/2 bg-white shadow sm:rounded-lg">
//// 省略 ////
</div>
</div>
</div>
画面左側は、詳細画面に表示される情報をレイアウトを変えて表示しています。
ソースコード
{meetingLogs.data.map(meetingLog => (
<div>
<div className="grid gap-1 grid-cols-2">
<div>
<div>
<label className="font-bold text-lg">ID</label>
<p className="mt-1">{meetingLog.id}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">利用者名</label>
<p className="mt-1">{meetingLog.member.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">体調</label>
<p className="mt-1">{meetingLog.condition}</p>
</div>
</div>
<div>
<div>
<label className="font-bold text-lg">作成者</label>
<p className="mt-1">{meetingLog.user.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">事業所</label>
<p className="mt-1">{meetingLog.member.office.name}</p>
</div>
<div className="mt-4">
<label className="font-bold text-lg">作成日</label>
<p className="mt-1">{meetingLog.created_at}</p>
</div>
</div>
</div>
<div>
<label className="font-bold text-lg">面談記録</label>
<div className="mt-1 whitespace-pre-wrap h-96 overflow-y-auto">{meetingLog.meeting_log}</div>
</div>
</div>
))}
<Pagenation links={meetingLogs.meta.links} queryParams={queryParams}/>
画面右側は、各種コンポーネントを利用して登録フォームを作っています。
ソースコード
<form
onSubmit={onSubmit}
className="p-6 sm:p-8"
>
<div className="">
<InputLabel
htmlFor="member_id"
value="利用者名"
/>
<SelectInput
id="member_id"
value={queryParams.member}
className="mt-1 block w-full"
onChange={(e) => {memberChanged(e.target.value) }}
>
<option value="">利用者名を選択してください</option>
{members.data.map(member => (
<option value={member.id}>{member.name}</option>
))}
</SelectInput>
<InputError message={errors.member_id} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel
htmlFor="title"
value="タイトル"
/>
<TextInput
id="title"
type="text"
value={data.title}
className="mt-1 block w-full"
onChange={(e) => setData("title", e.target.value)}
/>
<InputError message={errors.title} className="mt-2" />
</div>
<div className="mt-4">
<InputLabel
htmlFor="condition"
value="体調"
/>
<SelectInput
id="condition"
value={data.condition}
className="mt-1 block w-full"
onChange={(e) => setData("condition", e.target.value)}
>
<option value="">体調を選択してください</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</SelectInput>
<InputError message={errors.condition} className="mt-2" />
</div>
<div className="mt-4 max-h-96">
<InputLabel
htmlFor="meeting_log"
value="面談記録"
/>
<TextAreaInput
id="meeting_log"
rows="14"
value={data.meeting_log}
className="mt-1 block w-full"
onChange={(e) => setData("meeting_log", e.target.value)}
/>
<InputError message={errors.meeting_log} className="mt-2" />
</div>
<div className="mt-4 text-right">
<Link
href={route("meetinglog.index")}
className="bg-gray-300 py-1 px-3 text-gray-800 rounded shadow transition-all hover:bg-gray-200 mr-2"
>
戻る
</Link>
<button
className="bg-emerald-500 py-1 px-3 text-white rounded shadow transition-all hover:bg-emerald-400"
>
保存
</button>
</div>
</form>
各コンポーネントで次節で説明します。
Create.jsxのコンポーネント
- TextInput
ソースコード
import { forwardRef, useEffect, useRef } from 'react';
export default forwardRef(function TextInput({ type = 'text', className = '', isFocused = false, ...props }, ref) {
const input = ref ? ref : useRef();
useEffect(() => {
if (isFocused) {
input.current.focus();
}
}, []);
return (
<input
{...props}
type={type}
className={
'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm ' +
className
}
ref={input}
/>
);
});
TextInput
はテキスト入力欄を構成するコンポーネントです。
forwardRef
を呼び出すことで、コンポーネントがref
を受け取り、それを子コンポーネントに転送できるようになります。
最新の公式ドキュメントでは、親コンポーネントから子コンポーネントにref
を渡すことができます。
おそらく、以前のバージョンではこのような運用ができず、forwardRef
を利用してのみ、ref
の受け渡しができていたのだと思います。
公式ドキュメントで紹介されている利用例を見ると、テキスト入力欄のオートフォーカスなどがあるみたいです。
ページが表示された瞬間に、自動的にテキスト入力欄をフォーカスしたいときなどに使われるのだと思います。
今回のアプリケーションではそういった機能の実装は現段階でしていないので、forwarRef
は不要なのかもしれません。
- TextAeraInput
ソースコード
import { forwardRef, useEffect, useRef } from 'react';
export default forwardRef(function TextAreaInput({ className = '', isFocused = false, ...props }, ref) {
const input = ref ? ref : useRef();
useEffect(() => {
if (isFocused) {
input.current.focus();
}
}, []);
return (
<textarea
{...props}
className={
'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm ' +
className
}
ref={input}
/>
);
});
TextAreaInput
は、TextInput
のinput
タグをtextarea
に変更したものになります。
- InputLabel
ソースコード
export default function InputLabel({ value, className = '', children, ...props }) {
return (
<label {...props} className={`block font-medium text-sm text-gray-700 dark:text-gray-300 ` + className}>
{value ? value : children}
</label>
);
}
InputLabel
コンポーネントは、フォームの項目名を表示するためのコンポーネントです。
label
タグを用いて、表現しています。
追加のCSSはclassName
に記述します。
ラベルに表示する値を指定したい場合は、value
に値を代入して、value
が存在する場合はvalue
の値がlabel
の子要素として表示され、存在しない場合はコンポーネントの子要素(children
)が表示されます。
- InputError
ソースコード
export default function InputError({ message, className = '', ...props }) {
return message ? (
<p {...props} className={'text-sm text-red-600 dark:text-red-400 ' + className}>
{message}
</p>
) : null;
}
InputError
は、各項目に指定したmessage
に値が存在していた場合、そのメッセージを表示するためのコンポーネントです。
今回のアプリケーションでは主に、バリデーションチェック時のエラーを表示するために用いています。
コントローラーの実装:新規登録画面呼び出し
MeetingLogController.php
のcreate()
メソッドを編集して、新規登録画面を呼び出せるようにします。
ソースコード
public function create()
{
$query = MeetingLog::query();
$meetingLogs = [];
if (request("member")) {
$query->where("member_id", "=", request("member"));
$meetingLogs = $query->orderBy("created_at", "desc")->paginate(1);
}
$members = Member::where('office_id', '=', Auth::user()->office_id)->get();
$queryParams = request()->query();
return inertia("MeetingLog/Create", [
'members' => MemberResource::collection($members),
'meetingLogs' => MeetingLogResource::collection($meetingLogs) ?? [],
'queryParams' => $queryParams ?: null,
]);
}
- クエリ生成と初期化
$query = MeetingLog::query();
$meetingLogs = [];
まず、query()
メソッドを用いて、面談記録のクエリを生成します。
また、面談記録の一覧を格納する変数meetingLogs
を初期化しておきます。
- 面談記録のフィルタリング
if (request("member")) {
$query->where("member_id", "=", request("member"));
$meetingLogs = $query->orderBy("created_at", "desc")->paginate(1);
}
フロントエンド側から指定された利用者id
が存在している場合、そのid
に基づいた面談記録を作成日時で降順に並び替えた後、1ページごとのページネーションとして取得します。
- 利用者一覧とURLパラメーターの取得
$members = Member::where('office_id', '=', Auth::user()->office_id)->get();
$queryParams = request()->query();
$members
にアカウントの所属する事業所の利用者を取得し格納します。
また、URLパラメーターを$queryParams
に格納しています。
- データの送信
return inertia("MeetingLog/Create", [
'members' => MemberResource::collection($members),
'meetingLogs' => MeetingLogResource::collection($meetingLogs) ?? [],
'queryParams' => $queryParams ?: null,
]);
inertia()
メソッドを用いて、MeetingLog/Create.jsx
に対してデータを送信します。
前回記事で開設したMeetingLogController
の時と同様に、対応するリソースクラスに定義されたデータの形で送信しています。
リクエストの実装
面談記録をデータベースに新規登録する際、バリデーション処理を行うためにリクエストファイルを実装していきます。
ソースコード
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreMeetingLogRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
"title" => ['required', 'max:255'],
"user_id" => ['required', 'integer'],
"member_id" => ['required', 'integer'],
"condition" => ['required', 'integer', Rule::in(1, 2, 3, 4, 5)],
"meeting_log" => ['required', 'string'],
];
}
}
- リクエストの許可
authorize()
メソッドは、フォームリクエストを認可する条件を設定することができます。
例えば、ユーザーが編集しようとしているブログ記事を実際に所有しているか胴かを判断できます。
public function authorize(): bool
{
return true;
}
今回は、全てのリクエストを許可するようにしています。
- バリデーションルールの設定
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
"title" => ['required', 'max:255'],
"user_id" => ['required', 'integer'],
"member_id" => ['required', 'integer'],
"condition" => ['required', 'integer', Rule::in(1, 2, 3, 4, 5)],
"meeting_log" => ['required', 'string'],
];
}
rule()
メソッドにバリデーションルールを設定することができます。
キーのところに、指定するカラム名を、値のところにルールを記述していきます。
今回の場合は、全てを必須項目にするためにrequired
をしていています。
title
は255文字までという条件を追加し、他のカラムはデータ型を指定しています。
condition
は1から5までの数字のみ許容したいので、Rule
クラスのin
メソッドを用います。
コントローラーの実装:新規登録処理
MeetingLogController.php
のstore()
メソッドを編集して、新規登録画面で入力した情報をDBに保存する機能を実装します。
public function store(StoreMeetingLogRequest $request)
{
$data = $request->validated();
MeetingLog::create($data);
return to_route('meetinglog.index');
}
request
の内容をバリデーションした後、data
に格納して、面談記録(MeetingLog
)テーブルにcreate()
メソッドを利用して、保存しています。
ルーティング
最後に、ルーティングの設定をして行きます。
Route::middleware(['auth', 'verified'])->group(function () {
////省略////
+ Route::get('/meetinglog/create', [MeetingLogController::class, 'create'])
+ ->name('meetinglog.create');
});
おわりに
今回は、面談記録の詳細ページ閲覧機能と新規作成機能を実装しました。
次回は、面談記録の編集と削除機能を解説して、CRUD処理の解説を一通り終わらせていきたいと思います!
ではでは!
Discussion