영업 승인 로직 및 수당 계산 방식에 대한 업데이트
This commit is contained in:
@@ -237,7 +237,7 @@ try {
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
COALESCE(SUM(contract_amount), 0) as total_sales,
|
||||
COALESCE(SUM(commission_amount), 0) / 1.0 as total_comm, -- 매니저 수당
|
||||
COALESCE(SUM(payout_amount), 0) as total_comm, -- 정산 지급액 합계
|
||||
COUNT(*) as total_count
|
||||
FROM sales_tenant_products
|
||||
");
|
||||
@@ -246,18 +246,16 @@ try {
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
COALESCE(SUM(amount), 0) as total_sales,
|
||||
COALESCE(SUM(amount) * 0.25, 0) as total_comm, -- 영업수당(20%) + 관리수당(5%) 추정치
|
||||
COALESCE(SUM(amount) * 0.25, 0) as total_comm, -- 영업수당(20%) + 관리수당(5%)
|
||||
COUNT(*) as total_count
|
||||
FROM sales_record
|
||||
WHERE status = 'completed'
|
||||
");
|
||||
$srData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// 영업수당 계산 (sales_tenant_products 기준): 모든 건에 대해 최소 25% (영업 20 + 관리 5) 지급된다고 가정하거나
|
||||
// 혹은 DB에 기록된 실제 지급액이 있다면 그것을 사용해야 함. 여기서는 단순 합산.
|
||||
$totalStats = [
|
||||
'totalSales' => $tpData['total_sales'] + $srData['total_sales'],
|
||||
'totalCommission' => $tpData['total_comm'] + ($tpData['total_sales'] * 0.25) + $srData['total_comm'],
|
||||
'totalCommission' => $tpData['total_comm'] + $srData['total_comm'],
|
||||
'totalCount' => $tpData['total_count'] + $srData['total_count']
|
||||
];
|
||||
} else {
|
||||
|
||||
@@ -150,8 +150,23 @@ try {
|
||||
if (!in_array('operator_confirmed', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `operator_confirmed` tinyint(1) DEFAULT 0 AFTER `contract_date` ");
|
||||
}
|
||||
if (!in_array('join_approved', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `join_approved` tinyint(1) DEFAULT 0 AFTER `operator_confirmed` ");
|
||||
if (in_array('operator_confirmed', $prodCols)) {
|
||||
$pdo->exec("UPDATE `sales_tenant_products` SET `join_approved` = `operator_confirmed` ");
|
||||
}
|
||||
}
|
||||
if (!in_array('payment_approved', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `payment_approved` tinyint(1) DEFAULT 0 AFTER `join_approved` ");
|
||||
}
|
||||
if (!in_array('payout_rate', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `payout_rate` float DEFAULT 0 AFTER `payment_approved` ");
|
||||
}
|
||||
if (!in_array('payout_amount', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `payout_amount` decimal(15,2) DEFAULT 0.00 AFTER `payout_rate` ");
|
||||
}
|
||||
if (!in_array('sub_models', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `sub_models` text DEFAULT NULL AFTER `operator_confirmed` ");
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `sub_models` text DEFAULT NULL AFTER `payout_amount` ");
|
||||
}
|
||||
if (!in_array('commission_amount', $prodCols)) {
|
||||
$pdo->exec("ALTER TABLE `sales_tenant_products` ADD COLUMN `commission_amount` decimal(15,2) DEFAULT 0.00 AFTER `commission_rate` ");
|
||||
@@ -219,7 +234,7 @@ try {
|
||||
COUNT(DISTINCT t.id) as tenant_count,
|
||||
SUM(p.contract_amount) as total_revenue,
|
||||
SUM(p.commission_amount) as total_commission,
|
||||
SUM(CASE WHEN p.operator_confirmed = 1 THEN p.commission_amount ELSE 0 END) as confirmed_commission
|
||||
SUM(CASE WHEN p.payment_approved = 1 THEN p.commission_amount ELSE 0 END) as confirmed_commission
|
||||
FROM sales_tenants t
|
||||
LEFT JOIN sales_tenant_products p ON t.id = p.tenant_id
|
||||
";
|
||||
@@ -327,14 +342,62 @@ try {
|
||||
if ($currentUser['role'] !== 'operator') throw new Exception("권한이 없습니다.");
|
||||
|
||||
$product_id = $data['id'] ?? null;
|
||||
$confirmed = $data['confirmed'] ? 1 : 0;
|
||||
$field = $data['field'] ?? 'operator_confirmed'; // 'join_approved' or 'payment_approved'
|
||||
$value = $data['value'] ? 1 : 0;
|
||||
|
||||
if (!$product_id) throw new Exception("ID가 누락되었습니다.");
|
||||
if (!in_array($field, ['operator_confirmed', 'join_approved', 'payment_approved'])) {
|
||||
throw new Exception("유효하지 않은 필드입니다.");
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE sales_tenant_products SET operator_confirmed = ? WHERE id = ?");
|
||||
$stmt->execute([$confirmed, $product_id]);
|
||||
// 지급 승인(payment_approved) 시 수수료 계산 로직 적용
|
||||
if ($field === 'payment_approved' && $value === 1) {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT p.contract_amount, t.manager_id
|
||||
FROM sales_tenant_products p
|
||||
JOIN sales_tenants t ON p.tenant_id = t.id
|
||||
WHERE p.id = ?
|
||||
");
|
||||
$stmt->execute([$product_id]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row) {
|
||||
$manager_id = $row['manager_id'];
|
||||
$amount = $row['contract_amount'];
|
||||
|
||||
// 영업관리자 계층 확인
|
||||
// 1. 등록자 본인이 sales_admin인지 확인
|
||||
// 2. 상위(parent)가 sales_admin인지 확인
|
||||
$sales_admin_count = 0;
|
||||
$curr_id = $manager_id;
|
||||
$visited = [];
|
||||
|
||||
while ($curr_id && !in_array($curr_id, $visited)) {
|
||||
$visited[] = $curr_id;
|
||||
$stmtM = $pdo->prepare("SELECT role, parent_id FROM sales_member WHERE id = ?");
|
||||
$stmtM->execute([$curr_id]);
|
||||
$m = $stmtM->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$m) break;
|
||||
if ($m['role'] === 'sales_admin') {
|
||||
$sales_admin_count++;
|
||||
}
|
||||
$curr_id = $m['parent_id'];
|
||||
if (!$curr_id) break;
|
||||
}
|
||||
|
||||
$payout_rate = ($sales_admin_count >= 2) ? 25 : 20;
|
||||
$payout_amount = $amount * ($payout_rate / 100);
|
||||
|
||||
$stmtU = $pdo->prepare("UPDATE sales_tenant_products SET payout_rate = ?, payout_amount = ? WHERE id = ?");
|
||||
$stmtU->execute([$payout_rate, $payout_amount, $product_id]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $confirmed ? '승인되었습니다.' : '승인이 취소되었습니다.']);
|
||||
$stmt = $pdo->prepare("UPDATE sales_tenant_products SET $field = ? WHERE id = ?");
|
||||
$stmt->execute([$value, $product_id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '처리가 완료되었습니다.']);
|
||||
} elseif ($action === 'update_checklist') {
|
||||
$tenant_id = isset($data['tenant_id']) ? intval($data['tenant_id']) : null;
|
||||
$step_id = isset($data['step_id']) ? intval($data['step_id']) : null;
|
||||
@@ -519,13 +582,13 @@ try {
|
||||
if (!$product_id) throw new Exception("ID가 누락되었습니다.");
|
||||
|
||||
// 정보 및 권한 조회
|
||||
$stmt = $pdo->prepare("SELECT tenant_id, operator_confirmed FROM sales_tenant_products WHERE id = ?");
|
||||
$stmt = $pdo->prepare("SELECT tenant_id, join_approved FROM sales_tenant_products WHERE id = ?");
|
||||
$stmt->execute([$product_id]);
|
||||
$p = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$p) throw new Exception("해당 정보를 찾을 수 없습니다.");
|
||||
if (!checkTenantPermission($pdo, $p['tenant_id'], $currentUser)) throw new Exception("권한이 없습니다.");
|
||||
if ($p['operator_confirmed'] == 1) throw new Exception("이미 승인된 계약은 삭제할 수 없습니다.");
|
||||
if ($p['join_approved'] == 1) throw new Exception("이미 가입 승인된 계약은 삭제할 수 없습니다.");
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM sales_tenant_products WHERE id = ?");
|
||||
$stmt->execute([$product_id]);
|
||||
@@ -560,13 +623,13 @@ try {
|
||||
if (!$product_id) throw new Exception("ID가 누락되었습니다.");
|
||||
|
||||
// 정보 및 권한 조회
|
||||
$stmt = $pdo->prepare("SELECT tenant_id, operator_confirmed FROM sales_tenant_products WHERE id = ?");
|
||||
$stmt = $pdo->prepare("SELECT tenant_id, join_approved FROM sales_tenant_products WHERE id = ?");
|
||||
$stmt->execute([$product_id]);
|
||||
$p = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$p) throw new Exception("해당 정보를 찾을 수 없습니다.");
|
||||
if (!checkTenantPermission($pdo, $p['tenant_id'], $currentUser)) throw new Exception("권한이 없습니다.");
|
||||
if ($p['operator_confirmed'] == 1) throw new Exception("이미 승인된 계약은 수정할 수 없습니다.");
|
||||
if ($p['join_approved'] == 1) throw new Exception("이미 가입 승인된 계약은 수정할 수 없습니다.");
|
||||
|
||||
$product_name = $data['product_name'] ?? '';
|
||||
$contract_amount = $data['contract_amount'] ?? 0;
|
||||
|
||||
@@ -456,7 +456,7 @@
|
||||
const prodRes = await fetch(`api/sales_tenants.php?action=tenant_products&tenant_id=${tenant.id}`);
|
||||
const prodData = await prodRes.json();
|
||||
if (prodData.success) {
|
||||
pendingCount += prodData.data.filter(p => !p.operator_confirmed || p.operator_confirmed == 0).length;
|
||||
pendingCount += prodData.data.filter(p => p.join_approved == 0 || p.payment_approved == 0).length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -989,12 +989,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmProduct = async (productId, currentStatus, tenantId) => {
|
||||
const handleConfirmProduct = async (productId, field, currentValue, tenantId) => {
|
||||
try {
|
||||
const res = await fetch('api/sales_tenants.php?action=confirm_product', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: productId, confirmed: !currentStatus })
|
||||
body: JSON.stringify({ id: productId, field: field, value: !currentValue })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
@@ -1028,7 +1028,7 @@
|
||||
<th className="px-6 py-4 w-12"></th>
|
||||
<th className="px-6 py-4">테넌트명</th>
|
||||
<th className="px-6 py-4">담당 영업자</th>
|
||||
<th className="px-6 py-4 text-center">미승인 건수</th>
|
||||
<th className="px-6 py-4 text-center">미처리 건수</th>
|
||||
<th className="px-6 py-4 text-center">등록일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -1056,14 +1056,14 @@
|
||||
(() => {
|
||||
const products = tenantProducts[t.id];
|
||||
if (!products) return 'text-slate-300';
|
||||
const unconfirmed = products.filter(p => p.operator_confirmed == 0).length;
|
||||
const unconfirmed = products.filter(p => p.join_approved == 0 || p.payment_approved == 0).length;
|
||||
return unconfirmed > 0 ? 'text-red-500 animate-pulse' : 'text-slate-400';
|
||||
})()
|
||||
}`}>
|
||||
{(() => {
|
||||
const products = tenantProducts[t.id];
|
||||
if (!products) return '...';
|
||||
return products.filter(p => p.operator_confirmed == 0).length;
|
||||
return products.filter(p => p.join_approved == 0 || p.payment_approved == 0).length;
|
||||
})()}건
|
||||
</span>
|
||||
</td>
|
||||
@@ -1083,35 +1083,61 @@
|
||||
<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-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 text-center">승인 처리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{!tenantProducts[t.id] ? (
|
||||
<tr><td colSpan="5" className="px-4 py-8 text-center text-slate-400">로딩 중...</td></tr>
|
||||
<tr><td colSpan="6" className="px-4 py-8 text-center text-slate-400">로딩 중...</td></tr>
|
||||
) : tenantProducts[t.id].length === 0 ? (
|
||||
<tr><td colSpan="5" className="px-4 py-8 text-center text-slate-400">등록된 계약이 없습니다.</td></tr>
|
||||
<tr><td colSpan="6" 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-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">
|
||||
<button
|
||||
onClick={() => handleConfirmProduct(p.id, p.operator_confirmed == 1, t.id)}
|
||||
className={`px-4 py-1.5 rounded-xl font-bold transition-all flex items-center gap-1 border ${
|
||||
p.operator_confirmed == 1
|
||||
? 'bg-emerald-600 text-white border-emerald-600 hover:bg-emerald-700 shadow-md shadow-emerald-100'
|
||||
: 'bg-white text-slate-400 border-slate-200 hover:border-emerald-500 hover:text-emerald-600 hover:bg-emerald-50'
|
||||
onClick={() => handleConfirmProduct(p.id, 'join_approved', p.join_approved == 1, t.id)}
|
||||
className={`px-3 py-1 rounded-lg font-bold text-[10px] transition-all flex items-center gap-1 mx-auto border ${
|
||||
p.join_approved == 1
|
||||
? 'bg-blue-600 text-white border-blue-600 shadow-sm'
|
||||
: 'bg-white text-slate-400 border-slate-200 hover:border-blue-500 hover:text-blue-600'
|
||||
}`}
|
||||
>
|
||||
<LucideIcon name={p.operator_confirmed == 1 ? "check-circle" : "circle"} className="w-4 h-4" />
|
||||
{p.operator_confirmed == 1 ? '지급승인됨' : '지급승인'}
|
||||
<LucideIcon name={p.join_approved == 1 ? "check-circle" : "circle"} className="w-3 h-3" />
|
||||
{p.join_approved == 1 ? '가입승인됨' : '가입승인'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<button
|
||||
onClick={() => handleConfirmProduct(p.id, 'payment_approved', p.payment_approved == 1, t.id)}
|
||||
disabled={p.join_approved != 1}
|
||||
className={`px-3 py-1 rounded-lg font-bold text-[10px] transition-all flex items-center gap-1 mx-auto border ${
|
||||
p.payment_approved == 1
|
||||
? 'bg-emerald-600 text-white border-emerald-600 shadow-sm'
|
||||
: p.join_approved == 1
|
||||
? 'bg-white text-slate-400 border-slate-200 hover:border-emerald-500 hover:text-emerald-600'
|
||||
: 'bg-slate-50 text-slate-300 border-slate-100 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<LucideIcon name={p.payment_approved == 1 ? "check-circle" : "circle"} className="w-3 h-3" />
|
||||
{p.payment_approved == 1 ? '지급승인됨' : '지급승인'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{p.payment_approved == 1 ? (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="font-bold text-slate-900 font-mono">{formatCurrency(p.payout_amount)}</span>
|
||||
<span className="text-[10px] text-emerald-600 font-black">{p.payout_rate}% 적용</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-300">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-slate-400">{p.contract_date}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -3605,15 +3631,16 @@
|
||||
<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>
|
||||
<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-center">지급</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>
|
||||
<tr><td colSpan="7" 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>
|
||||
<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>
|
||||
@@ -3622,14 +3649,21 @@
|
||||
<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">
|
||||
{p.operator_confirmed == 1 ? (
|
||||
<span className="px-2 py-0.5 bg-emerald-100 text-emerald-700 rounded-full font-bold text-[10px] uppercase">Confirmed</span>
|
||||
{p.join_approved == 1 ? (
|
||||
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full font-black text-[9px] uppercase">Join OK</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 rounded-full font-bold text-[10px] uppercase">Pending</span>
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-400 rounded-full font-bold text-[9px] uppercase">Wait</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{(currentRole === '영업관리' || currentRole === '운영자') && p.operator_confirmed == 0 && (
|
||||
{p.payment_approved == 1 ? (
|
||||
<span className="px-2 py-0.5 bg-emerald-100 text-emerald-700 rounded-full font-black text-[9px] uppercase">Paid</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-400 rounded-full font-bold text-[9px] uppercase">Wait</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{(currentRole === '영업관리' || currentRole === '운영자') && p.join_approved == 0 && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => handleOpenEditProduct(t, p)}
|
||||
|
||||
Reference in New Issue
Block a user