Переглянути джерело

領収証発行依頼 まで

develop
sosuke.iwabuchi 2 роки тому
джерело
коміт
75069b25bd
21 змінених файлів з 1237 додано та 104 видалено
  1. +1
    -0
      package.json
  2. +24
    -15
      src/App.tsx
  3. +69
    -0
      src/api/custom/hello-techno/index.ts
  4. +52
    -0
      src/api/custom/hello-techno/receipt-issuing-order.ts
  5. +43
    -24
      src/api/index.ts
  6. +30
    -0
      src/api/receipt-issuing-order.ts
  7. +17
    -8
      src/api/url.ts
  8. +22
    -0
      src/codes/receipt-issuing-order.ts
  9. +28
    -53
      src/components/hook-form/RHFDatePicker.tsx
  10. +35
    -0
      src/contexts/BackDropContext.tsx
  11. +6
    -0
      src/hooks/useBackDrop.ts
  12. +1
    -1
      src/pages/dashboard/index.tsx
  13. +144
    -3
      src/pages/dashboard/receipt-issuing-order/create.tsx
  14. +137
    -0
      src/pages/dashboard/receipt-issuing-order/hooks/useConfirm.tsx
  15. +111
    -0
      src/pages/dashboard/receipt-issuing-order/hooks/useInputReceiptStep.tsx
  16. +83
    -0
      src/pages/dashboard/receipt-issuing-order/hooks/useInputSMSSendAddress.tsx
  17. +148
    -0
      src/pages/dashboard/receipt-issuing-order/hooks/useSelectParkingStep.tsx
  18. +257
    -0
      src/pages/dashboard/receipt-issuing-order/list.tsx
  19. +8
    -0
      src/routes/index.tsx
  20. +9
    -0
      src/theme/index.tsx
  21. +12
    -0
      yarn.lock

+ 1
- 0
package.json Переглянути файл

@@ -9,6 +9,7 @@
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.129",
"@mui/material": "^5.12.2",
"@mui/x-date-pickers": "^6.4.0",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",


+ 24
- 15
src/App.tsx Переглянути файл

@@ -8,22 +8,31 @@ import { BrowserRouter } from "react-router-dom";
import { Routes } from "routes";
import AppThemeProvider from "theme";

import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import ja from "date-fns/locale/ja";
import BackDropContextProvider from "contexts/BackDropContext";

export default function App() {
return (
<AuthContextProvider>
<PageContextProvider>
<WindowSizeContextProvider>
<BrowserRouter>
<AppThemeProvider>
<SnackbarProvider>
<CsrfTokenProvider />
<CssBaseline />
<Routes />
</SnackbarProvider>
</AppThemeProvider>
</BrowserRouter>
</WindowSizeContextProvider>
</PageContextProvider>
</AuthContextProvider>
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={ja}>
<AuthContextProvider>
<PageContextProvider>
<WindowSizeContextProvider>
<BrowserRouter>
<AppThemeProvider>
<SnackbarProvider>
<BackDropContextProvider>
<CsrfTokenProvider />
<CssBaseline />
<Routes />
</BackDropContextProvider>
</SnackbarProvider>
</AppThemeProvider>
</BrowserRouter>
</WindowSizeContextProvider>
</PageContextProvider>
</AuthContextProvider>
</LocalizationProvider>
);
}

+ 69
- 0
src/api/custom/hello-techno/index.ts Переглянути файл

@@ -0,0 +1,69 @@
import { APICommonResponse, ApiId, HttpMethod, makeParam, request } from "api";
import { getUrl } from "api/url";

export type HTCustomer = {
customer_code: string;
name: string;
};
export type HTParking = {
customer_code: string;
parking_management_code: string;
name: string;
};
export type HTAdjustData = {
customer_code: string;
parking_management_code: string;
adjust_seq_no: number;
adjust_datetime: string;
};

export type HTCustomersResponse = {
data: {
records: HTCustomer[];
};
} & APICommonResponse;

export const getHTCustomers = async (data = {}) => {
const res = await request<HTCustomersResponse>({
url: getUrl(ApiId.HT_CUSTOM_CUSTOMERS),
method: HttpMethod.GET,
});
return res;
};

export type HTParkingsRequest = {
customer_code: string;
};

export type HTParkingsResponse = {
data: {
records: HTParking[];
};
} & APICommonResponse;

export const getHTParkings = async (data: HTParkingsRequest) => {
console.log({ data });
const res = await request<HTParkingsResponse>({
url: getUrl(ApiId.HT_CUSTOM_PARKINGS),
method: HttpMethod.GET,
data: new URLSearchParams(data),
});
return res;
};

export type HTAdjustDataRequest = {
customer_code: string;
};

export type HTAdjustDataResponse = {
data: HTAdjustData;
} & APICommonResponse;

export const getHTAdjustData = async (data: HTAdjustDataRequest) => {
const res = await request<HTAdjustDataResponse>({
url: getUrl(ApiId.HT_CUSTOM_ADJUST_DATA),
method: HttpMethod.GET,
data: new URLSearchParams(data),
});
return res;
};

+ 52
- 0
src/api/custom/hello-techno/receipt-issuing-order.ts Переглянути файл

@@ -0,0 +1,52 @@
import {
APICommonResponse,
ApiId,
HttpMethod,
makeFormData,
request,
} from "api";
import { getUrl } from "api/url";

export type HTCustomer = {
customer_code: string;
name: string;
};
export type HTParking = {
customer_code: string;
parking_management_code: string;
name: string;
};
export type HTAdjustData = {
customer_code: string;
parking_management_code: string;
adjust_seq_no: number;
adjust_datetime: string;
};

export type CreateReceiptIssuingOrderRequest = {
customer_code: string;
parking_management_code: string;
adjust_seq_no?: string | number;
receipt_name: string;
receipt_use_datetime: Date | null;
receipt_amount: string | number;
memo?: string;
sms_phone_number: string;
};

export type HTCustomersResponse = {
data: {
records: HTCustomer[];
};
} & APICommonResponse;

export const createReceiptIssuingOrder = async (
data: CreateReceiptIssuingOrderRequest
) => {
const res = await request({
url: getUrl(ApiId.HT_CUSTOM_RECEIPT_ISSUING_ORDER_CREATE),
method: HttpMethod.POST,
data: makeFormData(data),
});
return res;
};

+ 43
- 24
src/api/index.ts Переглянути файл

@@ -12,6 +12,16 @@ export const ApiId = {

LOGIN: id++,
LOGOUT: id++,

RECEIPT_ISSUING_ORDERS: id++,
RECEIPT_ISSUING_ORDER: id++,
RECEIPT_ISSUING_ORDER_CREATE: id++,

// FOR CUSTOM
HT_CUSTOM_CUSTOMERS: id++,
HT_CUSTOM_PARKINGS: id++,
HT_CUSTOM_ADJUST_DATA: id++,
HT_CUSTOM_RECEIPT_ISSUING_ORDER_CREATE: id++,
} as const;
export type ApiId = (typeof ApiId)[keyof typeof ApiId];

@@ -201,34 +211,43 @@ export async function apiRequest<
if (setSending) {
setSending(true);
}
const res = await apiMethod(sendData);
if (setSending) {
setSending(false);
}

if (res?.result === ResultCode.SUCCESS) {
if (onSuccess) {
onSuccess(res, sendData);
try {
const res = await apiMethod(sendData);
if (setSending) {
setSending(false);
}
} else {
if (res?.messages.errors) {
if (errorSetter) {
const errorCount = setFormErrorMessages(
sendData,
errorSetter,
res.messages.errors
);
console.log("FormErrorCount", errorCount);

if (res?.result === ResultCode.SUCCESS) {
if (onSuccess) {
onSuccess(res, sendData);
}
} else {
if (res?.messages.errors) {
if (errorSetter) {
const errorCount = setFormErrorMessages(
sendData,
errorSetter,
res.messages.errors
);
console.log("FormErrorCount", errorCount);
}
}
if (onFailed) {
onFailed(res);
}
if (onFinaly) {
onFinaly(res);
}
}
return res;
} catch (e) {
console.error(e);
if (onFailed) {
onFailed(res);
onFailed(null);
}
if (onFinaly) {
onFinaly(null);
}
return null;
}

if (onFinaly) {
onFinaly(res);
}

return res;
}

+ 30
- 0
src/api/receipt-issuing-order.ts Переглянути файл

@@ -0,0 +1,30 @@
import { ReceiptIssuingOrderStatus } from "codes/receipt-issuing-order";
import { APICommonResponse, ApiId, HttpMethod, makeParam, request } from ".";
import { getUrl } from "./url";

export type ReceiptIssuingOrder = {
id: string;
status: ReceiptIssuingOrderStatus;
};

export type ReceiptIssuingOrdersRequest = {
address?: string;
status?: ReceiptIssuingOrderStatus;
};

export type ReceiptIssuingOrdersResponse = {
data: {
records: ReceiptIssuingOrder[];
};
} & APICommonResponse;

export const getReceiptIssuingOrders = async (
data: ReceiptIssuingOrdersRequest
) => {
const res = await request<ReceiptIssuingOrdersResponse>({
url: getUrl(ApiId.RECEIPT_ISSUING_ORDERS),
method: HttpMethod.GET,
data: makeParam(data),
});
return res;
};

+ 17
- 8
src/api/url.ts Переглянути файл

@@ -1,22 +1,31 @@
import { ApiId } from ".";
import { ApiId as A } from ".";

const urls = {
[ApiId.CSRF_TOKEN]: "sanctum/csrf-cookie",
[ApiId.ME]: "me",
[ApiId.LOGIN]: "login",
[ApiId.LOGOUT]: "logout",
[A.CSRF_TOKEN]: "sanctum/csrf-cookie",
[A.ME]: "me",
[A.LOGIN]: "login",
[A.LOGOUT]: "logout",

[A.RECEIPT_ISSUING_ORDERS]: "receipt-issuing-orders",

// FOR CUSTOM
[A.HT_CUSTOM_CUSTOMERS]: "custom/hello-techno/customers",
[A.HT_CUSTOM_PARKINGS]: "custom/hello-techno/parkings",
[A.HT_CUSTOM_ADJUST_DATA]: "custom/hello-techno/adjust-data",
[A.HT_CUSTOM_RECEIPT_ISSUING_ORDER_CREATE]:
"custom/hello-techno/receipt-issuing-order/create",
};

const prefixs = {
[ApiId.CSRF_TOKEN]: "",
[A.CSRF_TOKEN]: "",
};
const DEFAULT_API_URL_PREFIX = "api";

const getPrefix = (apiId: ApiId) => {
const getPrefix = (apiId: A) => {
return prefixs[apiId] ?? DEFAULT_API_URL_PREFIX;
};

export const getUrl = (apiId: ApiId) => {
export const getUrl = (apiId: A) => {
let url = getPrefix(apiId);
if (url.length !== 0) {
url += "/";


+ 22
- 0
src/codes/receipt-issuing-order.ts Переглянути файл

@@ -0,0 +1,22 @@
export const ReceiptIssuingOrderStatus = {
NONE: 0,
CREATED: 100, // 新規作成
SMS_SENDING: 200, // SMS送信中
SMS_RECEIVED: 300, // SMS送信完了
SMS_OPENED: 400, // SMS開封

// 郵送関連
MAIL_REQUEST: 500, // 郵送依頼中
PREPARING_FOR_MAIL: 510, // 郵送準備中
MAIL_DONE: 520, // 郵送完了

// Email送信関連
EMAIL_SENDING: 600, // Email送信中
EMAIL_DONE: 610, // Email送信完了

// PDFダウンロード
DOWNLOAD_DONE: 700, // ダウンロード完了
} as const;

export type ReceiptIssuingOrderStatus =
(typeof ReceiptIssuingOrderStatus)[keyof typeof ReceiptIssuingOrderStatus];

+ 28
- 53
src/components/hook-form/RHFDatePicker.tsx Переглянути файл

@@ -1,4 +1,3 @@
// form
import {
Controller,
ControllerFieldState,
@@ -6,15 +5,8 @@ import {
FieldValues,
useFormContext,
} from "react-hook-form";
// @mui
import ClearIcon from "@mui/icons-material/Clear";
import { DatePicker } from "@mui/lab";
import {
IconButton,
InputAdornment,
TextField,
TextFieldProps,
} from "@mui/material";
import { TextFieldProps } from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers";
import React, { useMemo } from "react";
import RHFTextField from "./RHFTextField";

@@ -35,36 +27,21 @@ const RHFDatePicker = ({
datePickerProps,
...other
}: Props) => {
const { control, watch, setValue } = useFormContext();
const {
control,
watch,
setValue,
formState: { errors },
} = useFormContext();

const value: Date | null = watch(name);

const handleClear = () => {
setValue(name, null);
};

const icon = useMemo(() => {
if (value !== null) {
return (
<InputAdornment position="end">
<IconButton onClick={handleClear} edge="end">
<ClearIcon />
</IconButton>
</InputAdornment>
);
} else {
return null;
}
}, [value]);

const iconMerge = (ele: React.ReactNode) => {
return (
<>
{icon}
{ele}
</>
);
};
const isError = useMemo(() => {
return !!errors[name];
}, [errors, name]);
const errorMessage = useMemo(() => {
return errors[name]?.message ?? "";
}, [errors, name]);

if (readOnly) {
return <RHFTextField name={name} readOnly {...other} variant="standard" />;
@@ -79,23 +56,21 @@ const RHFDatePicker = ({
}) => {
return (
<DatePicker
inputFormat="yyyy/MM/dd"
mask="____/__/__"
renderInput={(params: any) => (
<TextField
{...params}
fullWidth
error={fieldState.invalid}
helperText={fieldState.error?.message}
InputProps={{
...params.InputProps,
endAdornment: iconMerge(params.InputProps?.endAdornment),
}}
{...other}
/>
)}
{...datePickerProps}
slotProps={{
actionBar: {
actions: ["accept", "clear", "cancel"],
},
inputAdornment: {
position: "start",
},
textField: {
size: "small",
error: isError,
helperText: errorMessage,
},
}}
{...field}
{...datePickerProps}
/>
);
};


+ 35
- 0
src/contexts/BackDropContext.tsx Переглянути файл

@@ -0,0 +1,35 @@
import { Backdrop, CircularProgress, useTheme } from "@mui/material";
import { createContext, useState } from "react";

type Props = {
children: React.ReactNode;
};

type ContextProps = {
showBackDrop: boolean;
setShowBackDrop: (show: boolean) => void;
};
const defaultProps: ContextProps = {
showBackDrop: false,
setShowBackDrop: (show: boolean) => {},
};
export const BackDroptContext = createContext<ContextProps>(defaultProps);

export default function BackDropContextProvider({ children }: Props) {
const [showBackDrop, setShowBackDrop] = useState(false);
return (
<BackDroptContext.Provider value={{ showBackDrop, setShowBackDrop }}>
{children}
<Backdrop
open={showBackDrop}
sx={{
// display: 'frex',
zIndex: 9999,
opacity: "0.3 !important",
}}
>
<CircularProgress color="inherit" />
</Backdrop>
</BackDroptContext.Provider>
);
}

+ 6
- 0
src/hooks/useBackDrop.ts Переглянути файл

@@ -0,0 +1,6 @@
import { useContext } from "react";
import { BackDroptContext } from "contexts/BackDropContext";

export default function useBackDrop() {
return useContext(BackDroptContext);
}

+ 1
- 1
src/pages/dashboard/index.tsx Переглянути файл

@@ -13,5 +13,5 @@ export default function Overview() {
setHeaderTitle("Dashboard");
setTabs(null);
}, []);
return <Box></Box>;
return <Box sx={{ p: 1, m: 1 }}></Box>;
}

+ 144
- 3
src/pages/dashboard/receipt-issuing-order/create.tsx Переглянути файл

@@ -1,7 +1,16 @@
import { Box } from "@mui/material";
import { Box, Step, StepLabel, Stepper } from "@mui/material";
import { PageID, TabID } from "codes/page";
import useDashboard from "hooks/useDashBoard";
import { useEffect } from "react";
import { useEffect, useMemo, useState } from "react";
import useSelectParkingStep from "./hooks/useSelectParkingStep";
import useInputReceiptStep from "./hooks/useInputReceiptStep";
import useInputSMSSendAddress from "./hooks/useInputSMSSendAddress";
import useConfirm, { ConfirmDataProps } from "./hooks/useConfirm";
import useAPICall from "hooks/useAPICall";
import { createReceiptIssuingOrder } from "api/custom/hello-techno/receipt-issuing-order";
import useSnackbarCustom from "hooks/useSnackbarCustom";
import useBackDrop from "hooks/useBackDrop";
import { getValue } from "components/hook-form/RHFAutoComplete";

export default function ReceiptIssuingOrderCreate() {
const { setHeaderTitle, setTabs } = useDashboard(
@@ -9,9 +18,141 @@ export default function ReceiptIssuingOrderCreate() {
TabID.NONE
);

const { success, error } = useSnackbarCustom();
const { setShowBackDrop } = useBackDrop();

const [mode, setMode] = useState<
"parking_select" | "input_receipt" | "input_address" | "confirm" | "done"
>("parking_select");

const step = useMemo(() => {
switch (mode) {
case "parking_select":
return 0;
case "input_receipt":
return 1;
case "input_address":
return 2;
case "confirm":
return 3;
case "done":
return 4;
}
}, [mode]);

const getConfimData = (): ConfirmDataProps => {
return {
customerName: selectParkingStep.values("customerCode.label"),
parkingName: selectParkingStep.values("parkingManagementCode.label"),
adjustSeqNo: selectParkingStep.values("adjustSeqNo"),
amount: inputReceiptStep.values("amount"),
date: inputReceiptStep.values("date"),
name: inputReceiptStep.values("name"),
address: inputSMSSendAddress.values("address"),
memo: inputReceiptStep.values("memo"),
};
};
const getFormData = () => {
return {
...selectParkingStep.values(),
...inputReceiptStep.values(),
...inputSMSSendAddress.values(),
};
};

const selectParkingStep = useSelectParkingStep({
onNext: () => {
setMode("input_receipt");
},
});

const inputReceiptStep = useInputReceiptStep({
onNext: () => {
setMode("input_address");
},
onPrev: () => {
setMode("parking_select");
},
});
const inputSMSSendAddress = useInputSMSSendAddress({
onNext: () => {
confirm.setData(getConfimData());
setMode("confirm");
},
onPrev: () => {
setMode("input_receipt");
},
});

const confirm = useConfirm({
onNext: () => {
send();
},
onPrev: () => {
setMode("input_address");
},
});

const createAPI = useAPICall({
apiMethod: createReceiptIssuingOrder,
onSuccess: () => {
setMode("done");
success("成功しました");
},
onFailed: () => {
error("失敗しました");
},
});

const send = () => {
const { callAPI, makeSendData } = createAPI;

const formData = getFormData();
const sendData = makeSendData({
customer_code: getValue(formData.customerCode),
parking_management_code: getValue(formData.parkingManagementCode),
adjust_seq_no: formData.adjustSeqNo,
receipt_name: formData.name,
receipt_use_datetime: formData.date,
receipt_amount: formData.amount,
sms_phone_number: formData.address,
});

callAPI(sendData);
};

useEffect(() => {
setShowBackDrop(createAPI.sending);
}, [createAPI.sending]);

useEffect(() => {
setHeaderTitle("領収証発行依頼作成");
setTabs(null);
}, []);
return <Box>Create</Box>;
return (
<Box sx={{ p: 1, m: 1 }}>
<Stepper activeStep={step}>
<Step>
<StepLabel>駐車場選択</StepLabel>
</Step>
<Step>
<StepLabel>領収証情報入力</StepLabel>
</Step>
<Step>
<StepLabel>SMS送信先入力</StepLabel>
</Step>
<Step>
<StepLabel>確認</StepLabel>
</Step>
<Step>
<StepLabel>完了</StepLabel>
</Step>
</Stepper>
{mode === "parking_select" && selectParkingStep.element}
{mode === "input_receipt" && inputReceiptStep.element}
{mode === "input_address" && inputSMSSendAddress.element}
{mode === "confirm" && confirm.element}
{mode === "done" && <Box sx={{ p: 1, py: 3, m: 1 }}>受付しました。</Box>}
</Box>
);
}

+ 137
- 0
src/pages/dashboard/receipt-issuing-order/hooks/useConfirm.tsx Переглянути файл

@@ -0,0 +1,137 @@
import { yupResolver } from "@hookform/resolvers/yup";
import {
Box,
Button,
Stack,
Table,
TableBody,
TableCell,
TableRow,
TextField,
Typography,
} from "@mui/material";
import { HasChildren } from "@types";
import TextFieldEx from "components/form/TextFieldEx";
import {
FormProvider,
RHFAutoComplete,
RHFTextField,
} from "components/hook-form";
import {
AutoCompleteOption,
AutoCompleteOptionType,
getValue,
} from "components/hook-form/RHFAutoComplete";
import RHFDatePicker from "components/hook-form/RHFDatePicker";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { formatDateStr, formatDateTimeStr } from "utils/datetime";
import * as Yup from "yup";

type AreaBoxProps = {
title: string;
} & HasChildren;
function AreaBox({ title, children }: AreaBoxProps) {
return (
<Box sx={{ maxWidth: 500 }}>
<Typography variant="subtitle1">{title}</Typography>
{children}
</Box>
);
}

type Props = {
onNext?: VoidFunction;
onPrev?: VoidFunction;
};

export type ConfirmDataProps = {
customerName: string;
parkingName: string;
adjustSeqNo: string;
amount: string;
date: Date | null;
name: string;
address: string;
memo: string;
};
export default function useConfirm({ onNext, onPrev }: Props) {
const [adjustSeqNo, setAdjustSeqNo] = useState("");

const [data, setData] = useState<ConfirmDataProps>({
customerName: "",
parkingName: "",
adjustSeqNo: "",
amount: "",
date: null,
name: "",
address: "",
memo: "",
});

const handleNext = () => {
if (onNext) {
onNext();
}
};

const handlePrev = () => {
if (onPrev) {
onPrev();
}
};

const element = (
<Stack spacing={2} sx={{ p: 1, py: 3, m: 1 }}>
<Typography variant="h5">領収証発行内容確認</Typography>
<Table>
<TableBody>
<TableRow>
<TableCell>運営会社名</TableCell>
<TableCell>{data.customerName}</TableCell>
</TableRow>
<TableRow>
<TableCell>駐車場名</TableCell>
<TableCell>{data.parkingName}</TableCell>
</TableRow>
<TableRow>
<TableCell>精算連番</TableCell>
<TableCell>{data.adjustSeqNo}</TableCell>
</TableRow>
<TableRow>
<TableCell>利用日</TableCell>
<TableCell>{formatDateStr(data.date)}</TableCell>
</TableRow>
<TableRow>
<TableCell>宛名</TableCell>
<TableCell>{data.name}</TableCell>
</TableRow>
<TableRow>
<TableCell>金額</TableCell>
<TableCell>{data.amount}円</TableCell>
</TableRow>
<TableRow>
<TableCell>メモ</TableCell>
<TableCell>
<TextFieldEx readOnly value={data.memo} multiline />
</TableCell>
</TableRow>
<TableRow>
<TableCell>SMS送信先</TableCell>
<TableCell>{data.address}</TableCell>
</TableRow>
</TableBody>
</Table>
<Stack direction="row" spacing={2}>
<Button variant="text" onClick={handlePrev}>
戻る
</Button>
<Button variant="contained" onClick={handleNext}>
確定
</Button>
</Stack>
</Stack>
);

return { element, setData };
}

+ 111
- 0
src/pages/dashboard/receipt-issuing-order/hooks/useInputReceiptStep.tsx Переглянути файл

@@ -0,0 +1,111 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Box, Button, Stack, TextField, Typography } from "@mui/material";
import { HasChildren } from "@types";
import {
FormProvider,
RHFAutoComplete,
RHFTextField,
} from "components/hook-form";
import {
AutoCompleteOption,
AutoCompleteOptionType,
getValue,
} from "components/hook-form/RHFAutoComplete";
import RHFDatePicker from "components/hook-form/RHFDatePicker";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as Yup from "yup";

type AreaBoxProps = {
title: string;
} & HasChildren;
function AreaBox({ title, children }: AreaBoxProps) {
return (
<Box sx={{ maxWidth: 500 }}>
<Typography variant="subtitle1">{title}</Typography>
{children}
</Box>
);
}

type FormProps = {
amount: string;
date: Date | null;
name: string;
memo: string;
};

type Props = {
onNext?: VoidFunction;
onPrev?: VoidFunction;
};
export default function useInputReceiptStep({ onNext, onPrev }: Props) {
const [adjustSeqNo, setAdjustSeqNo] = useState("");

const form = useForm<FormProps>({
defaultValues: {
amount: "",
date: null,
name: "",
memo: "",
},
resolver: yupResolver(
Yup.object().shape({
date: Yup.date()
.required("必須項目です")
.typeError("正しく入力してください"),
amount: Yup.number().required("必須項目です"),
name: Yup.string().required("必須項目です"),
memo: Yup.string().nullable(),
})
),
});

const handleSubmit = (data: FormProps) => {
console.log(data);
if (onNext) {
onNext();
}
};

const handlePrev = () => {
if (onPrev) {
onPrev();
}
};

const element = (
<FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}>
<Stack spacing={2} sx={{ p: 1, m: 1 }}>
<AreaBox title="利用日">
<RHFDatePicker name="date" size="small" />
</AreaBox>
<AreaBox title="金額">
<RHFTextField
type="number"
name="amount"
size="small"
InputProps={{ endAdornment: <div>円</div> }}
sx={{ maxWidth: 150 }}
/>
</AreaBox>
<AreaBox title="宛名">
<RHFTextField name="name" size="small" />
</AreaBox>
<AreaBox title="メモ">
<RHFTextField name="memo" size="small" multiline rows={3} />
</AreaBox>
<Stack direction="row" spacing={2}>
<Button variant="text" onClick={handlePrev}>
戻る
</Button>
<Button variant="contained" type="submit">
次へ
</Button>
</Stack>
</Stack>
</FormProvider>
);

return { element, values: form.getValues, setValue: form.setValue };
}

+ 83
- 0
src/pages/dashboard/receipt-issuing-order/hooks/useInputSMSSendAddress.tsx Переглянути файл

@@ -0,0 +1,83 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Box, Button, Stack, Typography } from "@mui/material";
import { HasChildren } from "@types";
import {
FormProvider,
RHFAutoComplete,
RHFTextField,
} from "components/hook-form";
import {
AutoCompleteOption,
AutoCompleteOptionType,
getValue,
} from "components/hook-form/RHFAutoComplete";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as Yup from "yup";

type AreaBoxProps = {
title: string;
} & HasChildren;
function AreaBox({ title, children }: AreaBoxProps) {
return (
<Box sx={{ maxWidth: 500 }}>
<Typography variant="subtitle1">{title}</Typography>
{children}
</Box>
);
}

type FormProps = {
address: string;
};

type Props = {
onNext?: VoidFunction;
onPrev?: VoidFunction;
};
export default function useInputSMSSendAddress({ onNext, onPrev }: Props) {
const form = useForm<FormProps>({
defaultValues: {
address: "",
},
resolver: yupResolver(
Yup.object().shape({
address: Yup.string()
.required("必須項目です")
.matches(/^[0-9]{11}$/, "正しい電話番号を入力してください"),
})
),
});

const handleSubmit = () => {
if (onNext) {
onNext();
}
};

const handlePrev = () => {
if (onPrev) {
onPrev();
}
};

const element = (
<FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}>
<Stack spacing={2} sx={{ p: 1, m: 1 }}>
<AreaBox title="SMS送信先">
<RHFTextField name="address" size="small" />
</AreaBox>
<Stack direction="row" spacing={2}>
<Button variant="text" onClick={handlePrev}>
戻る
</Button>
<Button variant="contained" type="submit">
次へ
</Button>
</Stack>
</Stack>
</FormProvider>
);

return { element, values: form.getValues };
}

+ 148
- 0
src/pages/dashboard/receipt-issuing-order/hooks/useSelectParkingStep.tsx Переглянути файл

@@ -0,0 +1,148 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { Box, Button, Stack, Typography } from "@mui/material";
import { HasChildren } from "@types";
import { getHTCustomers, getHTParkings } from "api/custom/hello-techno";
import {
FormProvider,
RHFAutoComplete,
RHFTextField,
} from "components/hook-form";
import {
AutoCompleteOption,
AutoCompleteOptionType,
getValue,
} from "components/hook-form/RHFAutoComplete";
import useAPICall from "hooks/useAPICall";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as Yup from "yup";

type AreaBoxProps = {
title: string;
} & HasChildren;
function AreaBox({ title, children }: AreaBoxProps) {
return (
<Box sx={{ maxWidth: 500 }}>
<Typography variant="subtitle1">{title}</Typography>
{children}
</Box>
);
}

type FormProps = {
customerCode: AutoCompleteOptionType;
parkingManagementCode: AutoCompleteOptionType;
adjustSeqNo: string;
};

type Props = {
onNext?: VoidFunction;
};
export default function useSelectParkingStep({ onNext }: Props) {
const [customers, setCustomers] = useState<AutoCompleteOption[]>([]);
const [parkings, setParkings] = useState<AutoCompleteOption[]>([]);

const customerAPI = useAPICall({
apiMethod: getHTCustomers,
onSuccess: ({ data }) => {
const options: AutoCompleteOption[] = data.records.map(
({ customer_code, name }) => {
return {
label: name,
value: customer_code,
};
}
);
setCustomers(options);
},
});

const parkingAPI = useAPICall({
apiMethod: getHTParkings,
onSuccess: ({ data }) => {
const options: AutoCompleteOption[] = data.records.map(
({ parking_management_code, name }) => {
return {
label: name,
value: parking_management_code,
};
}
);
setParkings(options);
},
});

const form = useForm<FormProps>({
defaultValues: {
customerCode: null,
parkingManagementCode: null,
adjustSeqNo: "",
},
resolver: yupResolver(
Yup.object().shape({
customerCode: Yup.object().required("必須項目です"),
parkingManagementCode: Yup.object().required("必須項目です"),
adjustSeqNo: Yup.number()
.nullable()
.transform((value, originalValue) =>
String(originalValue).trim() === "" ? null : value
)
.typeError("数値を入力してください"),
})
),
});

const customerCode = form.watch("customerCode.value");

const handleSubmit = () => {
if (onNext) {
onNext();
}
};

// 顧客一覧取得
useEffect(() => {
customerAPI.callAPI({});
}, []);

// 駐車場一覧取得
useEffect(() => {
setParkings([]);
form.setValue("parkingManagementCode", null);

if (customerCode) {
parkingAPI.callAPI({ customer_code: customerCode });
}
}, [customerCode]);

const element = (
<FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}>
<Stack spacing={2} sx={{ p: 1, m: 1 }}>
<AreaBox title="運営会社">
<RHFAutoComplete
name="customerCode"
options={customers}
size="small"
/>
</AreaBox>
<AreaBox title="駐車場">
<RHFAutoComplete
name="parkingManagementCode"
options={parkings}
size="small"
/>
</AreaBox>
<AreaBox title="精算連番">
<RHFTextField name="adjustSeqNo" size="small" />
</AreaBox>
<Stack direction="row" spacing={2}>
<Button variant="contained" type="submit">
次へ
</Button>
</Stack>
</Stack>
</FormProvider>
);

return { element, values: form.getValues, setValue: form.setValue };
}

+ 257
- 0
src/pages/dashboard/receipt-issuing-order/list.tsx Переглянути файл

@@ -0,0 +1,257 @@
import {
Box,
Grid,
Table,
TableBody,
TableCell,
TableContainer,
TablePagination,
TableRow,
TextField,
} from "@mui/material";
import { Dictionary } from "@types";
import {
ReceiptIssuingOrder,
getReceiptIssuingOrders,
} from "api/receipt-issuing-order";
import { PageID, TabID } from "codes/page";
import { ReceiptIssuingOrderStatus } from "codes/receipt-issuing-order";
import { FormProvider, RHFTextField } from "components/hook-form";
import { TableHeadCustom } from "components/table";
import { SearchConditionContextProvider } from "contexts/SearchConditionContext";
import useAPICall from "hooks/useAPICall";
import useDashboard from "hooks/useDashBoard";
import useSearchConditionContext from "hooks/useSearchConditionContext";
import useTable, { UseTableReturn } from "hooks/useTable";
import ContractTabs from "layouts/dashbord/tab/ContractTabs";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { Contract } from "types/contract";

export default function ReceiptIssuingOrderList() {
const { setHeaderTitle, setTabs } = useDashboard(
PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST,
TabID.NONE
);

const table = useTable<ReceiptIssuingOrder>();

useEffect(() => {
setHeaderTitle("領収証発行依頼一覧");
setTabs(null);
}, []);

return (
<SearchConditionContextProvider>
<Page table={table} />
</SearchConditionContextProvider>
);
}

type CommonProps = {
table: UseTableReturn<ReceiptIssuingOrder>;
};
function Page({ table }: CommonProps) {
const {
order,
page,
sort,
rowsPerPage,
fetched,
fillteredRow,
isNotFound,
dataLength,
//
onSort,
onChangePage,
onChangeRowsPerPage,
//
setRowData,
//
ROWS_PER_PAGES,
} = table;
return (
<Box>
<SearchBox table={table} />
<TableBox table={table} />
</Box>
);
}

type FormProps = {
address: string;
};
function SearchBox({ table }: CommonProps) {
const {
condition,
initialized,
get,
addCondition: add,
} = useSearchConditionContext();

const form = useForm<FormProps>({
defaultValues: {
address: "",
},
});

const { callAPI: calGetReceiptIssuingOrders, makeSendData } = useAPICall({
apiMethod: getReceiptIssuingOrders,
form,
onSuccess: (res) => {
table.setRowData(res.data.records);
},
});

const handleSubmit = async (data: FormProps) => {
addCondition(data);
};

const addCondition = (data: FormProps) => {
add({ ...data });
};

const fetch = async (data: Dictionary) => {
const sendData = makeSendData({
...data,
});
calGetReceiptIssuingOrders(sendData);
};

// 初期値設定
useEffect(() => {
if (initialized) {
form.setValue("address", get("address"));
}
}, [initialized, condition]);

// Fetchアクション
useEffect(() => {
if (initialized) {
fetch(condition);
}
}, [condition, initialized]);

return (
<FormProvider methods={form} onSubmit={form.handleSubmit(handleSubmit)}>
<Box sx={{ p: 1, m: 1 }}>
<Grid container spacing={1}>
<Grid item xs={3}>
<RHFTextField
label="ステータス"
name="status"
fullWidth
size="small"
/>
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
<Grid item xs={3}>
<TextField label="a" fullWidth size="small" />
</Grid>
</Grid>
</Box>
</FormProvider>
);
}

function TableBox({ table }: CommonProps) {
const TABLE_HEAD = [
{ id: "id", label: "ID", align: "left" },
{ id: "name", label: "名前", align: "left" },
{ id: "emply", label: "---", align: "left" },
];
const {
order,
page,
sort,
rowsPerPage,
fetched,
fillteredRow,
isNotFound,
dataLength,
//
onSort,
onChangePage,
onChangeRowsPerPage,
//
setRowData,
//
ROWS_PER_PAGES,
} = table;

useEffect(() => {
setRowData([
{ id: "iwabuchi", status: ReceiptIssuingOrderStatus.NONE },
{ id: "iwabuchi", status: ReceiptIssuingOrderStatus.MAIL_DONE },
]);
}, []);

return (
<>
<TableContainer
sx={{
// minWidth: 800,
position: "relative",
}}
>
<Table size="small">
<TableHeadCustom
order={order}
orderBy={sort}
headLabel={TABLE_HEAD}
rowCount={1}
numSelected={0}
onSort={onSort}
/>

<TableBody>
{fillteredRow.map((row, index) => (
<Row data={row} key={index} />
))}
</TableBody>
</Table>
</TableContainer>

<Box sx={{ position: "relative" }}>
<TablePagination
rowsPerPageOptions={ROWS_PER_PAGES}
component="div"
count={dataLength}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={onChangePage}
onRowsPerPageChange={onChangeRowsPerPage}
/>
</Box>
</>
);
}

type RowProps = {
data: ReceiptIssuingOrder;
};
function Row({ data }: RowProps) {
return (
<TableRow hover sx={{ cursor: "pointer" }}>
<TableCell>{data.id}</TableCell>
<TableCell>{data.status}</TableCell>
</TableRow>
);
}

+ 8
- 0
src/routes/index.tsx Переглянути файл

@@ -54,6 +54,11 @@ const DashboardRoutes = (): RouteObject => {
element: <ReceiptIssuingOrderCreate />,
target: UserRole.NORMAL_ADMIN,
},
{
pageId: PageID.DASHBOARD_RECEIPT_ISSUING_ORDER_LIST,
element: <ReceiptIssuingOrderList />,
target: UserRole.NORMAL_ADMIN,
},
];

const children: RouteObject[] = useMemo(() => {
@@ -104,6 +109,9 @@ const ContractDetail = Loadable(
const ReceiptIssuingOrderCreate = Loadable(
lazy(() => import("pages/dashboard/receipt-issuing-order/create"))
);
const ReceiptIssuingOrderList = Loadable(
lazy(() => import("pages/dashboard/receipt-issuing-order/list"))
);

// その他 ---------------------------------
const Page403 = Loadable(lazy(() => import("pages/common/Page403")));


+ 9
- 0
src/theme/index.tsx Переглянути файл

@@ -149,6 +149,15 @@ theme = {
},
},
},
MuiInput: {
styleOverrides: {
root: {
"&.Mui-disabled:before": {
"border-bottom-style": "none",
},
},
},
},
},
};



+ 12
- 0
yarn.lock Переглянути файл

@@ -1825,6 +1825,18 @@
prop-types "^15.8.1"
react-is "^18.2.0"

"@mui/x-date-pickers@^6.4.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.4.0.tgz#97ca181edbc0d56abe08c93b036f97d8e9946572"
integrity sha512-EdKj+TVaEvx+nIljsv1BlDOYYwcMWVYB+ZMRoOCStFojY5uuDzhttQAP6DbsAPPrpKt1lzyecIukLfmIfIGcsA==
dependencies:
"@babel/runtime" "^7.21.0"
"@mui/utils" "^5.12.3"
"@types/react-transition-group" "^4.4.5"
clsx "^1.2.1"
prop-types "^15.8.1"
react-transition-group "^4.4.5"

"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"


Завантаження…
Відмінити
Зберегти