From ecadb7aa93dcff1813760329b25f753a135597db Mon Sep 17 00:00:00 2001 From: "sosuke.iwabuchi" Date: Thu, 14 Sep 2023 11:24:43 +0900 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E4=BD=93=E6=95=B4=E5=82=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 12 +- src/api/customer.ts | 36 +++ src/api/faq.ts | 47 ++++ src/api/index.ts | 7 + src/api/url.ts | 5 + src/components/hook-form/RHFUpload.tsx | 103 ++++++++ src/contexts/AuthContext.tsx | 7 +- src/hooks/useUpload.tsx | 98 +++++++ src/pages/dashboard/contract/detail.tsx | 4 +- .../dashboard/contract/sticker-re-order.tsx | 30 ++- src/pages/dashboard/other/ask.tsx | 242 ++++++++++++++++-- src/pages/dashboard/user/detail.tsx | 24 +- .../user/upload-other-license-images.tsx | 133 ++++++++++ .../user/upload-student-license-images.tsx | 130 ++++++++++ src/pages/index.ts | 2 + src/routes/path.ts | 4 + src/routes/sub/dashboard.tsx | 14 + 17 files changed, 857 insertions(+), 41 deletions(-) create mode 100644 src/api/customer.ts create mode 100644 src/api/faq.ts create mode 100644 src/components/hook-form/RHFUpload.tsx create mode 100644 src/hooks/useUpload.tsx create mode 100644 src/pages/dashboard/user/upload-other-license-images.tsx create mode 100644 src/pages/dashboard/user/upload-student-license-images.tsx diff --git a/src/api/auth.ts b/src/api/auth.ts index c33cff3..49d9628 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -1,11 +1,15 @@ import { APICommonResponse, ApiId, HttpMethod, request } from "."; import { getUrl } from "./url"; +export type Me = { + customer_name: string; + email: string; + student_license_images_upload_datetime: string | null; + other_license_images_upload_datetime: string | null; +}; + type MeResponse = { - data: { - customer_name: string; - email: string; - }; + data: Me; } & APICommonResponse; export const csrfToken = async () => { diff --git a/src/api/customer.ts b/src/api/customer.ts new file mode 100644 index 0000000..787c2b2 --- /dev/null +++ b/src/api/customer.ts @@ -0,0 +1,36 @@ +import { ApiId, HttpMethod, makeFormData, request } from "api"; +import { getUrl } from "./url"; + +// -------学生証アップロード--------------- +export type UploadStudentLicenseImagesRequest = { + images: File[]; +}; +export const uploadStudentLicenseImages = async ( + param: UploadStudentLicenseImagesRequest +) => { + const sendData = makeFormData(param); + const res = await request({ + url: getUrl(ApiId.UPLOAD_STUDENT_LICENSE_IMAGES), + method: HttpMethod.POST, + data: sendData, + multipart: true, + }); + return res; +}; + +// -------その他証明証アップロード--------------- +export type OtherLicenseImagesRequest = { + images: File[]; +}; +export const uploadOtherLicenseImages = async ( + param: OtherLicenseImagesRequest +) => { + const sendData = makeFormData(param); + const res = await request({ + url: getUrl(ApiId.UPLOAD_OTHER_LICENSE_IMAGES), + method: HttpMethod.POST, + data: sendData, + multipart: true, + }); + return res; +}; diff --git a/src/api/faq.ts b/src/api/faq.ts new file mode 100644 index 0000000..04794bb --- /dev/null +++ b/src/api/faq.ts @@ -0,0 +1,47 @@ +import { APICommonResponse, ApiId, HttpMethod, makeParam, request } from "api"; +import { getUrl } from "./url"; + +export type FAQ = { + genre: string | null; + question: string | null; + answer: string | null; +}; + +// -------FAQ一覧取得--------------- +type FAQsResponse = { + data: FAQ[]; +} & APICommonResponse; +export const getFAQs = async () => { + const res = await request({ + url: getUrl(ApiId.FAQ), + method: HttpMethod.GET, + }); + return res; +}; + +// -------FAQジャンル一覧取得--------------- +type FAQGenresResponse = { + data: string[]; +} & APICommonResponse; +export const getFAQGenres = async () => { + const res = await request({ + url: getUrl(ApiId.FAQ_GENRES), + method: HttpMethod.GET, + }); + return res; +}; + +// -------問い合わせ--------------- +type AskRequest = { + genre: string; + ask: string; +}; + +export const ask = async (data: AskRequest) => { + const res = await request({ + url: getUrl(ApiId.ASK), + method: HttpMethod.POST, + data: makeParam(data), + }); + return res; +}; diff --git a/src/api/index.ts b/src/api/index.ts index cf56d04..a014be6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -21,6 +21,13 @@ export const ApiId = { PARKING_CERTIFICATE_ORDER: id++, SEASON_TICKET_CONTRACT_TERMINATE_ORDER: id++, UPDATE_VEHICLE_INFO_ORDER: id++, + + FAQ: id++, + FAQ_GENRES: id++, + ASK: id++, + + UPLOAD_STUDENT_LICENSE_IMAGES: id++, + UPLOAD_OTHER_LICENSE_IMAGES: id++, } as const; export type ApiId = (typeof ApiId)[keyof typeof ApiId]; diff --git a/src/api/url.ts b/src/api/url.ts index d28d5f6..7d5fc89 100644 --- a/src/api/url.ts +++ b/src/api/url.ts @@ -15,6 +15,11 @@ const urls = { "season-ticket-contract/termination-order", [A.UPDATE_VEHICLE_INFO_ORDER]: "season-ticket-contract/update-vehicle-info-order", + [A.FAQ]: "faq", + [A.FAQ_GENRES]: "faq/genres", + [A.ASK]: "ask", + [A.UPLOAD_STUDENT_LICENSE_IMAGES]: "upload/student-license-images", + [A.UPLOAD_OTHER_LICENSE_IMAGES]: "upload/other-license-images", }; const prefixs = { diff --git a/src/components/hook-form/RHFUpload.tsx b/src/components/hook-form/RHFUpload.tsx new file mode 100644 index 0000000..f0956be --- /dev/null +++ b/src/components/hook-form/RHFUpload.tsx @@ -0,0 +1,103 @@ +import { Box, Button, Stack, Typography, styled } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; + +interface Props { + name: string; + onChangeFile?: (param: { imageData: string; fileName: string }) => void; +} + +export function RHFUpload({ name, onChangeFile }: Props) { + const fileInput = useRef(null); + const [fileName, setFileName] = useState(""); + const [imageData, setImageData] = useState(""); + + const { register, setValue, watch } = useFormContext(); + + const file: File[] = watch(name); + + const onChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length <= 0) return; + deployment(files); + }; + + const { ref, ...rest } = register(name, { + onChange, + }); + + const selectFile = () => { + if (!fileInput.current) return; + fileInput.current.removeAttribute("capture"); + fileInput.current.click(); + }; + // ファイルを選択した時の処理 + const deployment = (files: FileList) => { + const file = files[0]; + const fileReader = new FileReader(); + setFileName(file.name); + fileReader.onload = () => { + setImageData(fileReader.result as string); + if (onChangeFile) { + onChangeFile({ imageData, fileName }); + } + }; + fileReader.readAsDataURL(file); + }; + + const reset = () => { + setValue(name, []); + }; + + useEffect(() => { + if (file.length === 0) { + setFileName(""); + setImageData(""); + } + }, [file]); + + return ( + <> +
+ { + ref(e); + fileInput.current = e; + }} + accept="image/*" + style={{ display: "none" }} + {...rest} + /> + +
+
+ {fileName && ( + <> + + + +
{fileName}
+
+ + )} +
+ + ); +} diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 36ce7bd..ff68ab6 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,6 +1,6 @@ import { HasChildren } from "@types"; import { ResultCode } from "api"; -import { login as APILogin, logout as APILogout, me } from "api/auth"; +import { login as APILogin, logout as APILogout, Me, me } from "api/auth"; import useAPICall from "hooks/useAPICall"; import { createContext, memo, useEffect, useMemo, useState } from "react"; @@ -10,6 +10,7 @@ type Auth = { name: string; email: string; + user: Me | null; login: (email: string, password: string) => Promise; logout: VoidFunction; @@ -22,6 +23,7 @@ export const AuthContext = createContext({ name: "", email: "", + user: null, login: async (email: string, password: string) => false, logout: () => {}, @@ -33,6 +35,7 @@ function AuthContextProvider({ children }: Props) { const [initialized, setInitialized] = useState(false); const [name, setName] = useState(""); const [email, setEmail] = useState(""); + const [user, setUser] = useState(null); const authenticated = useMemo(() => { return !!email; @@ -46,6 +49,7 @@ function AuthContextProvider({ children }: Props) { setEmail(data.email); setName(data.customer_name); + setUser(data); }, onFailed: () => { clear(); @@ -99,6 +103,7 @@ function AuthContextProvider({ children }: Props) { authenticated, name, email, + user, // Func login, diff --git a/src/hooks/useUpload.tsx b/src/hooks/useUpload.tsx new file mode 100644 index 0000000..f7415b2 --- /dev/null +++ b/src/hooks/useUpload.tsx @@ -0,0 +1,98 @@ +import { Box, Button, Stack, Typography, styled } from "@mui/material"; +import { useRef, useState } from "react"; +import { Controller, useFormContext } from "react-hook-form"; + +interface Props {} + +export function useUpload(name: string, {}: Props = {}) { + const fileInput = useRef(null); + const [fileName, setFileName] = useState(""); + const [imageData, setImageData] = useState(""); + + const { register } = useFormContext(); + + const onChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length <= 0) return; + deployment(files); + }; + + const { ref, ...rest } = register(name, { + onChange, + }); + + const selectFile = () => { + if (!fileInput.current) return; + fileInput.current.removeAttribute("capture"); + fileInput.current.click(); + }; + // ファイルを選択した時の処理 + const deployment = (files: FileList) => { + const file = files[0]; + const fileReader = new FileReader(); + setFileName(file.name); + fileReader.onload = () => { + setImageData(fileReader.result as string); + }; + fileReader.readAsDataURL(file); + }; + + const reset = () => { + setFileName(""); + setImageData(""); + if (fileInput.current) { + fileInput.current.value = ""; + } + }; + + const uploadButton = ( +
+ { + ref(e); + fileInput.current = e; + }} + accept="image/*" + style={{ display: "none" }} + {...rest} + /> + +
+ ); + const view = ( +
+ {fileName && ( + <> + + + +
{fileName}
+
+ + )} +
+ ); + + return { + // element + uploadButton, + view, + + // hook + fileName, + imageData, + }; +} diff --git a/src/pages/dashboard/contract/detail.tsx b/src/pages/dashboard/contract/detail.tsx index f0155ee..dc5fa2f 100644 --- a/src/pages/dashboard/contract/detail.tsx +++ b/src/pages/dashboard/contract/detail.tsx @@ -41,7 +41,9 @@ export default function ContractDetail() { const { callAPI: callGetPaymentPlans } = useAPICall({ apiMethod: getPaymentPlans, backDrop: true, - onSuccess: ({ data }) => {}, + onSuccess: ({ data }) => { + setPaymentPlans(data); + }, onFailed: () => { select(null); moveToList(); diff --git a/src/pages/dashboard/contract/sticker-re-order.tsx b/src/pages/dashboard/contract/sticker-re-order.tsx index f214208..df7ead6 100644 --- a/src/pages/dashboard/contract/sticker-re-order.tsx +++ b/src/pages/dashboard/contract/sticker-re-order.tsx @@ -2,11 +2,13 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { HasChildren } from "@types"; import { reOrderSticker } from "api/season-ticket-contract"; import { FormProvider, RHFTextField } from "components/hook-form"; +import { RHFUpload } from "components/hook-form/RHFUpload"; import { useSeasonTicketContractContext } from "contexts/dashboard/SeasonTicketContractContext"; import useAPICall from "hooks/useAPICall"; import useDashboard from "hooks/useDashBoard"; import useNavigateCustom from "hooks/useNavigateCustom"; import useSnackbarCustom from "hooks/useSnackbarCustom"; +import { useUpload } from "hooks/useUpload"; import { PageID, TabID } from "pages"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -24,7 +26,9 @@ function AreaBox({ label, children }: AreaBoxProps) { ); } -type FormProps = {}; +type FormProps = { + upload1: File[]; +}; export default function StickerReOrder() { const { setHeaderTitle, setTabs } = useDashboard( @@ -32,7 +36,11 @@ export default function StickerReOrder() { TabID.NONE ); - const form = useForm({}); + const form = useForm({ + defaultValues: { + upload1: [], + }, + }); const { navigateWhenChanged, navigate } = useNavigateCustom(); @@ -53,12 +61,14 @@ export default function StickerReOrder() { }, }); - const handleSubmit = () => { - if (selectedseasonTicketContract === null) return; - callReOrderSticker({ - season_ticket_contract_record_no: - selectedseasonTicketContract.season_ticekt_contract_record_no ?? "", - }); + const handleSubmit = (data: FormProps) => { + console.log(data); + return; + // if (selectedseasonTicketContract === null) return; + // callReOrderSticker({ + // season_ticket_contract_record_no: + // selectedseasonTicketContract.season_ticekt_contract_record_no ?? "", + // }); }; useEffect(() => { @@ -119,6 +129,10 @@ export default function StickerReOrder() { maxRows={10} /> + + + + + 問い合わせしました。 - + ); + } + + return ( + + {!asking && ( + + + よくある質問 + + {FAQ.map((group, index) => { + return ( + + + ----{group.genre}----- + + {group.faq.map((ele, index) => { + return ( + + }> + {ele.question} + + + {ele.answer && + ele.answer.split("\n").map((line, index) => { + return {line}; + })} + + + ); + })} + + ); + })} + よくある質問で解決しない場合 + + + + + )} + {asking && ( + + + + + + + + + + + + + + + + + + + )} + ); } diff --git a/src/pages/dashboard/user/detail.tsx b/src/pages/dashboard/user/detail.tsx index 999a88d..753e394 100644 --- a/src/pages/dashboard/user/detail.tsx +++ b/src/pages/dashboard/user/detail.tsx @@ -1,7 +1,9 @@ import { Button, Stack } from "@mui/material"; import useDashboard from "hooks/useDashBoard"; +import useNavigateCustom from "hooks/useNavigateCustom"; import { PageID, TabID } from "pages"; import { useEffect } from "react"; +import { getPath } from "routes/path"; export default function UserDetail() { const { setHeaderTitle, setTabs } = useDashboard( @@ -9,6 +11,8 @@ export default function UserDetail() { TabID.NONE ); + const { navigateWhenChanged } = useNavigateCustom(); + useEffect(() => { setHeaderTitle("利用者情報"); setTabs(null); @@ -18,8 +22,24 @@ export default function UserDetail() { - - + + ); } diff --git a/src/pages/dashboard/user/upload-other-license-images.tsx b/src/pages/dashboard/user/upload-other-license-images.tsx new file mode 100644 index 0000000..6be39c0 --- /dev/null +++ b/src/pages/dashboard/user/upload-other-license-images.tsx @@ -0,0 +1,133 @@ +import { Box, Button, Stack, Typography } from "@mui/material"; +import { HasChildren } from "@types"; +import { + uploadOtherLicenseImages, + uploadStudentLicenseImages, +} from "api/customer"; +import RequireChip from "components/chip/RequireChip"; +import { FormProvider } from "components/hook-form"; +import { RHFUpload } from "components/hook-form/RHFUpload"; +import StackRow from "components/stack/StackRow"; +import useAPICall from "hooks/useAPICall"; +import useAuth from "hooks/useAuth"; +import useDashboard from "hooks/useDashBoard"; +import useSnackbarCustom from "hooks/useSnackbarCustom"; +import { PageID, TabID } from "pages"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; + +type AreaBoxProps = { + label: string; + require?: boolean; +} & HasChildren; +function AreaBox({ label, children, require }: AreaBoxProps) { + return ( + + + 〇{label} + + + {children} + + ); +} + +type FormProps = { + file1: File[]; + file2: File[]; + file3: File[]; +}; + +export default function OtherLicenseImagesUpload() { + const { setHeaderTitle, setTabs } = useDashboard( + PageID.DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD, + TabID.NONE + ); + + const { user } = useAuth(); + + const [done, setDone] = useState(false); + const { error } = useSnackbarCustom(); + + const form = useForm({ + defaultValues: { + file1: [], + file2: [], + file3: [], + }, + }); + + const { callAPI: callUploadOtherLicenseImages } = useAPICall({ + apiMethod: uploadOtherLicenseImages, + backDrop: true, + onSuccess: () => { + setDone(true); + }, + onFailed: () => { + error("失敗しました"); + }, + }); + + const file1 = form.watch("file1"); + const file2 = form.watch("file2"); + const file3 = form.watch("file3"); + + const handleSubmit = (data: FormProps) => { + const files = [...file1, ...file2, ...file3]; + callUploadOtherLicenseImages({ images: files }); + }; + + useEffect(() => { + if (file1.length === 0) { + if (file2.length !== 0) { + form.setValue("file2", []); + } + if (file3.length !== 0) { + form.setValue("file3", []); + } + } + if (file2.length === 0) { + if (file3.length !== 0) { + form.setValue("file3", []); + } + } + }, [file1, file2, file3]); + + useEffect(() => { + setHeaderTitle("障害者手帳アップロード"); + setTabs(null); + }, []); + if (done) { + return アップロードしました; + } + + return ( + + + + + {user?.other_license_images_upload_datetime ?? "-"} + + + + + + {file1.length !== 0 && ( + + + + )} + {file2.length !== 0 && ( + + + + )} + + + + + + ); +} diff --git a/src/pages/dashboard/user/upload-student-license-images.tsx b/src/pages/dashboard/user/upload-student-license-images.tsx new file mode 100644 index 0000000..48e4e68 --- /dev/null +++ b/src/pages/dashboard/user/upload-student-license-images.tsx @@ -0,0 +1,130 @@ +import { Box, Button, Stack, Typography } from "@mui/material"; +import { HasChildren } from "@types"; +import { uploadStudentLicenseImages } from "api/customer"; +import RequireChip from "components/chip/RequireChip"; +import { FormProvider } from "components/hook-form"; +import { RHFUpload } from "components/hook-form/RHFUpload"; +import StackRow from "components/stack/StackRow"; +import useAPICall from "hooks/useAPICall"; +import useAuth from "hooks/useAuth"; +import useDashboard from "hooks/useDashBoard"; +import useSnackbarCustom from "hooks/useSnackbarCustom"; +import { PageID, TabID } from "pages"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; + +type AreaBoxProps = { + label: string; + require?: boolean; +} & HasChildren; +function AreaBox({ label, children, require }: AreaBoxProps) { + return ( + + + 〇{label} + + + {children} + + ); +} + +type FormProps = { + file1: File[]; + file2: File[]; + file3: File[]; +}; + +export default function StudentLicenseImagesUpload() { + const { setHeaderTitle, setTabs } = useDashboard( + PageID.DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD, + TabID.NONE + ); + + const { user } = useAuth(); + + const [done, setDone] = useState(false); + const { error } = useSnackbarCustom(); + + const form = useForm({ + defaultValues: { + file1: [], + file2: [], + file3: [], + }, + }); + + const { callAPI: callUploadStudentLicenseImages } = useAPICall({ + apiMethod: uploadStudentLicenseImages, + backDrop: true, + onSuccess: () => { + setDone(true); + }, + onFailed: () => { + error("失敗しました"); + }, + }); + + const file1 = form.watch("file1"); + const file2 = form.watch("file2"); + const file3 = form.watch("file3"); + + const handleSubmit = (data: FormProps) => { + const files = [...file1, ...file2, ...file3]; + callUploadStudentLicenseImages({ images: files }); + }; + + useEffect(() => { + if (file1.length === 0) { + if (file2.length !== 0) { + form.setValue("file2", []); + } + if (file3.length !== 0) { + form.setValue("file3", []); + } + } + if (file2.length === 0) { + if (file3.length !== 0) { + form.setValue("file3", []); + } + } + }, [file1, file2, file3]); + + useEffect(() => { + setHeaderTitle("学生証画像アップロード"); + setTabs(null); + }, []); + if (done) { + return アップロードしました; + } + + return ( + + + + + {user?.student_license_images_upload_datetime ?? "-"} + + + + + + {file1.length !== 0 && ( + + + + )} + {file2.length !== 0 && ( + + + + )} + + + + + + ); +} diff --git a/src/pages/index.ts b/src/pages/index.ts index 9e7aa3a..e1f2cf8 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -18,6 +18,8 @@ export const PageID = { DASHBOARD_RECEIPT_DOWNLOAD: id++, DASHBOARD_USER_DETAIL: id++, + DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD: id++, + DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD: id++, DASHBOARD_ASK: id++, diff --git a/src/routes/path.ts b/src/routes/path.ts index d9c51f4..83d8bc5 100644 --- a/src/routes/path.ts +++ b/src/routes/path.ts @@ -50,6 +50,10 @@ const PATHS_DASHBOARD = { [makePathKey(PageID.DASHBOARD_RECEIPT_DOWNLOAD)]: "/dashboard/receipt/download", [makePathKey(PageID.DASHBOARD_USER_DETAIL)]: "/dashboard/user/detail", + [makePathKey(PageID.DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD)]: + "/dashboard/user/upload/student-license", + [makePathKey(PageID.DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD)]: + "/dashboard/user/upload/other-license", [makePathKey(PageID.DASHBOARD_ASK)]: "/dashboard/ask", }; diff --git a/src/routes/sub/dashboard.tsx b/src/routes/sub/dashboard.tsx index 85ffe19..9ccc211 100644 --- a/src/routes/sub/dashboard.tsx +++ b/src/routes/sub/dashboard.tsx @@ -23,6 +23,12 @@ export default function DashboardRoutes(): RouteObject[] { const UserDetail = Loadable( lazy(() => import("pages/dashboard/user/detail")) ); + const StudentLicenseImagesUpload = Loadable( + lazy(() => import("pages/dashboard/user/upload-student-license-images")) + ); + const OtherLicenseImagesUpload = Loadable( + lazy(() => import("pages/dashboard/user/upload-other-license-images")) + ); const Ask = Loadable(lazy(() => import("pages/dashboard/other/ask"))); const allChildren = [ @@ -42,6 +48,14 @@ export default function DashboardRoutes(): RouteObject[] { pageId: PageID.DASHBOARD_USER_DETAIL, element: , }, + { + pageId: PageID.DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD, + element: , + }, + { + pageId: PageID.DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD, + element: , + }, { pageId: PageID.DASHBOARD_ASK, element: ,