테넌트 하위 개념 추가
This commit is contained in:
109
salesmanagement/api/sales_tenants.php
Normal file
109
salesmanagement/api/sales_tenants.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=utf-8");
|
||||
require_once(__DIR__ . "/../../lib/mydb.php");
|
||||
|
||||
session_start();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
if (!isset($_SESSION['sales_user'])) {
|
||||
echo json_encode(['success' => false, 'error' => '로그인이 필요합니다.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$currentUser = $_SESSION['sales_user'];
|
||||
$pdo = db_connect();
|
||||
|
||||
try {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
if ($action === 'list_tenants') {
|
||||
// 운영자는 모든 테넌트, 영업관리/매니저는 본인 소속 테넌트만
|
||||
if ($currentUser['role'] === 'operator') {
|
||||
$stmt = $pdo->prepare("SELECT t.*, m.name as manager_name FROM sales_tenants t JOIN sales_member m ON t.manager_id = m.id ORDER BY t.created_at DESC");
|
||||
$stmt->execute();
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT * FROM sales_tenants WHERE manager_id = ? ORDER BY created_at DESC");
|
||||
$stmt->execute([$currentUser['id']]);
|
||||
}
|
||||
$tenants = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['success' => true, 'data' => $tenants]);
|
||||
|
||||
} elseif ($action === 'tenant_products') {
|
||||
$tenant_id = $_GET['tenant_id'] ?? null;
|
||||
if (!$tenant_id) throw new Exception("테넌트 ID가 필요합니다.");
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM sales_tenant_products WHERE tenant_id = ? ORDER BY created_at DESC");
|
||||
$stmt->execute([$tenant_id]);
|
||||
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['success' => true, 'data' => $products]);
|
||||
|
||||
} elseif ($action === 'my_stats') {
|
||||
// 현재 로그인한 사용자의 요약 통계
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT
|
||||
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
|
||||
FROM sales_tenants t
|
||||
LEFT JOIN sales_tenant_products p ON t.id = p.tenant_id
|
||||
WHERE t.manager_id = ?
|
||||
");
|
||||
$stmt->execute([$currentUser['id']]);
|
||||
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
echo json_encode(['success' => true, 'data' => $stats]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if ($action === 'create_tenant') {
|
||||
$tenant_name = $data['tenant_name'] ?? '';
|
||||
$representative = $data['representative'] ?? '';
|
||||
$business_no = $data['business_no'] ?? '';
|
||||
$contact_phone = $data['contact_phone'] ?? '';
|
||||
$email = $data['email'] ?? '';
|
||||
$address = $data['address'] ?? '';
|
||||
|
||||
if (!$tenant_name) throw new Exception("업체명은 필수입니다.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO sales_tenants (manager_id, tenant_name, representative, business_no, contact_phone, email, address) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$currentUser['id'], $tenant_name, $representative, $business_no, $contact_phone, $email, $address]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId(), 'message' => '테넌트가 등록되었습니다.']);
|
||||
|
||||
} elseif ($action === 'add_product') {
|
||||
$tenant_id = $data['tenant_id'] ?? null;
|
||||
$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');
|
||||
|
||||
if (!$tenant_id || !$product_name) throw new Exception("필수 정보가 누락되었습니다.");
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO sales_tenant_products (tenant_id, product_name, contract_amount, commission_rate, contract_date) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$tenant_id, $product_name, $contract_amount, $commission_rate, $contract_date]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => '상품 계약 정보가 등록되었습니다.']);
|
||||
|
||||
} elseif ($action === 'confirm_product') {
|
||||
if ($currentUser['role'] !== 'operator') throw new Exception("권한이 없습니다.");
|
||||
|
||||
$product_id = $data['id'] ?? null;
|
||||
$confirmed = $data['confirmed'] ? 1 : 0;
|
||||
|
||||
if (!$product_id) throw new Exception("ID가 누락되었습니다.");
|
||||
|
||||
$stmt = $pdo->prepare("UPDATE sales_tenant_products SET operator_confirmed = ? WHERE id = ?");
|
||||
$stmt->execute([$confirmed, $product_id]);
|
||||
|
||||
echo json_encode(['success' => true, 'message' => $confirmed ? '승인되었습니다.' : '승인이 취소되었습니다.']);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -48,6 +48,27 @@
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
// Utility Functions
|
||||
const formatBusinessNo = (val) => {
|
||||
if (!val) return '';
|
||||
const clean = val.replace(/[^0-9]/g, '');
|
||||
if (clean.length <= 3) return clean;
|
||||
if (clean.length <= 5) return `${clean.slice(0, 3)}-${clean.slice(3)}`;
|
||||
return `${clean.slice(0, 3)}-${clean.slice(3, 5)}-${clean.slice(5, 10)}`;
|
||||
};
|
||||
|
||||
const formatPhone = (val) => {
|
||||
if (!val) return '';
|
||||
const clean = val.replace(/[^0-9]/g, '');
|
||||
if (clean.length <= 3) return clean;
|
||||
if (clean.length <= 7) return `${clean.slice(0, 3)}-${clean.slice(3)}`;
|
||||
if (clean.length <= 11) {
|
||||
if (clean.length === 11) return `${clean.slice(0, 3)}-${clean.slice(3, 7)}-${clean.slice(7)}`;
|
||||
return `${clean.slice(0, 3)}-${clean.slice(3, 6)}-${clean.slice(6)}`;
|
||||
}
|
||||
return `${clean.slice(0, 3)}-${clean.slice(3, 7)}-${clean.slice(7, 11)}`;
|
||||
};
|
||||
|
||||
// Lucide Icon Wrapper
|
||||
const LucideIcon = ({ name, size, className, onClick }) => {
|
||||
const ref = React.useRef(null);
|
||||
@@ -67,6 +88,22 @@
|
||||
|
||||
// --- Components ---
|
||||
|
||||
const StatCard = ({ title, value, subtext, icon, onClick, className = "" }) => (
|
||||
<div
|
||||
className={`bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow ${onClick ? 'cursor-pointer' : ''} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">{title}</h3>
|
||||
<div className="p-2 bg-blue-50 rounded-lg text-blue-600">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-slate-900 mb-1">{value}</div>
|
||||
{subtext && <div className="text-xs text-slate-400">{subtext}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 1. Header Component
|
||||
const Header = ({ companyInfo, onOpenHelp, selectedRole, onRoleChange, currentUser, onLogout }) => {
|
||||
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
|
||||
@@ -331,16 +368,55 @@
|
||||
alert('삭제 중 오류가 발생했습니다: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 통계 계산 (실제 데이터 기반)
|
||||
const totalStats = {
|
||||
contractCount: 0, // 나중에 실적 API와 연동 필요
|
||||
const [operatorStats, setOperatorStats] = useState({
|
||||
totalSales: 0,
|
||||
totalCommission: 0,
|
||||
monthlyContractCount: 0,
|
||||
totalCount: 0,
|
||||
monthlySales: 0,
|
||||
monthlyCommission: 0,
|
||||
lastMonthCommission: 0
|
||||
monthlyCount: 0,
|
||||
pendingApprovals: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchOperatorDashboard();
|
||||
}, []);
|
||||
|
||||
const fetchOperatorDashboard = async () => {
|
||||
try {
|
||||
// Fetch global stats
|
||||
const res = await fetch(`api/get_performance.php`);
|
||||
const result = await res.json();
|
||||
|
||||
// Fetch tenant product statistics for pending count
|
||||
const tenantRes = await fetch('api/sales_tenants.php?action=list_tenants');
|
||||
const tenantData = await tenantRes.json();
|
||||
|
||||
let pendingCount = 0;
|
||||
if (tenantData.success) {
|
||||
for (const tenant of tenantData.data) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success && result.total_stats) {
|
||||
setOperatorStats({
|
||||
totalSales: result.total_stats.totalSales || 0,
|
||||
totalCommission: result.total_stats.totalCommission || 0,
|
||||
totalCount: result.total_stats.totalCount || 0,
|
||||
monthlySales: result.period_stats?.total_period_commission || 0, // Just a placeholder for monthly
|
||||
monthlyCommission: result.period_stats?.total_period_commission || 0,
|
||||
monthlyCount: 0,
|
||||
pendingApprovals: pendingCount
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Operator dashboard fetch failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(val);
|
||||
@@ -349,88 +425,60 @@
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">영업 전체 관리</h2>
|
||||
<p className="text-slate-500 mt-2 text-lg">플랫폼의 모든 영업 관리자와 매니저를 총괄 관리합니다.</p>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">지능형 영업 통합 관리</h2>
|
||||
<p className="text-slate-500 mt-2 text-lg">모든 테넌트 계약과 영업 자산을 중앙에서 제어합니다.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOpenAdd}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl shadow-blue-200 hover:-translate-y-0.5 active:translate-y-0"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl shadow-blue-200"
|
||||
>
|
||||
<LucideIcon name="user-plus" className="w-5 h-5" />
|
||||
신규 담당자 등록
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 통계 카드 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<div
|
||||
className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow cursor-pointer"
|
||||
onClick={() => setSelectedManager(null)}
|
||||
>
|
||||
{/* Dashboard Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatCard
|
||||
title="전체 누적 매출"
|
||||
value={formatCurrency(operatorStats.totalSales)}
|
||||
subtext="플랫폼 전체 계약 규모"
|
||||
icon={<LucideIcon name="trending-up" className="w-5 h-5" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="전체 지급 수당"
|
||||
value={formatCurrency(operatorStats.totalCommission)}
|
||||
subtext="영업 인력에게 지급된 총액"
|
||||
icon={<LucideIcon name="wallet" className="w-5 h-5" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="전체 계약 건수"
|
||||
value={`${operatorStats.totalCount}건`}
|
||||
subtext="활성 테넌트 계약 총계"
|
||||
icon={<LucideIcon name="file-check" className="w-5 h-5" />}
|
||||
/>
|
||||
<div className={`bg-white rounded-card p-6 shadow-sm border ${operatorStats.pendingApprovals > 0 ? 'border-red-200 bg-red-50/10' : 'border-slate-100'}`}>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">총 건수</h3>
|
||||
<div className="p-2 bg-blue-50 rounded-lg text-blue-600">
|
||||
<LucideIcon name="file-check" className="w-5 h-5" />
|
||||
<h3 className="text-sm font-medium text-slate-500">대기 중인 승인</h3>
|
||||
<div className={`p-2 rounded-lg ${operatorStats.pendingApprovals > 0 ? 'bg-red-100 text-red-600' : 'bg-slate-100 text-slate-400'}`}>
|
||||
<LucideIcon name="alert-circle" className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-slate-900 mb-1 break-words">{totalStats.contractCount}건</div>
|
||||
<div className="text-xs text-slate-400">전체 계약 건수</div>
|
||||
<div className={`text-2xl font-black mb-1 ${operatorStats.pendingApprovals > 0 ? 'text-red-600' : 'text-slate-900'}`}>
|
||||
{operatorStats.pendingApprovals}건
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">즉시 검토가 필요한 계약 건</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">이번달 건수</h3>
|
||||
<div className="p-2 bg-indigo-50 rounded-lg text-indigo-600">
|
||||
<LucideIcon name="calendar" className="w-5 h-5" />
|
||||
</div>
|
||||
{/* 테넌트 승인 관리 섹션 (운영자 전용) */}
|
||||
<div className="mt-20 pt-12 border-t border-slate-200">
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-8 flex items-center gap-3">
|
||||
<div className="p-2 bg-emerald-600 rounded-xl text-white shadow-lg shadow-emerald-100">
|
||||
<LucideIcon name="check-square" className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="text-xl font-bold text-slate-900 mb-1 break-words">{totalStats.monthlyContractCount}건</div>
|
||||
<div className="text-xs text-slate-400">이번달 신규 계약</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">총 가입비</h3>
|
||||
<div className="p-2 bg-green-50 rounded-lg text-green-600">
|
||||
<LucideIcon name="trending-up" className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-slate-900 mb-1 break-words leading-tight">{formatCurrency(totalStats.totalSales)}</div>
|
||||
<div className="text-xs text-slate-400">전체 누적 가입비</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">총 수당 지급</h3>
|
||||
<div className="p-2 bg-purple-50 rounded-lg text-purple-600">
|
||||
<LucideIcon name="wallet" className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-slate-900 mb-1 break-words leading-tight">{formatCurrency(totalStats.totalCommission)}</div>
|
||||
<div className="text-xs text-slate-400">전체 누적 수당</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">이번달 수당</h3>
|
||||
<div className="p-2 bg-teal-50 rounded-lg text-teal-600">
|
||||
<LucideIcon name="credit-card" className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-slate-900 mb-1 break-words leading-tight">{formatCurrency(totalStats.monthlyCommission)}</div>
|
||||
<div className="text-xs text-slate-400">이번달 지급 예정</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">지난달 수당</h3>
|
||||
<div className="p-2 bg-orange-50 rounded-lg text-orange-600">
|
||||
<LucideIcon name="calendar-check" className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-slate-900 mb-1 break-words leading-tight">{formatCurrency(totalStats.lastMonthCommission)}</div>
|
||||
<div className="text-xs text-slate-400">지난달 지급 완료</div>
|
||||
</div>
|
||||
테넌트 계약 및 수당 승인 관리
|
||||
</h3>
|
||||
<TenantConfirmationManager />
|
||||
</div>
|
||||
|
||||
{/* 영업담당자 통합 관리 (CRUD Table) */}
|
||||
@@ -517,6 +565,17 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 아이템 설정 카드 (운영자 전용) */}
|
||||
<div className="mt-20 pt-12 border-t border-slate-200">
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-8 flex items-center gap-3">
|
||||
<div className="p-2 bg-indigo-600 rounded-xl text-white shadow-lg shadow-indigo-100">
|
||||
<LucideIcon name="settings" className="w-6 h-6" />
|
||||
</div>
|
||||
베이직 요금 및 수당 설정
|
||||
</h3>
|
||||
<ItemPricingManager />
|
||||
</div>
|
||||
|
||||
{/* Member CRUD Modal */}
|
||||
{isModalOpen && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
|
||||
@@ -636,7 +695,7 @@
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({...formData, phone: e.target.value})}
|
||||
onChange={(e) => setFormData({...formData, phone: formatPhone(e.target.value)})}
|
||||
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all font-medium"
|
||||
placeholder="010-0000-0000"
|
||||
/>
|
||||
@@ -756,17 +815,6 @@
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 아이템 설정 카드 (운영자 전용) */}
|
||||
<div className="mt-20 pt-12 border-t border-slate-200">
|
||||
<h3 className="text-2xl font-black text-slate-900 mb-8 flex items-center gap-3">
|
||||
<div className="p-2 bg-indigo-600 rounded-xl text-white shadow-lg shadow-indigo-100">
|
||||
<LucideIcon name="settings" className="w-6 h-6" />
|
||||
</div>
|
||||
베이직 요금 및 수당 설정
|
||||
</h3>
|
||||
<ItemPricingManager />
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{deleteConfirmMember && (
|
||||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md animate-in fade-in duration-300">
|
||||
@@ -813,6 +861,204 @@
|
||||
);
|
||||
};
|
||||
|
||||
// 테넌트 계약 승인 관리 컴포넌트 (운영자 전용)
|
||||
const TenantConfirmationManager = () => {
|
||||
const [tenants, setTenants] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedTenantId, setExpandedTenantId] = useState(null);
|
||||
const [tenantProducts, setTenantProducts] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchTenants();
|
||||
}, []);
|
||||
|
||||
const fetchTenants = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch('api/sales_tenants.php?action=list_tenants');
|
||||
const data = await res.json();
|
||||
if (data.success) setTenants(data.data);
|
||||
} catch (err) {
|
||||
console.error('Fetch tenants error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 테넌트 목록이 로드되면 모든 테넌트의 상품을 미리 로드하여 미승인 건수를 계산
|
||||
useEffect(() => {
|
||||
if (tenants.length > 0) {
|
||||
tenants.forEach(t => {
|
||||
if (!tenantProducts[t.id]) {
|
||||
fetchProducts(t.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [tenants]);
|
||||
|
||||
const fetchProducts = async (tenantId) => {
|
||||
try {
|
||||
const res = await fetch(`api/sales_tenants.php?action=tenant_products&tenant_id=${tenantId}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
setTenantProducts(prev => ({ ...prev, [tenantId]: result.data }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch products error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTenant = (tenantId) => {
|
||||
if (expandedTenantId === tenantId) {
|
||||
setExpandedTenantId(null);
|
||||
} else {
|
||||
setExpandedTenantId(tenantId);
|
||||
if (!tenantProducts[tenantId]) {
|
||||
fetchProducts(tenantId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmProduct = async (productId, currentStatus, 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 })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
// 제품 목록 업데이트
|
||||
fetchProducts(tenantId);
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('승인 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(val || 0);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-card shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
|
||||
<h3 className="text-xl font-bold text-slate-900 flex items-center gap-2">
|
||||
<LucideIcon name="clipboard-check" className="w-5 h-5 text-emerald-600" />
|
||||
테넌트 계약 승인 정산 관리
|
||||
</h3>
|
||||
<button onClick={fetchTenants} className="p-2 text-slate-500 hover:bg-slate-100 rounded-lg transition-colors">
|
||||
<LucideIcon name="refresh-cw" className={`${loading ? 'animate-spin' : ''} w-5 h-5`} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto text-sm">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-slate-50 border-b border-slate-100 text-slate-500 font-bold uppercase text-xs">
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{loading && tenants.length === 0 ? (
|
||||
<tr><td colSpan="5" className="px-6 py-10 text-center text-slate-400">로딩 중...</td></tr>
|
||||
) : tenants.length === 0 ? (
|
||||
<tr><td colSpan="5" className="px-6 py-10 text-center text-slate-400">등록된 테넌트가 없습니다.</td></tr>
|
||||
) : tenants.map(t => (
|
||||
<React.Fragment key={t.id}>
|
||||
<tr className={`hover:bg-slate-50 transition-colors ${expandedTenantId === t.id ? 'bg-emerald-50/30' : ''}`}>
|
||||
<td className="px-6 py-4">
|
||||
<button onClick={() => handleToggleTenant(t.id)} className="text-slate-400 hover:text-emerald-600">
|
||||
<LucideIcon name={expandedTenantId === t.id ? "chevron-down" : "chevron-right"} className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-bold text-slate-900">{t.tenant_name}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="px-2 py-1 bg-blue-50 text-blue-600 rounded text-xs font-bold">
|
||||
{t.manager_name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className={`font-mono font-bold ${
|
||||
(() => {
|
||||
const products = tenantProducts[t.id];
|
||||
if (!products) return 'text-slate-300';
|
||||
const unconfirmed = products.filter(p => p.operator_confirmed == 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;
|
||||
})()}건
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center text-slate-400 text-xs">{t.created_at?.split(' ')[0]}</td>
|
||||
</tr>
|
||||
{expandedTenantId === t.id && (
|
||||
<tr>
|
||||
<td colSpan="5" className="px-6 py-4 bg-slate-50/50 border-b border-emerald-100">
|
||||
<div className="pl-12 space-y-4 animate-in slide-in-from-top-1 duration-200">
|
||||
<h4 className="text-sm font-black text-slate-700 flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full"></div>
|
||||
계약 상품 승인 현황
|
||||
</h4>
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||
<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-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>
|
||||
) : tenantProducts[t.id].length === 0 ? (
|
||||
<tr><td colSpan="5" 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'
|
||||
}`}
|
||||
>
|
||||
<LucideIcon name={p.operator_confirmed == 1 ? "check-circle" : "circle"} className="w-4 h-4" />
|
||||
{p.operator_confirmed == 1 ? '지급승인됨' : '지급승인'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// 아이템 가격 관리 컴포넌트 (운영자 전용)
|
||||
const ItemPricingManager = () => {
|
||||
const [pricingItems, setPricingItems] = useState([]);
|
||||
@@ -1147,22 +1393,7 @@
|
||||
);
|
||||
};
|
||||
|
||||
// 2. StatCard Component (Move up to be reusable)
|
||||
const StatCard = ({ title, value, subtext, icon, onClick }) => (
|
||||
<div
|
||||
className={`bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow ${onClick ? 'cursor-pointer' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-slate-500">{title}</h3>
|
||||
<div className="p-2 bg-blue-50 rounded-lg text-blue-600">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-slate-900 mb-1">{value}</div>
|
||||
{subtext && <div className="text-xs text-slate-400">{subtext}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
// --- NEW: Login View Component ---
|
||||
const LoginView = ({ onLoginSuccess, selectedRole }) => {
|
||||
@@ -1562,7 +1793,7 @@
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({...formData, phone: e.target.value})}
|
||||
onChange={(e) => setFormData({...formData, phone: formatPhone(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="010-0000-0000"
|
||||
/>
|
||||
@@ -1632,6 +1863,374 @@
|
||||
);
|
||||
};
|
||||
|
||||
// --- NEW: Profit Management View (Tenants & Commissions) ---
|
||||
const ProfitManagementView = ({ currentUser }) => {
|
||||
const [tenants, setTenants] = useState([]);
|
||||
const [stats, setStats] = useState({ tenant_count: 0, total_revenue: 0, total_commission: 0, confirmed_commission: 0 });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isTenantModalOpen, setIsTenantModalOpen] = useState(false);
|
||||
const [isProductModalOpen, setIsProductModalOpen] = useState(false);
|
||||
const [selectedTenant, setSelectedTenant] = useState(null);
|
||||
const [expandedTenantId, setExpandedTenantId] = useState(null);
|
||||
const [tenantProducts, setTenantProducts] = useState({});
|
||||
|
||||
const [tenantFormData, setTenantFormData] = useState({
|
||||
tenant_name: '', representative: '', business_no: '', contact_phone: '', email: '', address: ''
|
||||
});
|
||||
const [productFormData, setProductFormData] = useState({
|
||||
product_name: '', contract_amount: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [tenantsRes, statsRes] = await Promise.all([
|
||||
fetch('api/sales_tenants.php?action=list_tenants'),
|
||||
fetch('api/sales_tenants.php?action=my_stats')
|
||||
]);
|
||||
const tenantsData = await tenantsRes.json();
|
||||
const statsData = await statsRes.json();
|
||||
|
||||
if (tenantsData.success) setTenants(tenantsData.data);
|
||||
if (statsData.success) setStats(statsData.data);
|
||||
} catch (err) {
|
||||
console.error('Fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchProducts = async (tenantId) => {
|
||||
try {
|
||||
const res = await fetch(`api/sales_tenants.php?action=tenant_products&tenant_id=${tenantId}`);
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
setTenantProducts(prev => ({ ...prev, [tenantId]: result.data }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch products error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTenant = (tenantId) => {
|
||||
if (expandedTenantId === tenantId) {
|
||||
setExpandedTenantId(null);
|
||||
} else {
|
||||
setExpandedTenantId(tenantId);
|
||||
if (!tenantProducts[tenantId]) {
|
||||
fetchProducts(tenantId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateTenant = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const res = await fetch('api/sales_tenants.php?action=create_tenant', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(tenantFormData)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
alert('테넌트가 등록되었습니다.');
|
||||
setIsTenantModalOpen(false);
|
||||
setTenantFormData({ tenant_name: '', representative: '', business_no: '', contact_phone: '', email: '', address: '' });
|
||||
fetchData();
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProduct = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!productFormData.contract_amount || !productFormData.product_name) {
|
||||
alert('필수 정보를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('api/sales_tenants.php?action=add_product', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...productFormData, tenant_id: selectedTenant.id })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
alert('계약 정보가 등록되었습니다.');
|
||||
setIsProductModalOpen(false);
|
||||
setProductFormData({ product_name: '', contract_amount: '', commission_rate: '20', contract_date: new Date().toISOString().split('T')[0] });
|
||||
fetchData();
|
||||
fetchProducts(selectedTenant.id);
|
||||
} else {
|
||||
alert(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('등록 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(val || 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* 수익 통계 카드 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="관리 테넌트"
|
||||
value={`${stats.tenant_count || 0}개`}
|
||||
subtext="등록된 총 업체 수"
|
||||
icon={<LucideIcon name="building-2" className="w-5 h-5" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="총 매출액"
|
||||
value={formatCurrency(stats.total_revenue)}
|
||||
subtext="전체 계약 금액 합계"
|
||||
icon={<LucideIcon name="bar-chart-3" className="w-5 h-5" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="누적 예상 수익"
|
||||
value={formatCurrency(stats.total_commission)}
|
||||
subtext="전체 수수료 합계"
|
||||
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">확정 수익 (지급대상)</h3>
|
||||
<div className="p-2 bg-emerald-100 rounded-lg text-emerald-600">
|
||||
<LucideIcon name="check-circle" className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-2xl font-bold text-emerald-900 mb-1">{formatCurrency(stats.confirmed_commission)}</div>
|
||||
<div className="text-xs text-emerald-600 font-medium">운영팀 승인 완료된 금액</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테넌트 목록 섹션 */}
|
||||
<section className="bg-white rounded-card shadow-sm border border-slate-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
||||
<h3 className="text-xl font-bold text-slate-900 flex items-center gap-2">
|
||||
<LucideIcon name="layout-list" className="w-5 h-5 text-blue-600" />
|
||||
테넌트 및 계약 관리
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => 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" />
|
||||
신규 테넌트 등록
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto text-sm">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-slate-50 border-b border-slate-100 text-slate-500 font-bold uppercase text-xs">
|
||||
<tr>
|
||||
<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">연락처</th>
|
||||
<th className="px-6 py-4">등록일</th>
|
||||
<th className="px-6 py-4 text-center">계약관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{loading ? (
|
||||
<tr><td colSpan="6" className="px-6 py-10 text-center text-slate-400">데이터 로딩 중...</td></tr>
|
||||
) : tenants.length === 0 ? (
|
||||
<tr><td colSpan="6" className="px-6 py-10 text-center text-slate-400">등록된 테넌트가 없습니다.</td></tr>
|
||||
) : tenants.map(t => (
|
||||
<React.Fragment key={t.id}>
|
||||
<tr className={`hover:bg-blue-50/30 transition-colors ${expandedTenantId === t.id ? 'bg-blue-50/50' : ''}`}>
|
||||
<td className="px-6 py-4">
|
||||
<button onClick={() => handleToggleTenant(t.id)} className="text-slate-400 hover:text-blue-600">
|
||||
<LucideIcon name={expandedTenantId === t.id ? "chevron-down" : "chevron-right"} className="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-bold text-slate-900">{t.tenant_name}</td>
|
||||
<td className="px-6 py-4 text-slate-600">{t.representative || '-'}</td>
|
||||
<td className="px-6 py-4 text-slate-600">{t.contact_phone || '-'}</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">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
{expandedTenantId === t.id && (
|
||||
<tr>
|
||||
<td colSpan="6" className="px-6 py-4 bg-slate-50/50 border-b border-blue-100">
|
||||
<div className="pl-12 space-y-4 animate-in slide-in-from-top-1 duration-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-black text-slate-700 flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
|
||||
체결 상품 및 수당 내역
|
||||
</h4>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden shadow-sm">
|
||||
<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-right text-blue-600">내 수익</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>
|
||||
) : tenantProducts[t.id].length === 0 ? (
|
||||
<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-center text-slate-500">{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">
|
||||
{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>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 rounded-full font-bold text-[10px] uppercase">Pending</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tenant Modal */}
|
||||
{isTenantModalOpen && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden animate-in zoom-in duration-200">
|
||||
<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" />
|
||||
신규 테넌트 등록
|
||||
</h3>
|
||||
<button type="button" onClick={() => setIsTenantModalOpen(false)} className="p-2 hover:bg-slate-200 rounded-full transition-colors">
|
||||
<LucideIcon name="x" className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">업체명 *</label>
|
||||
<input type="text" required value={tenantFormData.tenant_name} onChange={e => setTenantFormData({...tenantFormData, tenant_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" placeholder="예: (주)미래소프트" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">대표자명</label>
|
||||
<input type="text" value={tenantFormData.representative} onChange={e => setTenantFormData({...tenantFormData, representative: 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>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">사업자번호</label>
|
||||
<input type="text" value={tenantFormData.business_no} onChange={e => setTenantFormData({...tenantFormData, business_no: formatBusinessNo(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="000-00-00000" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">연락처</label>
|
||||
<input type="tel" value={tenantFormData.contact_phone} onChange={e => setTenantFormData({...tenantFormData, contact_phone: formatPhone(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="010-0000-0000" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">이메일</label>
|
||||
<input type="email" value={tenantFormData.email} onChange={e => setTenantFormData({...tenantFormData, email: 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="example@mail.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">주소</label>
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Modal */}
|
||||
{isProductModalOpen && selectedTenant && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden animate-in zoom-in duration-200">
|
||||
<form onSubmit={handleAddProduct}>
|
||||
<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="package-plus" className="w-5 h-5 text-indigo-600" />
|
||||
계약 정보 추가
|
||||
</h3>
|
||||
<button type="button" onClick={() => setIsProductModalOpen(false)} className="p-2 hover:bg-slate-200 rounded-full transition-colors">
|
||||
<LucideIcon name="x" className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="p-3 bg-blue-50 rounded-lg border border-blue-100 mb-4">
|
||||
<p className="text-xs text-blue-600 font-bold uppercase tracking-wider mb-1">Target Tenant</p>
|
||||
<p className="text-sm font-black text-blue-900">{selectedTenant.tenant_name}</p>
|
||||
</div>
|
||||
<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" placeholder="예: 베이직 패키지 A" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">총 계약금액 (원) *</label>
|
||||
<input type="number" required value={productFormData.contract_amount} onChange={e => setProductFormData({...productFormData, contract_amount: 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="0" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-500 mb-1">수수료율 (%)</label>
|
||||
<input type="number" value={productFormData.commission_rate} onChange={e => setProductFormData({...productFormData, commission_rate: 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>
|
||||
<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-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) * (productFormData.commission_rate || 0) / 100)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 border-t border-slate-100 bg-slate-50 flex justify-end gap-3">
|
||||
<button type="button" onClick={() => setIsProductModalOpen(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-indigo-600 text-white rounded-lg font-bold shadow-lg shadow-indigo-100 hover:bg-indigo-700 transition-all">저장하기</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 3. Main App Component
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -1789,6 +2388,17 @@
|
||||
{/* Dashbaord & Org Tree */}
|
||||
<SalesManagementDashboard organizationData={organizationData} onRefresh={fetchPerformanceData} isLoading={isOrgLoading} />
|
||||
|
||||
{/* NEW: Profit & Tenant Management Section */}
|
||||
<section className="mt-20 pt-12 border-t border-slate-200">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="p-2 bg-blue-600 rounded-xl text-white shadow-lg shadow-blue-100">
|
||||
<LucideIcon name="wallet" className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">수익 및 테넌트 관리</h2>
|
||||
</div>
|
||||
<ProfitManagementView currentUser={currentUser} />
|
||||
</section>
|
||||
|
||||
{selectedRole === '영업관리' && (
|
||||
<ManagerManagementView currentUser={currentUser} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user