영업 승인 로직 및 수당 계산 방식에 대한 업데이트

This commit is contained in:
2026-01-05 04:25:12 +09:00
parent 651a80018a
commit b868603280
3 changed files with 136 additions and 41 deletions

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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)}