見出し画像

Next.js とSupabaseで求人マッチングアプリを作る⑦~求職者一覧、求職者詳細情報~

今回は求職者一覧と求職者詳細を作成します。

  • 企業一覧

  • 企業詳細

  • 求職者一覧

  • 求職者詳細

  • 求人一覧

  • 求人詳細

おおよそ前回の流れと一緒なので、もし自学目的であれば、前回のコードを見ながら作ってみたあと、答え合わせをしても面白いかもしれません。
それでは見ていきましょう!

Supabase側の設定

前回と同様に求職者テーブル(mst_job_seeker)の`SELECT`ポリシーをtrueに変更します。

mst_job_seekerのポリシー変更

Supabaseダッシュボード→`Table Editor`から`mst_job_seeker`を右クリックしてポリシー編集画面を開きます。
添付画像のようにtrueのみに変更します。

Next.jsの実装

components/userinfo/JobSeekerBasic.tsx

求職者情報の編集画面内ですが、先ほどのポリシー変更の影響を受けているためそこだけ修正します。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { redirect } from "next/navigation";
import { SubmitButton } from "../../components/SubmitButton";
import { Database } from "@/types/supabase";

export default function JobSeekerBasic() {
  const [message, setMessage] = useState("");
  const supabase = createClient();
  const [nationalityData, setNationalityData] = useState<
    Database["public"]["Tables"]["mst_nationality"]["Row"][]
  >([]);
  const [occupationData, setOccupationData] = useState<
    Database["public"]["Tables"]["mst_occupation"]["Row"][]
  >([]);
  const [residenceQualificationData, setResidenceQualificationData] = useState<
    Database["public"]["Tables"]["mst_residence_qualification"]["Row"][]
  >([]);

  // フォームデータを保持する
  const [lastname, setLastname] = useState("");
  const [firstname, setFirstname] = useState("");
  const [middlename, setMiddlename] = useState("");
  const [gender, setGender] = useState("");
  const [birthday, setBirthday] = useState("");
  const [zipcode, setZipcode] = useState("");
  const [address1, setAddress1] = useState("");
  const [address2, setAddress2] = useState("");
  const [phone, setPhone] = useState("");
  const [email1, setEmail1] = useState("");
  const [email2, setEmail2] = useState("");
  const [email3, setEmail3] = useState("");
  const [nationality_id, setNationality_id] = useState<string | null>(null);
  const [current_annual_income, setCurrent_annual_income] = useState<number>();
  const [desired_annual_income, setDesired_annual_income] = useState<number>();
  const [spouse, setSpouse] = useState<string | null>(null);
  const [desired_occupation_id_1, setDesired_occupation_id_1] = useState<
    string | null
  >(null);
  const [desired_occupation_id_2, setDesired_occupation_id_2] = useState<
    string | null
  >(null);
  const [desired_occupation_id_3, setDesired_occupation_id_3] = useState<
    string | null
  >(null);
  const [desired_change_job_date, setDesired_change_job_date] = useState<
    string | null
  >(null);
  const [residence_qualification_id, setResidence_qualification_id] = useState<
    string | null
  >(null);
  const [residence_qualification_expired, setResidence_qualification_expired] =
    useState<string | null>(null);
  const [
    residence_qualification_front_image_url,
    setResidence_qualification_front_image_url,
  ] = useState("");
  const [
    residence_qualification_back_image_url,
    setResidence_qualification_back_image_url,
  ] = useState("");
  const [profile_image_url, setProfile_image_url] = useState("");
  const [resume_file_url, setResume_file_url] = useState("");
  const [resume_file_name, setResume_file_name] = useState("");

  useEffect(() => {
    getNationalityData();
    getOccupationData();
    getResidenceQualificationData();
    setData();
  }, []);

  const setData = async () => {
    const {
      data: { user },
    } = await supabase.auth.getUser();

    if (!user) {
      return redirect("/login");
    }

    const { data, error } = await supabase
      .from("mst_job_seeker")
      .select()
      .eq("user_uid", user.id);

    if (data != null && data?.length != 0) {
      const job_seeker_data: Database["public"]["Tables"]["mst_job_seeker"]["Row"] =
        data[0];
      setLastname(job_seeker_data.last_name);
      setFirstname(job_seeker_data.first_name);
      if (job_seeker_data.middle_name != null) {
        setMiddlename(job_seeker_data.middle_name);
      }
      setGender("" + job_seeker_data.gender);
      setBirthday(job_seeker_data.birthday);
      if (job_seeker_data.zipcode != null) {
        setZipcode(job_seeker_data.zipcode);
      }
      if (job_seeker_data.address1 != null) {
        setAddress1(job_seeker_data.address1);
      }
      if (job_seeker_data.address2 != null) {
        setAddress2(job_seeker_data.address2);
      }
      if (job_seeker_data.phone != null) {
        setPhone(job_seeker_data.phone);
      }
      setEmail1(job_seeker_data.email1);
      if (job_seeker_data.email2 != null) {
        setEmail2(job_seeker_data.email2);
      }
      if (job_seeker_data.email3 != null) {
        setEmail3(job_seeker_data.email3);
      }
      if (job_seeker_data.nationality_id != null) {
        setNationality_id(job_seeker_data.nationality_id);
      }
      if (job_seeker_data.current_annual_income != null) {
        setCurrent_annual_income(job_seeker_data.current_annual_income);
      }
      if (job_seeker_data.desired_annual_income != null) {
        setDesired_annual_income(job_seeker_data.desired_annual_income);
      }
      if (job_seeker_data.spouse != null) {
        setSpouse("" + job_seeker_data.spouse);
      }
      if (job_seeker_data.desired_occupation_id_1 != null) {
        setDesired_occupation_id_1(job_seeker_data.desired_occupation_id_1);
      }
      if (job_seeker_data.desired_occupation_id_2 != null) {
        setDesired_occupation_id_2(job_seeker_data.desired_occupation_id_2);
      }
      if (job_seeker_data.desired_occupation_id_3 != null) {
        setDesired_occupation_id_3(job_seeker_data.desired_occupation_id_3);
      }
      if (job_seeker_data.desired_change_job_date != null) {
        setDesired_change_job_date(job_seeker_data.desired_change_job_date);
      }
      if (job_seeker_data.residence_qualification_id != null) {
        setResidence_qualification_id(
          job_seeker_data.residence_qualification_id
        );
      }
      if (job_seeker_data["residence qualification_expired"] != null) {
        setResidence_qualification_expired(
          job_seeker_data["residence qualification_expired"]
        );
      }
      if (job_seeker_data["residence qualification_front_image_url"] != null) {
        setResidence_qualification_front_image_url(
          job_seeker_data["residence qualification_front_image_url"]
        );
      }
      if (job_seeker_data["residence qualification_back_image_url"] != null) {
        setResidence_qualification_back_image_url(
          job_seeker_data["residence qualification_back_image_url"]
        );
      }
      if (job_seeker_data.profile_image_url != null) {
        setProfile_image_url(job_seeker_data.profile_image_url);
      }
      if (job_seeker_data.resume_file_url != null) {
        setResume_file_url(job_seeker_data.resume_file_url);
      }
      if (job_seeker_data.resume_file_name != null) {
        setResume_file_name(job_seeker_data.resume_file_name);
      }
    }
  };

  const getNationalityData = async () => {
    const { data, error } = await supabase.from("mst_nationality").select();
    if (error) {
      console.log(error);
      return;
    }

    const fixed_data: Database["public"]["Tables"]["mst_nationality"]["Row"][] =
      data;
    setNationalityData(fixed_data);
  };

  const getOccupationData = async () => {
    const { data, error } = await supabase.from("mst_occupation").select();
    if (error) {
      console.log(error);
      return;
    }

    const fixed_data: Database["public"]["Tables"]["mst_occupation"]["Row"][] =
      data;
    setOccupationData(fixed_data);
  };

  const getResidenceQualificationData = async () => {
    const { data, error } = await supabase
      .from("mst_residence_qualification")
      .select();
    if (error) {
      console.log(error);
      return;
    }

    const fixed_data: Database["public"]["Tables"]["mst_residence_qualification"]["Row"][] =
      data;
    setResidenceQualificationData(fixed_data);
  };

  const onSubmit = async () => {
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user) {
      return redirect("/login");
    }
    const timeStamp = new Date().toISOString();

    const { error } = await supabase.from("mst_job_seeker").upsert({
      user_uid: user.id,
      last_name: lastname,
      first_name: firstname,
      middle_name: middlename,
      gender: parseInt(gender),
      birthday: birthday,
      zipcode: zipcode,
      address1: address1,
      address2: address2,
      phone: phone,
      email1: email1,
      email2: email2,
      email3: email3,
      nationality_id: nationality_id,
      current_annual_income: current_annual_income,
      desired_annual_income: desired_annual_income,
      spouse: spouse,
      desired_occupation_id_1: desired_occupation_id_1,
      desired_occupation_id_2: desired_occupation_id_2,
      desired_occupation_id_3: desired_occupation_id_3,
      desired_change_job_date: desired_change_job_date,
      residence_qualification_id: residence_qualification_id,
      "residence qualification_expired": residence_qualification_expired,
      "residence qualification_front_image_url":
        residence_qualification_front_image_url,
      "residence qualification_back_image_url":
        residence_qualification_back_image_url,
      profile_image_url: profile_image_url,
      resume_file_url: resume_file_url,
      resume_file_name: resume_file_name,
      updated_at: timeStamp,
    });
    console.log(error);

    if (error) {
      setMessage("保存に失敗しました。");
      return;
    } else {
      setMessage("");
    }
    return redirect("/userpage");
  };

  return (
    <div className="flex-1 flex flex-col w-96 py-8 sm:max-w-md justify-center gap-2">
      <form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
        <h2>求職者の登録情報</h2>
        <label className="text-md" htmlFor="lastname">
          姓*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="lastname"
          placeholder="山田"
          value={lastname}
          onChange={(e) => setLastname(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="firstname">
          名*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="firstname"
          placeholder="太郎"
          value={firstname}
          onChange={(e) => setFirstname(e.target.value)}
          required
        />
        <label className="text-md" htmlFor="middlename">
          ミドルネーム
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="middlename"
          value={middlename}
          onChange={(e) => setMiddlename(e.target.value)}
          placeholder="ミドルネーム"
        />
        <label className="text-md" htmlFor="gender">
          性別*
        </label>
        <select
          name="gender"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={gender}
          onChange={(e) => setGender(e.target.value)}
          required
        >
          <option value="">--選択してください--</option>
          <option value="1">男性</option>
          <option value="0">女性</option>
          <option value="2">その他</option>
        </select>
        <label className="text-md" htmlFor="birthday">
          誕生日*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="birthday"
          value={birthday}
          onChange={(e) => setBirthday(e.target.value)}
          type="date"
          required
        />
        <label className="text-md" htmlFor="zipcode">
          郵便番号
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="zipcode"
          value={zipcode}
          onChange={(e) => setZipcode(e.target.value)}
          placeholder="0000000 ※ハイフンなしでご入力ください"
        />

        <label className="text-md" htmlFor="address1">
          住所1
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="address1"
          value={address1}
          onChange={(e) => setAddress1(e.target.value)}
          placeholder="東京都港区浜松町2丁目2番15号"
        />
        <label className="text-md" htmlFor="address2">
          住所2
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="address2"
          value={address2}
          onChange={(e) => setAddress2(e.target.value)}
          placeholder="浜松町ダイヤビル2F"
        />

        <label className="text-md" htmlFor="phone">
          電話番号
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="phone"
          value={phone}
          onChange={(e) => setPhone(e.target.value)}
          placeholder="00000000000 ※ハイフンなしでご入力ください"
        />

        <label className="text-md" htmlFor="email1">
          メールアドレス1*
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email1"
          value={email1}
          onChange={(e) => setEmail1(e.target.value)}
          placeholder="email1@example.com"
          type="email"
          required
        />
        <label className="text-md" htmlFor="email2">
          メールアドレス2
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email2"
          value={email2}
          onChange={(e) => setEmail2(e.target.value)}
          placeholder="email2@example.com"
          type="email"
        />
        <label className="text-md" htmlFor="email3">
          メールアドレス3
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="email3"
          value={email3}
          onChange={(e) => setEmail3(e.target.value)}
          placeholder="email3@example.com"
          type="email"
        />

        <label className="text-md" htmlFor="nationalityId">
          国籍
        </label>
        <select
          name="nationalityId"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={nationality_id ? nationality_id : ""}
          onChange={(e) => setNationality_id(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {nationalityData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.nationality}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="current_annual_income">
          現在の年収
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="current_annual_income"
          value={current_annual_income}
          onChange={(e) => setCurrent_annual_income(parseInt(e.target.value))}
          placeholder="現在の年収"
          type="number"
        />

        <label className="text-md" htmlFor="desired_annual_income">
          希望年収
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="desired_annual_income"
          value={desired_annual_income}
          onChange={(e) => setDesired_annual_income(parseInt(e.target.value))}
          placeholder="希望年収"
          type="number"
        />

        <label className="text-md" htmlFor="spouse">
          配偶者
        </label>
        <select
          name="spouse"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={spouse ? spouse : ""}
          onChange={(e) => setSpouse(e.target.value)}
        >
          <option value="">--選択してください--</option>
          <option value="false">なし</option>
          <option value="true">あり</option>
        </select>

        <label className="text-md" htmlFor="desired_occupation_id_1">
          希望職種1
        </label>
        <select
          name="desired_occupation_id_1"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={desired_occupation_id_1 ? desired_occupation_id_1 : ""}
          onChange={(e) => setDesired_occupation_id_1(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="desired_occupation_id_2">
          希望職種2
        </label>
        <select
          name="desired_occupation_id_2"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={desired_occupation_id_2 ? desired_occupation_id_2 : ""}
          onChange={(e) => setDesired_occupation_id_2(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="desired_occupation_id_3">
          希望職種3
        </label>
        <select
          name="desired_occupation_id_3"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={desired_occupation_id_3 ? desired_occupation_id_3 : ""}
          onChange={(e) => setDesired_occupation_id_3(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {occupationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.occupation}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="desired_change_job_date">
          転職希望日
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="desired_change_job_date"
          value={desired_change_job_date ? desired_change_job_date : ""}
          onChange={(e) => setDesired_change_job_date(e.target.value)}
          type="date"
        />

        <label className="text-md" htmlFor="residence_qualification_id">
          在留資格
        </label>
        <select
          name="residence_qualification_id"
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          value={residence_qualification_id ? residence_qualification_id : ""}
          onChange={(e) => setResidence_qualification_id(e.target.value)}
        >
          <option value="">--選択してください--</option>
          {residenceQualificationData.map((item) => (
            <option key={item.id} value={item.id}>
              {item.residence_qualification}
            </option>
          ))}
        </select>

        <label className="text-md" htmlFor="residence_qualification_expired">
          在留資格期限
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="residence_qualification_expired"
          value={
            residence_qualification_expired
              ? residence_qualification_expired
              : ""
          }
          onChange={(e) => setResidence_qualification_expired(e.target.value)}
          type="date"
        />

        <label
          className="text-md"
          htmlFor="residence_qualification_front_image_url"
        >
          在留資格カード表のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="residence_qualification_front_image_url"
          value={residence_qualification_front_image_url}
          onChange={(e) =>
            setResidence_qualification_front_image_url(e.target.value)
          }
          placeholder="在留資格カード表のファイルURL"
        />

        <label
          className="text-md"
          htmlFor="residence_qualification_back_image_url"
        >
          在留資格カード裏のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="residence_qualification_back_image_url"
          value={residence_qualification_back_image_url}
          onChange={(e) =>
            setResidence_qualification_back_image_url(e.target.value)
          }
          placeholder="在留資格カード裏のファイルURL"
        />

        <label className="text-md" htmlFor="profile_image_url">
          プロフィール画像のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="profile_image_url"
          value={profile_image_url}
          onChange={(e) => setProfile_image_url(e.target.value)}
          placeholder="プロフィール画像のファイルURL"
        />

        <label className="text-md" htmlFor="resume_file_url">
          履歴書のファイルURL
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="resume_file_url"
          value={resume_file_url}
          onChange={(e) => setResume_file_url(e.target.value)}
          placeholder="履歴書のファイルURL"
        />
        <label className="text-md" htmlFor="resume_file_name">
          履歴書のファイル名
        </label>
        <input
          className="rounded-md px-4 py-2 bg-inherit border mb-6"
          name="resume_file_name"
          value={resume_file_name}
          onChange={(e) => setResume_file_name(e.target.value)}
          placeholder="履歴書のファイル名"
        />

        <SubmitButton
          formAction={onSubmit}
          className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"
          pendingText="ユーザ情報更新中..."
        >
          保存
        </SubmitButton>
        {message !== "" && (
          <p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
            {message}
          </p>
        )}
      </form>
    </div>
  );
}

`setData`関数内の下記の部分が変更されています。
useridで絞ってデータ取得をする仕組みに変更しています。

const {
      data: { user },
    } = await supabase.auth.getUser();

    if (!user) {
      return redirect("/login");
    }

    const { data, error } = await supabase
      .from("mst_job_seeker")
      .select()
      .eq("user_uid", user.id);

app/jobseekerlist/page.tsx

求職者一覧ページです。
企業側でログインしたときだけ確認できるようリダイレクトの仕組みを作成しています。

import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { getUserTypeFromEnv, userType } from "@/utils/usertype";
import CompanyList from "@/components/jobseeker/CompanyLIst";
import JobSeekerList from "@/components/company/JobSeekerList";

export default async function JobSeekerListPage() {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    return redirect("/login");
  }

  const usertype = getUserTypeFromEnv();

  if (usertype !== userType.company) {
    return redirect("/");
  }

  return (
    <div className="flex-1 w-full flex flex-col items-center">
      <Header />
      <JobSeekerList></JobSeekerList>
      <Footer />
    </div>
  );
}

app/jobseekerdetail/page.tsx

求職者の詳細情報ページです。
求職者IDがurlパラメータに渡されていると、その求職者の情報を子コンポーネントで取得できます。

import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { getUserTypeFromEnv, userType } from "@/utils/usertype";
import JobSeekerDetail from "@/components/company/JobSeekerDetail";

export default async function JobSeekerDetailPage({
  searchParams,
}: {
  searchParams: { jobseekerid: string };
}) {
  const supabase = createClient();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    return redirect("/login");
  }

  const usertype = getUserTypeFromEnv();

  if (usertype !== userType.company) {
    return redirect("/");
  }

  return (
    <div className="flex-1 w-full flex flex-col items-center">
      <Header />
      <JobSeekerDetail jobseekerid={searchParams.jobseekerid}></JobSeekerDetail>
      <Footer />
    </div>
  );
}

components/company/JobSeekerList.tsx

求職者一覧を表示するコンポーネントです。
取得した求職者情報をすべて一覧で表示します。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { Database } from "@/types/supabase";
import Link from "next/link";

export default function JobSeekerList() {
  const supabase = createClient();
  const [jobSeekerData, setJobSeekerData] = useState<
    Database["public"]["Tables"]["mst_job_seeker"]["Row"][]
  >([]);

  useEffect(() => {
    getJobSeekerData();
  }, []);

  const getJobSeekerData = async () => {
    const { data, error } = await supabase.from("mst_job_seeker").select();
    if (error) {
      console.log(error);
      return;
    }

    console.log(data);

    const tmp_data: Database["public"]["Tables"]["mst_job_seeker"]["Row"][] =
      data as Database["public"]["Tables"]["mst_job_seeker"]["Row"][];
    setJobSeekerData(tmp_data);
  };

  return (
    <div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
      <h1>求職者一覧</h1>
      <ul>
        {jobSeekerData.map((item) => (
          <li
            key={item.user_uid}
            className="bg-gray-200 rounded-md p-5 mt-4 hover:bg-gray-300"
          >
            <Link
              href={"/jobseekerdetail?jobseekerid=" + item.user_uid}
              className="block w-full"
            >
              <h2>{item.last_name + " " + item.first_name}</h2>
              <p>ユーザID:{item.user_uid}</p>
              <div className="flex">
                <div className="w-1/2">
                  <h3>性別</h3>
                  <p>
                    {item.gender == 0
                      ? "女性"
                      : item.gender == 1
                      ? "男性"
                      : "その他"}
                  </p>
                </div>
                <div className="w-1/2">
                  <h3>生年月日</h3>
                  <p>{item.birthday}</p>
                </div>
              </div>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

components/company/JobSeekerDetail.tsx

求職者一覧から、リンクを通じて遷移してくる詳細ページのコンポーネントです。
現在画像アップロードの仕組みはないため、画像URLの表示はスキップしています。

"use client";
import { createClient } from "@/utils/supabase/client";
import { useState, useEffect } from "react";
import { Database } from "@/types/supabase";

type Props = {
  jobseekerid: string | null;
};
export default function JobSeekerDetail({ jobseekerid }: Props) {
  const supabase = createClient();

  const [nationalityData, setNationalityData] = useState<
    Database["public"]["Tables"]["mst_nationality"]["Row"][]
  >([]);
  const [occupationData, setOccupationData] = useState<
    Database["public"]["Tables"]["mst_occupation"]["Row"][]
  >([]);
  const [residenceQualificationData, setResidenceQualificationData] = useState<
    Database["public"]["Tables"]["mst_residence_qualification"]["Row"][]
  >([]);

  // フォームデータを保持する
  const [lastname, setLastname] = useState("");
  const [firstname, setFirstname] = useState("");
  const [middlename, setMiddlename] = useState("");
  const [gender, setGender] = useState("");
  const [birthday, setBirthday] = useState("");
  const [zipcode, setZipcode] = useState("");
  const [address1, setAddress1] = useState("");
  const [address2, setAddress2] = useState("");
  const [phone, setPhone] = useState("");
  const [email1, setEmail1] = useState("");
  const [email2, setEmail2] = useState("");
  const [email3, setEmail3] = useState("");
  const [nationality_name, setNationality_name] = useState<string | null>(null);
  const [current_annual_income, setCurrent_annual_income] = useState<number>();
  const [desired_annual_income, setDesired_annual_income] = useState<number>();
  const [spouse, setSpouse] = useState<string | null>(null);
  const [desired_occupation_name_1, setDesired_occupation_name_1] = useState<
    string | null
  >(null);
  const [desired_occupation_name_2, setDesired_occupation_name_2] = useState<
    string | null
  >(null);
  const [desired_occupation_name_3, setDesired_occupation_name_3] = useState<
    string | null
  >(null);
  const [desired_change_job_date, setDesired_change_job_date] = useState<
    string | null
  >(null);
  const [residence_qualification_name, setResidence_qualification_name] =
    useState<string | null>(null);
  const [residence_qualification_expired, setResidence_qualification_expired] =
    useState<string | null>(null);
  const [
    residence_qualification_front_image_url,
    setResidence_qualification_front_image_url,
  ] = useState("");
  const [
    residence_qualification_back_image_url,
    setResidence_qualification_back_image_url,
  ] = useState("");
  const [profile_image_url, setProfile_image_url] = useState("");
  const [resume_file_url, setResume_file_url] = useState("");
  const [resume_file_name, setResume_file_name] = useState("");

  useEffect(() => {
    setData();
  }, []);

  const setData = async () => {
    if (jobseekerid === "") {
      return;
    }
    const { data, error } = await supabase
      .from("mst_job_seeker")
      .select()
      .eq("user_uid", jobseekerid);

    if (data != null && data?.length != 0) {
      const job_seeker_data: Database["public"]["Tables"]["mst_job_seeker"]["Row"] =
        data[0];
      setLastname(job_seeker_data.last_name);
      setFirstname(job_seeker_data.first_name);
      if (job_seeker_data.middle_name != null) {
        setMiddlename(job_seeker_data.middle_name);
      }
      setGender("" + job_seeker_data.gender);
      setBirthday(job_seeker_data.birthday);
      if (job_seeker_data.zipcode != null) {
        setZipcode(job_seeker_data.zipcode);
      }
      if (job_seeker_data.address1 != null) {
        setAddress1(job_seeker_data.address1);
      }
      if (job_seeker_data.address2 != null) {
        setAddress2(job_seeker_data.address2);
      }
      if (job_seeker_data.phone != null) {
        setPhone(job_seeker_data.phone);
      }
      setEmail1(job_seeker_data.email1);
      if (job_seeker_data.email2 != null) {
        setEmail2(job_seeker_data.email2);
      }
      if (job_seeker_data.email3 != null) {
        setEmail3(job_seeker_data.email3);
      }
      if (job_seeker_data.nationality_id != null) {
        const name = await getNationalityName(job_seeker_data.nationality_id);
        setNationality_name(name);
      }
      if (job_seeker_data.current_annual_income != null) {
        setCurrent_annual_income(job_seeker_data.current_annual_income);
      }
      if (job_seeker_data.desired_annual_income != null) {
        setDesired_annual_income(job_seeker_data.desired_annual_income);
      }
      if (job_seeker_data.spouse != null) {
        setSpouse("" + job_seeker_data.spouse);
      }
      if (job_seeker_data.desired_occupation_id_1 != null) {
        const name = await getOccupationName(
          job_seeker_data.desired_occupation_id_1
        );
        setDesired_occupation_name_1(name);
      }
      if (job_seeker_data.desired_occupation_id_2 != null) {
        const name = await getOccupationName(
          job_seeker_data.desired_occupation_id_2
        );
        setDesired_occupation_name_2(name);
      }
      if (job_seeker_data.desired_occupation_id_3 != null) {
        const name = await getOccupationName(
          job_seeker_data.desired_occupation_id_3
        );
        setDesired_occupation_name_3(name);
      }
      if (job_seeker_data.desired_change_job_date != null) {
        setDesired_change_job_date(job_seeker_data.desired_change_job_date);
      }
      if (job_seeker_data.residence_qualification_id != null) {
        const name = await getResidenceQualificationName(
          job_seeker_data.residence_qualification_id
        );
        setResidence_qualification_name(name);
      }
      if (job_seeker_data["residence qualification_expired"] != null) {
        setResidence_qualification_expired(
          job_seeker_data["residence qualification_expired"]
        );
      }
      if (job_seeker_data["residence qualification_front_image_url"] != null) {
        setResidence_qualification_front_image_url(
          job_seeker_data["residence qualification_front_image_url"]
        );
      }
      if (job_seeker_data["residence qualification_back_image_url"] != null) {
        setResidence_qualification_back_image_url(
          job_seeker_data["residence qualification_back_image_url"]
        );
      }
      if (job_seeker_data.profile_image_url != null) {
        setProfile_image_url(job_seeker_data.profile_image_url);
      }
      if (job_seeker_data.resume_file_url != null) {
        setResume_file_url(job_seeker_data.resume_file_url);
      }
      if (job_seeker_data.resume_file_name != null) {
        setResume_file_name(job_seeker_data.resume_file_name);
      }
    }
  };

  const getNationalityName = async (id: string) => {
    const { data, error } = await supabase
      .from("mst_nationality")
      .select()
      .eq("id", id);
    if (error) {
      console.log(error);
      return;
    }
    return data[0].nationality;
  };

  const getOccupationName = async (id: string) => {
    const { data, error } = await supabase
      .from("mst_occupation")
      .select()
      .eq("id", id);
    if (error) {
      console.log(error);
      return;
    }

    return data[0].occupation;
  };

  const getResidenceQualificationName = async (id: string) => {
    const { data, error } = await supabase
      .from("mst_residence_qualification")
      .select()
      .eq("id", id);
    if (error) {
      console.log(error);
      return;
    }

    return data[0].residence_qualification;
  };

  return (
    <div className="flex-1 flex flex-col w-full py-8 sm:max-w-md justify-center gap-2 animate-in">
      <h1>求職者詳細</h1>
      <ul>
        <li>
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">姓</dt>
            <dd className="p-3">{lastname}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">名</dt>
            <dd className="p-3">{firstname}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">ミドルネーム</dt>
            <dd className="p-3">{middlename}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">性別</dt>
            <dd className="p-3">
              {gender == "0" ? "女性" : gender == "1" ? "男性" : "その他"}
            </dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">誕生日</dt>
            <dd className="p-3">{birthday}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">郵便番号</dt>
            <dd className="p-3">{zipcode}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">住所1</dt>
            <dd className="p-3">{address1}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">住所2</dt>
            <dd className="p-3">{address2}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">電話番号</dt>
            <dd className="p-3">{phone}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">メールアドレス1</dt>
            <dd className="p-3">{email1}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">メールアドレス2</dt>
            <dd className="p-3">{email2}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">メールアドレス3</dt>
            <dd className="p-3">{email3}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">国籍</dt>
            <dd className="p-3">{nationality_name}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">現在の年収</dt>
            <dd className="p-3">{current_annual_income}万円</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">希望年収</dt>
            <dd className="p-3">{desired_annual_income}万円</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">配偶者</dt>
            <dd className="p-3">{spouse === "true" ? "あり" : "なし"}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">希望職種1</dt>
            <dd className="p-3">{desired_occupation_name_1}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">希望職種2</dt>
            <dd className="p-3">{desired_occupation_name_2}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">希望職種3</dt>
            <dd className="p-3">{desired_occupation_name_3}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">転職希望日</dt>
            <dd className="p-3">{desired_change_job_date}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">在留資格</dt>
            <dd className="p-3">{residence_qualification_name}</dd>
          </dl>
        </li>
        <li className="mt-2">
          <dl className="flex">
            <dt className="p-3 w-48 bg-gray-200">在留資格期限</dt>
            <dd className="p-3">{residence_qualification_expired}</dd>
          </dl>
        </li>
      </ul>
    </div>
  );
}

それでは実装を確認してみます。

実装の確認

企業側のアプリにアクセスし、ログインしましょう。

求職者一覧へのリンクがヘッダーに表示されることがわかります。
これをクリックすると、求職者の一覧ページにアクセスできます。
求職者側のアプリで作成した求職者情報の一覧が表示されます。
(現在一つしか入れていないので一つしか表示されませんが、複数追加すれば複数表示されます。)

こちらの求職者リンクをクリックすると求職者の詳細情報が表示されます。

これで求職者の一覧情報と詳細情報にアクセスすることができるようになりました。

その他参考資料など

弊社では『マッチングワン』という『低コスト・短期にマッチングサービスを構築できる』サービスを展開しており、今回ご紹介するコードは、その『マッチングワン』でも使われているコードとなります。
本記事で紹介したようなプロダクトを開発されたい場合は、是非お問い合わせください。

またTodoONada株式会社では、この記事で紹介した以外にも、Supabase・Next.jsを使ったアプリの作成方法についてご紹介しています。
下記記事にて一覧を見ることができますので、ぜひこちらもご覧ください!

お問合せ&各種リンク

presented by

サポートしていただくと、筆者のやる気がガンガンアップします!