BillCare 초기 소스 등록

비즈케어 홈즈 홈상품 운용관리(BillCare) 프론트/백엔드/Docker 구성을 exdev git에 등록합니다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-07-12 05:05:17 +09:00
commit 03cd93e1f2
160 changed files with 47915 additions and 0 deletions
@@ -0,0 +1,290 @@
import { useEffect, useState } from 'react'
import { Button, Checkbox, DatePicker, Input, Select, message } from 'antd'
import dayjs from 'dayjs'
import Modal from './DraggableModal'
import { FormField, FormGrid, FormSection } from './OrderFormLayout'
import client, { apiPrefix, unwrap, type ApiResponse } from '../api/client'
import { options } from '../pages/care/homesShared'
type Props = {
open: boolean
selectedIds: number[]
progressStatuses: string[]
giftStatuses: string[]
centerOptions?: string[]
onCancel: () => void
onSuccess: () => void
}
const GIFT_PLACES = [
{ value: 'b01', label: '본사요청' },
{ value: 'b02', label: '자체발송' },
{ value: '본사', label: '본사발송' },
{ value: '영업점', label: '영업점발송' },
]
export default function ReceptionBulkModal({
open,
selectedIds,
progressStatuses,
giftStatuses,
centerOptions = [],
onCancel,
onSuccess,
}: Props) {
const [busy, setBusy] = useState(false)
const [pgAdminOnly, setPgAdminOnly] = useState(false)
const [pgStatus, setPgStatus] = useState<string | undefined>()
const [pgNotNotice, setPgNotNotice] = useState(false)
const [pgCenterId, setPgCenterId] = useState<string | undefined>()
const [pgMemo, setPgMemo] = useState('')
const [pgOpended, setPgOpended] = useState('')
const [pgCanceled, setPgCanceled] = useState('')
const [gPlace, setGPlace] = useState<string | undefined>()
const [gStatus, setGStatus] = useState<string | undefined>()
const [gProgressLog, setGProgressLog] = useState('')
const [gDeliveryCompany, setGDeliveryCompany] = useState('')
const [gTrackingNumber, setGTrackingNumber] = useState('')
const [gRequested, setGRequested] = useState('')
const [gSended, setGSended] = useState('')
useEffect(() => {
if (!open) return
setPgAdminOnly(false)
setPgStatus(undefined)
setPgNotNotice(false)
setPgCenterId(undefined)
setPgMemo('')
setPgOpended('')
setPgCanceled('')
setGPlace(undefined)
setGStatus(undefined)
setGProgressLog('')
setGDeliveryCompany('')
setGTrackingNumber('')
setGRequested('')
setGSended('')
}, [open])
const need = () => {
if (!selectedIds.length) {
message.warning('대상 접수를 선택하세요.')
return false
}
return true
}
const run = async (fn: () => Promise<void>) => {
setBusy(true)
try {
await fn()
onSuccess()
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '일괄 처리에 실패했습니다.')
} finally {
setBusy(false)
}
}
const runProgress = () => {
if (!need()) return
if (!pgStatus) {
message.warning('진행상황을 선택하세요.')
return
}
if (!window.confirm(`선택 ${selectedIds.length}건을 일괄 처리하시겠습니까?`)) return
void run(async () => {
const { data } = await client.post<ApiResponse<{ count: number }>>(`${apiPrefix()}/homes/receptions/bulk-progress`, {
ids: selectedIds,
status: pgStatus,
memo: pgMemo,
centerId: pgCenterId || null,
opended: pgOpended || null,
canceled: pgCanceled || null,
adminOnly: pgAdminOnly,
notNotice: pgNotNotice,
})
const res = unwrap(data) as { count: number }
message.success(`${res.count}건 진행상황을 변경했습니다.`)
})
}
const runGift = () => {
if (!need()) return
if (!gStatus) {
message.warning('사은품진행상황을 선택하세요.')
return
}
if (!window.confirm(`선택 ${selectedIds.length}건을 일괄 처리하시겠습니까?`)) return
void run(async () => {
const { data } = await client.post<ApiResponse<{ count: number }>>(`${apiPrefix()}/homes/receptions/bulk-gift`, {
ids: selectedIds,
place: gPlace || null,
status: gStatus,
giftProgressLog: gProgressLog,
deliveryCompany: gDeliveryCompany || null,
trackingNumber: gTrackingNumber || null,
requested: gRequested || null,
sended: gSended || null,
})
const res = unwrap(data) as { count: number }
message.success(`${res.count}건 사은품진행을 변경했습니다.`)
})
}
const runCenterWait = () => {
if (!need()) return
if (!window.confirm('입력처 정산대기로 일괄처리 하시겠습니까?')) return
void run(async () => {
const { data } = await client.post<ApiResponse<{ count: number }>>(`${apiPrefix()}/homes/receptions/bulk-center-progress`, {
ids: selectedIds,
})
const res = unwrap(data) as { count: number }
message.success(`${res.count}건을 입력처 정산대기로 변경했습니다.`)
})
}
return (
<Modal
title={`일괄진행 (${selectedIds.length}건 선택)`}
open={open}
onCancel={onCancel}
footer={null}
destroyOnHidden
width="96%"
style={{ maxWidth: 1040 }}
className="reception-bulk-modal reception-apply-modal order-form-modal"
styles={{ body: { maxHeight: 'calc(92vh - 56px)', overflowY: 'auto' } }}
>
<div className="order-form-embedded-inner order-form-page reception-apply-form reception-bulk-form">
<FormSection title="진행상황 일괄 처리">
<FormGrid>
<FormField label="관리자 전용로그" full>
<div className="order-form-inline">
<Checkbox checked={pgAdminOnly} onChange={(e) => setPgAdminOnly(e.target.checked)}>
관리자 전용로그
</Checkbox>
<span className="bulk-hint">■ 관리자만 보이고 영업점은 보이지 않습니다.</span>
</div>
</FormField>
<FormField label="진행상황" full>
<div className="order-form-inline">
<Select
allowClear
placeholder="진행상황 선택"
value={pgStatus}
onChange={(v) => setPgStatus(v)}
options={options(progressStatuses)}
style={{ flex: 1, minWidth: 180 }}
/>
<Checkbox checked={pgNotNotice} onChange={(e) => setPgNotNotice(e.target.checked)}>
하부점에 전달하지 않음
</Checkbox>
</div>
<p className="bulk-hint-line">■ 퀵전송으로 등록된 접수인 경우 진행상황을 하부점에 알리지 않습니다.</p>
<p className="bulk-hint-line">■ 진행상황을 1개 선택후 검색하여야 진행상황을 변경할 수 있습니다.</p>
</FormField>
<FormField label="입력처" full>
<Select
allowClear
placeholder="입력처 선택"
value={pgCenterId}
onChange={(v) => setPgCenterId(v)}
options={options(centerOptions.length ? centerOptions : ['본사'])}
/>
</FormField>
<FormField label="업무진행메모(LOG)" full>
<Input.TextArea rows={3} value={pgMemo} onChange={(e) => setPgMemo(e.target.value)} />
</FormField>
<FormField label="개통일자">
<DatePicker
value={pgOpended ? dayjs(pgOpended) : null}
onChange={(d) => setPgOpended(d ? d.format('YYYY-MM-DD') : '')}
style={{ width: '100%' }}
/>
</FormField>
<FormField label="해지일자">
<DatePicker
value={pgCanceled ? dayjs(pgCanceled) : null}
onChange={(d) => setPgCanceled(d ? d.format('YYYY-MM-DD') : '')}
style={{ width: '100%' }}
/>
</FormField>
</FormGrid>
<div className="bulk-foot">
<button type="button" className="bulk-submit" disabled={busy} onClick={runProgress}>선택 일괄 처리</button>
</div>
</FormSection>
<FormSection title="본사발송 사은품 일괄 처리">
<FormGrid>
<FormField label="사은품 지급처">
<Select
allowClear
placeholder="지급처 선택"
value={gPlace}
onChange={(v) => setGPlace(v)}
options={GIFT_PLACES}
/>
<p className="bulk-hint-line">■ 지급처를 검색하세요.</p>
</FormField>
<FormField label="사은품진행상황">
<Select
allowClear
placeholder="사은품 진행상황 선택"
value={gStatus}
onChange={(v) => setGStatus(v)}
options={options(giftStatuses)}
/>
</FormField>
<FormField label="사은품진행메모(LOG)" full>
<Input.TextArea rows={3} value={gProgressLog} onChange={(e) => setGProgressLog(e.target.value)} />
</FormField>
<FormField label="택배회사">
<Input value={gDeliveryCompany} onChange={(e) => setGDeliveryCompany(e.target.value)} />
</FormField>
<FormField label="송장번호">
<Input value={gTrackingNumber} onChange={(e) => setGTrackingNumber(e.target.value)} />
</FormField>
<FormField label="사은품지급요청일">
<DatePicker
value={gRequested ? dayjs(gRequested) : null}
onChange={(d) => setGRequested(d ? d.format('YYYY-MM-DD') : '')}
style={{ width: '100%' }}
/>
</FormField>
<FormField label="사은품지급완료일">
<DatePicker
value={gSended ? dayjs(gSended) : null}
onChange={(d) => setGSended(d ? d.format('YYYY-MM-DD') : '')}
style={{ width: '100%' }}
/>
</FormField>
</FormGrid>
<div className="bulk-foot">
<button type="button" className="bulk-submit" disabled={busy} onClick={runGift}>선택 일괄 처리</button>
</div>
</FormSection>
<FormSection title="입력처 - 정산대기로 일괄처리">
<FormGrid cols={1}>
<FormField label="안내" full>
<p className="bulk-desc-block" style={{ padding: 0, margin: 0 }}>
■ 개통완료된 고객중에 입력처 정산이 진행안된 고객은 입력처 정산대기로 일괄처리 진행할수 있습니다.
</p>
</FormField>
</FormGrid>
<div className="bulk-foot">
<button type="button" className="bulk-submit" disabled={busy} onClick={runCenterWait}>선택 일괄 처리</button>
</div>
</FormSection>
</div>
<div className="balance-modal-footer" style={{ marginTop: 12 }}>
<Button onClick={onCancel}>닫기</Button>
</div>
</Modal>
)
}