前回に続いて、今回は面接前~内定承諾までのステータスを変更する機能を作成します。
この機能は、企業側が各応募者とどこまでやり取りを進めていたか、わかりやすくするものでもありますし、応募者としても進捗が可視化されて安心することができます。
また今回は取り扱いませんが、一定期間ステータスが変化しなかった場合、行動を促したり、一定のステータスに達した案件について成功報酬を取る…といったことも可能です。
では早速作り方を見ていきましょう。
![](https://libproc.com/wp-content/uploads/2024/06/image-25.png)
![](https://libproc.com/wp-content/uploads/2024/06/image-22.png)
Next.jsの実装
チャットページの親側でユーザタイプを定義し、子コンポーネントに渡すことができるようにしました。
app/chats/page.tsx
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import ChatView from "@/components/chats/ChatView";
import {getUserTypeFromEnv} from "@/utils/usertype";
export default async function ChatsPage({
searchParams,
}: {
searchParams: { companyid: string };
}) {
const supabase = createClient();
const usertype = getUserTypeFromEnv()
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
return (
<div className="flex-1 w-full flex flex-col items-center">
<Header />
<ChatView usertype={usertype}></ChatView>
</div>
);
}
components/Header.tsx
今回の本題とは関係ありませんが、基本情報がない状態で応募やスカウト、求人の作成ができないようにヘッダーのだし分けを行っています。
import {createClient} from "@/utils/supabase/server";
import Link from "next/link";
import {redirect} from "next/navigation";
import {getUserTypeFromEnv, userType} from "@/utils/usertype";
import HeaderSearchForm from "@/components/jobseeker/HeaderSearchForm";
import Logo from "@/components/Logo";
export default async function Header() {
const supabase = createClient();
const usertype = getUserTypeFromEnv();
let userDataFlg = false;
const {
data: {user},
} = await supabase.auth.getUser();
const isUserDataExist = async () => {
if (usertype == userType.company) {
const {data, error } = await supabase.from("mst_company").select().eq("user_uid", user?.id)
if (data?.length ? data?.length : 0 > 0) {
userDataFlg = true
}
} else if (usertype == userType.job_seeker) {
const {data, error } = await supabase.from("mst_job_seeker").select().eq("user_uid", user?.id)
if (data?.length ? data?.length : 0 > 0) {
userDataFlg = true
}
}
}
await isUserDataExist()
const signOut = async () => {
"use server";
const supabase = createClient();
await supabase.auth.signOut();
return redirect("/login");
};
return (
<nav className="w-full p-3 text-sm border-b border-gray-300 bg-white">
<Logo></Logo>
{user && usertype == userType.company ? (
<div className="flex justify-end items-center gap-4">
{userDataFlg ? (<>
<Link className="text-center w-20" href="/myjoblist">
<div className="block">
<span className="material-symbols-outlined">
work
</span>
</div>
自社の求人
</Link>
<Link className="text-center w-20" href="/jobseekerlist">
<div className="block">
<span className="material-symbols-outlined">
badge
</span>
</div>
求職者一覧
</Link>
<Link className="text-center w-20" href="/chats">
<div className="block">
<span className="material-symbols-outlined">
chat
</span>
</div>
メッセージ
</Link>
</>) : null}
<Link className="text-center w-20" href="/userpage">
<div className="block">
<span className="material-symbols-outlined">
person
</span>
</div>
ユーザ
</Link>
<form className="content-center" action={signOut}>
<button
className="w-24 py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
ログアウト
</button>
</form>
</div>
) : user && usertype == userType.job_seeker ? (
<div className="w-full flex justify-between gap-4">
<HeaderSearchForm></HeaderSearchForm>
<div className="flex">
{userDataFlg ? (<>
<Link className="text-center w-20" href="/joblist">
<div className="block">
<span className="material-symbols-outlined">
work
</span>
</div>
求人一覧
</Link>
<Link className="text-center w-20" href="/companylist">
<div className="block">
<span className="material-symbols-outlined">
apartment
</span>
</div>
企業一覧
</Link>
<Link className="text-center w-20" href="/chats">
<div className="block">
<span className="material-symbols-outlined">
chat
</span>
</div>
メッセージ
</Link>
</>) : null}
<Link className="text-center w-20" href="/userpage">
<div className="block">
<span className="material-symbols-outlined">
person
</span>
</div>
ユーザ
</Link>
<form className="content-center" action={signOut}>
<button
className="w-24 py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
ログアウト
</button>
</form>
</div>
</div>
) : (
<div className="flex justify-end items-center gap-4">
<Link
href="/login"
className="w-24 py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
ログイン
</Link>
<Link
href="/signup"
className="w-24 py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
会員登録
</Link>
</div>
)}
</nav>
);
}
components/chats/ChatList.tsx
自分と相手のやり取りがわかりやすいよう、CSSを修正しています。
"use client"
import {createClient} from "@/utils/supabase/client";
import {useEffect, useState} from "react";
import {User} from "@supabase/gotrue-js";
import {Database} from "@/types/supabase";
import SendButton from "@/components/chats/SendButton";
type Props = {
user: User | null,
job_seeker_jobid: number | null
}
export default function ChatList({user, job_seeker_jobid}: Props) {
const supabase = createClient()
const [chatData, setChatData] = useState<Database["public"]["Tables"]["trn_apply_message"]["Row"][]>([]);
// リロードのためにトグルするやつ。
const [reloadFlg, setReloadFlg] = useState<boolean>(false);
useEffect(() => {
if (job_seeker_jobid == null || user == null) return
getChatData()
setReloadFlg(false)
}, [job_seeker_jobid, reloadFlg]);
const getChatData = async () => {
const {data, error} = await supabase.from("trn_apply_message").select().eq("job_seeker_job_id", job_seeker_jobid)
if (error) {
console.log(error);
return []
}
setChatData(data)
}
return (<div>
<ul>
{chatData.map((item) => (
<li className={user?.id == item.sender_id ? ("flex justify-end mb-2") : ("flex mb-2")} key={item.id}>
<div className={user?.id == item.sender_id ? ("ml-4 inline-block rounded-md p-2 bg-green-500 text-white") : ("mr-4 inline-block rounded-md p-2 bg-white")}>
{item.message}
</div>
</li>
))}
</ul>
<SendButton user={user} job_seeker_jobid={job_seeker_jobid} setReloadFlg={setReloadFlg}></SendButton>
</div>)
}
components/chats/ChatView.tsx
企業側のみステータスの変更ができるよう、変更しています。
また、UI上の変更としてチャットのサイドメニューに求人の名前と求職者の名前、年齢を表示しました。
![](https://libproc.com/wp-content/uploads/2024/06/image-21.png)
"use client"
import {useEffect, useState} from "react";
import {createClient} from "@/utils/supabase/client";
import {getUserTypeFromEnv, userType} from "@/utils/usertype";
import {Database} from "@/types/supabase";
import ChatList from "@/components/chats/ChatList";
import {User} from "@supabase/gotrue-js";
import {getYearsOld} from "@/utils/jobseekerUtils";
import {DatabaseType} from "@/utils/DatabaseType";
type Props = {
usertype: userType
}
export default function ChatView({usertype}: Props) {
const supabase = createClient();
const [jobSeekerJobs, setJobSeekerJobs] = useState<any[]>([]);
const [currentJobSeekerJobID, setCurrentJobSeekerJobID] = useState<number | null>(null);
const [currentUser, setCurrentUser] = useState<User | null>(null);
useEffect(() => {
getJobSeekerJob()
}, []);
// ユーザと結びつくjob_seeker_job IDを検索
const getJobSeekerJob = async () => {
let tmpUser: User | null
if (currentUser) {
tmpUser = currentUser
} else {
const {data: {user}} = await supabase.auth.getUser()
setCurrentUser(user)
tmpUser = user
}
const usertype = getUserTypeFromEnv()
let fixedJobList: Database["public"]["Tables"]["m2m_job_seeker_job"]["Row"][] = []
let result: any[] = []
if (usertype == userType.job_seeker && tmpUser != null) {
const {data, error} = await supabase.from("m2m_job_seeker_job").select().eq("job_seeker_id", tmpUser.id)
if (error) return;
fixedJobList = data as Database["public"]["Tables"]["m2m_job_seeker_job"]["Row"][]
} else if (usertype == userType.company && tmpUser != null) {
const jobList = await getJobListFromCompanyID(tmpUser.id)
for (let i = 0; i < jobList.length; i++) {
const {data, error} = await supabase.from("m2m_job_seeker_job").select().eq("job_id", jobList[i]["id"])
if (error) continue;
fixedJobList = fixedJobList.concat(data as Database["public"]["Tables"]["m2m_job_seeker_job"]["Row"][]);
}
}
// 求人名、求職者名取得
for (let i = 0; i < fixedJobList.length; i++) {
const jobList = await getJobListFromJobID(fixedJobList[i]["job_id"])
if (jobList.length == 0) continue
const {
data,
error
} = await supabase.from("mst_job_seeker").select().eq("user_uid", fixedJobList[i]["job_seeker_id"])
if (error || data == null || data.length == 0) continue
const tmp_data = data as Database["public"]["Tables"]["mst_job_seeker"]["Row"][]
const yearsOld = getYearsOld(tmp_data[0]["birthday"])
let statusStr = ""
switch (fixedJobList[i]["status"]) {
case "before_interview":
statusStr = "面接前"
break;
case "reject":
statusStr = "不採用"
break;
case "selection":
statusStr = "選考中"
break;
case "offer":
statusStr = "オファー"
break;
case "decline":
statusStr = "選考辞退"
break
case "accept":
statusStr = "内定承諾"
break
}
result.push({
id: fixedJobList[i]["id"],
jobName: jobList[0]["name"],
username: tmp_data[0]["last_name"] + " " + tmp_data[0]["first_name"],
yearsOld: yearsOld,
status: statusStr
})
}
setJobSeekerJobs(result)
}
const getJobListFromJobID = async (id: string) => {
const {data, error} = await supabase.from("trn_job").select().eq("id", id)
if (error) return []
return data as Database["public"]["Tables"]["trn_job"]["Row"][]
}
const getJobListFromCompanyID = async (companyId: string) => {
const {data, error} = await supabase.from("trn_job").select().eq("company_uid", companyId)
if (error) return []
return data as Database["public"]["Tables"]["trn_job"]["Row"][]
}
const [showStatusModal, setShowStatusModal] = useState<boolean>(false)
const [currentStatus, setCurrentStatus] = useState<string>("")
const handleStatus = async (id: string) => {
const result = window.confirm("この求職者のステータスを変更しますか?");
if (!result) return
setShowStatusModal(false)
const {error} = await supabase.from("m2m_job_seeker_job").update({status: currentStatus}).eq("id", id)
if (error) return
await getJobSeekerJob()
}
return (<div className="relative flex w-full max-w-4xl h-max min-h-screen pb-16">
<ul className="w-3/5 bg-white">
{jobSeekerJobs.map((item) => (
<li className="relative border-b-gray-200 border-b" key={item.id}>
<button onClick={() => {
setCurrentJobSeekerJobID(item.id)
}}
className="relative text-left p-4 bg-white w-full hover:bg-gray-200 flex items-center justify-between">
<div className="w-48 font-bold text-lg">{item.jobName}<br/><span
className="font-normal text-base">{item.username} {item.yearsOld}歳</span></div>
{usertype == userType.job_seeker ? (<div
className="relative w-16 bg-gray-300 rounded-md text-center">{item.status}</div>) : usertype == userType.company ? (
<button onClick={() => setShowStatusModal(true)}
className="relative w-16 bg-gray-300 rounded-md text-center hover:bg-gray-50">{item.status}</button>) : null}
</button>
{showStatusModal ? (
<div className="absolute top-0 right-0 p-4 z-50 border border-gray-300 bg-gray-100">
<div className="text-right">
<button onClick={() => setShowStatusModal(false)}>
<span className="material-symbols-outlined">
close
</span>
</button>
</div>
<p>ステータスを選択してください。</p>
<select className="border block mb-4"
onChange={(e) => setCurrentStatus(e.target.value)}
>
<option value="before_interview" selected={item.status === "面接前"}>面接前</option>
<option value="reject" selected={item.status === "不採用"}>不採用</option>
<option value="selection" selected={item.status === "選考中"}>選考中</option>
<option value="offer" selected={item.status === "オファー"}>オファー</option>
<option value="decline" selected={item.status === "選考辞退"}>選考辞退</option>
<option value="accept" selected={item.status === "内定承諾"}>内定承諾</option>
</select>
<div className="w-full text-right">
<button className="bg-green-500 hover:bg-green-600 rounded-md px-4 py-2 text-white"
onClick={() => handleStatus(item.id)}>保存
</button>
</div>
</div>) : null}
</li>
))}
</ul>
<div className="w-full bg-gray-200 border-l p-4">
<ChatList user={currentUser} job_seeker_jobid={currentJobSeekerJobID}></ChatList>
</div>
</div>)
}
utils/jobseekerUtils.ts
新規でutilファイルを作成し、誕生日から年齢を計算する処理を共通化しました。
export const getYearsOld = (birthday: string) => {
const dateArr = birthday.split("-");
const today = new Date();
let age = today.getFullYear() - parseInt(dateArr[0]);
const thisYearsBirthday = new Date(today.getFullYear(), parseInt(dateArr[1]) - 1, parseInt(dateArr[2]))
if(today < thisYearsBirthday){
//誕生日の調整
age--;
}
return age;
}
components/company/JobSeekerDataUtil.ts
上記の処理がもともとあった`JobSeekerDataUtil.ts`から該当部分を削除しています。
"use client"
import {Database} from "@/types/supabase";
import {createClient} from "@/utils/supabase/client";
import {getYearsOld} from "@/utils/jobseekerUtils";
export async function getFixedJobSeekerData(data: Database["public"]["Tables"]["mst_job_seeker"]["Row"][]) {
const supabase = createClient();
const getNationalityData = async () => {
const { data, error } = await supabase.from("mst_nationality").select();
if (error) {
console.log(error);
return [];
}
return data as Database["public"]["Tables"]["mst_nationality"]["Row"][]
}
const getWorkLocationData = async () => {
const {data, error} = await supabase.from("mst_work_location").select();
if (error) {
console.log(error);
return [];
}
return data as Database["public"]["Tables"]["mst_work_location"]["Row"][];
};
const result: any[] = []
const nationalityData = await getNationalityData();
const workLocationData = await getWorkLocationData();
for (let i = 0; i < data.length; i++) {
const age = getYearsOld(data[i]["birthday"])
let nationalityStr = ""
for (let j = 0; j < nationalityData.length; j++) {
const nationalityId = data[i]["nationality_id"]
if (nationalityId != null) {
if (data[i]["nationality_id"]! === nationalityData[j]["id"]) {
nationalityStr = nationalityData[j]["nationality"]
}
}
}
let workLocationStr = ""
for (let j = 0; j < workLocationData.length; j++) {
const workLocationId = data[i]["desired_work_location"]
if (workLocationId != null) {
if (data[i]["desired_work_location"]! === workLocationData[j]["id"]) {
workLocationStr = workLocationData[j]["work_location"]
}
}
}
result.push({
"user_uid": data[i]["user_uid"],
"gender": data[i]["gender"],
"age": age,
"nationality": nationalityStr,
"desired_annual_income": data[i]["desired_annual_income"],
"desired_change_job_date": data[i]["desired_change_job_date"],
"residence_qualification_expired": data[i]["residence qualification_expired"],
"workLocation": workLocationStr
})
}
return result
}
components/userinfo/CompanyBasic.tsx
基本情報の電話番号とメールアドレスは重複してはいけないのですが、現状のエラーメッセージだとなぜ保存に失敗したのかわからないため修正しました。
"use client";
import {createClient} from "@/utils/supabase/client";
import {useEffect, useState} from "react";
import {redirect} from "next/navigation";
import {SubmitButton} from "@/components/SubmitButton";
import {Database} from "@/types/supabase";
import DefaultInput from "@/components/common/DefaultInput";
import {InputType} from "@/utils/inputType";
import DefaultSelect from "@/components/common/DefaultSelect";
import {DatabaseType} from "@/utils/DatabaseType";
export default function CompanyBasic() {
const [message, setMessage] = useState("");
const supabase = createClient();
const [industryData, setIndustryData] = useState<
Database["public"]["Tables"]["mst_industry"]["Row"][]
>([]);
// 初回のデータかどうか?
const [isFirst, setIsFirst] = useState<boolean>(false);
// フォームデータを保持する
const [companyname, setCompanyname] = useState("");
const [zipcode, setZipcode] = useState("");
const [address1, setAddress1] = useState("");
const [address2, setAddress2] = useState("");
const [phone, setPhone] = useState<string | null>(null);
const [email, setEmail] = useState("");
const [companyurl, setCompanyurl] = useState<string | null>(null);
const [capital, setCapital] = useState<number | null>(null);
const [employee, setEmployee] = useState<number | null>(null);
const [annual_turnover, setAnnual_turnover] = useState<number | null>(null);
const [established_at, setEstablishd_at] = useState<string | null>(null);
const [industry_id_1, setIndustry_id_1] = useState<string | null>(null);
const [industry_id_2, setIndustry_id_2] = useState<string | null>(null);
const [industry_id_3, setIndustry_id_3] = useState<string | null>(null);
const [company_image_url_1, setCompany_image_url_1] = useState<string | null>(
null
);
const [company_image_url_2, setCompany_image_url_2] = useState<string | null>(
null
);
const [company_image_url_3, setCompany_image_url_3] = useState<string | null>(
null
);
const [company_image_url_4, setCompany_image_url_4] = useState<string | null>(
null
);
const [company_image_url_5, setCompany_image_url_5] = useState<string | null>(
null
);
useEffect(() => {
getIndustyData();
setData();
}, []);
const setData = async () => {
const {
data: {user},
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
const {data} = await supabase
.from("mst_company")
.select()
.eq("user_uid", user.id);
if (data != null && data?.length != 0) {
const company_data: Database["public"]["Tables"]["mst_company"]["Row"] =
data[0];
setCompanyname(company_data.name);
setZipcode(company_data.zipcode);
setAddress1(company_data.address1);
setAddress2(company_data.address2);
if (company_data.phone != null) {
setPhone(company_data.phone);
}
setEmail(company_data.email);
if (company_data.url != null) {
setCompanyurl(company_data.url);
}
if (company_data.capital != null) {
setCapital(company_data.capital);
}
if (company_data.employee != null) {
setEmployee(company_data.employee);
}
if (company_data.annual_turnover != null) {
setAnnual_turnover(company_data.annual_turnover);
}
if (company_data.established_at != null) {
setEstablishd_at(company_data.established_at);
}
if (company_data.industry_id_1 != null) {
setIndustry_id_1(company_data.industry_id_1);
}
if (company_data.industry_id_2 != null) {
setIndustry_id_2(company_data.industry_id_2);
}
if (company_data.industry_id_3 != null) {
setIndustry_id_3(company_data.industry_id_3);
}
if (company_data.company_image_url_1 != null) {
setCompany_image_url_1(company_data.company_image_url_1);
}
if (company_data.company_image_url_2 != null) {
setCompany_image_url_2(company_data.company_image_url_2);
}
if (company_data.company_image_url_3 != null) {
setCompany_image_url_3(company_data.company_image_url_3);
}
if (company_data.company_image_url_4 != null) {
setCompany_image_url_4(company_data.company_image_url_4);
}
if (company_data.company_image_url_5 != null) {
setCompany_image_url_5(company_data.company_image_url_5);
}
} else {
setIsFirst(true);
setEmail(user.email!);
}
};
const getIndustyData = async () => {
const {data, error} = await supabase.from("mst_industry").select();
if (error) {
console.log(error);
return;
}
const fixed_data: Database["public"]["Tables"]["mst_industry"]["Row"][] =
data;
setIndustryData(fixed_data);
};
const updateEmail = async () => {
const {data, error} = await supabase.auth.updateUser({
email: email,
});
console.log(error);
if (error) {
return;
}
console.log(data);
};
const onSubmit = async () => {
const {
data: {user},
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
if (user.email != email) {
await updateEmail();
}
const timeStamp = new Date().toISOString();
const {error} = await supabase.from("mst_company").upsert({
user_uid: user.id,
name: companyname,
zipcode: zipcode,
address1: address1,
address2: address2,
phone: phone,
email: email,
url: companyurl,
capital: capital,
employee: employee,
annual_turnover: annual_turnover,
established_at: established_at,
industry_id_1: industry_id_1,
industry_id_2: industry_id_2,
industry_id_3: industry_id_3,
company_image_url_1: company_image_url_1,
company_image_url_2: company_image_url_2,
company_image_url_3: company_image_url_3,
company_image_url_4: company_image_url_4,
company_image_url_5: company_image_url_5,
updated_at: timeStamp,
});
if (error) {
if (error.message === "duplicate key value violates unique constraint \"mst_job_seeker_email1_key\"") {
setMessage("保存に失敗しました。ご利用のメールアドレスは既に登録されています。")
return;
}
if (error.message === "duplicate key value violates unique constraint \"mst_job_seeker_phone_key\"") {
setMessage("保存に失敗しました。ご利用の電話番号は既に登録されています。")
return;
}
setMessage("保存に失敗しました。");
return;
} else {
setMessage("");
}
return redirect("/userpage");
};
return (
<div className="flex-1 flex flex-col w-96 py-8 max-w-xl justify-center gap-2">
<form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
<h2>企業の登録情報</h2>
{isFirst ? (
<>
<DefaultInput name={"companyname"} value={companyname} setter={setCompanyname} isRequired={true}
labelText={"会社名*"} placeholderText={"株式会社○○"}
inputType={InputType.text}/>
<DefaultInput name={"zipcode"} value={zipcode} setter={setZipcode} isRequired={true}
labelText={"郵便番号*"}
placeholderText={"0000000 ※ハイフンなしでご入力ください"}
inputType={InputType.text}/>
<DefaultInput name={"address1"} value={address1} setter={setAddress1} isRequired={true}
labelText={"住所1*"}
placeholderText={"東京都港区浜松町2丁目2番15号"} inputType={InputType.text}/>
<DefaultInput name={"address2"} value={address2} setter={setAddress2} isRequired={true}
labelText={"住所2*"} placeholderText={"浜松町ダイヤビル2F"}
inputType={InputType.text}/>
<DefaultInput name={"email"} value={email != null ? email : undefined} setter={setEmail}
isRequired={true}
labelText={"メールアドレス*"} placeholderText={"email@example.com"}
inputType={InputType.email}/>
<DefaultInput name={"employee"} value={employee != null ? employee : undefined}
setter={setEmployee} isRequired={false}
labelText={"従業員数*"} placeholderText={"50"} inputType={InputType.number}/>
<DefaultSelect name={"industry_id_1"} value={industry_id_1 ? industry_id_1 : ""}
setter={setIndustry_id_1} isRequired={false} labelText={"業界1*"}
selectData={industryData} databaseType={DatabaseType.mst_industry}/>
<DefaultInput name={"established_at"} value={established_at ? established_at : ""}
setter={setEstablishd_at} isRequired={false}
labelText={"設立*"} placeholderText={""} inputType={InputType.date}/>
</>
) : (
<>
<DefaultInput name={"companyname"} value={companyname} setter={setCompanyname} isRequired={true}
labelText={"会社名*"} placeholderText={"株式会社○○"}
inputType={InputType.text}/>
<DefaultInput name={"zipcode"} value={zipcode} setter={setZipcode} isRequired={true}
labelText={"郵便番号*"}
placeholderText={"0000000 ※ハイフンなしでご入力ください"}
inputType={InputType.text}/>
<DefaultInput name={"address1"} value={address1} setter={setAddress1} isRequired={true}
labelText={"住所1*"}
placeholderText={"東京都港区浜松町2丁目2番15号"} inputType={InputType.text}/>
<DefaultInput name={"address2"} value={address2} setter={setAddress2} isRequired={true}
labelText={"住所2*"} placeholderText={"浜松町ダイヤビル2F"}
inputType={InputType.text}/>
<DefaultInput name={"phone"} value={phone != null ? phone : undefined} setter={setPhone}
isRequired={false}
labelText={"電話番号"}
placeholderText={"00000000000 ※ハイフンなしでご入力ください"}
inputType={InputType.text}/>
<DefaultInput name={"email"} value={email != null ? email : undefined} setter={setEmail}
isRequired={true}
labelText={"メールアドレス*"} placeholderText={"email@example.com"}
inputType={InputType.email}/>
<DefaultInput name={"companyurl"} value={companyurl != null ? companyurl : undefined}
setter={setCompanyurl} isRequired={false}
labelText={"ホームページURL"} placeholderText={"https://example.com"}
inputType={InputType.text}/>
<DefaultInput name={"capital"} value={capital != null ? capital : undefined} setter={setCapital}
isRequired={false}
labelText={"資本金"} placeholderText={"100 ※単位は万円"}
inputType={InputType.number}/>
<DefaultInput name={"employee"} value={employee != null ? employee : undefined}
setter={setEmployee} isRequired={false}
labelText={"従業員数*"} placeholderText={"50"} inputType={InputType.number}/>
<DefaultInput name={"annual_turnover"}
value={annual_turnover != null ? annual_turnover : undefined}
setter={setAnnual_turnover} isRequired={false}
labelText={"年商"} placeholderText={"5000 ※単位は万円"}
inputType={InputType.number}/>
<DefaultInput name={"established_at"} value={established_at ? established_at : ""}
setter={setEstablishd_at} isRequired={false}
labelText={"設立*"} placeholderText={""} inputType={InputType.date}/>
<DefaultSelect name={"industry_id_1"} value={industry_id_1 ? industry_id_1 : ""}
setter={setIndustry_id_1} isRequired={false} labelText={"業界1*"}
selectData={industryData} databaseType={DatabaseType.mst_industry}/>
<DefaultSelect name={"industry_id_2"} value={industry_id_2 ? industry_id_2 : ""}
setter={setIndustry_id_2} isRequired={false} labelText={"業界2"}
selectData={industryData} databaseType={DatabaseType.mst_industry}/>
<DefaultSelect name={"industry_id_3"} value={industry_id_3 ? industry_id_3 : ""}
setter={setIndustry_id_3} isRequired={false} labelText={"業界3"}
selectData={industryData} databaseType={DatabaseType.mst_industry}/>
{/*<FileUploadInput name={"company_image_url_1"}*/}
{/* value={company_image_url_1 != null ? company_image_url_1 : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"会社の画像1のURL"} placeholderText={"会社の画像1のURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_1}/>*/}
{/*<FileUploadInput name={"company_image_url_2"}*/}
{/* value={company_image_url_2 != null ? company_image_url_2 : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"会社の画像2のURL"} placeholderText={"会社の画像2のURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_2}/>*/}
{/*<FileUploadInput name={"company_image_url_3"}*/}
{/* value={company_image_url_3 != null ? company_image_url_3 : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"会社の画像3のURL"} placeholderText={"会社の画像3のURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_3}/>*/}
{/*<FileUploadInput name={"company_image_url_4"}*/}
{/* value={company_image_url_4 != null ? company_image_url_4 : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"会社の画像4のURL"} placeholderText={"会社の画像4のURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_4}/>*/}
{/*<FileUploadInput name={"company_image_url_5"}*/}
{/* value={company_image_url_5 != null ? company_image_url_5 : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"会社の画像5のURL"} placeholderText={"会社の画像5のURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setCompany_image_url_5}/>*/}
</>
)}
<SubmitButton
formAction={onSubmit}
className="bg-green-500 hover:bg-green-600 rounded-md px-4 py-2 text-white mb-2"
pendingText="企業情報更新中..."
>
保存
</SubmitButton>
{message !== "" && (
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
{message}
</p>
)}
</form>
</div>
);
}
components/userinfo/JobSeekerBasic.tsx
JobSeekerBasicも同様に対応します。
"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";
import {InputType} from "@/utils/inputType";
import DefaultInput from "@/components/common/DefaultInput";
import DefaultSelect from "@/components/common/DefaultSelect";
import {DatabaseType} from "@/utils/DatabaseType";
import FileUploadInput from "@/components/common/FileUploadInput";
export default function JobSeekerBasic() {
const [message, setMessage] = useState("");
const supabase = createClient();
// 初回のデータ入力かどうか?
const [isFirst, setIsFirst] = useState<boolean>(false);
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 [workLocationData, setWorkLocationData] = useState<
Database["public"]["Tables"]["mst_work_location"]["Row"][]
>([]);
// フォームデータを保持する
const [lastname, setLastname] = useState("");
const [firstname, setFirstname] = useState("");
const [middlename, setMiddlename] = useState<string | null>(null);
const [gender, setGender] = useState("");
const [birthday, setBirthday] = useState("");
const [zipcode, setZipcode] = useState<string | null>(null);
const [address1, setAddress1] = useState<string | null>(null);
const [address2, setAddress2] = useState<string | null>(null);
const [phone, setPhone] = useState<string | null>(null);
const [email, setEmail] = useState("");
const [nationality_id, setNationality_id] = useState<string>("");
const [current_annual_income, setCurrent_annual_income] = useState<
number | null
>(null);
const [desired_annual_income, setDesired_annual_income] = useState<
number
>(0);
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
>("");
const [residence_qualification_id, setResidence_qualification_id] = useState<
string | null
>(null);
const [residence_qualification_expired, setResidence_qualification_expired] =
useState<string>("");
const [
residence_qualification_front_image_url,
setResidence_qualification_front_image_url,
] = useState<string | null>(null);
const [
residence_qualification_back_image_url,
setResidence_qualification_back_image_url,
] = useState<string | null>(null);
const [profile_image_url, setProfile_image_url] = useState<string | null>(
null
);
const [resume_file_url, setResume_file_url] = useState<string | null>(null);
const [resume_file_name, setResume_file_name] = useState<string | null>(null);
const [desired_work_location, setDesired_work_location] = useState<string>("");
useEffect(() => {
getNationalityData();
getOccupationData();
getResidenceQualificationData();
getWorklocationData();
setData();
}, []);
const updateEmail = async () => {
const {data, error} = await supabase.auth.updateUser({
email: email,
});
console.log(error);
if (error) {
return;
}
console.log(data);
};
const setData = async () => {
const {
data: {user},
} = await supabase.auth.getUser();
if (!user) {
return redirect("/login");
}
if (user.email != email) {
await updateEmail();
}
const {data} = 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);
}
setEmail(job_seeker_data.email);
setNationality_id(job_seeker_data.nationality_id);
if (job_seeker_data.current_annual_income != null) {
setCurrent_annual_income(job_seeker_data.current_annual_income);
}
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);
}
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
);
}
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);
}
setDesired_work_location(job_seeker_data.desired_work_location);
} else {
setIsFirst(true);
setEmail(user.email!);
}
};
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 getWorklocationData = async () => {
const {data, error} = await supabase.from("mst_work_location").select();
if (error) {
console.log(error);
return;
}
const fixed_data: Database["public"]["Tables"]["mst_work_location"]["Row"][] =
data;
setWorkLocationData(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,
email: email,
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,
desired_work_location: desired_work_location,
updated_at: timeStamp,
});
console.log(error);
if (error) {
if (error.message === "duplicate key value violates unique constraint \"mst_job_seeker_email1_key\"") {
setMessage("保存に失敗しました。ご利用のメールアドレスは既に登録されています。")
return;
}
if (error.message === "duplicate key value violates unique constraint \"mst_job_seeker_phone_key\"") {
setMessage("保存に失敗しました。ご利用の電話番号は既に登録されています。")
return;
}
setMessage("保存に失敗しました。");
return;
} else {
setMessage("");
}
return redirect("/userpage");
};
return (
<div className="flex-1 flex flex-col w-96 py-8 max-w-xl justify-center gap-2">
<form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
<h2>求職者の登録情報</h2>
{isFirst ? (
<>
<DefaultInput name={"lastname"} value={lastname} setter={setLastname} isRequired={true}
labelText={"姓*"} placeholderText={"山田"}
inputType={InputType.text}/>
<DefaultInput name={"firstname"} value={firstname} setter={setFirstname} isRequired={true}
labelText={"名*"} placeholderText={"太郎"}
inputType={InputType.text}/>
<DefaultSelect name={"gender"} value={gender}
setter={setGender} isRequired={true} labelText={"性別*"}
selectData={[]} databaseType={DatabaseType.mst_gender}/>
<DefaultInput name={"birthday"} value={birthday} setter={setBirthday} isRequired={true}
labelText={"誕生日*"} placeholderText={""}
inputType={InputType.date}/>
<DefaultInput name={"email"} value={email} setter={setEmail} isRequired={true}
labelText={"メールアドレス*"} placeholderText={"email@example.com"}
inputType={InputType.email}/>
<DefaultSelect name={"nationalityId"} value={nationality_id}
setter={setNationality_id} isRequired={true} labelText={"国籍*"}
selectData={nationalityData} databaseType={DatabaseType.mst_nationality}/>
<DefaultInput name={"desired_annual_income"}
value={desired_annual_income}
setter={setDesired_annual_income} isRequired={true}
labelText={"希望年収*"} placeholderText={"希望年収*"}
inputType={InputType.number}/>
<DefaultInput name={"desired_change_job_date"}
value={desired_change_job_date}
setter={setDesired_change_job_date} isRequired={true}
labelText={"転職希望日*"} placeholderText={""}
inputType={InputType.date}/>
<DefaultInput name={"residence_qualification_expired"}
value={residence_qualification_expired}
setter={setResidence_qualification_expired} isRequired={true}
labelText={"在留資格期限*"} placeholderText={""}
inputType={InputType.date}/>
<DefaultSelect name={"desired_work_location"}
value={desired_work_location}
setter={setDesired_work_location} isRequired={true} labelText={"希望勤務地*"}
selectData={workLocationData} databaseType={DatabaseType.mst_work_location}/>
</>
) : (
<>
<DefaultInput name={"lastname"} value={lastname} setter={setLastname} isRequired={true}
labelText={"姓*"} placeholderText={"山田"}
inputType={InputType.text}/>
<DefaultInput name={"firstname"} value={firstname} setter={setFirstname} isRequired={true}
labelText={"名*"} placeholderText={"太郎"}
inputType={InputType.text}/>
<DefaultInput name={"middlename"} value={middlename != null ? middlename : undefined}
setter={setMiddlename} isRequired={false}
labelText={"ミドルネーム"} placeholderText={"ミドルネーム"}
inputType={InputType.text}/>
<DefaultSelect name={"gender"} value={gender}
setter={setGender} isRequired={true} labelText={"性別*"}
selectData={[]} databaseType={DatabaseType.mst_gender}/>
<DefaultInput name={"birthday"} value={birthday} setter={setBirthday} isRequired={true}
labelText={"誕生日*"} placeholderText={""}
inputType={InputType.date}/>
<DefaultInput name={"zipcode"} value={zipcode != null ? zipcode : undefined}
setter={setZipcode} isRequired={false}
labelText={"郵便番号"} placeholderText={"0000000 ※ハイフンなしでご入力ください"}
inputType={InputType.text}/>
<DefaultInput name={"address1"} value={address1 != null ? address1 : undefined}
setter={setAddress1} isRequired={false}
labelText={"住所1"} placeholderText={"東京都港区浜松町2丁目2番15号"}
inputType={InputType.text}/>
<DefaultInput name={"address2"} value={address2 != null ? address2 : undefined}
setter={setAddress2} isRequired={false}
labelText={"住所2"} placeholderText={"浜松町ダイヤビル2F"}
inputType={InputType.text}/>
<DefaultInput name={"phone"} value={phone != null ? phone : undefined}
setter={setPhone} isRequired={false}
labelText={"電話番号"}
placeholderText={"00000000000 ※ハイフンなしでご入力ください"}
inputType={InputType.text}/>
<DefaultInput name={"email"} value={email} setter={setEmail} isRequired={true}
labelText={"メールアドレス*"} placeholderText={"email@example.com"}
inputType={InputType.email}/>
<DefaultSelect name={"nationalityId"} value={nationality_id}
setter={setNationality_id} isRequired={true} labelText={"国籍*"}
selectData={nationalityData} databaseType={DatabaseType.mst_nationality}/>
<DefaultInput name={"current_annual_income"}
value={current_annual_income != null ? current_annual_income : undefined}
setter={setCurrent_annual_income} isRequired={false}
labelText={"現在の年収"} placeholderText={"現在の年収"}
inputType={InputType.number}/>
<DefaultInput name={"desired_annual_income"}
value={desired_annual_income}
setter={setDesired_annual_income} isRequired={true}
labelText={"希望年収*"} placeholderText={"希望年収*"}
inputType={InputType.number}/>
<DefaultSelect name={"spouse"} value={spouse ? spouse : ""}
setter={setSpouse} isRequired={false} labelText={"配偶者"}
selectData={[]} databaseType={DatabaseType.mst_spouse}/>
<DefaultSelect name={"desired_occupation_id_1"}
value={desired_occupation_id_1 ? desired_occupation_id_1 : ""}
setter={setDesired_occupation_id_1} isRequired={false} labelText={"希望職種1"}
selectData={occupationData} databaseType={DatabaseType.mst_occupation}/>
<DefaultSelect name={"desired_occupation_id_2"}
value={desired_occupation_id_2 ? desired_occupation_id_2 : ""}
setter={setDesired_occupation_id_2} isRequired={false} labelText={"希望職種2"}
selectData={occupationData} databaseType={DatabaseType.mst_occupation}/>
<DefaultSelect name={"desired_occupation_id_3"}
value={desired_occupation_id_3 ? desired_occupation_id_3 : ""}
setter={setDesired_occupation_id_3} isRequired={false} labelText={"希望職種3"}
selectData={occupationData} databaseType={DatabaseType.mst_occupation}/>
<DefaultInput name={"desired_change_job_date"}
value={desired_change_job_date}
setter={setDesired_change_job_date} isRequired={true}
labelText={"転職希望日*"} placeholderText={""}
inputType={InputType.date}/>
<DefaultSelect name={"residence_qualification_id"}
value={residence_qualification_id ? residence_qualification_id : ""}
setter={setResidence_qualification_id} isRequired={false} labelText={"在留資格"}
selectData={residenceQualificationData}
databaseType={DatabaseType.mst_residence_qualification}/>
<DefaultInput name={"residence_qualification_expired"}
value={residence_qualification_expired}
setter={setResidence_qualification_expired} isRequired={true}
labelText={"在留資格期限*"} placeholderText={""}
inputType={InputType.date}/>
<DefaultSelect name={"desired_work_location"}
value={desired_work_location}
setter={setDesired_work_location} isRequired={true} labelText={"希望勤務地*"}
selectData={workLocationData} databaseType={DatabaseType.mst_work_location}/>
{/*<FileUploadInput name={"residence_qualification_front_image_url"}*/}
{/* value={residence_qualification_front_image_url != null ? residence_qualification_front_image_url : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"在留資格カード表のファイルURL"}*/}
{/* placeholderText={"在留資格カード表のファイルURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setResidence_qualification_front_image_url}/>*/}
{/*<FileUploadInput name={"residence_qualification_back_image_url"}*/}
{/* value={residence_qualification_back_image_url != null ? residence_qualification_back_image_url : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"在留資格カード裏のファイルURL"}*/}
{/* placeholderText={"在留資格カード裏のファイルURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setResidence_qualification_back_image_url}/>*/}
{/*<FileUploadInput name={"profile_image_url"}*/}
{/* value={profile_image_url != null ? profile_image_url : undefined}*/}
{/* isRequired={false}*/}
{/* labelText={"プロフィール画像のファイルURL"}*/}
{/* placeholderText={"プロフィール画像のファイルURL"}*/}
{/* inputType={InputType.text} isImage={true} fileSetter={setProfile_image_url}/>*/}
<FileUploadInput name={"resume_file_url"}
value={resume_file_url != null ? resume_file_url : undefined}
isRequired={false}
labelText={"履歴書のファイルURL"}
placeholderText={"履歴書のファイルURL"}
inputType={InputType.text} isImage={false} fileSetter={setResume_file_url}/>
<DefaultInput name={"resume_file_name"}
value={resume_file_name != null ? resume_file_name : undefined}
setter={setResume_file_name} isRequired={false}
labelText={"履歴書のファイル名"}
placeholderText={"履歴書のファイル名"}
inputType={InputType.text}/>
</>
)}
<SubmitButton
formAction={onSubmit}
className="bg-green-500 hover:bg-green-600 rounded-md px-4 py-2 text-white mb-2"
pendingText="ユーザ情報更新中..."
>
保存
</SubmitButton>
{message !== "" && (
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
{message}
</p>
)}
</form>
</div>
);
}
実装の確認
前回作成したメッセージ画面まで、企業側でログインして進めます。
![](https://libproc.com/wp-content/uploads/2024/06/image-23.png)
するとデフォルトの`面接前`という表記が追加されていることがわかります。
こちらをクリックすると、ダイアログが表示されます。
![](https://libproc.com/wp-content/uploads/2024/06/image-20.png)
別の項目を選択してOKを押すと確認ダイアログが表示されます。
![](https://libproc.com/wp-content/uploads/2024/06/image-19.png)
OKを押すとステータスの更新が確認できます。
![](https://libproc.com/wp-content/uploads/2024/06/image-24.png)
求職者側でも変更が確認できました。
![](https://libproc.com/wp-content/uploads/2024/06/image-24.png)
その他参考資料など
弊社では『マッチングワン』という『低コスト・短期にマッチングサービスを構築できる』サービスを展開しており、今回ご紹介するコードは、その『マッチングワン』でも使われているコードとなります。
本記事で紹介したようなプロダクトを開発されたい場合は、是非お問い合わせください。
またTodoONada株式会社では、この記事で紹介した以外にも、Supabase・Next.jsを使ったアプリの作成方法についてご紹介しています。
下記記事にて一覧を見ることができますので、ぜひこちらもご覧ください!https://note.com/embed/notes/n522396165049
お問合せ&各種リンク
- お問合せ:GoogleForm
- ホームページ:https://libproc.com
- 運営会社:TodoONada株式会社
- Twitter:https://twitter.com/Todoonada_corp
- Instagram:https://www.instagram.com/todoonada_corp/
- Youtube:https://www.youtube.com/@todoonada_corp/
- Tiktok:https://www.tiktok.com/@todoonada_corp
presented by
![](https://libproc.com/wp-content/uploads/2024/06/image-18.png)