feat: [ecard] 분개 모달에 거래처 선택 드롭다운 추가
- TradingPartnerSelect 컴포넌트 추가 (거래처 검색/선택) - CardJournalModal 테이블에 거래처 컬럼 추가 - 분개 라인별 trading_partner_id/name 저장/조회 지원 - EcardController storeJournal/getJournal에 거래처 필드 추가
This commit is contained in:
@@ -1865,6 +1865,8 @@ public function storeJournal(Request $request): JsonResponse
|
||||
'lines.*.account_name' => 'required|string|max:100',
|
||||
'lines.*.debit_amount' => 'required|integer|min:0',
|
||||
'lines.*.credit_amount' => 'required|integer|min:0',
|
||||
'lines.*.trading_partner_id' => 'nullable|integer',
|
||||
'lines.*.trading_partner_name' => 'nullable|string|max:100',
|
||||
'lines.*.description' => 'nullable|string|max:300',
|
||||
]);
|
||||
|
||||
@@ -1926,6 +1928,8 @@ public function storeJournal(Request $request): JsonResponse
|
||||
'account_name' => $line['account_name'],
|
||||
'debit_amount' => $line['debit_amount'],
|
||||
'credit_amount' => $line['credit_amount'],
|
||||
'trading_partner_id' => $line['trading_partner_id'] ?? null,
|
||||
'trading_partner_name' => $line['trading_partner_name'] ?? null,
|
||||
'description' => $line['description'] ?? null,
|
||||
]);
|
||||
}
|
||||
@@ -2003,6 +2007,8 @@ public function getJournal(Request $request): JsonResponse
|
||||
'account_name' => $line->account_name,
|
||||
'debit_amount' => $line->debit_amount,
|
||||
'credit_amount' => $line->credit_amount,
|
||||
'trading_partner_id' => $line->trading_partner_id,
|
||||
'trading_partner_name' => $line->trading_partner_name,
|
||||
'description' => $line->description,
|
||||
]),
|
||||
],
|
||||
|
||||
@@ -556,6 +556,96 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// TradingPartnerSelect - 거래처 드롭다운
|
||||
// ============================================
|
||||
const TradingPartnerSelect = ({ value, valueName, onChange }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [partners, setPartners] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||
const containerRef = useRef(null);
|
||||
const listRef = useRef(null);
|
||||
|
||||
const displayText = valueName || '';
|
||||
|
||||
// 검색어 변경 시 API 조회
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setLoading(true);
|
||||
const url = search
|
||||
? `/finance/journal-entries/trading-partners?search=${encodeURIComponent(search)}`
|
||||
: '/finance/journal-entries/trading-partners';
|
||||
fetch(url)
|
||||
.then(res => res.json())
|
||||
.then(data => { if (data.success) setPartners(data.data || []); })
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [isOpen, search]);
|
||||
|
||||
useEffect(() => { setHighlightIndex(-1); }, [search]);
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
||||
setIsOpen(false); setSearch(''); setHighlightIndex(-1);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleSelect = (partner) => {
|
||||
onChange(partner.id, partner.name);
|
||||
setIsOpen(false); setSearch(''); setHighlightIndex(-1);
|
||||
};
|
||||
const handleClear = (e) => { e.stopPropagation(); onChange(null, ''); setSearch(''); };
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const maxIdx = partners.length - 1;
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); setHighlightIndex(prev => prev < maxIdx ? prev + 1 : 0); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlightIndex(prev => prev > 0 ? prev - 1 : maxIdx); }
|
||||
else if (e.key === 'Enter' && partners.length > 0) { e.preventDefault(); handleSelect(partners[highlightIndex >= 0 ? highlightIndex : 0]); }
|
||||
else if (e.key === 'Escape') { setIsOpen(false); setSearch(''); setHighlightIndex(-1); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<div onClick={() => setIsOpen(!isOpen)}
|
||||
className={`w-full px-3 py-1.5 text-sm border rounded-lg cursor-pointer flex items-center justify-between gap-1 ${isOpen ? 'border-purple-500 ring-1 ring-purple-500' : 'border-stone-200'} bg-white`}>
|
||||
<span className={displayText ? 'text-stone-900 truncate' : 'text-stone-400 text-xs'}>{displayText || '거래처 선택'}</span>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{value && <button onClick={handleClear} className="text-stone-400 hover:text-stone-600">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</button>}
|
||||
<svg className={`w-3 h-3 text-stone-400 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="absolute z-[60] mt-1 w-72 bg-white border border-stone-200 rounded-lg shadow-lg">
|
||||
<div className="p-2 border-b border-stone-100">
|
||||
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)} onKeyDown={handleKeyDown}
|
||||
placeholder="거래처명 또는 사업자번호 검색..." className="w-full px-2.5 py-1.5 text-sm border border-stone-200 rounded-lg focus:ring-1 focus:ring-purple-500 outline-none" autoFocus />
|
||||
</div>
|
||||
<div ref={listRef} className="max-h-48 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="px-3 py-2 text-xs text-stone-400 text-center">검색 중...</div>
|
||||
) : partners.length === 0 ? (
|
||||
<div className="px-3 py-2 text-xs text-stone-400 text-center">검색 결과 없음</div>
|
||||
) : partners.map((p, index) => (
|
||||
<div key={p.id} onClick={() => handleSelect(p)}
|
||||
className={`px-3 py-1.5 text-sm cursor-pointer ${index === highlightIndex ? 'bg-purple-600 text-white font-semibold' : value === p.id ? 'bg-purple-100 text-purple-700' : 'text-stone-700 hover:bg-purple-50'}`}>
|
||||
<span className="font-medium">{p.name}</span>
|
||||
{p.biz_no && <span className={`ml-1 text-xs ${index === highlightIndex ? 'text-purple-100' : 'text-stone-400'}`}>({p.biz_no})</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// CardJournalModal - 복식부기 분개 모달
|
||||
// ============================================
|
||||
@@ -580,14 +670,14 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t
|
||||
|
||||
if (isDeductible) {
|
||||
return [
|
||||
{ dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: supplyAmount, credit_amount: 0, description: '' },
|
||||
{ dc_type: 'debit', account_code: '135', account_name: '부가세대급금', debit_amount: taxAmount, credit_amount: 0, description: '' },
|
||||
{ dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, description: '' },
|
||||
{ dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: supplyAmount, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' },
|
||||
{ dc_type: 'debit', account_code: '135', account_name: '부가세대급금', debit_amount: taxAmount, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' },
|
||||
{ dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, trading_partner_id: null, trading_partner_name: '', description: '' },
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{ dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: totalAmount, credit_amount: 0, description: '' },
|
||||
{ dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, description: '' },
|
||||
{ dc_type: 'debit', account_code: expenseCode, account_name: expenseName, debit_amount: totalAmount, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' },
|
||||
{ dc_type: 'credit', account_code: '205', account_name: '미지급비용', debit_amount: 0, credit_amount: totalAmount, trading_partner_id: null, trading_partner_name: '', description: '' },
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -610,6 +700,8 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t
|
||||
account_name: l.account_name,
|
||||
debit_amount: l.debit_amount,
|
||||
credit_amount: l.credit_amount,
|
||||
trading_partner_id: l.trading_partner_id || null,
|
||||
trading_partner_name: l.trading_partner_name || '',
|
||||
description: l.description || '',
|
||||
})));
|
||||
setIsEditMode(true);
|
||||
@@ -627,6 +719,8 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t
|
||||
account_name: l.account_name,
|
||||
debit_amount: l.debit_amount,
|
||||
credit_amount: l.credit_amount,
|
||||
trading_partner_id: l.trading_partner_id || null,
|
||||
trading_partner_name: l.trading_partner_name || '',
|
||||
description: l.description || '',
|
||||
})));
|
||||
setIsEditMode(true);
|
||||
@@ -651,7 +745,7 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t
|
||||
};
|
||||
|
||||
const addLine = () => {
|
||||
setLines(prev => [...prev, { dc_type: 'debit', account_code: '', account_name: '', debit_amount: 0, credit_amount: 0, description: '' }]);
|
||||
setLines(prev => [...prev, { dc_type: 'debit', account_code: '', account_name: '', debit_amount: 0, credit_amount: 0, trading_partner_id: null, trading_partner_name: '', description: '' }]);
|
||||
};
|
||||
|
||||
const removeLine = (idx) => {
|
||||
@@ -746,8 +840,9 @@ className="flex-1 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 t
|
||||
<tr className="bg-stone-100">
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200 w-16">차/대</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200">계정과목</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200 w-36">차변금액</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200 w-36">대변금액</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200">거래처</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200 w-32">차변금액</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200 w-32">대변금액</th>
|
||||
<th className="px-3 py-2 text-center text-xs font-semibold text-stone-600 border-b border-stone-200 w-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -773,6 +868,15 @@ className={`px-2 py-0.5 rounded text-xs font-medium cursor-pointer hover:opacity
|
||||
accountCodes={accountCodes}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<TradingPartnerSelect
|
||||
value={line.trading_partner_id}
|
||||
valueName={line.trading_partner_name}
|
||||
onChange={(id, name) => {
|
||||
setLines(prev => prev.map((l, i) => i === idx ? { ...l, trading_partner_id: id, trading_partner_name: name } : l));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -804,7 +908,7 @@ className="text-red-400 hover:text-red-600 disabled:opacity-30 disabled:cursor-n
|
||||
))}
|
||||
{/* 합계 */}
|
||||
<tr className={`font-bold ${isBalanced ? 'bg-green-50' : 'bg-red-50'}`}>
|
||||
<td colSpan="2" className="px-3 py-2 text-center text-sm">합계</td>
|
||||
<td colSpan="3" className="px-3 py-2 text-center text-sm">합계</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{formatCurrency(totalDebit)}</td>
|
||||
<td className="px-3 py-2 text-right text-sm">{formatCurrency(totalCredit)}</td>
|
||||
<td></td>
|
||||
|
||||
Reference in New Issue
Block a user