面談記録管理アプリ開発:面談記録の編集機能と削除機能の実装
はじめに
今回は、面談記録の編集と削除機能を実装していきます。
Edit.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";
export default function Edit({ auth, members, currentLog, meetingLogs, queryParams = null }) {
queryParams = queryParams || {}
const { data, setData, post, errors, reset } = useForm({
title: currentLog.title || "",
user_id: auth.user.id,
member_id: queryParams.member || currentLog.member.id,
condition: currentLog.condition || "",
meeting_log: currentLog.meeting_log || "",
_method: "PUT"
})
const memberChanged = (value) => {
if (value) {
queryParams["member"] = value;
queryParams["page"] = 1;
} else {
delete queryParams["member"];
}
router.get(route('meetinglog.edit', [currentLog.id, queryParams]));
}
const onSubmit = (e) => {
e.preventDefault();
post(route('meetinglog.update', [currentLog.id]));
}
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">
{data.title} の編集
</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 key={meetingLog.id}>
<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="p-6 text-gray-900 w-1/2 sm:p-8 bg-white shadow sm:rounded-lg">
<div className="text-center">面談記録がありません</div>
</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={data.member_id}
className="mt-1 block w-full"
onChange={(e) => { memberChanged(e.target.value) }}
>
<option value="">利用者名を選択してください</option>
{members.data.map(member => (
<option key={member.id} 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, reset } = useForm({
title: currentLog.title || "",
user_id: auth.user.id,
member_id: queryParams.member || currentLog.member.id,
condition: currentLog.condition || "",
meeting_log: currentLog.meeting_log || "",
_method: "PUT"
})
構造はCreate.jsx
と同様ですが、へ登録済みのデータを参照するために、各項目に現在入力されているデータを入れています。
Create.jsx
と記述方法が統一されていないのでリファクタリング予定です。
- memberChanged
const memberChanged = (value) => {
if (value) {
queryParams["member"] = value;
queryParams["page"] = 1;
} else {
delete queryParams["member"];
}
router.get(route('meetinglog.edit', [currentLog.id, queryParams]));
}
こちらも、Create.jsx
と同様です。
リダイレクト先を編集ページにしています。
- onSubmit
const onSubmit = (e) => {
e.preventDefault();
post(route('meetinglog.update', [currentLog.id]));
}
保存ボタンを押した際のイベント処理を記述しています。
こちらもCreate.jsx
と同様です。
データの送信先が、meetinglog.update
になっています。
こちらも同様の処理が複数ファイルに渡って存在しているのでフィファクタリング予定です。
- 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">
{data.title} の編集
</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 key={meetingLog.id}>
<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="p-6 text-gray-900 w-1/2 sm:p-8 bg-white shadow sm:rounded-lg">
<div className="text-center">面談記録がありません</div>
</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={data.member_id}
className="mt-1 block w-full"
onChange={(e) => { memberChanged(e.target.value) }}
>
<option value="">利用者名を選択してください</option>
{members.data.map(member => (
<option key={member.id} 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>
);
Create.jsx
同様の構造となっています。
前回記事で解説しているので割愛
コントローラーの実装:編集画面呼び出し
まず、編集画面を呼び出すためのコントローラーの設定をします。
MeetingLogController.php
のedit()
メソッドを以下のように書き換えて、編集画面を呼び出せるようにします。
ソースコード
public function edit(MeetingLog $meetingLog)
{
$query = MeetingLog::query();
$meetingLogs = [];
$officeId = User::select("office_id")->where("id", "=", $meetingLog->user_id);
$members = Member::where('office_id', '=', $officeId)->get();
if (request("member") || $meetingLog) {
$memberId = request("member") ?? $meetingLog->member_id;
$query->where("member_id", "=", $memberId);
$meetingLogs = $query->orderBy("created_at", "desc")->paginate(1);
}
$queryParams = request()->query();
return inertia('MeetingLog/Edit', [
'members' => MemberResource::collection($members),
'currentLog' => new MeetingLogResource($meetingLog),
'meetingLogs' => MeetingLogResource::collection($meetingLogs) ?? [],
'queryParams' => $queryParams ?: null,
]);
}
- 初期設定
$query = MeetingLog::query();
$meetingLogs = [];
新規登録機能の実装時と同様に、query()
メソッドを用いて、面談記録のクエリを作成します。
また、面談記録の一覧を格納する変数meetingLogs
を初期化しておきます。
- 利用者一覧の取得
$officeId = User::select("office_id")->where("id", "=", $meetingLog->user_id);
$members = Member::where('office_id', '=', $officeId)->get();
編集したい面談記録に登録されているゆり従業員IDからその従業員の事業所IDを絞り込んで、その事業所IDをもとに、その事業所に所属する利用者の一覧を取得しています。
この利用者一覧は、面談記録の利用者を変更する際のSelectInput
の一覧表示に利用されます。
- 利用者選択時に過去の面談記録の取得
if (request("member") || $meetingLog) {
$memberId = request("member") ?? $meetingLog->member_id;
$query->where("member_id", "=", $memberId);
$meetingLogs = $query->orderBy("created_at", "desc")->paginate(1);
}
フロントエンド側から指定された利用者idが存在している場合、そのid
に基づいた面談記録を作成日時で降順に並び替えた後、1ページごとのページネーションとして取得します。
- データの送信
$queryParams = request()->query();
return inertia('MeetingLog/Edit', [
'members' => MemberResource::collection($members),
'currentLog' => new MeetingLogResource($meetingLog),
'meetingLogs' => MeetingLogResource::collection($meetingLogs) ?? [],
'queryParams' => $queryParams ?: null,
]);
inertia()
メソッドを用いて、MeetingLog/Edit.jsx
に対してデータを送信します。
以前記事で解説したMeetingLogController
の時と同様に、対応するリソースクラスに定義されたデータの形で送信しています。
コントローラーの実装:編集内容をDBに記録
つぎに、編集内容をデーターベースに保存する機能の実装を行います。
MeetingLogController.php
のupdate()
メソッドを以下のように書き換えて行きます。
public function update(UpdateMeetingLogRequest $request, MeetingLog $meetingLog)
{
$data = $request->validated();
$meetingLog->update($data);
return to_route('meetinglog.index');
}
create()
メソッドと同様に、request
をバリデーションした後、data
変数に格納します。
その後、update()
を用いて、該当する面談記録を更新します。
そして、面談一覧画面にリダイレクトしています。
コントローラーの実装:面談記録の削除機能
MeetingLogController.php
のdestroy()
メソッドを以下のように書き換えて、削除機能を実装します。
public function destroy(MeetingLog $meetingLog)
{
$meetingLog->delete();
return to_route('meetinglog.index');
}
delete()
メソッドを用いて、該当する面談記録を削除し、to_route()
メソッドを用いて面談記録一覧ページにリダイレクトしています。
ルーティング
そして、編集・削除機能それぞれのルーティングをしていきます。
Route::middleware(['auth', 'verified'])->group(function () {
////省略////
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');
});
おわりに
今回は、面談記録の編集機能と削除機能について解説しました!
次回は、面談記録の詳細画面に実装しているチャット機能の解説をしていこうと思います!
ではでは!
Discussion