import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Area, Column } from '@ant-design/plots' import { Card, Form, Input, Modal, Spin, Tabs, message } from 'antd' import { BookOutlined, CalendarOutlined, CustomerServiceOutlined, EditOutlined, LeftOutlined, RightOutlined, ScheduleOutlined, TeamOutlined } from '@ant-design/icons' import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client' import { homeAccentColors } from '../../theme' import { useTheme } from '../../themeContext' import { money } from './homesShared' type SparkBlock = { open?: number today?: number total?: number monthNew?: number monthAmount?: number settleMonth?: number spark?: number[] } type ChartPanel = { title?: string label?: string offset?: number apply?: number accept?: number done?: number cancel?: number settleAmount?: number count?: number daily?: { day: number; value: number }[] } type StatusBlock = { total?: number items?: Record } type Postit = { slot: number title?: string contents?: string authorName?: string noteDate?: string } type Dash = { pending?: SparkBlock booking?: SparkBlock customer?: SparkBlock abook?: SparkBlock contractTotal?: number customerTotal?: number postits?: Postit[] internet?: ChartPanel home?: ChartPanel internetStatus?: StatusBlock homeStatus?: StatusBlock notices?: { id: number; title?: string; createdDate?: string }[] questions?: { id: number; title?: string; createdDate?: string }[] csPhone?: string } function Wave({ color }: { color: string }) { return ( ) } function MiniBars({ values, color }: { values: number[]; color: string }) { const max = Math.max(1, ...values) return (
{values.map((v, i) => ( ))}
) } export default function HomeDashboardPage() { const { theme } = useTheme() const accents = useMemo(() => homeAccentColors(theme), [theme]) const qc = useQueryClient() const [offset, setOffset] = useState(0) const [period, setPeriod] = useState('day') const [editSlot, setEditSlot] = useState(null) const [form] = Form.useForm() const query = useQuery({ queryKey: ['homes-dashboard', offset, period], queryFn: async () => { const { data } = await client.get>(`${apiPrefix()}/homes/dashboard`, { params: { offset, period } }) return unwrap(data) } }) const savePostit = useMutation({ mutationFn: async (values: { contents: string }) => { if (!editSlot) return const { data } = await client.put(`${apiPrefix()}/homes/dashboard/postits/${editSlot.slot}`, { contents: values.contents, authorName: localStorage.getItem('userName') || '홍길동' }) return unwrap(data) }, onSuccess: () => { message.success('저장했습니다.') setEditSlot(null) qc.invalidateQueries({ queryKey: ['homes-dashboard'] }) }, onError: (e: Error) => message.error(e.message) }) if (query.isLoading) { return (
) } const d = query.data || {} const pending = d.pending || {} const booking = d.booking || {} const customer = d.customer || {} const abook = d.abook || {} const internet = d.internet || {} const home = d.home || {} const internetStatus = d.internetStatus || {} const homeStatus = d.homeStatus || {} const columnCfg = (daily: { day: number; value: number }[] | undefined, fill: string) => ({ data: daily || [], xField: 'day', yField: 'value', height: 180, autoFit: true, color: fill, columnStyle: { radius: [3, 3, 0, 0] }, xAxis: { label: { style: { fill: accents.plot.axisLabelFill, fontSize: 10 } }, line: null, tickLine: null }, yAxis: { label: { style: { fill: accents.plot.axisLabelFill, fontSize: 10 } }, grid: { line: { style: { stroke: accents.plot.gridStroke } } } }, tooltip: { title: (v: number) => `${v}일` } }) const areaCfg = (daily: { day: number; value: number }[] | undefined, stroke: string, fill: string) => ({ data: daily || [], xField: 'day', yField: 'value', height: 180, autoFit: true, smooth: true, color: stroke, areaStyle: { fill }, line: { color: stroke, size: 2 }, xAxis: { label: { style: { fill: accents.plot.axisLabelFill, fontSize: 10 } }, line: null, tickLine: null }, yAxis: { label: { style: { fill: accents.plot.axisLabelFill, fontSize: 10 } }, grid: { line: { style: { stroke: accents.plot.gridStroke } } } } }) return (
일정관리
{pending.open ?? 0} / {pending.total ?? 0}
오늘 약속 {pending.today ?? 0}건
미결 {pending.open ?? 0} 오늘 {pending.today ?? 0}
예약관리
{booking.open ?? 0} / {booking.total ?? 0}
접수 대기 현황
접수 {booking.open ?? 0} 전체 {booking.total ?? 0}
등록고객
{customer.total ?? d.customerTotal ?? 0}명
이달 신규 {customer.monthNew ?? 0}
이달장부
{money(abook.monthAmount)}
정산 {money(abook.settleMonth)}
{(d.postits || []).map((p) => (
{p.title || `포스트잇 #${p.slot}`}
{p.contents || ''}
by {p.authorName || '-'} {p.noteDate || ''}
))}
인터넷접수 (영업기준)
} >
{internet.label} 계약 {internet.count ?? 0}건
신청 {internet.apply ?? 0}
접수 {internet.accept ?? 0}
완료 {internet.done ?? 0}
정산금액 {money(internet.settleAmount)}
인터넷현황 {internetStatus.total ?? 0}
{Object.entries(internetStatus.items || {}).map(([k, v]) => (
{k} {v}
))}
홈렌탈접수 (영업기준)
} >
{home.label} 계약 {home.count ?? 0}건
신청 {home.apply ?? 0}
접수 {home.accept ?? 0}
완료 {home.done ?? 0}
정산금액 {money(home.settleAmount)}
홈렌탈현황 {homeStatus.total ?? 0}
{Object.entries(homeStatus.items || {}).map(([k, v]) => (
{k} {v}
))}
공지사항 } >
    {(d.notices || []).length === 0 ? (
  • 등록된 공지가 없습니다.
  • ) : ( (d.notices || []).map((n) => (
  • {n.title} {n.createdDate}
  • )) )}
사용문의} >
    {(d.questions || []).length === 0 ? (
  • 등록된 문의가 없습니다.
  • ) : ( (d.questions || []).map((q) => (
  • {q.title} {q.createdDate}
  • )) )}
고객센터 } >
{d.csPhone || '0000-0000'}
setEditSlot(null)} onOk={() => form.submit()} confirmLoading={savePostit.isPending} destroyOnHidden >
savePostit.mutate(v)}>
) }