기능수정
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>비즈케어</title>
|
||||
<title>BizCare</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AppstoreOutlined, CalendarOutlined, ShoppingCartOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import SchedulePanel from './SchedulePanel'
|
||||
import BillCareLogo from './BillCareLogo'
|
||||
import BizCareLogo from './BizCareLogo'
|
||||
import AccountPopover from './AccountPopover'
|
||||
import { careMenuSections, filterMenuSections, resolveSelectedKey, sectionContainsPath, shopMenuSections } from '../menu'
|
||||
import { fetchMyMenus } from '../api/auth'
|
||||
@@ -143,7 +143,7 @@ export default function AgentShell({ shop = false }: Props) {
|
||||
<div className={`topbar-brand${collapsed ? ' is-collapsed' : ''}`} style={collapsed ? undefined : { width: panelWidth }}>
|
||||
{!collapsed && (
|
||||
<div className="brand-inline">
|
||||
<BillCareLogo title="BillCare" size="header" />
|
||||
<BizCareLogo title="BizCare" size="header" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+11
-11
@@ -3,7 +3,7 @@ import logoMark from '../assets/logo_mark_light.png'
|
||||
import { useTheme } from '../themeContext'
|
||||
import type { Theme } from '../theme'
|
||||
|
||||
interface BillCareLogoProps {
|
||||
interface BizCareLogoProps {
|
||||
className?: string
|
||||
title?: string
|
||||
size?: 'header' | 'login'
|
||||
@@ -18,25 +18,25 @@ function isDarkTheme(theme: Theme) {
|
||||
* logo_image.png — 라이트 헤더 가로 로고
|
||||
* logo_mark_light.png / logo_image_theme.png — 마크 (로그인·다크 헤더)
|
||||
*/
|
||||
export default function BillCareLogo({
|
||||
export default function BizCareLogo({
|
||||
className = '',
|
||||
title = 'BillCare',
|
||||
title = 'BizCare',
|
||||
size = 'header',
|
||||
forceTheme,
|
||||
}: BillCareLogoProps) {
|
||||
}: BizCareLogoProps) {
|
||||
const { theme } = useTheme()
|
||||
const active = forceTheme ?? theme
|
||||
const dark = isDarkTheme(active)
|
||||
const root = ['billcare-logo', `billcare-logo--${size}`, `billcare-logo--${active}`, className]
|
||||
const root = ['bizcare-logo', `bizcare-logo--${size}`, `bizcare-logo--${active}`, className]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
// 로그인: 항상 마크 + BillCare (색상은 CSS로 고정)
|
||||
// 로그인: 항상 마크 + BizCare (색상은 CSS로 고정)
|
||||
if (size === 'login') {
|
||||
return (
|
||||
<div className={root} title={title}>
|
||||
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
|
||||
<span className="billcare-logo-title">{title}</span>
|
||||
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
|
||||
<span className="bizcare-logo-title">{title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -54,11 +54,11 @@ export default function BillCareLogo({
|
||||
)
|
||||
}
|
||||
|
||||
// 다크/블루 헤더: 마크 + BillCare
|
||||
// 다크/블루 헤더: 마크 + BizCare
|
||||
return (
|
||||
<div className={root} title={title}>
|
||||
<img src={logoMark} alt="" className="billcare-logo-mark" draggable={false} />
|
||||
<span className="billcare-logo-title">{title}</span>
|
||||
<img src={logoMark} alt="" className="bizcare-logo-mark" draggable={false} />
|
||||
<span className="bizcare-logo-title">{title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export type MultiSelectOption = [string, string]
|
||||
|
||||
type Props = {
|
||||
options: MultiSelectOption[]
|
||||
value: string[]
|
||||
onChange: (v: string[]) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
/** 클릭 시 펼쳐지는 체크박스 다중선택 (billing_react MultiSelect 이식) */
|
||||
export default function MultiSelect({ options, value, onChange, placeholder, className }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number; maxHeight: number } | null>(null)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const toggleRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const updatePanelPos = () => {
|
||||
const el = toggleRef.current
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
setPanelPos({
|
||||
top: r.bottom + 2,
|
||||
left: r.left,
|
||||
width: Math.max(r.width, 160),
|
||||
maxHeight: Math.min(300, Math.max(120, window.innerHeight - r.bottom - 12)),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
updatePanelPos()
|
||||
const onScrollOrResize = () => updatePanelPos()
|
||||
window.addEventListener('resize', onScrollOrResize)
|
||||
window.addEventListener('scroll', onScrollOrResize, true)
|
||||
return () => {
|
||||
window.removeEventListener('resize', onScrollOrResize)
|
||||
window.removeEventListener('scroll', onScrollOrResize, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
const h = (e: MouseEvent) => {
|
||||
const t = e.target as Node
|
||||
if (rootRef.current?.contains(t) || panelRef.current?.contains(t)) return
|
||||
setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', h)
|
||||
return () => document.removeEventListener('mousedown', h)
|
||||
}, [])
|
||||
|
||||
const toggle = (k: string) => onChange(value.includes(k) ? value.filter((x) => x !== k) : [...value, k])
|
||||
const selectAll = () => onChange(options.map(([k]) => k))
|
||||
const selectNone = () => onChange([])
|
||||
const text = value.length === 0
|
||||
? (placeholder || '전체')
|
||||
: options.filter(([k]) => value.includes(k)).map(([, v]) => v).join(', ')
|
||||
|
||||
return (
|
||||
<div className={`ms${open ? ' is-open' : ''}${className ? ` ${className}` : ''}`} ref={rootRef}>
|
||||
<button
|
||||
ref={toggleRef}
|
||||
type="button"
|
||||
className="ms-toggle"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
title={text}
|
||||
>
|
||||
<span className="ms-text" style={value.length ? undefined : { color: 'var(--muted)' }}>{text}</span>
|
||||
<span className="ms-caret">▾</span>
|
||||
</button>
|
||||
{open && panelPos && createPortal(
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="ms-panel ms-panel-portal"
|
||||
style={{ top: panelPos.top, left: panelPos.left, width: panelPos.width, maxHeight: panelPos.maxHeight }}
|
||||
>
|
||||
<div className="ms-actions">
|
||||
<button type="button" className="ms-action" onClick={selectAll}>전체 선택</button>
|
||||
<button type="button" className="ms-action ms-action-muted" onClick={selectNone}>전체 해제</button>
|
||||
</div>
|
||||
{options.map(([k, v]) => (
|
||||
<label key={k} className="ms-item">
|
||||
<input type="checkbox" checked={value.includes(k)} onChange={() => toggle(k)} /> {v}
|
||||
</label>
|
||||
))}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function mapOptions(items: { id: number; name: string }[]): MultiSelectOption[] {
|
||||
return items.map((x) => [String(x.id), x.name])
|
||||
}
|
||||
|
||||
export function entriesOptions(map: Record<string, string> | undefined): MultiSelectOption[] {
|
||||
return map ? Object.entries(map) : []
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FormEvent, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { login } from '../api/auth'
|
||||
import BillCareLogo from '../components/BillCareLogo'
|
||||
import BizCareLogo from '../components/BizCareLogo'
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate()
|
||||
@@ -34,7 +34,7 @@ export default function LoginPage() {
|
||||
<div className="login-wrap">
|
||||
<form className="login-box" onSubmit={submit}>
|
||||
<div className="login-brand">
|
||||
<BillCareLogo title="BillCare" size="login" />
|
||||
<BizCareLogo title="BizCare" size="login" />
|
||||
<p>통합 영업·정산 관리 시스템</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import ReceptionApplyModal from '../../components/ReceptionApplyModal'
|
||||
import ReceptionBulkModal from '../../components/ReceptionBulkModal'
|
||||
import ReceptionProgressModal from '../../components/ReceptionProgressModal'
|
||||
import MultiSelect, { entriesOptions, mapOptions, type MultiSelectOption } from '../../components/MultiSelect'
|
||||
import { DownOutlined } from '@ant-design/icons'
|
||||
import client, { apiPrefix, unwrap, type ApiResponse } from '../../api/client'
|
||||
import { options } from './homesShared'
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
PROGRESS_STATUS,
|
||||
GIFT_STATUS,
|
||||
CENTER_STATUS,
|
||||
GIFT_PLACE,
|
||||
} from './receptionStatuses'
|
||||
|
||||
type ReceptionRow = {
|
||||
@@ -96,11 +98,20 @@ type ReceptionResult = {
|
||||
productCategories?: string[]
|
||||
}
|
||||
|
||||
type IspProduct = { id: number; name: string; type?: string }
|
||||
|
||||
const SIDO_LIST = [
|
||||
'서울', '경기', '인천', '강원', '충북', '충남', '대전', '전북', '전남', '광주',
|
||||
'경북', '대구', '경남', '울산', '부산', '제주',
|
||||
]
|
||||
|
||||
const PRODUCT_TYPE_OPTS: MultiSelectOption[] = [
|
||||
['e01', '인터넷'],
|
||||
['e02', '전화'],
|
||||
['e03', 'TV'],
|
||||
['e04', '렌탈'],
|
||||
]
|
||||
|
||||
const initialFilters = () => ({
|
||||
page: 0,
|
||||
size: 50,
|
||||
@@ -118,19 +129,69 @@ const initialFilters = () => ({
|
||||
ispName: '',
|
||||
branchEmployee: '',
|
||||
branchName: '',
|
||||
giftType: undefined as string | undefined,
|
||||
productType: undefined as string | undefined,
|
||||
progressStatus: undefined as string | undefined,
|
||||
giftStatus: undefined as string | undefined,
|
||||
giftPlace: undefined as string | undefined,
|
||||
customerSido: undefined as string | undefined,
|
||||
carrier: undefined as string | undefined,
|
||||
branch: undefined as string | undefined,
|
||||
settleStatus: undefined as string | undefined,
|
||||
centerName: '',
|
||||
paymentTransferSsid: '',
|
||||
paymentTransferNumber: '',
|
||||
giftType: [] as string[],
|
||||
productType: [] as string[],
|
||||
progressStatus: [] as string[],
|
||||
giftStatus: [] as string[],
|
||||
centerStatus: [] as string[],
|
||||
customerSido: [] as string[],
|
||||
ispId: [] as string[],
|
||||
productNames: [] as string[],
|
||||
productIds: [] as string[],
|
||||
branches: [] as string[],
|
||||
branchIds: [] as string[],
|
||||
processors: [] as string[],
|
||||
centerNames: [] as string[],
|
||||
branchGroupIds: [] as string[],
|
||||
giftPlace: '',
|
||||
opended: undefined as string | undefined,
|
||||
giftSended: undefined as string | undefined,
|
||||
})
|
||||
|
||||
type Filters = ReturnType<typeof initialFilters>
|
||||
|
||||
const csv = (arr: string[]) => (arr.length ? arr.join(',') : undefined)
|
||||
|
||||
/** API 쿼리 파라미터 — 배열은 comma join */
|
||||
const toSearchParams = (f: Filters, opts?: { omitPaging?: boolean }) => ({
|
||||
...(opts?.omitPaging ? {} : { page: f.page, size: f.size }),
|
||||
orderDateStart: f.orderDateStart || undefined,
|
||||
orderDateEnd: f.orderDateEnd || undefined,
|
||||
sorting: f.sorting,
|
||||
postIt: f.postIt || undefined,
|
||||
agreementName: f.agreementName || undefined,
|
||||
customerSsid: f.customerSsid || undefined,
|
||||
customerEmail: f.customerEmail || undefined,
|
||||
customerName: f.customerName || undefined,
|
||||
customerPhone: f.customerPhone || undefined,
|
||||
contractNumber: f.contractNumber || undefined,
|
||||
productName: f.productName || undefined,
|
||||
ispName: f.ispName || undefined,
|
||||
branchEmployee: f.branchEmployee || undefined,
|
||||
branchName: f.branchName || undefined,
|
||||
centerName: f.centerName || undefined,
|
||||
paymentTransferSsid: f.paymentTransferSsid || undefined,
|
||||
paymentTransferNumber: f.paymentTransferNumber || undefined,
|
||||
giftType: csv(f.giftType),
|
||||
productType: csv(f.productType),
|
||||
progressStatus: csv(f.progressStatus),
|
||||
giftStatus: csv(f.giftStatus),
|
||||
centerStatus: csv(f.centerStatus),
|
||||
customerSido: csv(f.customerSido),
|
||||
ispId: csv(f.ispId),
|
||||
productNames: csv(f.productNames),
|
||||
ispProductId: csv(f.productNames.length ? f.productNames : f.productIds),
|
||||
branch: csv(f.branches.length ? f.branches : f.branchIds),
|
||||
processor: csv(f.processors.length ? f.processors : f.centerNames),
|
||||
branchGroupId: csv(f.branchGroupIds),
|
||||
giftPlace: f.giftPlace || undefined,
|
||||
opended: f.opended || undefined,
|
||||
giftSended: f.giftSended || undefined,
|
||||
})
|
||||
|
||||
const num = (n?: number | null) => (n == null ? '' : Number(n).toLocaleString())
|
||||
|
||||
const giftPlaceLabel = (v?: string) => placeLabel(v)
|
||||
@@ -164,39 +225,57 @@ export default function ReceptionListPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const branchGroupsQuery = useQuery({
|
||||
queryKey: ['homes-branch-groups'],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<{ id: number; name: string }[]>>(
|
||||
`${apiPrefix()}/homes/branch-groups`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
},
|
||||
})
|
||||
|
||||
const inputCentersQuery = useQuery({
|
||||
queryKey: ['homes-input-centers'],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<{ id: number; name?: string }[]>>(
|
||||
`${apiPrefix()}/homes/input-centers`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
},
|
||||
})
|
||||
|
||||
const branchesQuery = useQuery({
|
||||
queryKey: ['homes-branches-office'],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<{ id: number; name?: string; officeName?: string }[]>>(
|
||||
`${apiPrefix()}/homes/branches`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
},
|
||||
})
|
||||
|
||||
const productsQuery = useQuery({
|
||||
queryKey: ['homes-reception-products-multi', filters.ispId],
|
||||
queryFn: async () => {
|
||||
const results = await Promise.all(
|
||||
filters.ispId.map(async (id) => {
|
||||
const { data } = await client.get<ApiResponse<IspProduct[]>>(
|
||||
`${apiPrefix()}/homes/reception-isps/${id}/products`,
|
||||
)
|
||||
return unwrap(data) || []
|
||||
}),
|
||||
)
|
||||
return results.flat()
|
||||
},
|
||||
enabled: filters.ispId.length > 0,
|
||||
})
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['homes-receptions', filters],
|
||||
queryFn: async () => {
|
||||
const { data } = await client.get<ApiResponse<ReceptionResult>>(`${apiPrefix()}/homes/receptions`, {
|
||||
params: {
|
||||
page: filters.page,
|
||||
size: filters.size,
|
||||
orderDateStart: filters.orderDateStart || undefined,
|
||||
orderDateEnd: filters.orderDateEnd || undefined,
|
||||
sorting: filters.sorting,
|
||||
postIt: filters.postIt || undefined,
|
||||
agreementName: filters.agreementName || undefined,
|
||||
customerSsid: filters.customerSsid || undefined,
|
||||
customerEmail: filters.customerEmail || undefined,
|
||||
customerName: filters.customerName || undefined,
|
||||
customerPhone: filters.customerPhone || undefined,
|
||||
contractNumber: filters.contractNumber || undefined,
|
||||
productName: filters.productName || undefined,
|
||||
ispName: filters.ispName || undefined,
|
||||
branchEmployee: filters.branchEmployee || undefined,
|
||||
branchName: filters.branchName || undefined,
|
||||
giftType: filters.giftType || undefined,
|
||||
productType: filters.productType || undefined,
|
||||
progressStatus: filters.progressStatus || undefined,
|
||||
giftStatus: filters.giftStatus || undefined,
|
||||
giftPlace: filters.giftPlace || undefined,
|
||||
customerSido: filters.customerSido || undefined,
|
||||
carrier: filters.carrier || undefined,
|
||||
branch: filters.branch || undefined,
|
||||
settleStatus: filters.settleStatus || undefined,
|
||||
opended: filters.opended || undefined,
|
||||
giftSended: filters.giftSended || undefined,
|
||||
},
|
||||
params: toSearchParams(filters),
|
||||
})
|
||||
return unwrap(data)
|
||||
},
|
||||
@@ -218,8 +297,52 @@ export default function ReceptionListPage() {
|
||||
const giftSelectOpts = statusOptions(giftLabels as Record<string, string>)
|
||||
const settleSelectOpts = statusOptions(settleLabels as Record<string, string>)
|
||||
|
||||
const setD = (patch: Partial<ReturnType<typeof initialFilters>>) => setDraft((p) => ({ ...p, ...patch }))
|
||||
const applyF = (patch: Partial<ReturnType<typeof initialFilters>>) => {
|
||||
const giftTypeOpts: MultiSelectOption[] = (
|
||||
query.data?.giftCategories || ['상품권', '가전', '캐시백', '없음']
|
||||
).map((x) => [x, x])
|
||||
|
||||
const progressMsOpts = entriesOptions(progressLabels as Record<string, string>)
|
||||
const giftMsOpts = entriesOptions(giftLabels as Record<string, string>)
|
||||
const centerStatusMsOpts = entriesOptions(settleLabels as Record<string, string>)
|
||||
const sidoMsOpts: MultiSelectOption[] = SIDO_LIST.map((s) => [s, s])
|
||||
const ispMsOpts = mapOptions(ispQuery.data || [])
|
||||
const branchGroupMsOpts = mapOptions(branchGroupsQuery.data || [])
|
||||
|
||||
const processorMsOpts: MultiSelectOption[] = useMemo(() => {
|
||||
const names = new Set<string>()
|
||||
for (const p of query.data?.processors || []) if (p) names.add(p)
|
||||
for (const c of inputCentersQuery.data || []) if (c.name) names.add(c.name)
|
||||
names.add('본사')
|
||||
return Array.from(names).sort().map((n) => [n, n] as MultiSelectOption)
|
||||
}, [query.data?.processors, inputCentersQuery.data])
|
||||
|
||||
const branchMsOpts: MultiSelectOption[] = useMemo(() => {
|
||||
const names = new Set<string>()
|
||||
for (const b of query.data?.branches || []) if (b) names.add(b)
|
||||
for (const b of branchesQuery.data || []) {
|
||||
const n = b.officeName || b.name
|
||||
if (n) names.add(n)
|
||||
}
|
||||
return Array.from(names).sort().map((n) => [n, n] as MultiSelectOption)
|
||||
}, [query.data?.branches, branchesQuery.data])
|
||||
|
||||
const productMsOpts: MultiSelectOption[] = useMemo(() => {
|
||||
let items = productsQuery.data || []
|
||||
if (filters.productType.length) {
|
||||
items = items.filter((p) => filters.productType.includes(p.type || ''))
|
||||
}
|
||||
const seen = new Set<string>()
|
||||
const opts: MultiSelectOption[] = []
|
||||
for (const p of items) {
|
||||
if (!p.name || seen.has(p.name)) continue
|
||||
seen.add(p.name)
|
||||
opts.push([p.name, p.name])
|
||||
}
|
||||
return opts
|
||||
}, [productsQuery.data, filters.productType])
|
||||
|
||||
const setD = (patch: Partial<Filters>) => setDraft((p) => ({ ...p, ...patch }))
|
||||
const applyF = (patch: Partial<Filters>) => {
|
||||
const next = { ...draft, ...patch, page: 0 }
|
||||
setDraft(next)
|
||||
setFilters(next)
|
||||
@@ -325,7 +448,7 @@ export default function ReceptionListPage() {
|
||||
const downloadExcel = async () => {
|
||||
try {
|
||||
const { data } = await client.get(`${apiPrefix()}/homes/receptions/excel`, {
|
||||
params: { ...filters, page: undefined, size: undefined },
|
||||
params: toSearchParams(filters, { omitPaging: true }),
|
||||
responseType: 'blob',
|
||||
})
|
||||
const url = URL.createObjectURL(data)
|
||||
@@ -405,11 +528,6 @@ export default function ReceptionListPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar order-top-buttons">
|
||||
<Button type="primary" onClick={search}>검색</Button>
|
||||
<Button onClick={reset}>검색 초기화</Button>
|
||||
</div>
|
||||
|
||||
<div className="order-filter">
|
||||
<div className="order-date-row">
|
||||
<DatePicker
|
||||
@@ -447,28 +565,27 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</label>
|
||||
<div className="order-attach-buttons">
|
||||
<Button type="primary" className="order-search-btn" onClick={search}>검색</Button>
|
||||
<Button onClick={reset}>검색 초기화</Button>
|
||||
<Button onClick={downloadExcel}>Excel 다운로드</Button>
|
||||
<Button onClick={downloadExcel}>사은품 Excel 다운로드</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="order-result-row">
|
||||
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
|
||||
</div>
|
||||
|
||||
<div className="order-search-fields">
|
||||
<Select
|
||||
allowClear
|
||||
<MultiSelect
|
||||
className="ff"
|
||||
placeholder="사은품"
|
||||
options={giftTypeOpts}
|
||||
value={draft.giftType}
|
||||
onChange={(v) => setD({ giftType: v })}
|
||||
options={options(query.data?.giftCategories || ['상품권', '가전', '캐시백', '없음'])}
|
||||
/>
|
||||
<input className="ff-i" placeholder="포스트잇" value={draft.postIt} onChange={(e) => setD({ postIt: e.target.value })} />
|
||||
<input className="ff-i" placeholder="약정" value={draft.agreementName} onChange={(e) => setD({ agreementName: e.target.value })} />
|
||||
<input className="ff-i" placeholder="주민번호" value={draft.customerSsid} onChange={(e) => setD({ customerSsid: e.target.value })} />
|
||||
<input className="ff-i" placeholder="이메일" value={draft.customerEmail} onChange={(e) => setD({ customerEmail: e.target.value })} />
|
||||
<input className="ff-i" placeholder="납입자주민" value={draft.paymentTransferSsid} onChange={(e) => setD({ paymentTransferSsid: e.target.value })} />
|
||||
<input className="ff-i" placeholder="납입계좌번호" value={draft.paymentTransferNumber} onChange={(e) => setD({ paymentTransferNumber: e.target.value })} />
|
||||
<label className="ff-d">
|
||||
개통일자
|
||||
<DatePicker
|
||||
@@ -484,9 +601,18 @@ export default function ReceptionListPage() {
|
||||
onChange={(d) => setD({ giftSended: d ? d.format('YYYY-MM-DD') : undefined })}
|
||||
/>
|
||||
</label>
|
||||
<input className="ff-i" placeholder="입력처명" value={draft.centerName} onChange={(e) => setD({ centerName: e.target.value })} />
|
||||
<input className="ff-i" placeholder="영업점명" value={draft.branchName} onChange={(e) => setD({ branchName: e.target.value })} />
|
||||
<MultiSelect
|
||||
className="ff"
|
||||
placeholder="영업점 그룹"
|
||||
options={branchGroupMsOpts}
|
||||
value={draft.branchGroupIds}
|
||||
onChange={(v) => setD({ branchGroupIds: v })}
|
||||
/>
|
||||
<input className="ff-i" placeholder="통신사명" value={draft.ispName} onChange={(e) => setD({ ispName: e.target.value })} />
|
||||
<input className="ff-i" placeholder="상품명" value={draft.productName} onChange={(e) => setD({ productName: e.target.value })} />
|
||||
<span className="order-result muted">검색 결과: 총 {(query.data?.total ?? 0).toLocaleString()} 건</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -523,50 +649,45 @@ export default function ReceptionListPage() {
|
||||
</select>
|
||||
</th>
|
||||
<th className="col-center">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="입력처"
|
||||
value={draft.settleStatus}
|
||||
onChange={(v) => applyF({ settleStatus: v })}
|
||||
options={settleSelectOpts}
|
||||
style={{ width: '100%' }}
|
||||
options={processorMsOpts}
|
||||
value={draft.processors}
|
||||
onChange={(v) => applyF({ processors: v, centerNames: v })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-branch">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="영업점"
|
||||
value={draft.branch}
|
||||
onChange={(v) => applyF({ branch: v })}
|
||||
options={options(query.data?.branches || [])}
|
||||
style={{ width: '100%' }}
|
||||
options={branchMsOpts}
|
||||
value={draft.branches}
|
||||
onChange={(v) => applyF({ branches: v, branchIds: v })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-isp">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="통신사"
|
||||
value={draft.carrier}
|
||||
onChange={(v) => applyF({ carrier: v })}
|
||||
options={options(query.data?.carriers || [])}
|
||||
style={{ width: '100%' }}
|
||||
options={ispMsOpts}
|
||||
value={draft.ispId}
|
||||
onChange={(v) => applyF({ ispId: v, productNames: [], productIds: [] })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-ptype">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="상품종류"
|
||||
options={PRODUCT_TYPE_OPTS}
|
||||
value={draft.productType}
|
||||
onChange={(v) => applyF({ productType: v })}
|
||||
options={options(query.data?.productCategories || ['인터넷', '전화', 'TV', '렌탈'])}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-product"> </th>
|
||||
<th className="col-product">
|
||||
<MultiSelect
|
||||
placeholder="상품"
|
||||
options={productMsOpts}
|
||||
value={draft.productNames}
|
||||
onChange={(v) => applyF({ productNames: v, productIds: v })}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-customer">
|
||||
<input
|
||||
placeholder="고객명"
|
||||
@@ -576,14 +697,11 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</th>
|
||||
<th className="col-region">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="지역"
|
||||
options={sidoMsOpts}
|
||||
value={draft.customerSido}
|
||||
onChange={(v) => applyF({ customerSido: v })}
|
||||
options={options(SIDO_LIST)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-idnum">
|
||||
@@ -595,14 +713,11 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</th>
|
||||
<th className="col-status">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
<MultiSelect
|
||||
placeholder="진행현황"
|
||||
options={progressMsOpts}
|
||||
value={draft.progressStatus}
|
||||
onChange={(v) => applyF({ progressStatus: v })}
|
||||
options={progressSelectOpts}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-contract">
|
||||
@@ -614,33 +729,29 @@ export default function ReceptionListPage() {
|
||||
/>
|
||||
</th>
|
||||
<th className="col-giftplace">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
placeholder="지급처"
|
||||
value={draft.giftPlace}
|
||||
onChange={(v) => applyF({ giftPlace: v })}
|
||||
options={[
|
||||
{ value: 'b01', label: '본사요청' },
|
||||
{ value: 'b02', label: '자체발송' },
|
||||
{ value: '본사', label: '본사' },
|
||||
{ value: '영업점', label: '영업점' },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<select value={draft.giftPlace} onChange={(e) => applyF({ giftPlace: e.target.value })}>
|
||||
<option value="">모두</option>
|
||||
{Object.entries(GIFT_PLACE).map(([code, label]) => (
|
||||
<option key={code} value={code}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
</th>
|
||||
<th className="col-gift">
|
||||
<Select
|
||||
allowClear
|
||||
size="small"
|
||||
placeholder="사은품진행"
|
||||
<MultiSelect
|
||||
placeholder="사은품진행현황"
|
||||
options={giftMsOpts}
|
||||
value={draft.giftStatus}
|
||||
onChange={(v) => applyF({ giftStatus: v })}
|
||||
options={giftSelectOpts}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</th>
|
||||
<th className="col-cstatus"> </th>
|
||||
<th className="col-cstatus">
|
||||
<MultiSelect
|
||||
placeholder="입력처진행현황"
|
||||
options={centerStatusMsOpts}
|
||||
value={draft.centerStatus}
|
||||
onChange={(v) => applyF({ centerStatus: v })}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
+182
-31
@@ -43,14 +43,14 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
text-align: center;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.login-box .login-brand .billcare-logo {
|
||||
.login-box .login-brand .bizcare-logo {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.login-box .login-brand .billcare-logo-mark {
|
||||
.login-box .login-brand .bizcare-logo-mark {
|
||||
height: 64px;
|
||||
width: auto;
|
||||
}
|
||||
.login-box .login-brand .billcare-logo-title {
|
||||
.login-box .login-brand .bizcare-logo-title {
|
||||
font-size: 28px;
|
||||
color: var(--login-text) !important;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.billcare-logo {
|
||||
.bizcare-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -116,39 +116,39 @@ body { margin: 0; background: var(--bg); color: var(--text); }
|
||||
max-width: 100%;
|
||||
background: transparent !important;
|
||||
}
|
||||
.billcare-logo--login {
|
||||
.bizcare-logo--login {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.billcare-logo--header.billcare-logo--dark,
|
||||
.billcare-logo--header.billcare-logo--blue {
|
||||
.bizcare-logo--header.bizcare-logo--dark,
|
||||
.bizcare-logo--header.bizcare-logo--blue {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
img.billcare-logo,
|
||||
.billcare-logo img,
|
||||
.billcare-logo-mark {
|
||||
img.bizcare-logo,
|
||||
.bizcare-logo img,
|
||||
.bizcare-logo-mark {
|
||||
display: block;
|
||||
background: transparent !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
img.billcare-logo--header {
|
||||
img.bizcare-logo--header {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
max-width: min(150px, 100%);
|
||||
object-position: center;
|
||||
}
|
||||
.billcare-logo--header .billcare-logo-mark {
|
||||
.bizcare-logo--header .bizcare-logo-mark {
|
||||
height: 30px;
|
||||
width: auto;
|
||||
}
|
||||
.billcare-logo--login .billcare-logo-mark {
|
||||
.bizcare-logo--login .bizcare-logo-mark {
|
||||
height: 64px;
|
||||
width: auto;
|
||||
}
|
||||
.billcare-logo-title {
|
||||
.bizcare-logo-title {
|
||||
font-family: 'Pretendard', -apple-system, 'Malgun Gothic', sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
@@ -157,14 +157,14 @@ img.billcare-logo--header {
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.billcare-logo--header .billcare-logo-title {
|
||||
.bizcare-logo--header .bizcare-logo-title {
|
||||
font-size: 19px;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
.billcare-logo--login .billcare-logo-title {
|
||||
.bizcare-logo--login .bizcare-logo-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
.billcare-logo--blue .billcare-logo-title {
|
||||
.bizcare-logo--blue .bizcare-logo-title {
|
||||
color: #e8f4ff;
|
||||
}
|
||||
.brand-inline {
|
||||
@@ -177,7 +177,7 @@ img.billcare-logo--header {
|
||||
overflow: hidden;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.brand-inline .billcare-logo {
|
||||
.brand-inline .bizcare-logo {
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -4251,7 +4251,7 @@ html.theme-light .reception-order-page {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.order-result { margin-left: auto; font-size: 12px; }
|
||||
.order-result { margin-left: auto; font-size: 12px; white-space: nowrap; }
|
||||
.order-attach-buttons {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
@@ -4259,15 +4259,10 @@ html.theme-light .reception-order-page {
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.order-result-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.order-result-row .order-result {
|
||||
margin-left: 0;
|
||||
.order-attach-buttons .order-search-btn.ant-btn-primary,
|
||||
.order-attach-buttons .order-search-btn.ant-btn-primary:hover,
|
||||
.order-attach-buttons .order-search-btn.ant-btn-primary:focus {
|
||||
color: #000 !important;
|
||||
}
|
||||
.order-search-fields {
|
||||
display: flex;
|
||||
@@ -4352,12 +4347,168 @@ table.order-list tr.order-col-filter select {
|
||||
width: 100%;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
table.order-list tr.order-col-filter select option {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
table.order-list tr.order-col-filter .ms-toggle {
|
||||
padding: 2px 4px;
|
||||
font-size: 10px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
table.order-list tr.order-col-filter .ms-panel,
|
||||
table.order-list tr.order-col-filter .ms-panel-portal {
|
||||
min-width: 140px;
|
||||
}
|
||||
.order-search-fields .ff .ms-toggle {
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
/* MultiSelect — 테마 토큰 기반 */
|
||||
.ms { position: relative; display: inline-block; width: 100%; min-width: 0; }
|
||||
.ms-toggle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
background: var(--surface) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.ms-toggle:hover {
|
||||
border-color: color-mix(in srgb, var(--primary) 45%, var(--border)) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.ms.is-open .ms-toggle,
|
||||
.ms-toggle:focus-visible {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 25%, transparent) !important;
|
||||
outline: none;
|
||||
}
|
||||
.ms-toggle .ms-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
color: inherit;
|
||||
}
|
||||
.ms-toggle .ms-caret { color: var(--muted); font-size: 10px; flex-shrink: 0; }
|
||||
|
||||
.ms-panel,
|
||||
.ms-panel-portal {
|
||||
background: var(--elevated, var(--surface)) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-lg, var(--shadow)) !important;
|
||||
padding: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in srgb, var(--primary) 45%, var(--muted)) var(--surface-2);
|
||||
}
|
||||
.ms-panel {
|
||||
position: absolute;
|
||||
z-index: 60;
|
||||
top: calc(100% + 2px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ms-panel-portal {
|
||||
position: fixed;
|
||||
z-index: 5000;
|
||||
right: auto;
|
||||
min-width: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar,
|
||||
.ms-panel-portal::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar-track,
|
||||
.ms-panel-portal::-webkit-scrollbar-track {
|
||||
background: var(--surface-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar-thumb,
|
||||
.ms-panel-portal::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in srgb, var(--primary) 40%, var(--muted));
|
||||
border-radius: 4px;
|
||||
border: 2px solid var(--surface-2);
|
||||
}
|
||||
.ms-panel::-webkit-scrollbar-thumb:hover,
|
||||
.ms-panel-portal::-webkit-scrollbar-thumb:hover {
|
||||
background: color-mix(in srgb, var(--primary) 60%, var(--muted));
|
||||
}
|
||||
|
||||
.ms-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 2px 2px 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 4px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--elevated, var(--surface));
|
||||
z-index: 1;
|
||||
}
|
||||
.ms-action {
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
background: transparent !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 3px;
|
||||
color: var(--text) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ms-action:hover {
|
||||
background: color-mix(in srgb, var(--primary) 14%, transparent) !important;
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, var(--border)) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.ms-action-muted { color: var(--muted) !important; }
|
||||
|
||||
.ms-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
.ms-item:hover {
|
||||
background: color-mix(in srgb, var(--primary) 14%, transparent) !important;
|
||||
}
|
||||
.ms-item:has(input:checked) {
|
||||
background: color-mix(in srgb, var(--primary) 18%, transparent);
|
||||
}
|
||||
.ms-item input[type='checkbox'] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
accent-color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
table.order-list tbody tr { cursor: pointer; }
|
||||
table.order-list tbody tr:hover td { background: color-mix(in srgb, var(--primary) 8%, var(--surface)); }
|
||||
table.order-list .c-name { color: var(--c-name); }
|
||||
|
||||
Reference in New Issue
Block a user