| @@ -30,6 +30,8 @@ export const PageID = { | |||
| DASHBOARD_LOGIN_USER_CREATE: id++, | |||
| DASHBOARD_LOGIN_USER_CHANGE_PASSWORD: id++, | |||
| RECEIPT_REQUEST: id++, | |||
| PAGE_403: id++, | |||
| PAGE_404: id++, | |||
| } as const; | |||
| @@ -14,12 +14,14 @@ import { ReactNode, useEffect, useMemo } from "react"; | |||
| export { default as TableHeadCustom } from "./TableHeadCustom"; | |||
| type SimpleDataListProps = { | |||
| data: { | |||
| title: string; | |||
| value?: string; | |||
| end?: ReactNode; | |||
| }[]; | |||
| export type SimpleData = { | |||
| title: string; | |||
| value?: string; | |||
| end?: ReactNode; | |||
| }; | |||
| export type SimpleDataListProps = { | |||
| data: SimpleData[]; | |||
| tableSx?: SxProps; | |||
| }; | |||
| export const SimpleDataList = ({ data, tableSx }: SimpleDataListProps) => { | |||
| @@ -0,0 +1,157 @@ | |||
| import { yupResolver } from "@hookform/resolvers/yup"; | |||
| import { HasChildren } from "@types"; | |||
| import { createContext, useMemo, useState } from "react"; | |||
| import { UseFormReturn, useForm } from "react-hook-form"; | |||
| import { useParams } from "react-router-dom"; | |||
| import * as Yup from "yup"; | |||
| const schema = Yup.object().shape({ | |||
| reason: Yup.string().required("入力してください"), | |||
| reason_text: Yup.string(), | |||
| user_company_name: Yup.string(), | |||
| user_first_name: Yup.string().required("入力してください"), | |||
| user_last_name: Yup.string().required("入力してください"), | |||
| put_in_date: Yup.date() | |||
| .required("必須項目です") | |||
| .typeError("正しく入力してください"), | |||
| put_in_hour: Yup.string().required("入力してください"), | |||
| put_in_minute: Yup.string().required("入力してください"), | |||
| put_out_date: Yup.date() | |||
| .required("必須項目です") | |||
| .typeError("正しく入力してください"), | |||
| put_out_hour: Yup.string().required("入力してください"), | |||
| put_out_minute: Yup.string().required("入力してください"), | |||
| parking_name: Yup.string().required("入力してください"), | |||
| room_no: Yup.string().required("入力してください"), | |||
| receipt_amount: Yup.number() | |||
| .typeError("数値を入力してください") | |||
| .test("range", "1-50000まで入力できます", function (value) { | |||
| const val = Number(value); | |||
| return 1 <= val && val <= 50000; | |||
| }), | |||
| paying_type: Yup.string().required("入力してください"), | |||
| phone_number: Yup.string() | |||
| .required("必須項目です") | |||
| .matches(/^[0-9]{10,11}$/, "正しい電話番号を入力してください"), | |||
| email: Yup.string() | |||
| .required("必須項目です") | |||
| .matches(/^.+@.+$/, "正しいEmailを入力してください"), | |||
| memo: Yup.string(), | |||
| }); | |||
| export type ReceiptRequestFormProps = { | |||
| reason: string; | |||
| reason_text: string; | |||
| user_company_name: string; | |||
| user_first_name: string; | |||
| user_last_name: string; | |||
| put_in_date: Date | null; | |||
| put_in_hour: string; | |||
| put_in_minute: string; | |||
| put_out_date: Date | null; | |||
| put_out_hour: string; | |||
| put_out_minute: string; | |||
| parking_name: string; | |||
| room_no: string; | |||
| receipt_amount: string; | |||
| paying_type: string; | |||
| phone_number: string; | |||
| email: string; | |||
| memo: string; | |||
| }; | |||
| export function getDefaultReceiptRequestFormValues(): ReceiptRequestFormProps { | |||
| return { | |||
| reason: "", | |||
| reason_text: "", | |||
| user_company_name: "", | |||
| user_first_name: "", | |||
| user_last_name: "", | |||
| put_in_date: null, | |||
| put_in_hour: "", | |||
| put_in_minute: "", | |||
| put_out_date: null, | |||
| put_out_hour: "", | |||
| put_out_minute: "", | |||
| parking_name: "", | |||
| room_no: "", | |||
| receipt_amount: "", | |||
| paying_type: "", | |||
| phone_number: "", | |||
| email: "", | |||
| memo: "", | |||
| }; | |||
| } | |||
| type Mode = "agree" | "input" | "confirm" | "complete"; | |||
| type ReceiptRequest = { | |||
| step: number; | |||
| mode: Mode; | |||
| inputData: ReceiptRequestFormProps; | |||
| form: UseFormReturn<ReceiptRequestFormProps> | undefined; | |||
| setMode: (mode: Mode) => void; | |||
| setInputData: (input: ReceiptRequestFormProps) => void; | |||
| }; | |||
| export const ReceiptRequestContext = createContext<ReceiptRequest>({ | |||
| step: 0, | |||
| mode: "agree", | |||
| inputData: getDefaultReceiptRequestFormValues(), | |||
| form: undefined, | |||
| setMode: (mode: Mode) => {}, | |||
| setInputData: (input: ReceiptRequestFormProps) => {}, | |||
| }); | |||
| type Props = HasChildren; | |||
| export function ReceiptRequestContextProvider({ children }: Props) { | |||
| const { contractId: paramContractId } = useParams(); | |||
| const [mode, setMode] = useState<Mode>("agree"); | |||
| const [inputData, setInputData] = useState<ReceiptRequestFormProps>( | |||
| getDefaultReceiptRequestFormValues() | |||
| ); | |||
| const step = useMemo(() => { | |||
| if (mode === "agree") return 0; | |||
| if (mode === "input") return 1; | |||
| if (mode === "confirm") return 2; | |||
| if (mode === "complete") return 3; | |||
| throw new Error("ステップ不正"); | |||
| }, [mode]); | |||
| const form = useForm<ReceiptRequestFormProps>({ | |||
| defaultValues: getDefaultReceiptRequestFormValues(), | |||
| resolver: yupResolver(schema), | |||
| }); | |||
| return ( | |||
| <ReceiptRequestContext.Provider | |||
| value={{ | |||
| step, | |||
| mode, | |||
| inputData, | |||
| form, | |||
| setMode, | |||
| setInputData, | |||
| }} | |||
| > | |||
| {children} | |||
| </ReceiptRequestContext.Provider> | |||
| ); | |||
| } | |||
| @@ -2,6 +2,7 @@ import { useLocation, useNavigate } from "react-router"; | |||
| import { Dictionary } from "@types"; | |||
| import { PageID } from "codes/page"; | |||
| import { getPath } from "routes/path"; | |||
| import { scrollToTop } from "utils/page"; | |||
| export default function useNavigateCustom() { | |||
| const navigate = useNavigate(); | |||
| @@ -54,6 +55,8 @@ export default function useNavigateCustom() { | |||
| navigate(newPath); | |||
| } | |||
| } | |||
| scrollToTop(); | |||
| }; | |||
| return { navigate, navigateWhenChanged }; | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| import { ReceiptRequestContext } from "contexts/ReceiptRequestContext"; | |||
| import { useContext } from "react"; | |||
| export default function useReceiptRequest() { | |||
| const context = useContext(ReceiptRequestContext); | |||
| return context; | |||
| } | |||
| @@ -0,0 +1,153 @@ | |||
| import { yupResolver } from "@hookform/resolvers/yup"; | |||
| import { Box, Button, Stack, Typography } from "@mui/material"; | |||
| import { PageID } from "codes/page"; | |||
| import { FormProvider, RHFCheckbox } from "components/hook-form"; | |||
| import useReceiptRequest from "hooks/useReceiptRequest"; | |||
| import { useMemo } from "react"; | |||
| import { useForm } from "react-hook-form"; | |||
| import { getPath } from "routes/path"; | |||
| import { scrollToTop } from "utils/page"; | |||
| import * as Yup from "yup"; | |||
| type FormProps = { | |||
| accept_privacy_policy: boolean; | |||
| accept_correct_entry: boolean; | |||
| accept_site_policy: boolean; | |||
| }; | |||
| const schema = Yup.object().shape({ | |||
| accept_privacy_policy: Yup.boolean().test( | |||
| "accept", | |||
| "同意が必要です", | |||
| function (value) { | |||
| return !!value; | |||
| } | |||
| ), | |||
| accept_correct_entry: Yup.boolean().test( | |||
| "accept", | |||
| "同意が必要です", | |||
| function (value) { | |||
| return !!value; | |||
| } | |||
| ), | |||
| accept_site_policy: Yup.boolean().test( | |||
| "accept", | |||
| "同意が必要です", | |||
| function (value) { | |||
| return !!value; | |||
| } | |||
| ), | |||
| }); | |||
| export default function useAgreement() { | |||
| const { setMode } = useReceiptRequest(); | |||
| const privacyPolicyUrl: string = useMemo(() => { | |||
| return getPath(PageID.APP_PRIVACY_POLICY); | |||
| }, []); | |||
| const form = useForm<FormProps>({ | |||
| defaultValues: { | |||
| accept_privacy_policy: false, | |||
| accept_correct_entry: false, | |||
| accept_site_policy: false, | |||
| }, | |||
| resolver: yupResolver(schema), | |||
| }); | |||
| const acceptPrivacyPolicy = form.watch("accept_privacy_policy"); | |||
| const acceptCorrectEntry = form.watch("accept_correct_entry"); | |||
| const acceptSitePolicy = form.watch("accept_site_policy"); | |||
| const canSubmit = useMemo(() => { | |||
| return acceptPrivacyPolicy && acceptCorrectEntry && acceptSitePolicy; | |||
| }, [acceptPrivacyPolicy, acceptCorrectEntry, acceptSitePolicy]); | |||
| const handleSubmit = () => { | |||
| scrollToTop("auto"); | |||
| setMode("input"); | |||
| }; | |||
| const element = ( | |||
| <FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}> | |||
| <Box sx={{ m: 1, p: 1 }}> | |||
| <Box> | |||
| <Typography variant="body2"> | |||
| このページは当社時間貸し駐車場・駐輪場ご利用者の方が領収書の発行(再発行)を申請するためのページです。 | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| ※上記以外(月極駐車場、予約制駐車場等)ではご利用になれませんのでご注意ください。 | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| 領収書の受け取り方法は①WEB発行(メール)②郵送のどちらかになります。 | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| 以下、各項目についてご確認およびご同意頂き、申請フォームへお進みください。 | |||
| </Typography> | |||
| </Box> | |||
| <Box mt={3}> | |||
| <Typography variant="subtitle1"><ご申請の流れ></Typography> | |||
| <Typography variant="body2">1.ご申請(ご利用者様)</Typography> | |||
| <Typography variant="body2"> | |||
| 2.ご申請内容の確認(運営会社) | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| 3.メールにて領収証取得用URL送付(運営会社) | |||
| </Typography> | |||
| </Box> | |||
| <Stack spacing={1} mt={3}> | |||
| <Box> | |||
| <Stack | |||
| direction="row" | |||
| spacing={0} | |||
| alignItems="center" | |||
| // justifyContent="center" | |||
| > | |||
| <RHFCheckbox | |||
| label=" 個人情報保護方針への同意" | |||
| name="accept_privacy_policy" | |||
| /> | |||
| <Typography variant="body1"> | |||
| <a href={privacyPolicyUrl} target="_blank"> | |||
| 【 規約 】 | |||
| </a> | |||
| </Typography> | |||
| </Stack> | |||
| </Box> | |||
| <Box> | |||
| <RHFCheckbox label="注意事項の確認" name="accept_site_policy" /> | |||
| <Typography variant="body1"><注意事項></Typography> | |||
| <Typography variant="body2"> | |||
| ・申請内容に不明な点がある場合、別途お電話等で確認をとらせて頂く場合がございますのでご了承ください。 | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| ・WEB上で領収証PDFをダウンロード可能です。 | |||
| </Typography> | |||
| <Typography variant="body2"> | |||
| ・郵送を希望する場合、到着までに時間を要します。お急ぎの場合はダウンロードやEmail送付をご利用ください。 | |||
| </Typography> | |||
| </Box> | |||
| <Box> | |||
| <RHFCheckbox | |||
| label={ | |||
| <Typography variant="body1" color="error"> | |||
| 申請内容に虚偽・誤りが無いことへの同意 | |||
| </Typography> | |||
| } | |||
| name="accept_correct_entry" | |||
| /> | |||
| </Box> | |||
| <Box> | |||
| <Button type="submit" variant="contained" disabled={!canSubmit}> | |||
| 申請内容の入力へ進む | |||
| </Button> | |||
| </Box> | |||
| </Stack> | |||
| </Box> | |||
| </FormProvider> | |||
| ); | |||
| return { element }; | |||
| } | |||
| @@ -0,0 +1,114 @@ | |||
| import { yupResolver } from "@hookform/resolvers/yup"; | |||
| import { Box, Button, Stack, Typography } from "@mui/material"; | |||
| import { PageID } from "codes/page"; | |||
| import { FormProvider, RHFCheckbox } from "components/hook-form"; | |||
| import StackRow from "components/stack/StackRow"; | |||
| import { SimpleData, SimpleDataList } from "components/table"; | |||
| import { intlFormat } from "date-fns"; | |||
| import useReceiptRequest from "hooks/useReceiptRequest"; | |||
| import { useMemo } from "react"; | |||
| import { useForm } from "react-hook-form"; | |||
| import { getPath } from "routes/path"; | |||
| import { sprintf } from "sprintf-js"; | |||
| import { formatDateStr } from "utils/datetime"; | |||
| import { numberFormat } from "utils/number"; | |||
| import { scrollToTop } from "utils/page"; | |||
| import * as Yup from "yup"; | |||
| export default function useConfirm() { | |||
| const { setMode, inputData } = useReceiptRequest(); | |||
| const data: SimpleData[] = useMemo(() => { | |||
| return [ | |||
| { | |||
| title: "申請理由", | |||
| value: inputData.reason, | |||
| }, | |||
| { | |||
| title: "申請者名(会社名)", | |||
| value: inputData.user_company_name, | |||
| }, | |||
| { | |||
| title: "申請者名", | |||
| value: sprintf( | |||
| "%s %s", | |||
| inputData.user_first_name, | |||
| inputData.user_last_name | |||
| ), | |||
| }, | |||
| { | |||
| title: "入庫時刻", | |||
| value: sprintf( | |||
| "%s %s:%s", | |||
| formatDateStr(inputData.put_in_date), | |||
| inputData.put_in_hour, | |||
| inputData.put_in_minute | |||
| ), | |||
| }, | |||
| { | |||
| title: "精算時刻", | |||
| value: sprintf( | |||
| "%s %s:%s", | |||
| formatDateStr(inputData.put_out_date), | |||
| inputData.put_out_hour, | |||
| inputData.put_out_minute | |||
| ), | |||
| }, | |||
| { | |||
| title: "駐車場名", | |||
| value: inputData.parking_name, | |||
| }, | |||
| { | |||
| title: "車室番号", | |||
| value: inputData.room_no, | |||
| }, | |||
| { | |||
| title: "ご利用金額", | |||
| value: sprintf("%s円", numberFormat(inputData.receipt_amount)), | |||
| }, | |||
| { | |||
| title: "支払方法", | |||
| value: inputData.paying_type, | |||
| }, | |||
| { | |||
| title: "ご連絡先電話番号", | |||
| value: inputData.phone_number, | |||
| }, | |||
| { | |||
| title: "ご連絡先Email", | |||
| value: inputData.email, | |||
| }, | |||
| { | |||
| title: "その他伝達事項", | |||
| value: inputData.memo, | |||
| }, | |||
| ]; | |||
| }, [inputData]); | |||
| const handlePrev = () => { | |||
| scrollToTop("auto"); | |||
| setMode("input"); | |||
| }; | |||
| const element = ( | |||
| <Box sx={{ m: 1, p: 1 }}> | |||
| <Stack spacing={3} sx={{ p: 1, m: 1 }} textAlign="left"> | |||
| <Box> | |||
| 下記の内容で申請を行います。 | |||
| 内容にお間違いがないか確認したうえ、確定を行ってください。 | |||
| </Box> | |||
| <SimpleDataList data={data}></SimpleDataList> | |||
| <StackRow> | |||
| <Button variant="outlined" onClick={handlePrev}> | |||
| 戻る | |||
| </Button> | |||
| <Button variant="contained" type="submit"> | |||
| 確定 | |||
| </Button> | |||
| </StackRow> | |||
| </Stack> | |||
| </Box> | |||
| ); | |||
| return { element }; | |||
| } | |||
| @@ -0,0 +1,249 @@ | |||
| import { yupResolver } from "@hookform/resolvers/yup"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Divider, | |||
| Link, | |||
| Stack, | |||
| Typography, | |||
| TypographyProps, | |||
| } from "@mui/material"; | |||
| import { HasChildren } from "@types"; | |||
| import { PageID } from "codes/page"; | |||
| import RequireChip from "components/chip/RequireChip"; | |||
| import { | |||
| FormProvider, | |||
| RHFCheckbox, | |||
| RHFSelect, | |||
| RHFTextField, | |||
| } from "components/hook-form"; | |||
| import RHFDatePicker from "components/hook-form/RHFDatePicker"; | |||
| import { SelectOptionProps } from "components/hook-form/RHFSelect"; | |||
| import RHFPrefCodeSelect from "components/hook-form/ex/RHFPrefCodeSelect"; | |||
| import StackRow from "components/stack/StackRow"; | |||
| import { | |||
| ReceiptRequestFormProps, | |||
| getDefaultReceiptRequestFormValues, | |||
| } from "contexts/ReceiptRequestContext"; | |||
| import useReceiptRequest from "hooks/useReceiptRequest"; | |||
| import { range } from "lodash"; | |||
| import { useMemo } from "react"; | |||
| import { useForm } from "react-hook-form"; | |||
| import { getPath } from "routes/path"; | |||
| import { sprintf } from "sprintf-js"; | |||
| import { scrollToTop } from "utils/page"; | |||
| import * as Yup from "yup"; | |||
| type AreaBoxProps = { | |||
| title: string; | |||
| require?: boolean; | |||
| } & HasChildren; | |||
| function AreaBox({ title, children, require }: AreaBoxProps) { | |||
| return ( | |||
| <Stack sx={{ maxWidth: 500 }}> | |||
| <StackRow> | |||
| <Typography variant="subtitle1">〇 {title} </Typography> | |||
| <RequireChip require={require ?? false} /> | |||
| </StackRow> | |||
| {children} | |||
| </Stack> | |||
| ); | |||
| } | |||
| function Explain({ children, ...props }: TypographyProps) { | |||
| return ( | |||
| <Typography variant="body2" {...props}> | |||
| {children} | |||
| </Typography> | |||
| ); | |||
| } | |||
| export default function useInput() { | |||
| const { setMode, setInputData, form } = useReceiptRequest(); | |||
| const hours: SelectOptionProps[] = useMemo(() => { | |||
| return [ | |||
| { | |||
| label: "--", | |||
| value: "", | |||
| }, | |||
| ...range(0, 24).map((val) => ({ | |||
| label: sprintf("%02d", val), | |||
| value: String(val), | |||
| })), | |||
| ]; | |||
| }, []); | |||
| const minutes: SelectOptionProps[] = useMemo(() => { | |||
| return [ | |||
| { | |||
| label: "--", | |||
| value: "", | |||
| }, | |||
| ...range(0, 60).map((val) => ({ | |||
| label: sprintf("%02d", val), | |||
| value: String(val), | |||
| })), | |||
| ]; | |||
| }, []); | |||
| const reasons: SelectOptionProps[] = useMemo(() => { | |||
| return [ | |||
| { | |||
| label: "--", | |||
| value: "", | |||
| }, | |||
| { | |||
| label: "取り忘れ", | |||
| value: "取り忘れ", | |||
| }, | |||
| { | |||
| label: "紛失", | |||
| value: "紛失", | |||
| }, | |||
| { | |||
| label: "その他", | |||
| value: "その他", | |||
| }, | |||
| ]; | |||
| }, []); | |||
| const payingTypes: SelectOptionProps[] = useMemo(() => { | |||
| return [ | |||
| { | |||
| label: "--", | |||
| value: "", | |||
| }, | |||
| { | |||
| label: "現金", | |||
| value: "現金", | |||
| }, | |||
| { | |||
| label: "クレジットカード", | |||
| value: "クレジットカード", | |||
| }, | |||
| { | |||
| label: "電子マネー", | |||
| value: "電子マネー", | |||
| }, | |||
| ]; | |||
| }, []); | |||
| const handleSubmit = () => { | |||
| if (!form) return; | |||
| setInputData(form.getValues()); | |||
| scrollToTop("auto"); | |||
| setMode("confirm"); | |||
| }; | |||
| const handlePrev = () => { | |||
| setMode("agree"); | |||
| }; | |||
| const element = !!form && ( | |||
| <FormProvider | |||
| methods={form} | |||
| onSubmit={form.handleSubmit(handleSubmit, handleSubmit)} | |||
| > | |||
| <Box sx={{ m: 1, p: 1 }}> | |||
| <Stack spacing={3} sx={{ p: 1, m: 1 }} textAlign="left"> | |||
| <AreaBox title="申請理由" require> | |||
| <RHFSelect name="reason" size="small" options={reasons} /> | |||
| <Explain> </Explain> | |||
| <Explain>※その他の場合に入力してください</Explain> | |||
| <RHFTextField name="reason_text" size="small" /> | |||
| </AreaBox> | |||
| <Divider /> | |||
| <AreaBox title="申請者名(会社名)"> | |||
| <Explain>※法人の方の場合のみ入力してください</Explain> | |||
| <RHFTextField name="user_company_name" size="small" /> | |||
| </AreaBox> | |||
| <AreaBox title="申請者名" require> | |||
| <StackRow> | |||
| <StackRow alignItems="center"> | |||
| <Typography variant="body2">姓</Typography> | |||
| <RHFTextField name="user_first_name" size="small" /> | |||
| <Typography variant="body2">名</Typography> | |||
| <RHFTextField name="user_last_name" size="small" /> | |||
| <Typography variant="body2">様</Typography> | |||
| </StackRow> | |||
| </StackRow> | |||
| </AreaBox> | |||
| <AreaBox title="入庫時刻" require> | |||
| <Explain>※不明の場合はおおよそで構いません</Explain> | |||
| <Stack spacing={1}> | |||
| <RHFDatePicker name="put_in_date" size="small" /> | |||
| <StackRow alignItems="center"> | |||
| <RHFSelect name="put_in_hour" size="small" options={hours} /> | |||
| <Typography variant="body2">時</Typography> | |||
| <RHFSelect | |||
| name="put_in_minute" | |||
| size="small" | |||
| options={minutes} | |||
| /> | |||
| <Typography variant="body2">分</Typography> | |||
| </StackRow> | |||
| </Stack> | |||
| </AreaBox> | |||
| <AreaBox title="精算時刻" require> | |||
| <Explain>※不明の場合はおおよそで構いません</Explain> | |||
| <Stack spacing={1}> | |||
| <RHFDatePicker name="put_out_date" size="small" /> | |||
| <StackRow alignItems="center"> | |||
| <RHFSelect name="put_out_hour" size="small" options={hours} /> | |||
| <Typography variant="body2">時</Typography> | |||
| <RHFSelect | |||
| name="put_out_minute" | |||
| size="small" | |||
| options={minutes} | |||
| /> | |||
| <Typography variant="body2">分</Typography> | |||
| </StackRow> | |||
| </Stack> | |||
| </AreaBox> | |||
| <AreaBox title="駐車場名" require> | |||
| <Explain>駐車場名はこちらで検索することができます</Explain> | |||
| <StackRow> | |||
| <Link | |||
| href="https://www.app-value.com/ypark/map.php" | |||
| target="_blank" | |||
| variant="body2" | |||
| > | |||
| ユアー・パーキング駐車場検索 | |||
| </Link> | |||
| </StackRow> | |||
| <RHFTextField name="parking_name" size="small" /> | |||
| </AreaBox> | |||
| <AreaBox title="車室番号" require> | |||
| <Explain>※分からない場合は「不明」と入力してください</Explain> | |||
| <RHFTextField name="room_no" size="small" /> | |||
| </AreaBox> | |||
| <AreaBox title="ご利用金額" require> | |||
| <RHFTextField name="receipt_amount" type="number" size="small" /> | |||
| </AreaBox> | |||
| <AreaBox title="支払方法" require> | |||
| <RHFSelect name="paying_type" size="small" options={payingTypes} /> | |||
| </AreaBox> | |||
| <AreaBox title="ご連絡先電話番号" require> | |||
| <Explain>※ハイフン不要</Explain> | |||
| <RHFTextField name="phone_number" size="small" /> | |||
| </AreaBox> | |||
| <AreaBox title="ご連絡先Email" require> | |||
| <RHFTextField name="email" size="small" /> | |||
| </AreaBox> | |||
| <AreaBox title="その他伝達事項"> | |||
| <RHFTextField name="memo" size="small" multiline rows={5} /> | |||
| </AreaBox> | |||
| </Stack> | |||
| <StackRow> | |||
| <Button variant="outlined" onClick={handlePrev}> | |||
| 戻る | |||
| </Button> | |||
| <Button variant="contained" type="submit"> | |||
| 次へ | |||
| </Button> | |||
| </StackRow> | |||
| </Box> | |||
| </FormProvider> | |||
| ); | |||
| return { element }; | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| import { | |||
| Box, | |||
| Paper, | |||
| Step, | |||
| StepLabel, | |||
| Stepper, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { ReceiptRequestContextProvider } from "contexts/ReceiptRequestContext"; | |||
| import useAgreement from "./hooks/useAgreement"; | |||
| import useReceiptRequest from "hooks/useReceiptRequest"; | |||
| import useInput from "./hooks/useInput"; | |||
| import useConfirm from "./hooks/useConfirm"; | |||
| export default function ReceiptRequest() { | |||
| return ( | |||
| <ReceiptRequestContextProvider> | |||
| <App /> | |||
| </ReceiptRequestContextProvider> | |||
| ); | |||
| } | |||
| function App() { | |||
| const { mode, step } = useReceiptRequest(); | |||
| const agreement = useAgreement(); | |||
| const input = useInput(); | |||
| const confirm = useConfirm(); | |||
| return ( | |||
| <Paper sx={{ mx: 1, my: 2, px: 1, py: 3 }}> | |||
| <Box mt={3}> | |||
| <Typography variant="h5">領収証発行(再発行)申請</Typography> | |||
| </Box> | |||
| <Box mt={2}> | |||
| <Stepper activeStep={step}> | |||
| <Step> | |||
| <StepLabel>個人情報保護指針への同意</StepLabel> | |||
| </Step> | |||
| <Step> | |||
| <StepLabel>ご申請内容の入力</StepLabel> | |||
| </Step> | |||
| <Step> | |||
| <StepLabel>ご申請内容の確認</StepLabel> | |||
| </Step> | |||
| <Step> | |||
| <StepLabel>ご申請完了</StepLabel> | |||
| </Step> | |||
| </Stepper> | |||
| </Box> | |||
| {mode === "agree" && agreement.element} | |||
| {mode === "input" && input.element} | |||
| {mode === "confirm" && confirm.element} | |||
| </Paper> | |||
| ); | |||
| } | |||
| @@ -6,6 +6,7 @@ import AppRoutes from "./sub/app"; | |||
| import AuthRoutes from "./sub/auth"; | |||
| import CommonRoutes from "./sub/common"; | |||
| import DashboardRoutes from "./sub/dashboard"; | |||
| import RequestReceiptRoutes from "./sub/receipt-request"; | |||
| export const Loadable = (Component: ElementType) => (props: any) => { | |||
| return ( | |||
| @@ -22,6 +23,7 @@ export function Routes() { | |||
| AuthRoutes(), | |||
| AppRoutes(), | |||
| DashboardRoutes(), | |||
| RequestReceiptRoutes(), | |||
| { | |||
| path: "*", | |||
| element: initialized ? <Page404 /> : <LoadingScreen />, | |||
| @@ -83,6 +83,9 @@ const PATHS = { | |||
| [makePathKey(PageID.DASHBOARD_LOGIN_USER_CHANGE_PASSWORD)]: | |||
| "/dashboard/login-user/change-password/:id", | |||
| // 領収証リクエスト | |||
| [makePathKey(PageID.RECEIPT_REQUEST)]: "/request/receipt/:contractId", | |||
| // その他 | |||
| [makePathKey(PageID.PAGE_403)]: "403", | |||
| [makePathKey(PageID.PAGE_404)]: "404", | |||
| @@ -0,0 +1,23 @@ | |||
| import { PageID } from "codes/page"; | |||
| import SimpleLayout from "layouts/simple"; | |||
| import { lazy, useMemo } from "react"; | |||
| import { RouteObject } from "react-router-dom"; | |||
| import { Loadable } from "routes"; | |||
| import { getRoute } from "routes/path"; | |||
| export default function ReceiptRequestRoutes(): RouteObject { | |||
| const ReceiptRequest = Loadable(lazy(() => import("pages/receipt-request"))); | |||
| const children: RouteObject[] = useMemo(() => { | |||
| return [ | |||
| { | |||
| path: getRoute(PageID.RECEIPT_REQUEST), | |||
| element: <ReceiptRequest />, | |||
| }, | |||
| ]; | |||
| }, []); | |||
| return { | |||
| element: <SimpleLayout />, | |||
| children, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,57 @@ | |||
| import { sprintf } from "sprintf-js"; | |||
| import * as Yup from "yup"; | |||
| type Require = { | |||
| require?: boolean; | |||
| }; | |||
| type Range = { | |||
| min?: number; | |||
| max?: number; | |||
| }; | |||
| export const Rule = { | |||
| DEFAULT_STR_MAX_LENGTH: 200, | |||
| DEFAULT_NUMBER_MAX_LENGTH: 50000, | |||
| string: function (param: Require & Range) { | |||
| const ret = Yup.string(); | |||
| if (param.require) { | |||
| ret.required("入力してください"); | |||
| } | |||
| // 最大値最小値設定 | |||
| const min = getMin(param); | |||
| const max = getMax(param, this.DEFAULT_STR_MAX_LENGTH); | |||
| const message = sprintf("%d-%d文字入力できます", min, max); | |||
| ret.test("range", message, function (value) { | |||
| const len = value ? value.length : 0; | |||
| return min <= len && len <= max; | |||
| }); | |||
| ret.typeError("入力を確認してください"); | |||
| return ret; | |||
| }, | |||
| number: function (param: Require & Range) { | |||
| const ret = Yup.number(); | |||
| if (param.require) { | |||
| ret.required("入力してください"); | |||
| } | |||
| }, | |||
| }; | |||
| function getMin(param: Range & Require): number { | |||
| if (param.min !== undefined) return param.min; | |||
| if (param.require) { | |||
| return 1; | |||
| } else { | |||
| return 0; | |||
| } | |||
| } | |||
| function getMax(param: Range, defaultValue: number): number { | |||
| if (param.max !== undefined) return param.max; | |||
| return 200; | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| const NumberFormat = new Intl.NumberFormat(); | |||
| export function numberFormat(value: string | number): string { | |||
| if (typeof value === "number") { | |||
| return NumberFormat.format(value); | |||
| } | |||
| return NumberFormat.format(Number(value)); | |||
| } | |||
| @@ -1,3 +1,3 @@ | |||
| export const scrollToTop = () => { | |||
| window.scroll({ top: 0, behavior: "smooth" }); | |||
| export const scrollToTop = (behavior: ScrollBehavior = "smooth") => { | |||
| window.scroll({ top: 0, behavior }); | |||
| }; | |||