| @@ -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 () => { | |||
| @@ -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++, | |||
| 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]; | |||
| @@ -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 = { | |||
| @@ -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 { 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<boolean>; | |||
| logout: VoidFunction; | |||
| @@ -22,6 +23,7 @@ export const AuthContext = createContext<Auth>({ | |||
| 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<Me | null>(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, | |||
| @@ -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({ | |||
| apiMethod: getPaymentPlans, | |||
| backDrop: true, | |||
| onSuccess: ({ data }) => {}, | |||
| onSuccess: ({ data }) => { | |||
| setPaymentPlans(data); | |||
| }, | |||
| onFailed: () => { | |||
| select(null); | |||
| moveToList(); | |||
| @@ -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<FormProps>({}); | |||
| const form = useForm<FormProps>({ | |||
| 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} | |||
| /> | |||
| </AreaBox> | |||
| <AreaBox label="アップロード"> | |||
| <RHFUpload name="upload1" /> | |||
| </AreaBox> | |||
| <Box> | |||
| <Button variant="contained" type="submit"> | |||
| 確定 | |||
| @@ -6,11 +6,49 @@ import { | |||
| Box, | |||
| Button, | |||
| Stack, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import useDashboard from "hooks/useDashBoard"; | |||
| import { PageID, TabID } from "pages"; | |||
| import { useEffect } from "react"; | |||
| import { useEffect, useMemo, useState } from "react"; | |||
| 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() { | |||
| const { setHeaderTitle, setTabs } = useDashboard( | |||
| @@ -18,36 +56,190 @@ export default function Ask() { | |||
| 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(() => { | |||
| setHeaderTitle("問い合わせ"); | |||
| setTabs(null); | |||
| }, [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> | |||
| <Button>問い合わせへ進む</Button> | |||
| <Typography variant="h6">問い合わせしました。</Typography> | |||
| </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 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() { | |||
| <Stack> | |||
| <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> | |||
| ); | |||
| } | |||
| @@ -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_USER_DETAIL: id++, | |||
| DASHBOARD_USER_STUDENT_LICENSE_IMAGES_UPLOAD: id++, | |||
| DASHBOARD_USER_OTHER_LICENSE_IMAGES_UPLOAD: id++, | |||
| DASHBOARD_ASK: id++, | |||
| @@ -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", | |||
| }; | |||
| @@ -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: <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, | |||
| element: <Ask />, | |||