feat:거래처 매출/매입 구분 라디오버튼 추가
- 등록/수정 모달에 매출/매입 라디오버튼 (기본값: 매출) - 통계 카드에 매출/매입 건수 표시 - 필터 바에 매출/매입 필터 버튼 - 테이블에 매출/매입 뱃지 표시 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ public function index(Request $request): JsonResponse
|
||||
return [
|
||||
'id' => $partner->id,
|
||||
'name' => $partner->name,
|
||||
'tradeType' => $partner->trade_type,
|
||||
'type' => $partner->type,
|
||||
'category' => $partner->category,
|
||||
'bizNo' => $partner->biz_no,
|
||||
@@ -66,8 +67,9 @@ public function index(Request $request): JsonResponse
|
||||
$allPartners = TradingPartner::forTenant($tenantId);
|
||||
$stats = [
|
||||
'total' => (clone $allPartners)->count(),
|
||||
'sales' => (clone $allPartners)->where('trade_type', 'sales')->count(),
|
||||
'purchase' => (clone $allPartners)->where('trade_type', 'purchase')->count(),
|
||||
'active' => (clone $allPartners)->where('status', 'active')->count(),
|
||||
'inactive' => (clone $allPartners)->where('status', 'inactive')->count(),
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
@@ -90,6 +92,7 @@ public function store(Request $request): JsonResponse
|
||||
$partner = TradingPartner::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'name' => $request->input('name'),
|
||||
'trade_type' => $request->input('tradeType', 'sales'),
|
||||
'type' => $request->input('type'),
|
||||
'category' => $request->input('category'),
|
||||
'biz_no' => $request->input('bizNo'),
|
||||
@@ -110,6 +113,7 @@ public function store(Request $request): JsonResponse
|
||||
'data' => [
|
||||
'id' => $partner->id,
|
||||
'name' => $partner->name,
|
||||
'tradeType' => $partner->trade_type,
|
||||
'type' => $partner->type,
|
||||
'category' => $partner->category,
|
||||
'bizNo' => $partner->biz_no,
|
||||
@@ -139,6 +143,7 @@ public function update(Request $request, int $id): JsonResponse
|
||||
|
||||
$partner->update([
|
||||
'name' => $request->input('name'),
|
||||
'trade_type' => $request->input('tradeType', $partner->trade_type),
|
||||
'type' => $request->input('type'),
|
||||
'category' => $request->input('category'),
|
||||
'biz_no' => $request->input('bizNo'),
|
||||
@@ -159,6 +164,7 @@ public function update(Request $request, int $id): JsonResponse
|
||||
'data' => [
|
||||
'id' => $partner->id,
|
||||
'name' => $partner->name,
|
||||
'tradeType' => $partner->trade_type,
|
||||
'type' => $partner->type,
|
||||
'category' => $partner->category,
|
||||
'bizNo' => $partner->biz_no,
|
||||
|
||||
@@ -14,6 +14,7 @@ class TradingPartner extends Model
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'trade_type',
|
||||
'type',
|
||||
'category',
|
||||
'biz_no',
|
||||
|
||||
@@ -50,10 +50,11 @@
|
||||
|
||||
function PartnersManagement() {
|
||||
const [partners, setPartners] = useState([]);
|
||||
const [stats, setStats] = useState({ total: 0, active: 0, inactive: 0 });
|
||||
const [stats, setStats] = useState({ total: 0, sales: 0, purchase: 0, active: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterTradeType, setFilterTradeType] = useState('all');
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [filterCategory, setFilterCategory] = useState('all');
|
||||
|
||||
@@ -68,6 +69,7 @@ function PartnersManagement() {
|
||||
|
||||
const initialFormState = {
|
||||
name: '',
|
||||
tradeType: 'sales',
|
||||
type: '',
|
||||
category: '',
|
||||
bizNo: '',
|
||||
@@ -105,9 +107,10 @@ function PartnersManagement() {
|
||||
const matchesSearch = item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(item.ceo || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(item.manager || '').toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesTradeType = filterTradeType === 'all' || item.tradeType === filterTradeType;
|
||||
const matchesType = filterType === 'all' || item.type === filterType;
|
||||
const matchesCategory = filterCategory === 'all' || item.category === filterCategory;
|
||||
return matchesSearch && matchesType && matchesCategory;
|
||||
return matchesSearch && matchesTradeType && matchesType && matchesCategory;
|
||||
});
|
||||
|
||||
const handleAdd = () => { setModalMode('add'); setFormData(initialFormState); setShowModal(true); };
|
||||
@@ -210,8 +213,8 @@ function PartnersManagement() {
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const rows = [['거래처 관리'], [], ['거래처명', '대표자', '업태', '종목', '사업자번호', '주소', '연락처', '이메일', '담당자', '상태'],
|
||||
...filteredPartners.map(item => [item.name, item.ceo || '', item.type || '', item.category || '', item.bizNo, item.address || '', item.contact, item.email, item.manager, item.status === 'active' ? '활성' : '비활성'])];
|
||||
const rows = [['거래처 관리'], [], ['거래구분', '거래처명', '대표자', '업태', '종목', '사업자번호', '주소', '연락처', '이메일', '담당자', '상태'],
|
||||
...filteredPartners.map(item => [item.tradeType === 'purchase' ? '매입' : '매출', item.name, item.ceo || '', item.type || '', item.category || '', item.bizNo, item.address || '', item.contact, item.email, item.manager, item.status === 'active' ? '활성' : '비활성'])];
|
||||
const csvContent = rows.map(row => row.join(',')).join('\n');
|
||||
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = '거래처목록.csv'; link.click();
|
||||
@@ -233,27 +236,36 @@ function PartnersManagement() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-gray-500">총 거래처</span><Building2 className="w-5 h-5 text-gray-400" /></div>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.total}개</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-blue-200 p-6">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-blue-700">매출</span></div>
|
||||
<p className="text-2xl font-bold text-blue-600">{stats.sales}개</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-amber-200 p-6">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-amber-700">매입</span></div>
|
||||
<p className="text-2xl font-bold text-amber-600">{stats.purchase}개</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-emerald-200 p-6">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-emerald-700">활성</span></div>
|
||||
<p className="text-2xl font-bold text-emerald-600">{stats.active}개</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-2"><span className="text-sm text-gray-500">비활성</span></div>
|
||||
<p className="text-2xl font-bold text-gray-900">{stats.inactive}개</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
<div className="md:col-span-2 relative">
|
||||
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input type="text" placeholder="거래처명, 대표자, 담당자 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500" />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{[{v:'all',l:'전체'},{v:'sales',l:'매출'},{v:'purchase',l:'매입'}].map(o => (
|
||||
<button key={o.v} onClick={() => setFilterTradeType(o.v)} className={`flex-1 px-3 py-2 rounded-lg text-sm font-medium ${filterTradeType === o.v ? (o.v === 'sales' ? 'bg-blue-600 text-white' : o.v === 'purchase' ? 'bg-amber-600 text-white' : 'bg-gray-800 text-white') : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}>{o.l}</button>
|
||||
))}
|
||||
</div>
|
||||
<select value={filterType} onChange={(e) => setFilterType(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg"><option value="all">전체 업태</option>{[...new Set(partners.map(p => p.type).filter(Boolean))].map(t => <option key={t} value={t}>{t}</option>)}</select>
|
||||
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg"><option value="all">전체 종목</option>{[...new Set(partners.map(p => p.category).filter(Boolean))].map(c => <option key={c} value={c}>{c}</option>)}</select>
|
||||
</div>
|
||||
@@ -279,7 +291,7 @@ function PartnersManagement() {
|
||||
<tr><td colSpan="7" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
|
||||
) : filteredPartners.map(item => (
|
||||
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => handleEdit(item)}>
|
||||
<td className="px-6 py-4"><p className="text-sm font-medium text-gray-900">{item.name}</p>{item.bizNo && <p className="text-xs text-gray-400">{item.bizNo}</p>}</td>
|
||||
<td className="px-6 py-4"><div className="flex items-center gap-2"><span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${item.tradeType === 'purchase' ? 'bg-amber-100 text-amber-700' : 'bg-blue-100 text-blue-700'}`}>{item.tradeType === 'purchase' ? '매입' : '매출'}</span><p className="text-sm font-medium text-gray-900">{item.name}</p></div>{item.bizNo && <p className="text-xs text-gray-400 ml-9">{item.bizNo}</p>}</td>
|
||||
<td className="px-6 py-4"><span className="text-sm text-gray-900">{item.ceo || '-'}</span></td>
|
||||
<td className="px-6 py-4">{item.type && <span className="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs font-medium">{item.type}</span>}{item.category && <p className="text-xs text-gray-400 mt-1">{item.category}</p>}</td>
|
||||
<td className="px-6 py-4"><p className="text-sm text-gray-600">{item.contact || item.email}</p></td>
|
||||
@@ -325,6 +337,21 @@ className={`mb-4 border-2 border-dashed rounded-xl p-6 text-center cursor-pointe
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">거래 구분 *</label>
|
||||
<div className="flex gap-4">
|
||||
<label className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg border-2 cursor-pointer transition-all ${formData.tradeType === 'sales' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-gray-200 bg-white text-gray-500 hover:border-gray-300'}`}>
|
||||
<input type="radio" name="tradeType" value="sales" checked={formData.tradeType === 'sales'} onChange={(e) => setFormData(prev => ({ ...prev, tradeType: e.target.value }))} className="hidden" />
|
||||
<span className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.tradeType === 'sales' ? 'border-blue-500' : 'border-gray-300'}`}>{formData.tradeType === 'sales' && <span className="w-2 h-2 rounded-full bg-blue-500"></span>}</span>
|
||||
<span className="text-sm font-medium">매출</span>
|
||||
</label>
|
||||
<label className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg border-2 cursor-pointer transition-all ${formData.tradeType === 'purchase' ? 'border-amber-500 bg-amber-50 text-amber-700' : 'border-gray-200 bg-white text-gray-500 hover:border-gray-300'}`}>
|
||||
<input type="radio" name="tradeType" value="purchase" checked={formData.tradeType === 'purchase'} onChange={(e) => setFormData(prev => ({ ...prev, tradeType: e.target.value }))} className="hidden" />
|
||||
<span className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${formData.tradeType === 'purchase' ? 'border-amber-500' : 'border-gray-300'}`}>{formData.tradeType === 'purchase' && <span className="w-2 h-2 rounded-full bg-amber-500"></span>}</span>
|
||||
<span className="text-sm font-medium">매입</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">거래처명 *</label><input type="text" value={formData.name} onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="거래처명" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div><label className="block text-sm font-medium text-gray-700 mb-1">사업자번호</label><input type="text" value={formData.bizNo} onChange={(e) => setFormData(prev => ({ ...prev, bizNo: e.target.value }))} placeholder="123-45-67890" className="w-full px-3 py-2 border border-gray-300 rounded-lg" /></div>
|
||||
|
||||
Reference in New Issue
Block a user