🙆

面談記録管理アプリ開発:面談記録の編集機能と削除機能の実装

に公開

はじめに

今回は、面談記録の編集と削除機能を実装していきます。

Edit.jsxの実装

まずは、フロントエンド側を実装していきます。

ソースコード
resources/js/Pages/MeetingLog/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.phpedit()メソッドを以下のように書き換えて、編集画面を呼び出せるようにします。

ソースコード
app/Http/Controllers/MeetingLogController.php
    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.phpupdate()メソッドを以下のように書き換えて行きます。

app/Http/Controllers/MeetingLogController.php
    public function update(UpdateMeetingLogRequest $request, MeetingLog $meetingLog)
    {
        $data = $request->validated();

        $meetingLog->update($data);

        return to_route('meetinglog.index');
    }

create()メソッドと同様に、requestをバリデーションした後、data変数に格納します。

その後、update()を用いて、該当する面談記録を更新します。

そして、面談一覧画面にリダイレクトしています。

コントローラーの実装:面談記録の削除機能

MeetingLogController.phpdestroy()メソッドを以下のように書き換えて、削除機能を実装します。

app/Http/Controllers/MeetingLogController.php
public function destroy(MeetingLog $meetingLog)
    {
        $meetingLog->delete();
        return to_route('meetinglog.index');
    }

delete()メソッドを用いて、該当する面談記録を削除し、to_route()メソッドを用いて面談記録一覧ページにリダイレクトしています。

ルーティング

そして、編集・削除機能それぞれのルーティングをしていきます。

routes/web.php
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