diff --git a/src/pages/DashboardPage/CompletedContractsModal.tsx b/src/pages/DashboardPage/CompletedContractsModal.tsx new file mode 100644 index 0000000..ec44f45 --- /dev/null +++ b/src/pages/DashboardPage/CompletedContractsModal.tsx @@ -0,0 +1,263 @@ +import { + Modal, + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, + TablePagination, + CircularProgress, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +interface Contract { + uid: number; + lessorOrSellerNames: string[]; + lesseeOrBuyerNames: string[]; + category: "SALE" | "DEPOSIT" | "MONTHLY" | null; + contractDate: string | null; + contractStartDate: string | null; + contractEndDate: string | null; + status: string; + address: string; +} + +interface CompletedContractsModalProps { + open: boolean; + onClose: () => void; + contracts: Contract[]; + loading: boolean; + totalCount: number; + page: number; + rowsPerPage: number; + onPageChange: (newPage: number) => void; + onRowsPerPageChange: (newRowsPerPage: number) => void; +} + +const CONTRACT_STATUS_TYPES = [ + { value: "LISTED", name: "매물 등록", color: "default" }, + { value: "NEGOTIATING", name: "협상 중", color: "info" }, + { value: "INTENT_SIGNED", name: "가계약", color: "warning" }, + { value: "CANCELLED", name: "계약 취소", color: "error" }, + { value: "CONTRACTED", name: "계약 체결", color: "success" }, + { value: "IN_PROGRESS", name: "계약 진행 중", color: "primary" }, + { value: "PAID_COMPLETE", name: "잔금 지급 완료", color: "secondary" }, + { value: "REGISTERED", name: "등기 완료", color: "success" }, + { value: "MOVED_IN", name: "입주 완료", color: "success" }, + { value: "TERMINATED", name: "계약 해지", color: "error" }, +] as const; + +const categoryKoreanMap: Record = { + SALE: "매매", + DEPOSIT: "전세", + MONTHLY: "월세", +}; + +const CompletedContractsModal = ({ + open, + onClose, + contracts = [], + loading, + totalCount, + page, + rowsPerPage, + onPageChange, + onRowsPerPageChange, +}: CompletedContractsModalProps) => { + const navigate = useNavigate(); + + const getColor = (color: string) => { + switch (color) { + case "primary": + return "#1976d2"; + case "success": + return "#2e7d32"; + case "error": + return "#d32f2f"; + case "warning": + return "#ed6c02"; + case "info": + return "#0288d1"; + case "secondary": + return "#9c27b0"; + default: + return "#999"; + } + }; + + const getStatusChip = (status: string) => { + const statusInfo = CONTRACT_STATUS_TYPES.find( + (item) => item.value === status + ); + if (!statusInfo) return status; + + return ( + + ); + }; + + const getCategoryChip = (category: string | null) => { + if (!category || category === "null") return "-"; + const label = categoryKoreanMap[category] ?? category; + const colorMap: Record = { + SALE: "#4caf50", + DEPOSIT: "#2196f3", + MONTHLY: "#ff9800", + }; + return ( + + ); + }; + + const formatDate = (date: string | null) => { + if (!date) return "-"; + return new Date(date).toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + }; + + const handleRowClick = (contractUid: number) => { + navigate(`/contracts/${contractUid}`); + }; + + return ( + + + + 완료된 계약 목록 + + + + + + + + + 임대/매도인 + 임차/매수인 + 주소 + 계약 카테고리 + 계약일 + 계약 시작일 + 계약 종료일 + 상태 + + + + {loading ? ( + + + + + + ) : contracts.length === 0 ? ( + + + 완료된 계약이 없습니다. + + + ) : ( + contracts.map((contract) => ( + handleRowClick(contract.uid)} + > + + {contract.lessorOrSellerNames?.join(", ") || "-"} + + + {contract.lesseeOrBuyerNames?.join(", ") || "-"} + + {contract.address} + + {getCategoryChip(contract.category)} + + + {formatDate(contract.contractDate)} + + + {formatDate(contract.contractStartDate)} + + + {formatDate(contract.contractEndDate)} + + + {getStatusChip(contract.status)} + + + )) + )} + +
+
+ onPageChange(newPage)} + onRowsPerPageChange={(e) => + onRowsPerPageChange(parseInt(e.target.value, 10)) + } + rowsPerPageOptions={[10, 25, 50]} + labelRowsPerPage="페이지당 행 수" + labelDisplayedRows={({ from, to, count }) => + `${count}개 중 ${from}-${to}개` + } + /> +
+
+
+
+ ); +}; + +export default CompletedContractsModal; diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx index 1a7703c..0a44edd 100644 --- a/src/pages/DashboardPage/DashboardPage.tsx +++ b/src/pages/DashboardPage/DashboardPage.tsx @@ -22,6 +22,9 @@ import { Alert, Chip, Tooltip, + CircularProgress, + Modal, + TablePagination, } from "@mui/material"; import "./DashboardPage.css"; import AssignmentIcon from "@mui/icons-material/Assignment"; @@ -31,7 +34,7 @@ import PersonAddIcon from "@mui/icons-material/PersonAdd"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import dayjs from "dayjs"; import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; import isSameOrBefore from "dayjs/plugin/isSameOrBefore"; @@ -43,6 +46,10 @@ import SurveyDetailModal from "@pages/DashboardPage/SurveyDetailModal"; import { Schedule } from "../../interfaces/schedule"; import useUserStore from "@stores/useUserStore"; import { formatDate } from "@utils/dateUtil"; +import RecentCustomersModal from "./RecentCustomersModal"; +import RecentContractsModal from "./RecentContractsModal"; +import OngoingContractsModal from "./OngoingContractsModal"; +import CompletedContractsModal from "./CompletedContractsModal"; dayjs.extend(isSameOrAfter); dayjs.extend(isSameOrBefore); @@ -51,10 +58,10 @@ interface Contract { uid: number; lessorOrSellerNames: string[]; lesseeOrBuyerNames: string[]; - category: string; - contractDate: string; - contractStartDate: string; - contractEndDate: string; + category: "SALE" | "DEPOSIT" | "MONTHLY" | null; + contractDate: string | null; + contractStartDate: string | null; + contractEndDate: string | null; status: string; address: string; } @@ -112,6 +119,19 @@ interface SurveyDetail { const SURVEY_PAGE_SIZE = 10; +const CONTRACT_STATUS_TYPES = [ + { value: "LISTED", name: "매물 등록", color: "default" }, + { value: "NEGOTIATING", name: "협상 중", color: "info" }, + { value: "INTENT_SIGNED", name: "가계약", color: "warning" }, + { value: "CANCELLED", name: "계약 취소", color: "error" }, + { value: "CONTRACTED", name: "계약 체결", color: "success" }, + { value: "IN_PROGRESS", name: "계약 진행 중", color: "primary" }, + { value: "PAID_COMPLETE", name: "잔금 지급 완료", color: "secondary" }, + { value: "REGISTERED", name: "등기 완료", color: "success" }, + { value: "MOVED_IN", name: "입주 완료", color: "success" }, + { value: "TERMINATED", name: "계약 해지", color: "error" }, +] as const; + const DashboardPage = () => { const [selectedDate, setSelectedDate] = useState(new Date()); const [counselTab, setcounselTab] = useState<"request" | "latest">("request"); @@ -120,7 +140,8 @@ const DashboardPage = () => { ); const navigate = useNavigate(); const [recentCustomers, setRecentCustomers] = useState(0); - const [recentContracts, setRecentContracts] = useState(0); + const [recentContractsCount, setRecentContractsCount] = useState(0); + const [recentContracts, setRecentContracts] = useState([]); const [recentContractsList, setRecentContractsList] = useState( [] ); @@ -156,6 +177,35 @@ const DashboardPage = () => { ); const [isSurveyDetailModalOpen, setIsSurveyDetailModalOpen] = useState(false); const [surveyDetailLoading, setSurveyDetailLoading] = useState(false); + const [isRecentCustomersModalOpen, setIsRecentCustomersModalOpen] = + useState(false); + const [recentContractsModalOpen, setRecentContractsModalOpen] = + useState(false); + const [recentContractsLoading, setRecentContractsLoading] = useState(false); + const [recentContractsPage, setRecentContractsPage] = useState(0); + const [recentContractsRowsPerPage, setRecentContractsRowsPerPage] = + useState(10); + const [ongoingContractsOpen, setOngoingContractsOpen] = useState(false); + const [ongoingContractsList, setOngoingContractsList] = useState( + [] + ); + const [ongoingContractsPage, setOngoingContractsPage] = useState(0); + const [ongoingContractsRowsPerPage, setOngoingContractsRowsPerPage] = + useState(10); + const [ongoingContractsLoading, setOngoingContractsLoading] = useState(false); + const [ongoingContractsTotalCount, setOngoingContractsTotalCount] = + useState(0); + const [completedContractsOpen, setCompletedContractsOpen] = useState(false); + const [completedContractsList, setCompletedContractsList] = useState< + Contract[] + >([]); + const [completedContractsPage, setCompletedContractsPage] = useState(0); + const [completedContractsRowsPerPage, setCompletedContractsRowsPerPage] = + useState(10); + const [completedContractsLoading, setCompletedContractsLoading] = + useState(false); + const [completedContractsTotalCount, setCompletedContractsTotalCount] = + useState(0); const fetchWeeklySchedules = async () => { try { @@ -182,7 +232,7 @@ const DashboardPage = () => { (async () => { const [ recentCustomersRes, - recentContractsRes, + recentContractsCountRes, ongoingContractsRes, completedContractsRes, ] = await Promise.all([ @@ -195,8 +245,8 @@ const DashboardPage = () => { if (recentCustomersRes.data.success) { setRecentCustomers(recentCustomersRes.data.data); } - if (recentContractsRes.data.success) { - setRecentContracts(recentContractsRes.data.data); + if (recentContractsCountRes.data.success) { + setRecentContractsCount(recentContractsCountRes.data.data); } if (ongoingContractsRes.data.success) { setOngoingContracts(ongoingContractsRes.data.data); @@ -204,6 +254,17 @@ const DashboardPage = () => { if (completedContractsRes.data.success) { setCompletedContracts(completedContractsRes.data.data); } + + // 최근 계약 목록 초기 데이터 로딩 + const recentContractsListRes = await apiClient.get("/contracts", { + params: { recent: true, page: 1, size: 10 }, + }); + setRecentContracts( + recentContractsListRes.data?.data?.contracts ?? [] + ); + setRecentContractsCount( + recentContractsListRes.data?.data?.totalElements ?? 0 + ); })(), ]); } catch (error) { @@ -328,11 +389,15 @@ const DashboardPage = () => { }, }), ]); - setExpiringContracts(expiringRes.data?.data?.contracts ?? []); - setRecentContractsList(recentRes.data?.data?.contracts ?? []); + const expiringContracts = expiringRes.data?.data?.contracts ?? []; + const recentContracts = recentRes.data?.data?.contracts ?? []; + setExpiringContracts(expiringContracts); + setRecentContractsList(recentContracts); + setRecentContractsCount(recentContracts.length); } catch { setExpiringContracts([]); setRecentContractsList([]); + setRecentContractsCount(0); } finally { setContractLoading(false); } @@ -340,6 +405,30 @@ const DashboardPage = () => { fetchAllContracts(); }, []); + // 최근 계약 목록을 가져오는 함수 + const fetchRecentContracts = async (page: number) => { + setRecentContractsLoading(true); + try { + const response = await apiClient.get("/contracts", { + params: { + recent: true, + page: page + 1, // 0-based를 1-based로 변환 + size: recentContractsRowsPerPage, + }, + }); + const contracts = response.data?.data?.contracts ?? []; + const totalElements = response.data?.data?.totalElements ?? 0; + setRecentContracts(contracts); + setRecentContractsCount(totalElements); + } catch (error) { + console.error("Failed to fetch recent contracts:", error); + setRecentContracts([]); + setRecentContractsCount(0); + } finally { + setRecentContractsLoading(false); + } + }; + // 더보기 클릭 핸들러 const handleMoreClick = (daySchedules: Schedule[], dayStr: string) => { setSelectedDaySchedules(daySchedules); @@ -462,6 +551,68 @@ const DashboardPage = () => { setSelectedSurvey(null); }; + // 최근 계약 건수 카드 클릭 핸들러 + const handleRecentContractsClick = () => { + setRecentContractsPage(0); // 0-based로 초기화 + fetchRecentContracts(0); + setRecentContractsModalOpen(true); + }; + + const fetchOngoingContracts = useCallback(async () => { + try { + setOngoingContractsLoading(true); + const response = await apiClient.get("/contracts", { + params: { + progress: true, + page: ongoingContractsPage + 1, + size: ongoingContractsRowsPerPage, + }, + }); + + if (response.data.success) { + setOngoingContractsList(response.data.data.contracts); + setOngoingContractsTotalCount(response.data.data.totalElements); + } + } catch (error) { + console.error("Failed to fetch ongoing contracts:", error); + } finally { + setOngoingContractsLoading(false); + } + }, [ongoingContractsPage, ongoingContractsRowsPerPage]); + + useEffect(() => { + if (ongoingContractsOpen) { + fetchOngoingContracts(); + } + }, [ongoingContractsOpen, fetchOngoingContracts]); + + const fetchCompletedContracts = useCallback(async () => { + try { + setCompletedContractsLoading(true); + const response = await apiClient.get("/contracts", { + params: { + progress: false, + page: completedContractsPage + 1, + size: completedContractsRowsPerPage, + }, + }); + if (response.data.success) { + setCompletedContractsList(response.data.data.contracts); + setCompletedContractsTotalCount(response.data.data.totalElements); + } + } catch (error) { + console.error("Failed to fetch completed contracts:", error); + } finally { + setCompletedContractsLoading(false); + } + }, [completedContractsPage, completedContractsRowsPerPage]); + + useEffect(() => { + if (completedContractsOpen) { + fetchCompletedContracts(); + } + }, [completedContractsOpen]); + return ( { }} > setIsRecentCustomersModalOpen(true)} sx={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "space-between", p: 2, + "&:hover": { cursor: "pointer" }, }} > @@ -531,42 +684,35 @@ const DashboardPage = () => { {isLoading ? ( - - - - + ) : ( - 0 && { - cursor: "pointer", - textDecoration: "underline", - "&:hover": { - color: "#0D3B7A", - }, - }), - }} - onClick={() => - recentCustomers > 0 && navigate("/customers") - } - > - {recentCustomers} - + <> + 0 && { + cursor: "pointer", + textDecoration: "underline", + "&:hover": { + color: "#0D3B7A", + }, + }), + }} + > + {recentCustomers} + + + 명 + + )} - - 명 - @@ -604,7 +750,11 @@ const DashboardPage = () => { flexDirection: "column", justifyContent: "space-between", p: 2, + "&:hover": { + cursor: "pointer", + }, }} + onClick={handleRecentContractsClick} > @@ -619,42 +769,35 @@ const DashboardPage = () => { {isLoading ? ( - - - - + ) : ( - 0 && { - cursor: "pointer", - textDecoration: "underline", - "&:hover": { - color: "#0D3B7A", - }, - }), - }} - onClick={() => - recentContracts > 0 && navigate("/contracts") - } - > - {recentContracts} - + <> + 0 && { + cursor: "pointer", + textDecoration: "underline", + "&:hover": { + color: "#0D3B7A", + }, + }), + }} + > + {recentContractsCount} + + + 명 + + )} - - 건 - @@ -692,7 +835,13 @@ const DashboardPage = () => { flexDirection: "column", justifyContent: "space-between", p: 2, + "&:hover": { + cursor: "pointer", + }, }} + onClick={() => + ongoingContracts > 0 && setOngoingContractsOpen(true) + } > @@ -707,13 +856,7 @@ const DashboardPage = () => { {isLoading ? ( - - - - + ) : ( { }, }), }} - onClick={() => - ongoingContracts > 0 && navigate("/contracts") - } > {ongoingContracts} @@ -748,7 +888,6 @@ const DashboardPage = () => { - { flexDirection: "column", justifyContent: "space-between", p: 2, + "&:hover": { + cursor: "pointer", + }, }} + onClick={() => setCompletedContractsOpen(true)} > @@ -795,13 +938,7 @@ const DashboardPage = () => { {isLoading ? ( - - - - + ) : ( { }, }), }} - onClick={() => - completedContracts > 0 && navigate("/contracts") - } > {completedContracts} @@ -1600,6 +1734,78 @@ const DashboardPage = () => { surveyDetail={selectedSurvey} isLoading={surveyDetailLoading} /> + {/* 최근 유입 고객 모달 */} + setIsRecentCustomersModalOpen(false)} + surveyResponses={surveyResponses} + onSurveyClick={handleSurveyClick} + /> + {/* 최근 계약 목록 모달 */} + { + setRecentContractsModalOpen(false); + setRecentContractsPage(0); // 0-based로 초기화 + setRecentContractsRowsPerPage(10); + }} + contracts={recentContracts} + loading={recentContractsLoading} + totalCount={recentContractsCount} + page={recentContractsPage} + rowsPerPage={recentContractsRowsPerPage} + onPageChange={(newPage) => { + setRecentContractsPage(newPage); + fetchRecentContracts(newPage); + }} + onRowsPerPageChange={(newRowsPerPage) => { + setRecentContractsRowsPerPage(newRowsPerPage); + setRecentContractsPage(0); + fetchRecentContracts(0); + }} + /> + {/* Ongoing Contracts Modal */} + { + setOngoingContractsOpen(false); + setOngoingContractsPage(0); + setOngoingContractsRowsPerPage(10); + }} + contracts={ongoingContractsList} + loading={ongoingContractsLoading} + totalCount={ongoingContractsTotalCount} + page={ongoingContractsPage} + rowsPerPage={ongoingContractsRowsPerPage} + onPageChange={(newPage) => { + setOngoingContractsPage(newPage); + }} + onRowsPerPageChange={(newRowsPerPage) => { + setOngoingContractsRowsPerPage(newRowsPerPage); + setOngoingContractsPage(0); + }} + /> + {/* Completed Contracts Modal */} + { + setCompletedContractsOpen(false); + setCompletedContractsPage(0); + setCompletedContractsRowsPerPage(10); + }} + contracts={completedContractsList} + loading={completedContractsLoading} + totalCount={completedContractsTotalCount} + page={completedContractsPage} + rowsPerPage={completedContractsRowsPerPage} + onPageChange={(newPage) => { + setCompletedContractsPage(newPage); + }} + onRowsPerPageChange={(newRowsPerPage) => { + setCompletedContractsRowsPerPage(newRowsPerPage); + setCompletedContractsPage(0); + }} + /> {/* Toast 메시지 */} void; + contracts: Contract[]; + loading: boolean; + totalCount: number; + page: number; + rowsPerPage: number; + onPageChange: (newPage: number) => void; + onRowsPerPageChange: (newRowsPerPage: number) => void; +} + +const CONTRACT_STATUS_TYPES = [ + { value: "LISTED", name: "매물 등록", color: "default" }, + { value: "NEGOTIATING", name: "협상 중", color: "info" }, + { value: "INTENT_SIGNED", name: "가계약", color: "warning" }, + { value: "CANCELLED", name: "계약 취소", color: "error" }, + { value: "CONTRACTED", name: "계약 체결", color: "success" }, + { value: "IN_PROGRESS", name: "계약 진행 중", color: "primary" }, + { value: "PAID_COMPLETE", name: "잔금 지급 완료", color: "secondary" }, + { value: "REGISTERED", name: "등기 완료", color: "success" }, + { value: "MOVED_IN", name: "입주 완료", color: "success" }, + { value: "TERMINATED", name: "계약 해지", color: "error" }, +] as const; + +const categoryKoreanMap: Record = { + SALE: "매매", + DEPOSIT: "전세", + MONTHLY: "월세", +}; + +const OngoingContractsModal = ({ + open, + onClose, + contracts, + loading, + totalCount, + page, + rowsPerPage, + onPageChange, + onRowsPerPageChange, +}: OngoingContractsModalProps) => { + const navigate = useNavigate(); + + const getColor = (color: string) => { + switch (color) { + case "primary": + return "#1976d2"; + case "success": + return "#2e7d32"; + case "error": + return "#d32f2f"; + case "warning": + return "#ed6c02"; + case "info": + return "#0288d1"; + case "secondary": + return "#9c27b0"; + default: + return "#999"; + } + }; + + const getStatusChip = (status: string) => { + const statusInfo = CONTRACT_STATUS_TYPES.find( + (item) => item.value === status + ); + if (!statusInfo) return status; + + return ( + + ); + }; + + const getCategoryChip = (category: string | null) => { + if (!category || category === "null") return "-"; + const label = categoryKoreanMap[category] ?? category; + const colorMap: Record = { + SALE: "#4caf50", + DEPOSIT: "#2196f3", + MONTHLY: "#ff9800", + }; + return ( + + ); + }; + + const formatDate = (date: string | null) => { + if (!date) return "-"; + return new Date(date).toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + }; + + const handleRowClick = (contractUid: number) => { + navigate(`/contracts/${contractUid}`); + }; + + return ( + + + + 진행중인 계약 목록 + + + + + + + + + 임대/매도인 + 임차/매수인 + 주소 + 계약 카테고리 + 계약일 + 계약 시작일 + 계약 종료일 + 상태 + + + + {loading ? ( + + + + + + ) : contracts.length === 0 ? ( + + + 진행중인 계약이 없습니다. + + + ) : ( + contracts.map((contract) => ( + handleRowClick(contract.uid)} + > + + {contract.lessorOrSellerNames.join(", ") || "-"} + + + {contract.lesseeOrBuyerNames.join(", ") || "-"} + + {contract.address} + + {getCategoryChip(contract.category)} + + + {formatDate(contract.contractDate)} + + + {formatDate(contract.contractStartDate)} + + + {formatDate(contract.contractEndDate)} + + + {getStatusChip(contract.status)} + + + )) + )} + +
+
+ onPageChange(newPage)} + onRowsPerPageChange={(e) => + onRowsPerPageChange(parseInt(e.target.value, 10)) + } + rowsPerPageOptions={[10, 25, 50]} + labelRowsPerPage="페이지당 행 수" + labelDisplayedRows={({ from, to, count }) => + `${count}개 중 ${from}-${to}개` + } + /> +
+
+
+
+ ); +}; + +export default OngoingContractsModal; diff --git a/src/pages/DashboardPage/RecentContractsModal.tsx b/src/pages/DashboardPage/RecentContractsModal.tsx new file mode 100644 index 0000000..da9febc --- /dev/null +++ b/src/pages/DashboardPage/RecentContractsModal.tsx @@ -0,0 +1,262 @@ +import { + Modal, + Box, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, + TablePagination, +} from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +interface Contract { + uid: number; + lessorOrSellerNames: string[]; + lesseeOrBuyerNames: string[]; + category: "SALE" | "DEPOSIT" | "MONTHLY" | null; + contractDate: string | null; + contractStartDate: string | null; + contractEndDate: string | null; + status: string; + address?: string; +} + +interface RecentContractsModalProps { + open: boolean; + onClose: () => void; + contracts: Contract[]; + loading: boolean; + totalCount: number; + page: number; + rowsPerPage: number; + onPageChange: (newPage: number) => void; + onRowsPerPageChange: (newRowsPerPage: number) => void; +} + +const CONTRACT_STATUS_TYPES = [ + { value: "LISTED", name: "매물 등록", color: "default" }, + { value: "NEGOTIATING", name: "협상 중", color: "info" }, + { value: "INTENT_SIGNED", name: "가계약", color: "warning" }, + { value: "CANCELLED", name: "계약 취소", color: "error" }, + { value: "CONTRACTED", name: "계약 체결", color: "success" }, + { value: "IN_PROGRESS", name: "계약 진행 중", color: "primary" }, + { value: "PAID_COMPLETE", name: "잔금 지급 완료", color: "secondary" }, + { value: "REGISTERED", name: "등기 완료", color: "success" }, + { value: "MOVED_IN", name: "입주 완료", color: "success" }, + { value: "TERMINATED", name: "계약 해지", color: "error" }, +] as const; + +const RecentContractsModal = ({ + open, + onClose, + contracts, + loading, + totalCount, + page, + rowsPerPage, + onPageChange, + onRowsPerPageChange, +}: RecentContractsModalProps) => { + const navigate = useNavigate(); + + const getColor = (color: string) => { + switch (color) { + case "primary": + return "#1976d2"; + case "success": + return "#2e7d32"; + case "error": + return "#d32f2f"; + case "warning": + return "#ed6c02"; + case "info": + return "#0288d1"; + case "secondary": + return "#9c27b0"; + default: + return "#999"; + } + }; + + return ( + + + + 최근 계약 목록 + + + + + + 임대/매도인 + 임차/매수인 + 주소 + 계약 카테고리 + 계약일 + 계약 시작일 + 계약 종료일 + 상태 + + + + {loading ? ( + + + 불러오는 중... + + + ) : contracts.length === 0 ? ( + + + 계약 데이터가 없습니다 + + + ) : ( + contracts.map((contract) => ( + { + navigate(`/contracts/${contract.uid}`); + onClose(); + }} + > + + {Array.isArray(contract.lessorOrSellerNames) + ? contract.lessorOrSellerNames.length === 0 + ? "-" + : contract.lessorOrSellerNames.length === 1 + ? contract.lessorOrSellerNames[0] + : `${contract.lessorOrSellerNames[0]} 외 ${ + contract.lessorOrSellerNames.length - 1 + }명` + : "-"} + + + {Array.isArray(contract.lesseeOrBuyerNames) + ? contract.lesseeOrBuyerNames.length === 0 + ? "-" + : contract.lesseeOrBuyerNames.length === 1 + ? contract.lesseeOrBuyerNames[0] + : `${contract.lesseeOrBuyerNames[0]} 외 ${ + contract.lesseeOrBuyerNames.length - 1 + }명` + : "-"} + + + {contract.address ?? "-"} + + + {(() => { + const categoryKoreanMap: Record = { + SALE: "매매", + DEPOSIT: "전세", + MONTHLY: "월세", + }; + const colorMap: Record = { + SALE: "#4caf50", + DEPOSIT: "#2196f3", + MONTHLY: "#ff9800", + }; + if (!contract.category) return "-"; + return ( + + ); + })()} + + + {contract.contractDate ?? "-"} + + + {contract.contractStartDate ?? "-"} + + + {contract.contractEndDate ?? "-"} + + + {(() => { + const statusInfo = CONTRACT_STATUS_TYPES.find( + (item) => item.value === contract.status + ); + return statusInfo ? ( + + ) : ( + contract.status + ); + })()} + + + )) + )} + +
+
+ onPageChange(newPage)} + onRowsPerPageChange={(e) => + onRowsPerPageChange(parseInt(e.target.value, 10)) + } + rowsPerPageOptions={[10, 25, 50]} + labelRowsPerPage="페이지당 행 수" + labelDisplayedRows={({ from, to, count }) => + `${count}개 중 ${from}-${to}개` + } + /> +
+
+ ); +}; + +export default RecentContractsModal; diff --git a/src/pages/DashboardPage/RecentCustomersModal.tsx b/src/pages/DashboardPage/RecentCustomersModal.tsx new file mode 100644 index 0000000..8bfd398 --- /dev/null +++ b/src/pages/DashboardPage/RecentCustomersModal.tsx @@ -0,0 +1,115 @@ +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import { formatDate } from "@utils/dateUtil"; + +interface SurveyResponse { + id: number; + name: string; + phoneNumber: string; + submittedAt: string; + surveyResponseUid: number; +} + +interface RecentCustomersModalProps { + open: boolean; + onClose: () => void; + surveyResponses: SurveyResponse[]; + onSurveyClick: (surveyResponseUid: number) => void; +} + +const RecentCustomersModal = ({ + open, + onClose, + surveyResponses, + onSurveyClick, +}: RecentCustomersModalProps) => { + return ( + + + + 최근 유입 고객 + + + + + + + + 고객명 + 연락처 + 제출일 + + + + {Array.isArray(surveyResponses) && + surveyResponses.length === 0 ? ( + + + 신규 설문이 없습니다. + + + ) : ( + Array.isArray(surveyResponses) && + surveyResponses.map((res) => ( + onSurveyClick(res.surveyResponseUid)} + sx={{ + cursor: "pointer", + "&:hover": { + backgroundColor: "rgba(22, 79, 158, 0.04)", + }, + }} + > + {res.name} + {res.phoneNumber} + {formatDate(res.submittedAt)} + + )) + )} + +
+
+
+ + + +
+ ); +}; + +export default RecentCustomersModal;