계약 관리 화면에서 더욱 간결하고 직관적으로 테넌트와 계약을 관리

This commit is contained in:
2026-01-04 17:39:59 +09:00
parent c029440396
commit bed13dc633
2 changed files with 149 additions and 38 deletions

View File

@@ -478,6 +478,7 @@ try {
$stmt->execute([$manager_val, $tenant_id]);
echo json_encode(['success' => true, 'message' => $manager_val ? '담당 매니저가 지정되었습니다.' : '담당 매니저 지정이 취소되었습니다.']);
} elseif ($action === 'update_product') {
$product_id = $data['id'] ?? null;
if (!$product_id) throw new Exception("ID가 누락되었습니다.");
@@ -492,6 +493,10 @@ try {
if ($p['operator_confirmed'] == 1) throw new Exception("이미 승인된 계약은 수정할 수 없습니다.");
$product_name = $data['product_name'] ?? '';
$contract_amount = $data['contract_amount'] ?? 0;
$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;
$commission_amount = ($contract_amount * $commission_rate) / 100;
@@ -499,6 +504,37 @@ try {
$stmt->execute([$product_name, $contract_amount, $commission_rate, $commission_amount, $contract_date, $sub_models, $product_id]);
echo json_encode(['success' => true, 'message' => '계약 정보가 수정되었습니다.']);
} elseif ($action === 'update_tenant') {
$tenant_id = $data['id'] ?? null;
if (!checkTenantPermission($pdo, $tenant_id, $currentUser)) throw new Exception("권한이 없습니다.");
$tenant_name = $data['tenant_name'] ?? '';
$representative = $data['representative'] ?? '';
$business_no = $data['business_no'] ?? '';
$contact_phone = $data['contact_phone'] ?? '';
$email = $data['email'] ?? '';
$address = $data['address'] ?? '';
$sales_manager_id = $data['sales_manager_id'] ?? null;
if (!$tenant_name) throw new Exception("업체명은 필수입니다.");
$stmt = $pdo->prepare("UPDATE sales_tenants SET tenant_name = ?, representative = ?, business_no = ?, contact_phone = ?, email = ?, address = ?, sales_manager_id = ? WHERE id = ?");
$stmt->execute([$tenant_name, $representative, $business_no, $contact_phone, $email, $address, $sales_manager_id, $tenant_id]);
echo json_encode(['success' => true, 'message' => '테넌트 정보가 수정되었습니다.']);
} elseif ($action === 'delete_tenant') {
$tenant_id = $data['id'] ?? null;
if (!checkTenantPermission($pdo, $tenant_id, $currentUser)) throw new Exception("권한이 없습니다.");
// 관련 데이터 삭제 (계약, 시나리오, 상담기록)
$pdo->prepare("DELETE FROM sales_tenant_products WHERE tenant_id = ?")->execute([$tenant_id]);
$pdo->prepare("DELETE FROM sales_tenant_scenarios WHERE tenant_id = ?")->execute([$tenant_id]);
$pdo->prepare("DELETE FROM sales_tenant_consultations WHERE tenant_id = ?")->execute([$tenant_id]);
$pdo->prepare("DELETE FROM sales_tenants WHERE id = ?")->execute([$tenant_id]);
echo json_encode(['success' => true, 'message' => '테넌트가 삭제되었습니다.']);
}
break;
}

View File

@@ -2881,6 +2881,7 @@
const [isSaving, setIsSaving] = useState(false);
const [potentialManagerList, setPotentialManagerList] = useState([]);
const [activeManagerPopover, setActiveManagerPopover] = useState(null);
const [editingTenantId, setEditingTenantId] = useState(null);
const popoverRef = useRef(null);
// Filtered manager list based on role
@@ -3067,16 +3068,19 @@
const handleCreateTenant = async (e) => {
e.preventDefault();
try {
const res = await fetch('api/sales_tenants.php?action=create_tenant', {
const action = editingTenantId ? 'update_tenant' : 'create_tenant';
const payload = editingTenantId ? { ...tenantFormData, id: editingTenantId } : tenantFormData;
const res = await fetch(`api/sales_tenants.php?action=${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tenantFormData)
body: JSON.stringify(payload)
});
const result = await res.json();
if (result.success) {
alert(result.message);
setIsTenantModalOpen(false);
const newTenantId = result.id;
setEditingTenantId(null);
await fetchData();
setTenantFormData({ tenant_name: '', representative: '', business_no: '', contact_phone: '', email: '', address: '', sales_manager_id: currentUser.id });
@@ -3085,7 +3089,41 @@
alert(result.error);
}
} catch (err) {
alert('등록 중 오류가 발생했습니다.');
alert('처리 중 오류가 발생했습니다.');
}
};
const handleOpenEditTenant = (t) => {
setEditingTenantId(t.id);
setTenantFormData({
tenant_name: t.tenant_name,
representative: t.representative,
business_no: t.business_no,
contact_phone: t.contact_phone,
email: t.email,
address: t.address,
sales_manager_id: t.sales_manager_id
});
setIsTenantModalOpen(true);
};
const handleDeleteTenant = async (tenantId) => {
if (!confirm('정말로 이 테넌트를 삭제하시겠습니까? 관련 계약 및 모든 기록이 삭제됩니다.')) return;
try {
const res = await fetch('api/sales_tenants.php?action=delete_tenant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: tenantId })
});
const result = await res.json();
if (result.success) {
alert('삭제되었습니다.');
fetchData();
} else {
alert(result.error);
}
} catch (err) {
alert('삭제 중 오류가 발생했습니다.');
}
};
@@ -3226,7 +3264,14 @@
</h3>
{currentRole === '영업관리' && (
<button
onClick={() => setIsTenantModalOpen(true)}
onClick={() => {
setEditingTenantId(null);
setTenantFormData({
tenant_name: '', representative: '', business_no: '', contact_phone: '', email: '', address: '',
sales_manager_id: currentUser ? currentUser.id : ''
});
setIsTenantModalOpen(true);
}}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-bold flex items-center gap-2 transition-all shadow-md"
>
<LucideIcon name="plus" className="w-4 h-4" />
@@ -3337,36 +3382,45 @@
</div>
</td>
<td className="px-6 py-4 text-slate-400 text-xs">{t.created_at?.split(' ')[0]}</td>
<td className="px-6 py-4 text-center">
<div className="flex items-center gap-2 justify-center">
{currentRole === '영업관리' && (
<>
<button
onClick={() => setActiveSalesScenarioTenant(t)}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-all border border-blue-700 flex items-center gap-1 shadow-sm"
>
<LucideIcon name="trending-up" className="w-3.5 h-3.5" />
영업 진행
</button>
<button
onClick={() => { setSelectedTenant(t); setIsProductModalOpen(true); }}
className="px-3 py-1.5 bg-indigo-50 text-indigo-600 rounded-lg font-bold hover:bg-indigo-100 transition-all border border-indigo-100"
>
계약 추가
</button>
</>
)}
{(currentRole === '매니저' || (currentRole === '영업관리' && t.sales_manager_id == currentUser.id)) && (
<td className="px-6 py-4 text-center">
<div className="flex items-center gap-2 justify-center">
{currentRole === '영업관리' && (
<button
onClick={() => setActiveSalesScenarioTenant(t)}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg font-bold hover:bg-blue-700 transition-all border border-blue-700 flex items-center gap-1 shadow-sm"
>
<LucideIcon name="trending-up" className="w-3.5 h-3.5" />
영업 진행
</button>
)}
{(currentRole === '매니저' || (currentRole === '영업관리' && t.sales_manager_id == currentUser.id)) && (
<button
onClick={() => setActiveManagerScenarioTenant(t)}
className="px-3 py-1.5 bg-emerald-600 text-white rounded-lg font-bold hover:bg-emerald-700 transition-all border border-emerald-700 flex items-center gap-1 shadow-sm"
>
<LucideIcon name="clipboard-check" className="w-3.5 h-3.5" />
매니저 진행
</button>
)}
<div className="flex items-center gap-1 ml-2 border-l border-slate-200 pl-2">
<button
onClick={() => setActiveManagerScenarioTenant(t)}
className="px-3 py-1.5 bg-emerald-600 text-white rounded-lg font-bold hover:bg-emerald-700 transition-all border border-emerald-700 flex items-center gap-1 shadow-sm"
onClick={() => handleOpenEditTenant(t)}
className="p-1.5 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
title="수정 및 계약 관리"
>
<LucideIcon name="clipboard-check" className="w-3.5 h-3.5" />
업무 프로세스
<LucideIcon name="edit-2" className="w-4 h-4" />
</button>
)}
</div>
</td>
<button
onClick={() => handleDeleteTenant(t.id)}
className="p-1.5 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
title="삭제"
>
<LucideIcon name="trash-2" className="w-4 h-4" />
</button>
</div>
</div>
</td>
</tr>
{expandedTenantId === t.id && (
<tr>
@@ -3455,8 +3509,8 @@
<form onSubmit={handleCreateTenant}>
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<h3 className="text-xl font-bold text-slate-900 flex items-center gap-2">
<LucideIcon name="building" className="w-5 h-5 text-blue-600" />
신규 테넌트 등록
<LucideIcon name={editingTenantId ? "edit-3" : "building"} className="w-5 h-5 text-blue-600" />
{editingTenantId ? '테넌트 정보 수정' : '신규 테넌트 등록'}
<button
type="button"
onClick={fillRandomTenantData}
@@ -3467,7 +3521,7 @@
<span className="absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-white text-[10px] px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50 shadow-xl">랜덤 데이터 채우기</span>
</button>
</h3>
<button type="button" onClick={() => setIsTenantModalOpen(false)} className="p-2 hover:bg-slate-200 rounded-full transition-colors">
<button type="button" onClick={() => { setIsTenantModalOpen(false); setEditingTenantId(null); }} className="p-2 hover:bg-slate-200 rounded-full transition-colors">
<LucideIcon name="x" className="w-5 h-5 text-slate-500" />
</button>
</div>
@@ -3516,9 +3570,30 @@
<input type="text" value={tenantFormData.address} onChange={e => setTenantFormData({...tenantFormData, address: 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" placeholder="상세 주소를 입력하세요" />
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50 flex justify-end gap-3">
<button type="button" onClick={() => setIsTenantModalOpen(false)} className="px-4 py-2 text-slate-700 bg-white border border-slate-300 rounded-lg font-medium">취소</button>
<button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-lg font-bold shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all">등록하기</button>
<div className="p-6 border-t border-slate-100 bg-slate-50 flex justify-between items-center gap-3">
<div className="flex gap-2">
{editingTenantId && (
<button
type="button"
onClick={() => {
const t = tenants.find(tt => tt.id === editingTenantId);
setSelectedTenant(t);
setIsTenantModalOpen(false);
setIsProductModalOpen(true);
}}
className="px-4 py-2 bg-indigo-50 text-indigo-600 rounded-lg font-bold hover:bg-indigo-100 transition-all border border-indigo-100 flex items-center gap-1"
>
<LucideIcon name="plus-circle" className="w-4 h-4" />
계약 추가
</button>
)}
</div>
<div className="flex gap-2">
<button type="button" onClick={() => { setIsTenantModalOpen(false); setEditingTenantId(null); }} className="px-4 py-2 text-slate-700 bg-white border border-slate-300 rounded-lg font-medium">취소</button>
<button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-lg font-bold shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all">
{editingTenantId ? '수정 완료' : '등록하기'}
</button>
</div>
</div>
</form>
</div>