| @@ -1,11 +1,15 @@ | |||||
| import { APICommonResponse, ApiId, HttpMethod, request } from "."; | import { APICommonResponse, ApiId, HttpMethod, request } from "."; | ||||
| import { getUrl } from "./url"; | 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 = { | type MeResponse = { | ||||
| data: { | |||||
| customer_name: string; | |||||
| email: string; | |||||
| }; | |||||
| data: Me; | |||||
| } & APICommonResponse; | } & APICommonResponse; | ||||
| export const csrfToken = async () => { | export const csrfToken = async () => { | ||||
| @@ -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; | |||||
| }; | |||||
| @@ -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<FAQsResponse>({ | |||||
| url: getUrl(ApiId.FAQ), | |||||
| method: HttpMethod.GET, | |||||
| }); | |||||
| return res; | |||||
| }; | |||||
| // -------FAQジャンル一覧取得--------------- | |||||
| type FAQGenresResponse = { | |||||
| data: string[]; | |||||
| } & APICommonResponse; | |||||
| export const getFAQGenres = async () => { | |||||
| const res = await request<FAQGenresResponse>({ | |||||
| 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; | |||||
| }; | |||||
| @@ -21,6 +21,13 @@ export const ApiId = { | |||||
| PARKING_CERTIFICATE_ORDER: id++, | PARKING_CERTIFICATE_ORDER: id++, | ||||
| SEASON_TICKET_CONTRACT_TERMINATE_ORDER: id++, | SEASON_TICKET_CONTRACT_TERMINATE_ORDER: id++, | ||||
| UPDATE_VEHICLE_INFO_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; | } as const; | ||||
| export type ApiId = (typeof ApiId)[keyof typeof ApiId]; | export type ApiId = (typeof ApiId)[keyof typeof ApiId]; | ||||
| @@ -15,6 +15,11 @@ const urls = { | |||||
| "season-ticket-contract/termination-order", | "season-ticket-contract/termination-order", | ||||
| [A.UPDATE_VEHICLE_INFO_ORDER]: | [A.UPDATE_VEHICLE_INFO_ORDER]: | ||||
| "season-ticket-contract/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 = { | const prefixs = { | ||||
| @@ -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<HTMLInputElement | null>(null); | |||||
| const [fileName, setFileName] = useState(""); | |||||
| const [imageData, setImageData] = useState(""); | |||||
| const { register, setValue, watch } = useFormContext(); | |||||
| const file: File[] = watch(name); | |||||
| const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| 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 ( | |||||
| <> | |||||
| <div> | |||||
| <input | |||||
| type="file" | |||||
| id="file" | |||||
| ref={(e) => { | |||||
| ref(e); | |||||
| fileInput.current = e; | |||||
| }} | |||||
| accept="image/*" | |||||
| style={{ display: "none" }} | |||||
| {...rest} | |||||
| /> | |||||
| <button onClick={selectFile} type="button"> | |||||
| 📁 ファイルから選択 | |||||
| </button> | |||||
| </div> | |||||
| <div> | |||||
| {fileName && ( | |||||
| <> | |||||
| <Box position="relative"> | |||||
| <img | |||||
| src={imageData} | |||||
| style={{ margin: "auto", maxWidth: "100%" }} | |||||
| /> | |||||
| <button | |||||
| onClick={reset} | |||||
| type="button" | |||||
| style={{ | |||||
| position: "absolute", | |||||
| top: 0, | |||||
| right: 0, | |||||
| }} | |||||
| > | |||||
| ❌ | |||||
| </button> | |||||
| <div>{fileName}</div> | |||||
| </Box> | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| @@ -1,6 +1,6 @@ | |||||
| import { HasChildren } from "@types"; | import { HasChildren } from "@types"; | ||||
| import { ResultCode } from "api"; | 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 useAPICall from "hooks/useAPICall"; | ||||
| import { createContext, memo, useEffect, useMemo, useState } from "react"; | import { createContext, memo, useEffect, useMemo, useState } from "react"; | ||||
| @@ -10,6 +10,7 @@ type Auth = { | |||||
| name: string; | name: string; | ||||
| email: string; | email: string; | ||||
| user: Me | null; | |||||
| login: (email: string, password: string) => Promise<boolean>; | login: (email: string, password: string) => Promise<boolean>; | ||||
| logout: VoidFunction; | logout: VoidFunction; | ||||
| @@ -22,6 +23,7 @@ export const AuthContext = createContext<Auth>({ | |||||
| name: "", | name: "", | ||||
| email: "", | email: "", | ||||
| user: null, | |||||
| login: async (email: string, password: string) => false, | login: async (email: string, password: string) => false, | ||||
| logout: () => {}, | logout: () => {}, | ||||
| @@ -33,6 +35,7 @@ function AuthContextProvider({ children }: Props) { | |||||
| const [initialized, setInitialized] = useState(false); | const [initialized, setInitialized] = useState(false); | ||||
| const [name, setName] = useState(""); | const [name, setName] = useState(""); | ||||
| const [email, setEmail] = useState(""); | const [email, setEmail] = useState(""); | ||||
| const [user, setUser] = useState<Me | null>(null); | |||||
| const authenticated = useMemo(() => { | const authenticated = useMemo(() => { | ||||
| return !!email; | return !!email; | ||||
| @@ -46,6 +49,7 @@ function AuthContextProvider({ children }: Props) { | |||||
| setEmail(data.email); | setEmail(data.email); | ||||
| setName(data.customer_name); | setName(data.customer_name); | ||||
| setUser(data); | |||||
| }, | }, | ||||
| onFailed: () => { | onFailed: () => { | ||||
| clear(); | clear(); | ||||
| @@ -99,6 +103,7 @@ function AuthContextProvider({ children }: Props) { | |||||
| authenticated, | authenticated, | ||||
| name, | name, | ||||
| email, | email, | ||||
| user, | |||||
| // Func | // Func | ||||
| login, | login, | ||||
| @@ -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<HTMLInputElement | null>(null); | |||||
| const [fileName, setFileName] = useState(""); | |||||
| const [imageData, setImageData] = useState(""); | |||||
| const { register } = useFormContext(); | |||||
| const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| 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 = ( | |||||
| <div> | |||||
| <input | |||||
| type="file" | |||||
| id="file" | |||||
| ref={(e) => { | |||||
| ref(e); | |||||
| fileInput.current = e; | |||||
| }} | |||||
| accept="image/*" | |||||
| style={{ display: "none" }} | |||||
| {...rest} | |||||
| /> | |||||
| <button onClick={selectFile} type="button"> | |||||
| 📁 ファイルから選択 | |||||
| </button> | |||||
| </div> | |||||
| ); | |||||
| const view = ( | |||||
| <div> | |||||
| {fileName && ( | |||||
| <> | |||||
| <Box position="relative"> | |||||
| <img src={imageData} style={{ margin: "auto", maxWidth: "100%" }} /> | |||||
| <button | |||||
| onClick={reset} | |||||
| style={{ | |||||
| position: "absolute", | |||||
| top: 0, | |||||
| right: 0, | |||||
| }} | |||||
| > | |||||
| ❌ | |||||
| </button> | |||||
| <div>{fileName}</div> | |||||
| </Box> | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| return { | |||||
| // element | |||||
| uploadButton, | |||||
| view, | |||||
| // hook | |||||
| fileName, | |||||
| imageData, | |||||
| }; | |||||
| } | |||||
| @@ -41,7 +41,9 @@ export default function ContractDetail() { | |||||
| const { callAPI: callGetPaymentPlans } = useAPICall({ | const { callAPI: callGetPaymentPlans } = useAPICall({ | ||||
| apiMethod: getPaymentPlans, | apiMethod: getPaymentPlans, | ||||
| backDrop: true, | backDrop: true, | ||||
| onSuccess: ({ data }) => {}, | |||||
| onSuccess: ({ data }) => { | |||||
| setPaymentPlans(data); | |||||
| }, | |||||
| onFailed: () => { | onFailed: () => { | ||||
| select(null); | select(null); | ||||
| moveToList(); | moveToList(); | ||||
| @@ -2,11 +2,13 @@ import { Box, Button, Stack, Typography } from "@mui/material"; | |||||
| import { HasChildren } from "@types"; | import { HasChildren } from "@types"; | ||||
| import { reOrderSticker } from "api/season-ticket-contract"; | import { reOrderSticker } from "api/season-ticket-contract"; | ||||
| import { FormProvider, RHFTextField } from "components/hook-form"; | import { FormProvider, RHFTextField } from "components/hook-form"; | ||||
| import { RHFUpload } from "components/hook-form/RHFUpload"; | |||||
| import { useSeasonTicketContractContext } from "contexts/dashboard/SeasonTicketContractContext"; | import { useSeasonTicketContractContext } from "contexts/dashboard/SeasonTicketContractContext"; | ||||
| import useAPICall from "hooks/useAPICall"; | import useAPICall from "hooks/useAPICall"; | ||||
| import useDashboard from "hooks/useDashBoard"; | import useDashboard from "hooks/useDashBoard"; | ||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | import useNavigateCustom from "hooks/useNavigateCustom"; | ||||
| import useSnackbarCustom from "hooks/useSnackbarCustom"; | import useSnackbarCustom from "hooks/useSnackbarCustom"; | ||||
| import { useUpload } from "hooks/useUpload"; | |||||
| import { PageID, TabID } from "pages"; | import { PageID, TabID } from "pages"; | ||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||
| import { useForm } from "react-hook-form"; | 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() { | export default function StickerReOrder() { | ||||
| const { setHeaderTitle, setTabs } = useDashboard( | const { setHeaderTitle, setTabs } = useDashboard( | ||||
| @@ -32,7 +36,11 @@ export default function StickerReOrder() { | |||||
| TabID.NONE | TabID.NONE | ||||
| ); | ); | ||||
| const form = useForm<FormProps>({}); | |||||
| const form = useForm<FormProps>({ | |||||
| defaultValues: { | |||||
| upload1: [], | |||||
| }, | |||||
| }); | |||||
| const { navigateWhenChanged, navigate } = useNavigateCustom(); | 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(() => { | useEffect(() => { | ||||
| @@ -119,6 +129,10 @@ export default function StickerReOrder() { | |||||
| maxRows={10} | maxRows={10} | ||||
| /> | /> | ||||
| </AreaBox> | </AreaBox> | ||||
| <AreaBox label="アップロード"> | |||||
| <RHFUpload name="upload1" /> | |||||
| </AreaBox> | |||||
| <Box> | <Box> | ||||
| <Button variant="contained" type="submit"> | <Button variant="contained" type="submit"> | ||||
| 確定 | 確定 | ||||
| @@ -6,11 +6,49 @@ import { | |||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| Stack, | Stack, | ||||
| Typography, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import useDashboard from "hooks/useDashBoard"; | import useDashboard from "hooks/useDashBoard"; | ||||
| import { PageID, TabID } from "pages"; | import { PageID, TabID } from "pages"; | ||||
| import { useEffect } from "react"; | |||||
| import { useEffect, useMemo, useState } from "react"; | |||||
| import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; | ||||
| import useAPICall from "hooks/useAPICall"; | |||||
| import { FAQ, ask, getFAQGenres, getFAQs } from "api/faq"; | |||||
| import useAuth from "hooks/useAuth"; | |||||
| import { FormProvider, RHFSelect, RHFTextField } from "components/hook-form"; | |||||
| import { useForm } from "react-hook-form"; | |||||
| import { HasChildren } from "@types"; | |||||
| import StackRow from "components/stack/StackRow"; | |||||
| import RequireChip from "components/chip/RequireChip"; | |||||
| import { SelectOptionProps } from "components/hook-form/RHFSelect"; | |||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||||
| import useSnackbarCustom from "hooks/useSnackbarCustom"; | |||||
| type AreaBoxProps = { | |||||
| label: string; | |||||
| require?: boolean; | |||||
| } & HasChildren; | |||||
| function AreaBox({ label, children, require }: AreaBoxProps) { | |||||
| return ( | |||||
| <Box> | |||||
| <StackRow> | |||||
| <Typography variant="subtitle1">〇{label}</Typography> | |||||
| <RequireChip require={require ?? false} /> | |||||
| </StackRow> | |||||
| {children} | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| type FAQGroup = { | |||||
| genre: string; | |||||
| faq: FAQ[]; | |||||
| }; | |||||
| type FormProps = { | |||||
| genre: string; | |||||
| ask: string; | |||||
| }; | |||||
| export default function Ask() { | export default function Ask() { | ||||
| const { setHeaderTitle, setTabs } = useDashboard( | const { setHeaderTitle, setTabs } = useDashboard( | ||||
| @@ -18,36 +56,190 @@ export default function Ask() { | |||||
| TabID.NONE | TabID.NONE | ||||
| ); | ); | ||||
| const { authenticated } = useAuth(); | |||||
| const { navigate } = useNavigateCustom(); | |||||
| const { error } = useSnackbarCustom(); | |||||
| const [FAQ, setFAQ] = useState<FAQGroup[]>([]); | |||||
| const { callAPI: callGetFAQs } = useAPICall({ | |||||
| apiMethod: getFAQs, | |||||
| backDrop: true, | |||||
| onSuccess: ({ data }) => { | |||||
| const list: FAQGroup[] = []; | |||||
| let tmpGroup: FAQGroup | null = null; | |||||
| data.forEach((ele) => { | |||||
| if (tmpGroup === null) { | |||||
| tmpGroup = { | |||||
| genre: ele.genre ?? "", | |||||
| faq: [], | |||||
| }; | |||||
| } | |||||
| if (tmpGroup.genre === ele.genre) { | |||||
| tmpGroup.faq.push(ele); | |||||
| } else { | |||||
| list.push(tmpGroup); | |||||
| tmpGroup = { | |||||
| genre: ele.genre ?? "", | |||||
| faq: [ele], | |||||
| }; | |||||
| } | |||||
| }); | |||||
| if (tmpGroup !== null) { | |||||
| list.push(tmpGroup); | |||||
| } | |||||
| setFAQ(list); | |||||
| }, | |||||
| }); | |||||
| const [asking, setAsking] = useState(false); | |||||
| const [genres, setGenres] = useState<string[]>([]); | |||||
| const [done, setDone] = useState(false); | |||||
| const genreOptions: SelectOptionProps[] = useMemo(() => { | |||||
| return genres.map((ele) => { | |||||
| return { | |||||
| label: ele, | |||||
| value: ele, | |||||
| }; | |||||
| }); | |||||
| }, [genres]); | |||||
| const form = useForm<FormProps>({ | |||||
| defaultValues: { | |||||
| genre: "", | |||||
| ask: "", | |||||
| }, | |||||
| }); | |||||
| const { callAPI: callGetFAQGenres } = useAPICall({ | |||||
| apiMethod: getFAQGenres, | |||||
| backDrop: true, | |||||
| onSuccess: ({ data }) => { | |||||
| setGenres(data); | |||||
| }, | |||||
| }); | |||||
| const { callAPI: callAsk } = useAPICall({ | |||||
| apiMethod: ask, | |||||
| backDrop: true, | |||||
| form, | |||||
| onSuccess: () => { | |||||
| setDone(true); | |||||
| }, | |||||
| onFailed: () => { | |||||
| error("失敗しました"); | |||||
| }, | |||||
| }); | |||||
| const handleSubmit = (data: FormProps) => { | |||||
| callAsk(data); | |||||
| }; | |||||
| useEffect(() => { | |||||
| if (authenticated) { | |||||
| callGetFAQs({}); | |||||
| } | |||||
| }, [authenticated]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setHeaderTitle("問い合わせ"); | setHeaderTitle("問い合わせ"); | ||||
| setTabs(null); | setTabs(null); | ||||
| }, [setHeaderTitle, setTabs]); | }, [setHeaderTitle, setTabs]); | ||||
| return ( | |||||
| <Stack> | |||||
| <Box>よくある質問</Box> | |||||
| <Box> | |||||
| <Accordion> | |||||
| <AccordionSummary expandIcon={<ExpandMoreIcon />}> | |||||
| 支払をしたか確認をしたい | |||||
| </AccordionSummary> | |||||
| <AccordionDetails> | |||||
| 契約一覧より支払状況が確認できます。 | |||||
| </AccordionDetails> | |||||
| </Accordion> | |||||
| <Accordion> | |||||
| <AccordionSummary expandIcon={<ExpandMoreIcon />}> | |||||
| 解約をしたい | |||||
| </AccordionSummary> | |||||
| <AccordionDetails> | |||||
| 契約一覧より解約申請をしてください | |||||
| </AccordionDetails> | |||||
| </Accordion> | |||||
| </Box> | |||||
| <Box>よくある質問で解決しない場合</Box> | |||||
| useEffect(() => { | |||||
| if (asking && genres.length === 0) { | |||||
| callGetFAQGenres({}); | |||||
| } | |||||
| }, [asking, genres]); | |||||
| if (done) { | |||||
| return ( | |||||
| <Box> | <Box> | ||||
| <Button>問い合わせへ進む</Button> | |||||
| <Typography variant="h6">問い合わせしました。</Typography> | |||||
| </Box> | </Box> | ||||
| </Stack> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||||
| {!asking && ( | |||||
| <Stack spacing={2}> | |||||
| <Box> | |||||
| <Typography variant="h6">よくある質問</Typography> | |||||
| </Box> | |||||
| {FAQ.map((group, index) => { | |||||
| return ( | |||||
| <Box key={index}> | |||||
| <Typography variant="subtitle1"> | |||||
| ----{group.genre}----- | |||||
| </Typography> | |||||
| {group.faq.map((ele, index) => { | |||||
| return ( | |||||
| <Accordion key={index}> | |||||
| <AccordionSummary expandIcon={<ExpandMoreIcon />}> | |||||
| {ele.question} | |||||
| </AccordionSummary> | |||||
| <AccordionDetails> | |||||
| {ele.answer && | |||||
| ele.answer.split("\n").map((line, index) => { | |||||
| return <Typography key={index}>{line}</Typography>; | |||||
| })} | |||||
| </AccordionDetails> | |||||
| </Accordion> | |||||
| ); | |||||
| })} | |||||
| </Box> | |||||
| ); | |||||
| })} | |||||
| <Box>よくある質問で解決しない場合</Box> | |||||
| <Box> | |||||
| <Button | |||||
| onClick={() => { | |||||
| setAsking(true); | |||||
| }} | |||||
| > | |||||
| 問い合わせへ進む | |||||
| </Button> | |||||
| </Box> | |||||
| </Stack> | |||||
| )} | |||||
| {asking && ( | |||||
| <Box sx={{ mt: 1 }}> | |||||
| <Stack spacing={2}> | |||||
| <Box> | |||||
| <Button | |||||
| onClick={() => { | |||||
| setAsking(false); | |||||
| }} | |||||
| > | |||||
| 戻る | |||||
| </Button> | |||||
| </Box> | |||||
| <AreaBox label="ジャンル" require> | |||||
| <RHFSelect name="genre" size="small" options={genreOptions} /> | |||||
| </AreaBox> | |||||
| <AreaBox label="問い合わせ内容" require> | |||||
| <RHFTextField | |||||
| name="ask" | |||||
| size="small" | |||||
| multiline | |||||
| minRows={3} | |||||
| maxRows={10} | |||||
| /> | |||||
| </AreaBox> | |||||
| <Box> | |||||
| <Button variant="contained" type="submit"> | |||||
| 確定 | |||||
| </Button> | |||||
| </Box> | |||||
| </Stack> | |||||
| </Box> | |||||
| )} | |||||
| </FormProvider> | |||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,7 +1,9 @@ | |||||
| import { Button, Stack } from "@mui/material"; | import { Button, Stack } from "@mui/material"; | ||||
| import useDashboard from "hooks/useDashBoard"; | import useDashboard from "hooks/useDashBoard"; | ||||
| import useNavigateCustom from "hooks/useNavigateCustom"; | |||||
| import { PageID, TabID } from "pages"; | import { PageID, TabID } from "pages"; | ||||
| import { useEffect } from "react"; | import { useEffect } from "react"; | ||||
| import { getPath } from "routes/path"; | |||||
| export default function UserDetail() { | export default function UserDetail() { | ||||
| const { setHeaderTitle, setTabs } = useDashboard( | const { setHeaderTitle, setTabs } = useDashboard( | ||||
| @@ -9,6 +11,8 @@ export default function UserDetail() { | |||||
| TabID.NONE | TabID.NONE | ||||
| ); | ); | ||||
| const { navigateWhenChanged } = useNavigateCustom(); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setHeaderTitle("利用者情報"); | setHeaderTitle("利用者情報"); | ||||
| setTabs(null); | setTabs(null); | ||||
| @@ -18,8 +22,24 @@ export default function UserDetail() { | |||||
| <Stack> | <Stack> | ||||
| <Button>ユーザー情報変更</Button> | <Button>ユーザー情報変更</Button> | ||||
| <Button>口座情報変更</Button> | <Button>口座情報変更</Button> | ||||
| <Button>学生証アップロード</Button> | |||||
| <Button>障害者手帳アップロード</Button> | |||||
| <Button | |||||
| onClick={() => { | |||||
| navigateWhenChanged( | |||||
| getPath(PageID.DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD) | |||||
| ); | |||||
| }} | |||||
| > | |||||
| 学生証アップロード | |||||
| </Button> | |||||
| <Button | |||||
| onClick={() => { | |||||
| navigateWhenChanged( | |||||
| getPath(PageID.DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD) | |||||
| ); | |||||
| }} | |||||
| > | |||||
| 障害者手帳アップロード | |||||
| </Button> | |||||
| </Stack> | </Stack> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -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 ( | |||||
| <Box> | |||||
| <StackRow> | |||||
| <Typography variant="subtitle1">〇{label}</Typography> | |||||
| <RequireChip require={require ?? false} /> | |||||
| </StackRow> | |||||
| {children} | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| 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<FormProps>({ | |||||
| 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 <Box>アップロードしました</Box>; | |||||
| } | |||||
| return ( | |||||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||||
| <Stack spacing={2}> | |||||
| <AreaBox label="前回アップロード日時"> | |||||
| <Typography> | |||||
| {user?.other_license_images_upload_datetime ?? "-"} | |||||
| </Typography> | |||||
| </AreaBox> | |||||
| <AreaBox label="1枚目"> | |||||
| <RHFUpload name="file1" /> | |||||
| </AreaBox> | |||||
| {file1.length !== 0 && ( | |||||
| <AreaBox label="2枚目"> | |||||
| <RHFUpload name="file2" /> | |||||
| </AreaBox> | |||||
| )} | |||||
| {file2.length !== 0 && ( | |||||
| <AreaBox label="3枚目"> | |||||
| <RHFUpload name="file3" /> | |||||
| </AreaBox> | |||||
| )} | |||||
| <Box> | |||||
| <Button type="submit" variant="contained"> | |||||
| 送信 | |||||
| </Button> | |||||
| </Box> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| ); | |||||
| } | |||||
| @@ -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 ( | |||||
| <Box> | |||||
| <StackRow> | |||||
| <Typography variant="subtitle1">〇{label}</Typography> | |||||
| <RequireChip require={require ?? false} /> | |||||
| </StackRow> | |||||
| {children} | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| 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<FormProps>({ | |||||
| 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 <Box>アップロードしました</Box>; | |||||
| } | |||||
| return ( | |||||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||||
| <Stack spacing={2}> | |||||
| <AreaBox label="前回アップロード日時"> | |||||
| <Typography> | |||||
| {user?.student_license_images_upload_datetime ?? "-"} | |||||
| </Typography> | |||||
| </AreaBox> | |||||
| <AreaBox label="1枚目"> | |||||
| <RHFUpload name="file1" /> | |||||
| </AreaBox> | |||||
| {file1.length !== 0 && ( | |||||
| <AreaBox label="2枚目"> | |||||
| <RHFUpload name="file2" /> | |||||
| </AreaBox> | |||||
| )} | |||||
| {file2.length !== 0 && ( | |||||
| <AreaBox label="3枚目"> | |||||
| <RHFUpload name="file3" /> | |||||
| </AreaBox> | |||||
| )} | |||||
| <Box> | |||||
| <Button type="submit" variant="contained"> | |||||
| 送信 | |||||
| </Button> | |||||
| </Box> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| ); | |||||
| } | |||||
| @@ -18,6 +18,8 @@ export const PageID = { | |||||
| DASHBOARD_RECEIPT_DOWNLOAD: id++, | DASHBOARD_RECEIPT_DOWNLOAD: id++, | ||||
| DASHBOARD_USER_DETAIL: id++, | DASHBOARD_USER_DETAIL: id++, | ||||
| DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD: id++, | |||||
| DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD: id++, | |||||
| DASHBOARD_ASK: id++, | DASHBOARD_ASK: id++, | ||||
| @@ -50,6 +50,10 @@ const PATHS_DASHBOARD = { | |||||
| [makePathKey(PageID.DASHBOARD_RECEIPT_DOWNLOAD)]: | [makePathKey(PageID.DASHBOARD_RECEIPT_DOWNLOAD)]: | ||||
| "/dashboard/receipt/download", | "/dashboard/receipt/download", | ||||
| [makePathKey(PageID.DASHBOARD_USER_DETAIL)]: "/dashboard/user/detail", | [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", | [makePathKey(PageID.DASHBOARD_ASK)]: "/dashboard/ask", | ||||
| }; | }; | ||||
| @@ -23,6 +23,12 @@ export default function DashboardRoutes(): RouteObject[] { | |||||
| const UserDetail = Loadable( | const UserDetail = Loadable( | ||||
| lazy(() => import("pages/dashboard/user/detail")) | 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 Ask = Loadable(lazy(() => import("pages/dashboard/other/ask"))); | ||||
| const allChildren = [ | const allChildren = [ | ||||
| @@ -42,6 +48,14 @@ export default function DashboardRoutes(): RouteObject[] { | |||||
| pageId: PageID.DASHBOARD_USER_DETAIL, | pageId: PageID.DASHBOARD_USER_DETAIL, | ||||
| element: <UserDetail />, | element: <UserDetail />, | ||||
| }, | }, | ||||
| { | |||||
| pageId: PageID.DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD, | |||||
| element: <StudentLicenseImagesUpload />, | |||||
| }, | |||||
| { | |||||
| pageId: PageID.DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD, | |||||
| element: <OtherLicenseImagesUpload />, | |||||
| }, | |||||
| { | { | ||||
| pageId: PageID.DASHBOARD_ASK, | pageId: PageID.DASHBOARD_ASK, | ||||
| element: <Ask />, | element: <Ask />, | ||||