|
| 1 | +import { useBackendAdminClient, useExportOrdersMutation } from "@frontend/common/hooks/useAdminAPI"; |
| 2 | +import { timestampedFilename, triggerBlobDownload } from "@frontend/common/utils"; |
| 3 | +import { FileDownload } from "@mui/icons-material"; |
| 4 | +import { |
| 5 | + Button, |
| 6 | + Checkbox, |
| 7 | + CircularProgress, |
| 8 | + Dialog, |
| 9 | + DialogActions, |
| 10 | + DialogContent, |
| 11 | + DialogTitle, |
| 12 | + FormControl, |
| 13 | + FormControlLabel, |
| 14 | + InputLabel, |
| 15 | + MenuItem, |
| 16 | + Select, |
| 17 | + Stack, |
| 18 | +} from "@mui/material"; |
| 19 | +import { ErrorBoundary, Suspense } from "@suspensive/react"; |
| 20 | +import { FC, useState } from "react"; |
| 21 | + |
| 22 | +import { ChoicePicker } from "@apps/pyconkr-admin/components/elements/choice_picker"; |
| 23 | +import { ErrorFallback } from "@apps/pyconkr-admin/components/elements/error_fallback"; |
| 24 | +import { addErrorSnackbar, addSnackbar } from "@apps/pyconkr-admin/utils/snackbar"; |
| 25 | + |
| 26 | +type Scope = "event" | "categorygroup" | "category" | "product"; |
| 27 | + |
| 28 | +const SCOPES: { value: Scope; label: string; paramKey: string; source: { app: string; resource: string } }[] = [ |
| 29 | + { value: "event", label: "이벤트별", paramKey: "event_id", source: { app: "event", resource: "event" } }, |
| 30 | + { value: "categorygroup", label: "카테고리 그룹별", paramKey: "category_group_id", source: { app: "shop", resource: "categorygroup" } }, |
| 31 | + { value: "category", label: "카테고리별", paramKey: "category_id", source: { app: "shop", resource: "category" } }, |
| 32 | + { value: "product", label: "상품별", paramKey: "product_id", source: { app: "shop", resource: "product" } }, |
| 33 | +]; |
| 34 | + |
| 35 | +const ExportDialogBody: FC<{ onClose: () => void }> = ErrorBoundary.with({ fallback: ErrorFallback }, ({ onClose }) => { |
| 36 | + const client = useBackendAdminClient(); |
| 37 | + const exportMutation = useExportOrdersMutation(client); |
| 38 | + |
| 39 | + const [scope, setScope] = useState<Scope>("event"); |
| 40 | + const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]); |
| 41 | + const [includeRefunded, setIncludeRefunded] = useState(false); |
| 42 | + |
| 43 | + const { paramKey, source } = SCOPES.find((s) => s.value === scope)!; |
| 44 | + |
| 45 | + const changeScope = (next: Scope) => { |
| 46 | + setScope(next); |
| 47 | + setSelectedIds([]); |
| 48 | + }; |
| 49 | + |
| 50 | + const handleExport = () => { |
| 51 | + const params: Record<string, string> = { [paramKey]: selectedIds.join(",") }; |
| 52 | + if (includeRefunded) params.include_refunded = "true"; |
| 53 | + exportMutation.mutate(params, { |
| 54 | + onSuccess: (blob) => { |
| 55 | + triggerBlobDownload(blob, timestampedFilename("order_export", "xlsx")); |
| 56 | + addSnackbar("주문 내보내기를 완료했습니다.", "success"); |
| 57 | + onClose(); |
| 58 | + }, |
| 59 | + onError: addErrorSnackbar, |
| 60 | + }); |
| 61 | + }; |
| 62 | + |
| 63 | + return ( |
| 64 | + <> |
| 65 | + <DialogContent dividers> |
| 66 | + <Stack spacing={2} sx={{ pt: 1 }}> |
| 67 | + <FormControl size="small" fullWidth> |
| 68 | + <InputLabel id="order-export-scope">범위</InputLabel> |
| 69 | + <Select labelId="order-export-scope" label="범위" value={scope} onChange={(e) => changeScope(e.target.value as Scope)}> |
| 70 | + {SCOPES.map((s) => ( |
| 71 | + <MenuItem key={s.value} value={s.value}> |
| 72 | + {s.label} |
| 73 | + </MenuItem> |
| 74 | + ))} |
| 75 | + </Select> |
| 76 | + </FormControl> |
| 77 | + |
| 78 | + {/* key={scope} 로 범위 변경 시 ChoicePicker 를 remount — source(selectables) 와 내부 필터 상태를 새로 시작. */} |
| 79 | + <Suspense fallback={<CircularProgress size={20} />}> |
| 80 | + <ChoicePicker key={scope} multiple label="대상" source={source} value={selectedIds} onChange={setSelectedIds} /> |
| 81 | + </Suspense> |
| 82 | + |
| 83 | + <FormControlLabel |
| 84 | + control={<Checkbox size="small" checked={includeRefunded} onChange={(e) => setIncludeRefunded(e.target.checked)} />} |
| 85 | + label="환불 포함" |
| 86 | + slotProps={{ typography: { variant: "body2" } }} |
| 87 | + /> |
| 88 | + </Stack> |
| 89 | + </DialogContent> |
| 90 | + <DialogActions> |
| 91 | + <Button onClick={onClose} color="inherit"> |
| 92 | + 취소 |
| 93 | + </Button> |
| 94 | + <Button |
| 95 | + variant="contained" |
| 96 | + startIcon={<FileDownload />} |
| 97 | + onClick={handleExport} |
| 98 | + disabled={selectedIds.length === 0 || exportMutation.isPending} |
| 99 | + > |
| 100 | + {exportMutation.isPending ? "내보내는 중…" : `내보내기${selectedIds.length ? ` (${selectedIds.length})` : ""}`} |
| 101 | + </Button> |
| 102 | + </DialogActions> |
| 103 | + </> |
| 104 | + ); |
| 105 | +}); |
| 106 | + |
| 107 | +export const OrderExportDialog: FC<{ open: boolean; onClose: () => void }> = ({ open, onClose }) => ( |
| 108 | + <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> |
| 109 | + <DialogTitle>범위별 주문 내보내기</DialogTitle> |
| 110 | + <ExportDialogBody onClose={onClose} /> |
| 111 | + </Dialog> |
| 112 | +); |
0 commit comments