137 lines
5.7 KiB
TypeScript
137 lines
5.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { X } from 'lucide-react'
|
|
import { Modal } from '@/components'
|
|
import { useMeQuery } from '@/apis'
|
|
import type { SessionField } from '@/apis/auth/auth.type'
|
|
import type { ListItem } from '../types'
|
|
|
|
export interface ExtraInfoPopupProps {
|
|
target: ListItem
|
|
onClose: () => void
|
|
/** 부가정보 저장 (custom = {key: value}) */
|
|
onSubmit: (custom: Record<string, unknown>) => void
|
|
}
|
|
|
|
// 협상완료 부가정보 입력 팝업. 필드 정의(session_fields)는 회사 설정(/me)에서 오고, 기존 입력값(target.custom)으로 프리필.
|
|
export function ExtraInfoPopup({ target, onClose, onSubmit }: ExtraInfoPopupProps) {
|
|
const { data: user } = useMeQuery()
|
|
const fields: SessionField[] = user?.sessionFields ?? []
|
|
|
|
const [values, setValues] = useState<Record<string, unknown>>(() => {
|
|
const init: Record<string, unknown> = {}
|
|
for (const f of fields) init[f.key] = target.custom?.[f.key] ?? (f.type === 'boolean' ? false : '')
|
|
return init
|
|
})
|
|
const [opinion, setOpinion] = useState<string>(() => String(target.custom?.opinion ?? ''))
|
|
|
|
const set = (key: string, value: unknown) => setValues((v) => ({ ...v, [key]: value }))
|
|
|
|
const handleSubmit = () => {
|
|
// 빈 값은 제외하고 저장(부분 입력 허용)
|
|
const out: Record<string, unknown> = {}
|
|
for (const f of fields) {
|
|
const v = values[f.key]
|
|
if (f.type === 'boolean') out[f.key] = !!v
|
|
else if (v !== '' && v != null) out[f.key] = f.type === 'number' ? Number(v) : v
|
|
}
|
|
if (opinion.trim()) out.opinion = opinion.trim()
|
|
onSubmit(out)
|
|
onClose()
|
|
}
|
|
|
|
return (
|
|
<Modal onClose={onClose}>
|
|
<div className="w-full max-w-md overflow-hidden rounded-2xl border border-border bg-white shadow-xl animate-scale-in">
|
|
<div className="flex items-center justify-between border-b border-border p-5">
|
|
<div>
|
|
<h3 className="text-base font-bold text-neutral-90">협상완료 부가정보</h3>
|
|
<p className="mt-0.5 text-xs text-neutral-60">{target.qt_number} · {target.item_name}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="닫기"
|
|
className="flex size-8 items-center justify-center rounded-full text-neutral-60 hover:bg-neutral-10"
|
|
>
|
|
<X className="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4 p-5">
|
|
{fields.map((f) => (
|
|
<div key={f.key} className="space-y-1.5">
|
|
<label className="block text-sm font-semibold text-neutral-80">{f.label}</label>
|
|
{f.type === 'boolean' ? (
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={!!values[f.key]}
|
|
onClick={() => set(f.key, !values[f.key])}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
values[f.key] ? 'bg-brand-600' : 'bg-neutral-30'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block size-5 transform rounded-full bg-white shadow transition-transform ${
|
|
values[f.key] ? 'translate-x-5' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
) : f.type === 'select' ? (
|
|
<select
|
|
value={String(values[f.key] ?? '')}
|
|
onChange={(e) => set(f.key, e.target.value)}
|
|
className="h-11 w-full rounded-xl border border-border bg-white px-3 text-sm outline-none focus:border-brand-600 focus:ring-1 focus:ring-brand-600"
|
|
>
|
|
<option value="">선택</option>
|
|
{(f.options ?? []).map((o) => (
|
|
<option key={o} value={o}>{o}</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<input
|
|
type={f.type === 'number' ? 'number' : 'text'}
|
|
value={String(values[f.key] ?? '')}
|
|
onChange={(e) => set(f.key, e.target.value)}
|
|
className="h-11 w-full rounded-xl border border-border bg-white px-3 text-sm outline-none focus:border-brand-600 focus:ring-1 focus:ring-brand-600"
|
|
placeholder={f.label}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="space-y-1.5">
|
|
<label className="block text-sm font-semibold text-neutral-80">
|
|
의견 <span className="font-normal text-neutral-50">(선택)</span>
|
|
</label>
|
|
<textarea
|
|
value={opinion}
|
|
onChange={(e) => setOpinion(e.target.value)}
|
|
rows={2}
|
|
maxLength={255}
|
|
placeholder="추가로 남길 의견"
|
|
className="w-full resize-none rounded-xl border border-border bg-white px-3 py-2 text-sm outline-none focus:border-brand-600 focus:ring-1 focus:ring-brand-600"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2 border-t border-border p-5">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="h-11 flex-1 rounded-xl border border-border text-sm font-bold text-neutral-70 hover:bg-neutral-10"
|
|
>
|
|
취소
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSubmit}
|
|
className="h-11 flex-1 rounded-xl bg-brand-600 text-sm font-bold text-white hover:bg-brand-700 disabled:opacity-40"
|
|
>
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|