계약정보 관리 시스템에서 가입비와 구독료를 분리하고, 영업 수당 산정 로직을 가입비의 20%로 고정하는 작업을 완료
This commit is contained in:
@@ -325,16 +325,16 @@ try {
|
||||
|
||||
$product_name = $data['product_name'] ?? '';
|
||||
$contract_amount = $data['contract_amount'] ?? 0;
|
||||
$commission_rate = $data['commission_rate'] ?? 0;
|
||||
$subscription_fee = $data['subscription_fee'] ?? 0;
|
||||
$contract_date = $data['contract_date'] ?? date('Y-m-d');
|
||||
$sub_models = isset($data['sub_models']) ? json_encode($data['sub_models']) : null;
|
||||
|
||||
if (!$tenant_id || !$product_name) throw new Exception("필수 정보가 누락되었습니다.");
|
||||
|
||||
$commission_amount = $contract_amount; // 매니저 수익은 1개월치 구독료 전액 (100%)
|
||||
$commission_amount = $contract_amount * 0.20; // 가입비의 20% 수당
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO sales_tenant_products (tenant_id, product_name, contract_amount, commission_rate, commission_amount, contract_date, sub_models) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$tenant_id, $product_name, $contract_amount, 100, $commission_amount, $contract_date, $sub_models]);
|
||||
$stmt = $pdo->prepare("INSERT INTO sales_tenant_products (tenant_id, product_name, contract_amount, subscription_fee, commission_rate, commission_amount, contract_date, sub_models) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$tenant_id, $product_name, $contract_amount, $subscription_fee, 20, $commission_amount, $contract_date, $sub_models]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '상품 계약 정보가 등록되었습니다.']);
|
||||
|
||||
@@ -636,11 +636,12 @@ try {
|
||||
$commission_rate = $data['commission_rate'] ?? 0;
|
||||
$contract_date = $data['contract_date'] ?? date('Y-m-d');
|
||||
$sub_models = isset($data['sub_models']) ? json_encode($data['sub_models']) : null;
|
||||
$subscription_fee = $data['subscription_fee'] ?? 0;
|
||||
|
||||
$commission_amount = $contract_amount; // 매니저 수익은 1개월치 구독료 전액 (100%)
|
||||
$commission_amount = $contract_amount * 0.20; // 가입비의 20% 수당
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE sales_tenant_products SET product_name = ?, contract_amount = ?, commission_rate = ?, commission_amount = ?, contract_date = ?, sub_models = ? WHERE id = ?");
|
||||
$stmt->execute([$product_name, $contract_amount, 100, $commission_amount, $contract_date, $sub_models, $product_id]);
|
||||
$stmt = $pdo->prepare("UPDATE sales_tenant_products SET product_name = ?, contract_amount = ?, subscription_fee = ?, commission_rate = ?, commission_amount = ?, contract_date = ?, sub_models = ? WHERE id = ?");
|
||||
$stmt->execute([$product_name, $contract_amount, $subscription_fee, 20, $commission_amount, $contract_date, $sub_models, $product_id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '계약 정보가 수정되었습니다.']);
|
||||
|
||||
|
||||
@@ -1078,23 +1078,25 @@
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-100 text-slate-600 font-bold border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="px-4 py-2">상품명</th>
|
||||
<th className="px-4 py-2 text-right">계약금액</th>
|
||||
<th className="px-4 py-2 text-center">가입 승인</th>
|
||||
<th className="px-4 py-2 text-center">지급 승인</th>
|
||||
<th className="px-4 py-2 text-right">정산 지급액</th>
|
||||
<th className="px-4 py-2 text-center">계약일</th>
|
||||
<th className="px-4 py-2">상품명</th>
|
||||
<th className="px-4 py-2 text-right">가입비</th>
|
||||
<th className="px-4 py-2 text-right text-slate-400">월 구독료</th>
|
||||
<th className="px-4 py-2 text-center">가입 승인</th>
|
||||
<th className="px-4 py-2 text-center">지급 승인</th>
|
||||
<th className="px-4 py-2 text-right">정산 지급액</th>
|
||||
<th className="px-4 py-2 text-center">계약일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{!tenantProducts[t.id] ? (
|
||||
<tr><td colSpan="6" className="px-4 py-8 text-center text-slate-400">로딩 중...</td></tr>
|
||||
) : tenantProducts[t.id].length === 0 ? (
|
||||
<tr><td colSpan="6" className="px-4 py-8 text-center text-slate-400">등록된 계약이 없습니다.</td></tr>
|
||||
{!tenantProducts[t.id] ? (
|
||||
<tr><td colSpan="7" className="px-4 py-8 text-center text-slate-400">로딩 중...</td></tr>
|
||||
) : tenantProducts[t.id].length === 0 ? (
|
||||
<tr><td colSpan="7" className="px-4 py-8 text-center text-slate-400">등록된 계약이 없습니다.</td></tr>
|
||||
) : tenantProducts[t.id].map(p => (
|
||||
<tr key={p.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-4 py-3 font-medium text-slate-800">{p.product_name}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-600 font-mono">{formatCurrency(p.contract_amount)}</td>
|
||||
<tr key={p.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-4 py-3 font-medium text-slate-800">{p.product_name}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-600 font-mono">{formatCurrency(p.contract_amount)}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-400 font-mono italic">{formatCurrency(p.subscription_fee || 0)}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<button
|
||||
onClick={() => handleConfirmProduct(p.id, 'join_approved', p.join_approved == 1, t.id)}
|
||||
@@ -3056,7 +3058,7 @@
|
||||
sales_manager_id: currentUser ? currentUser.id : ''
|
||||
});
|
||||
const [productFormData, setProductFormData] = useState({
|
||||
product_name: '', contract_amount: '', commission_rate: '100', contract_date: new Date().toISOString().split('T')[0]
|
||||
product_name: '', contract_amount: '', subscription_fee: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
|
||||
const fillRandomTenantData = () => {
|
||||
@@ -3104,6 +3106,7 @@
|
||||
...productFormData,
|
||||
product_name: `선택모델(${selectedIds.length}종)`,
|
||||
contract_amount: total,
|
||||
subscription_fee: 0, // 모델 선택 시 구독료는 일단 0으로 (필요시 수동입력)
|
||||
commission_rate: '20',
|
||||
contract_date: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
@@ -3314,7 +3317,7 @@
|
||||
alert(editProductId ? '계약 정보가 수정되었습니다.' : '계약 정보가 등록되었습니다.');
|
||||
setIsProductModalOpen(false);
|
||||
setEditProductId(null);
|
||||
setProductFormData({ product_name: '', contract_amount: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0] });
|
||||
setProductFormData({ product_name: '', contract_amount: '', subscription_fee: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0] });
|
||||
setSelectedCategory(null);
|
||||
setSelectedSubModels([]);
|
||||
fetchData();
|
||||
@@ -3356,6 +3359,7 @@
|
||||
setProductFormData({
|
||||
product_name: product.product_name,
|
||||
contract_amount: product.contract_amount,
|
||||
subscription_fee: product.subscription_fee || 0,
|
||||
commission_rate: product.commission_rate,
|
||||
contract_date: product.contract_date
|
||||
});
|
||||
@@ -3395,21 +3399,21 @@
|
||||
/>
|
||||
{currentRole !== '매니저' && (
|
||||
<StatCard
|
||||
title="총 매출액"
|
||||
title="총 가입비 실적"
|
||||
value={formatCurrency(stats.total_revenue)}
|
||||
subtext="전체 계약 금액 합계"
|
||||
subtext="전체 가입비 합계"
|
||||
icon={<LucideIcon name="bar-chart-3" className="w-5 h-5" />}
|
||||
/>
|
||||
)}
|
||||
<StatCard
|
||||
title={currentRole === '매니저' ? "지급예정 구독료" : "누적 예상 수익"}
|
||||
title={currentRole === '매니저' ? "지급예정 구독료" : "누적 가입비 수당"}
|
||||
value={formatCurrency(stats.total_commission)}
|
||||
subtext={currentRole === '매니저' ? "전체 계약 건 예상 구독료" : "전체 수수료 합계"}
|
||||
subtext={currentRole === '매니저' ? "전체 계약 건 예상 구독료" : "전체 가입비 수당 합계"}
|
||||
icon={<LucideIcon name="coins" className="w-5 h-5" />}
|
||||
/>
|
||||
<div className="bg-gradient-to-br from-emerald-50 to-teal-50 rounded-card p-6 shadow-sm border border-emerald-200">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-emerald-700">{currentRole === '매니저' ? "지급완료 구독액" : "확정 수익 (지급대상)"}</h3>
|
||||
<h3 className="text-sm font-medium text-emerald-700">{currentRole === '매니저' ? "지급완료 구독액" : "확정 가입비 수당 (지급대상)"}</h3>
|
||||
<div className="p-2 bg-emerald-100 rounded-lg text-emerald-600">
|
||||
<LucideIcon name="check-circle" className="w-5 h-5" />
|
||||
</div>
|
||||
@@ -3624,7 +3628,8 @@
|
||||
<thead className="bg-slate-100 text-slate-600 font-bold border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="px-4 py-2">상품명</th>
|
||||
<th className="px-4 py-2 text-right">계약금액</th>
|
||||
<th className="px-4 py-2 text-right">가입비</th>
|
||||
<th className="px-4 py-2 text-right text-slate-500">월 구독료</th>
|
||||
<th className="px-4 py-2 text-center">수익기준</th>
|
||||
<th className="px-4 py-2 text-right text-blue-600">내 수익</th>
|
||||
<th className="px-4 py-2 text-center">계약일</th>
|
||||
@@ -3635,14 +3640,15 @@
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{!tenantProducts[t.id] ? (
|
||||
<tr><td colSpan="7" className="px-4 py-8 text-center text-slate-400">로딩 중...</td></tr>
|
||||
<tr><td colSpan="9" className="px-4 py-8 text-center text-slate-400">로딩 중...</td></tr>
|
||||
) : tenantProducts[t.id].length === 0 ? (
|
||||
<tr><td colSpan="7" className="px-4 py-8 text-center text-slate-400">등록된 계약 정보가 없습니다.</td></tr>
|
||||
<tr><td colSpan="9" className="px-4 py-8 text-center text-slate-400">등록된 계약 정보가 없습니다.</td></tr>
|
||||
) : tenantProducts[t.id].map(p => (
|
||||
<tr key={p.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="px-4 py-3 font-medium text-slate-800">{p.product_name}</td>
|
||||
<td className="px-4 py-3 text-right text-slate-600 font-mono">{formatCurrency(p.contract_amount)}</td>
|
||||
<td className="px-4 py-3 text-center text-slate-500">1개월분</td>
|
||||
<td className="px-4 py-3 text-right text-slate-400 font-mono italic">{formatCurrency(p.subscription_fee || 0)}</td>
|
||||
<td className="px-4 py-3 text-center text-slate-500">가입비 ({Number(p.commission_rate)}%)</td>
|
||||
<td className="px-4 py-3 text-right font-bold text-blue-600 font-mono">{formatCurrency(p.commission_amount)}</td>
|
||||
<td className="px-4 py-3 text-center text-slate-400">{p.contract_date}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
@@ -3817,7 +3823,7 @@
|
||||
<button type="button" onClick={() => {
|
||||
setIsProductModalOpen(false);
|
||||
setEditProductId(null);
|
||||
setProductFormData({ product_name: '', contract_amount: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0] });
|
||||
setProductFormData({ product_name: '', contract_amount: '', subscription_fee: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0] });
|
||||
}} className="p-2 hover:bg-slate-200 rounded-full transition-colors">
|
||||
<LucideIcon name="x" className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
@@ -3846,7 +3852,8 @@
|
||||
...productFormData,
|
||||
product_name: pkg.name,
|
||||
contract_amount: dbPrice.join_fee,
|
||||
commission_rate: '100' // 1개월치 전액
|
||||
subscription_fee: dbPrice.subscription_fee,
|
||||
commission_rate: '20' // 가입비의 20%
|
||||
});
|
||||
setSelectedSubModels([]);
|
||||
} else {
|
||||
@@ -3854,7 +3861,8 @@
|
||||
...productFormData,
|
||||
product_name: '선택모델 하이브리드',
|
||||
contract_amount: 0,
|
||||
commission_rate: '100'
|
||||
subscription_fee: 0,
|
||||
commission_rate: '20'
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -3906,7 +3914,8 @@
|
||||
setProductFormData({
|
||||
...productFormData,
|
||||
product_name: newSubModels.length > 0 ? `선택모델(${newSubModels.length}종)` : '',
|
||||
contract_amount: total
|
||||
contract_amount: total,
|
||||
subscription_fee: 0
|
||||
});
|
||||
}}
|
||||
className="w-4 h-4 text-indigo-600 rounded"
|
||||
@@ -3922,34 +3931,47 @@
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 border-t border-slate-100 pt-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">상품명 (자동설정)</label>
|
||||
<input type="text" required value={productFormData.product_name} onChange={e => setProductFormData({...productFormData, product_name: e.target.value})} className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50" placeholder="위에서 상품을 선택하세요" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">총 계약금액 (자동설정)</label>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">가입비 (자동설정)</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={productFormData.contract_amount ? Number(productFormData.contract_amount).toLocaleString() : ''}
|
||||
onChange={e => setProductFormData({...productFormData, contract_amount: e.target.value.replace(/[^0-9]/g, '')})}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50 font-bold text-blue-600 text-right"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">월 구독료</label>
|
||||
<input
|
||||
type="text"
|
||||
value={productFormData.subscription_fee ? Number(productFormData.subscription_fee).toLocaleString() : ''}
|
||||
onChange={e => setProductFormData({...productFormData, subscription_fee: e.target.value.replace(/[^0-9]/g, '')})}
|
||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 font-bold text-slate-700 text-right"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">수익 기준</label>
|
||||
<div className="w-full px-3 py-2 bg-slate-100 border border-slate-200 rounded-lg text-slate-500 text-sm font-medium">1개월 구독료 (100%)</div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">상품명 (자동설정)</label>
|
||||
<input type="text" required value={productFormData.product_name} onChange={e => setProductFormData({...productFormData, product_name: e.target.value})} className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 bg-slate-50" placeholder="위에서 상품을 선택하세요" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">계약일</label>
|
||||
<input type="date" value={productFormData.contract_date} onChange={e => setProductFormData({...productFormData, contract_date: e.target.value})} className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-amber-50 rounded-lg border border-amber-100 mb-2">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs font-bold text-amber-700">수익 기준:</span>
|
||||
<span className="text-xs font-bold text-amber-900">가입비의 20%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-indigo-50 rounded-lg border border-indigo-100 flex justify-between items-center">
|
||||
<span className="text-xs font-bold text-indigo-700">예상 내 수익:</span>
|
||||
<span className="text-lg font-black text-indigo-900">
|
||||
{formatCurrency((productFormData.contract_amount || 0))}
|
||||
{formatCurrency((productFormData.contract_amount || 0) * 0.20)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user