Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -113,8 +113,8 @@ const mockData: CEODashboardData = {
|
||||
cards: [
|
||||
{ id: 'cm1', label: '카드', amount: 30123000, previousLabel: '미정리 5건 (가지급금 예정)' },
|
||||
{ id: 'cm2', label: '가지급금', amount: 350000000, previousLabel: '전월 대비 +10.5%' },
|
||||
{ id: 'cm3', label: '법인세 예상 가산', amount: 3123000, previousLabel: '전월 대비 +10.5%' },
|
||||
{ id: 'cm4', label: '종합세 예상 가산', amount: 3123000, previousLabel: '추가 사안 +10.5%' },
|
||||
{ id: 'cm3', label: '법인세 예상 가중', amount: 3123000, previousLabel: '추가 세금 +10.5%' },
|
||||
{ id: 'cm4', label: '대표자 종합세 예상 가중', amount: 3123000, previousLabel: '추가 세금 +10.5%' },
|
||||
],
|
||||
checkPoints: [
|
||||
{
|
||||
@@ -682,51 +682,59 @@ export function CEODashboard() {
|
||||
},
|
||||
},
|
||||
me4: {
|
||||
title: '총 예상 지출 상세',
|
||||
title: '당월 지출 예상 상세',
|
||||
summaryCards: [
|
||||
{ label: '총 예상 지출', value: 350000000, unit: '원' },
|
||||
{ label: '전월 대비', value: '+10.5%', isComparison: true, isPositive: true },
|
||||
{ label: '당월 지출 예상', value: 6000000, unit: '원' },
|
||||
{ label: '전월 대비', value: '-12.5%', isComparison: true, isPositive: false },
|
||||
{ label: '총 계좌 잔액', value: 10000000, unit: '원' },
|
||||
],
|
||||
barChart: {
|
||||
title: '월별 총 지출 추이',
|
||||
data: [
|
||||
{ name: '1월', value: 280000000 },
|
||||
{ name: '2월', value: 300000000 },
|
||||
{ name: '3월', value: 290000000 },
|
||||
{ name: '4월', value: 320000000 },
|
||||
{ name: '5월', value: 310000000 },
|
||||
{ name: '6월', value: 340000000 },
|
||||
{ name: '7월', value: 350000000 },
|
||||
],
|
||||
dataKey: 'value',
|
||||
xAxisKey: 'name',
|
||||
color: '#A78BFA',
|
||||
},
|
||||
pieChart: {
|
||||
title: '지출 항목별 비율',
|
||||
data: [
|
||||
{ name: '매입', value: 305000000, percentage: 87, color: '#60A5FA' },
|
||||
{ name: '카드', value: 30000000, percentage: 9, color: '#34D399' },
|
||||
{ name: '발행어음', value: 15000000, percentage: 4, color: '#FBBF24' },
|
||||
],
|
||||
},
|
||||
table: {
|
||||
title: '지출 항목별 내역',
|
||||
title: '당월 지출 승인 내역서',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'category', label: '항목', align: 'left' },
|
||||
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
|
||||
{ key: 'ratio', label: '비율', align: 'center' },
|
||||
{ key: 'paymentDate', label: '예상 지급일', align: 'center' },
|
||||
{ key: 'item', label: '항목', align: 'left' },
|
||||
{ key: 'amount', label: '지출금액', align: 'right', format: 'currency', highlightColor: 'red' },
|
||||
{ key: 'vendor', label: '거래처', align: 'center' },
|
||||
{ key: 'account', label: '계좌', align: 'center' },
|
||||
],
|
||||
data: [
|
||||
{ category: '매입', amount: 305000000, ratio: '87%' },
|
||||
{ category: '카드', amount: 30000000, ratio: '9%' },
|
||||
{ category: '발행어음', amount: 15000000, ratio: '4%' },
|
||||
{ paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '(발행 어음) 123123123', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '거래처명 12월분', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '품의 사유...', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
{ paymentDate: '2025-12-12', item: '적요 내용', amount: 1000000, vendor: '회사명', account: '국민은행 1234' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'vendor',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '회사명', label: '회사명' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 350000000,
|
||||
totalLabel: '2025/12 계',
|
||||
totalValue: 6000000,
|
||||
totalColumnKey: 'amount',
|
||||
footerSummary: [
|
||||
{ label: '지출 합계', value: 6000000 },
|
||||
{ label: '계좌 잔액', value: 10000000 },
|
||||
{ label: '최종 차액', value: 4000000 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -743,6 +751,244 @@ export function CEODashboard() {
|
||||
console.log('당월 예상 지출 클릭');
|
||||
}, []);
|
||||
|
||||
// 카드/가지급금 관리 카드 클릭 (개별 카드 클릭 시 상세 모달)
|
||||
const handleCardManagementCardClick = useCallback((cardId: string) => {
|
||||
const cardConfigs: Record<string, DetailModalConfig> = {
|
||||
cm1: {
|
||||
title: '카드 사용 상세',
|
||||
summaryCards: [
|
||||
{ label: '당월 카드 사용', value: 30123000, unit: '원' },
|
||||
{ label: '전월 대비', value: '+10.5%', isComparison: true, isPositive: true },
|
||||
{ label: '미정리 건수', value: '5건' },
|
||||
],
|
||||
barChart: {
|
||||
title: '월별 카드 사용 추이',
|
||||
data: [
|
||||
{ name: '7월', value: 28000000 },
|
||||
{ name: '8월', value: 32000000 },
|
||||
{ name: '9월', value: 27000000 },
|
||||
{ name: '10월', value: 35000000 },
|
||||
{ name: '11월', value: 29000000 },
|
||||
{ name: '12월', value: 30123000 },
|
||||
],
|
||||
dataKey: 'value',
|
||||
xAxisKey: 'name',
|
||||
color: '#60A5FA',
|
||||
},
|
||||
pieChart: {
|
||||
title: '사용자별 카드 사용 비율',
|
||||
data: [
|
||||
{ name: '대표이사', value: 15000000, percentage: 50, color: '#60A5FA' },
|
||||
{ name: '경영지원팀', value: 9000000, percentage: 30, color: '#34D399' },
|
||||
{ name: '영업팀', value: 6123000, percentage: 20, color: '#FBBF24' },
|
||||
],
|
||||
},
|
||||
table: {
|
||||
title: '카드 사용 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'cardName', label: '카드명', align: 'left' },
|
||||
{ key: 'user', label: '사용자', align: 'center' },
|
||||
{ key: 'date', label: '사용일시', align: 'center', format: 'date' },
|
||||
{ key: 'store', label: '가맹점명', align: 'left' },
|
||||
{ key: 'amount', label: '사용금액', align: 'right', format: 'currency' },
|
||||
{ key: 'usageType', label: '사용유형', align: 'center', highlightValue: '미설정' },
|
||||
],
|
||||
data: [
|
||||
{ cardName: '법인카드1', user: '대표이사', date: '2026-01-05 18:30', store: '스타벅스 강남점', amount: 45000, usageType: '복리후생비' },
|
||||
{ cardName: '법인카드1', user: '대표이사', date: '2026-01-04 12:15', store: '한식당', amount: 350000, usageType: '접대비' },
|
||||
{ cardName: '법인카드2', user: '경영지원팀', date: '2026-01-03 14:20', store: '오피스디포', amount: 125000, usageType: '소모품비' },
|
||||
{ cardName: '법인카드1', user: '대표이사', date: '2026-01-02 19:45', store: '골프장', amount: 850000, usageType: '미설정' },
|
||||
{ cardName: '법인카드3', user: '영업팀', date: '2026-01-02 11:30', store: 'GS칼텍스', amount: 80000, usageType: '교통비' },
|
||||
{ cardName: '법인카드2', user: '경영지원팀', date: '2026-01-01 16:00', store: '이마트', amount: 230000, usageType: '미설정' },
|
||||
{ cardName: '법인카드1', user: '대표이사', date: '2025-12-30 20:30', store: '백화점', amount: 1500000, usageType: '미설정' },
|
||||
{ cardName: '법인카드3', user: '영업팀', date: '2025-12-29 09:15', store: '커피빈', amount: 32000, usageType: '복리후생비' },
|
||||
{ cardName: '법인카드2', user: '경영지원팀', date: '2025-12-28 13:45', store: '문구점', amount: 55000, usageType: '소모품비' },
|
||||
{ cardName: '법인카드1', user: '대표이사', date: '2025-12-27 21:00', store: '호텔', amount: 450000, usageType: '미설정' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'user',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '대표이사', label: '대표이사' },
|
||||
{ value: '경영지원팀', label: '경영지원팀' },
|
||||
{ value: '영업팀', label: '영업팀' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'usageType',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '미설정', label: '미설정' },
|
||||
{ value: '복리후생비', label: '복리후생비' },
|
||||
{ value: '접대비', label: '접대비' },
|
||||
{ value: '소모품비', label: '소모품비' },
|
||||
{ value: '교통비', label: '교통비' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
{ value: 'amountDesc', label: '금액 높은순' },
|
||||
{ value: 'amountAsc', label: '금액 낮은순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 30123000,
|
||||
totalColumnKey: 'amount',
|
||||
},
|
||||
},
|
||||
cm2: {
|
||||
title: '가지급금 상세',
|
||||
summaryCards: [
|
||||
{ label: '가지급금', value: '4.5억원' },
|
||||
{ label: '인정이자 4.6%', value: 6000000, unit: '원' },
|
||||
{ label: '미정정', value: '10건' },
|
||||
],
|
||||
table: {
|
||||
title: '가지급금 관련 내역',
|
||||
columns: [
|
||||
{ key: 'no', label: 'No.', align: 'center' },
|
||||
{ key: 'date', label: '발생일시', align: 'center' },
|
||||
{ key: 'target', label: '대상', align: 'center' },
|
||||
{ key: 'category', label: '구분', align: 'center' },
|
||||
{ key: 'amount', label: '금액', align: 'right', format: 'currency' },
|
||||
{ key: 'status', label: '상태', align: 'center', highlightValue: '미정정' },
|
||||
{ key: 'content', label: '내용', align: 'left' },
|
||||
],
|
||||
data: [
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '카드명', amount: 1000000, status: '미정정', content: '미정정' },
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '카드명', amount: 1000000, status: '접비(미정리)', content: '접대비 불인정' },
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '계좌명', amount: 1000000, status: '미정정', content: '접대비 불인정' },
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '계좌명', amount: 1000000, status: '미정정', content: '미정정' },
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '-', amount: 1000000, status: '미정정', content: '미정정' },
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '카드명', amount: 1000000, status: '접대비', content: '접대비 불인정' },
|
||||
{ date: '2025-12-12 12:12', target: '홍길동', category: '카드명', amount: 1000000, status: '-', content: '복리후생비, 주말/심야 카드 사용' },
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
key: 'target',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '홍길동', label: '홍길동' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'category',
|
||||
options: [
|
||||
{ value: 'all', label: '전체' },
|
||||
{ value: '카드명', label: '카드명' },
|
||||
{ value: '계좌명', label: '계좌명' },
|
||||
],
|
||||
defaultValue: 'all',
|
||||
},
|
||||
{
|
||||
key: 'sortOrder',
|
||||
options: [
|
||||
{ value: 'latest', label: '최신순' },
|
||||
{ value: 'oldest', label: '등록순' },
|
||||
{ value: 'amountDesc', label: '금액 높은순' },
|
||||
{ value: 'amountAsc', label: '금액 낮은순' },
|
||||
],
|
||||
defaultValue: 'latest',
|
||||
},
|
||||
],
|
||||
showTotal: true,
|
||||
totalLabel: '합계',
|
||||
totalValue: 111000000,
|
||||
totalColumnKey: 'amount',
|
||||
},
|
||||
},
|
||||
cm3: {
|
||||
title: '법인세 예상 가중 상세',
|
||||
summaryCards: [
|
||||
{ label: '법인세 예상 증가', value: 3123000, unit: '원' },
|
||||
{ label: '인정 이자', value: 6000000, unit: '원' },
|
||||
{ label: '가지급금', value: '4.5억원' },
|
||||
{ label: '인정이자', value: 6000000, unit: '원' },
|
||||
],
|
||||
comparisonSection: {
|
||||
leftBox: {
|
||||
title: '없을때 법인세',
|
||||
items: [
|
||||
{ label: '과세표준', value: '3억원' },
|
||||
{ label: '법인세', value: 50970000, unit: '원' },
|
||||
],
|
||||
borderColor: 'orange',
|
||||
},
|
||||
rightBox: {
|
||||
title: '있을때 법인세',
|
||||
items: [
|
||||
{ label: '과세표준', value: '3.06억원' },
|
||||
{ label: '법인세', value: 54093000, unit: '원' },
|
||||
],
|
||||
borderColor: 'blue',
|
||||
},
|
||||
vsLabel: '법인세 예상 증가',
|
||||
vsValue: 3123000,
|
||||
vsSubLabel: '법인 세율 -12.5%',
|
||||
},
|
||||
referenceTable: {
|
||||
title: '법인세 과세표준 (2024년 기준)',
|
||||
columns: [
|
||||
{ key: 'bracket', label: '과세표준', align: 'left' },
|
||||
{ key: 'rate', label: '세율', align: 'center' },
|
||||
{ key: 'formula', label: '계산식', align: 'left' },
|
||||
],
|
||||
data: [
|
||||
{ bracket: '2억원 이하', rate: '9%', formula: '과세표준 × 9%' },
|
||||
{ bracket: '2억원 초과 ~ 200억원 이하', rate: '19%', formula: '1,800만원 + (2억원 초과분 × 19%)' },
|
||||
{ bracket: '200억원 초과 ~ 3,000억원 이하', rate: '21%', formula: '37.62억원 + (200억원 초과분 × 21%)' },
|
||||
{ bracket: '3,000억원 초과', rate: '24%', formula: '625.62억원 + (3,000억원 초과분 × 24%)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
cm4: {
|
||||
title: '대표자 종합소득세 예상 가중 상세',
|
||||
summaryCards: [
|
||||
{ label: '합계', value: 3123000, unit: '원' },
|
||||
{ label: '전월 대비', value: '+12.5%', isComparison: true, isPositive: false },
|
||||
{ label: '가지급금', value: '4.5억원' },
|
||||
{ label: '인정 이자', value: 6000000, unit: '원' },
|
||||
],
|
||||
table: {
|
||||
title: '종합소득세 과세표준 (2024년 기준)',
|
||||
columns: [
|
||||
{ key: 'bracket', label: '과세표준', align: 'left' },
|
||||
{ key: 'rate', label: '세율', align: 'center' },
|
||||
{ key: 'deduction', label: '누진공제', align: 'right' },
|
||||
{ key: 'formula', label: '계산식', align: 'left' },
|
||||
],
|
||||
data: [
|
||||
{ bracket: '1,400만원 이하', rate: '6%', deduction: '-', formula: '과세표준 × 6%' },
|
||||
{ bracket: '1,400만원 초과 ~ 5,000만원 이하', rate: '15%', deduction: '126만원', formula: '과세표준 × 15% - 126만원' },
|
||||
{ bracket: '5,000만원 초과 ~ 8,800만원 이하', rate: '24%', deduction: '576만원', formula: '과세표준 × 24% - 576만원' },
|
||||
{ bracket: '8,800만원 초과 ~ 1.5억원 이하', rate: '35%', deduction: '1,544만원', formula: '과세표준 × 35% - 1,544만원' },
|
||||
{ bracket: '1.5억원 초과 ~ 3억원 이하', rate: '38%', deduction: '1,994만원', formula: '과세표준 × 38% - 1,994만원' },
|
||||
{ bracket: '3억원 초과 ~ 5억원 이하', rate: '40%', deduction: '2,594만원', formula: '과세표준 × 40% - 2,594만원' },
|
||||
{ bracket: '5억원 초과 ~ 10억원 이하', rate: '42%', deduction: '3,594만원', formula: '과세표준 × 42% - 3,594만원' },
|
||||
{ bracket: '10억원 초과', rate: '45%', deduction: '6,594만원', formula: '과세표준 × 45% - 6,594만원' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const config = cardConfigs[cardId];
|
||||
if (config) {
|
||||
setDetailModalConfig(config);
|
||||
setIsDetailModalOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 접대비 클릭
|
||||
const handleEntertainmentClick = useCallback(() => {
|
||||
// TODO: 접대비 상세 팝업 열기
|
||||
@@ -863,7 +1109,10 @@ export function CEODashboard() {
|
||||
|
||||
{/* 카드/가지급금 관리 */}
|
||||
{dashboardSettings.cardManagement && (
|
||||
<CardManagementSection data={data.cardManagement} />
|
||||
<CardManagementSection
|
||||
data={data.cardManagement}
|
||||
onCardClick={handleCardManagementCardClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 접대비 현황 */}
|
||||
|
||||
@@ -36,6 +36,8 @@ import type {
|
||||
HorizontalBarChartConfig,
|
||||
TableConfig,
|
||||
TableFilterConfig,
|
||||
ComparisonSectionConfig,
|
||||
ReferenceTableConfig,
|
||||
} from '../types';
|
||||
|
||||
interface DetailModalProps {
|
||||
@@ -194,6 +196,155 @@ const HorizontalBarChartSection = ({ config }: { config: HorizontalBarChartConfi
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* VS 비교 섹션 컴포넌트
|
||||
*/
|
||||
const ComparisonSection = ({ config }: { config: ComparisonSectionConfig }) => {
|
||||
const formatValue = (value: string | number, unit?: string): string => {
|
||||
if (typeof value === 'number') {
|
||||
return formatCurrency(value) + (unit || '원');
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const borderColorClass = {
|
||||
orange: 'border-orange-400',
|
||||
blue: 'border-blue-400',
|
||||
};
|
||||
|
||||
const titleBgClass = {
|
||||
orange: 'bg-orange-50',
|
||||
blue: 'bg-blue-50',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-stretch gap-4">
|
||||
{/* 왼쪽 박스 */}
|
||||
<div className={cn(
|
||||
"flex-1 border-2 rounded-lg overflow-hidden",
|
||||
borderColorClass[config.leftBox.borderColor]
|
||||
)}>
|
||||
<div className={cn(
|
||||
"px-4 py-2 text-sm font-medium text-gray-700",
|
||||
titleBgClass[config.leftBox.borderColor]
|
||||
)}>
|
||||
{config.leftBox.title}
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{config.leftBox.items.map((item, index) => (
|
||||
<div key={index}>
|
||||
<p className="text-xs text-gray-500 mb-1">{item.label}</p>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
{formatValue(item.value, item.unit)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VS 영역 */}
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<span className="text-2xl font-bold text-gray-400 mb-2">VS</span>
|
||||
<div className="bg-red-50 rounded-lg px-4 py-2 text-center">
|
||||
<p className="text-xs text-gray-600 mb-1">{config.vsLabel}</p>
|
||||
<p className="text-xl font-bold text-red-500">
|
||||
{typeof config.vsValue === 'number'
|
||||
? formatCurrency(config.vsValue) + '원'
|
||||
: config.vsValue}
|
||||
</p>
|
||||
{config.vsSubLabel && (
|
||||
<p className="text-xs text-gray-500 mt-1">{config.vsSubLabel}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽 박스 */}
|
||||
<div className={cn(
|
||||
"flex-1 border-2 rounded-lg overflow-hidden",
|
||||
borderColorClass[config.rightBox.borderColor]
|
||||
)}>
|
||||
<div className={cn(
|
||||
"px-4 py-2 text-sm font-medium text-gray-700",
|
||||
titleBgClass[config.rightBox.borderColor]
|
||||
)}>
|
||||
{config.rightBox.title}
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{config.rightBox.items.map((item, index) => (
|
||||
<div key={index}>
|
||||
<p className="text-xs text-gray-500 mb-1">{item.label}</p>
|
||||
<p className="text-lg font-bold text-gray-900">
|
||||
{formatValue(item.value, item.unit)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 참조 테이블 컴포넌트 (필터 없는 정보성 테이블)
|
||||
*/
|
||||
const ReferenceTableSection = ({ config }: { config: ReferenceTableConfig }) => {
|
||||
const getAlignClass = (align?: string): string => {
|
||||
switch (align) {
|
||||
case 'center':
|
||||
return 'text-center';
|
||||
case 'right':
|
||||
return 'text-right';
|
||||
default:
|
||||
return 'text-left';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-medium text-gray-800 mb-3">{config.title}</h4>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
{config.columns.map((column) => (
|
||||
<th
|
||||
key={column.key}
|
||||
className={cn(
|
||||
"px-4 py-3 text-xs font-medium text-gray-600",
|
||||
getAlignClass(column.align)
|
||||
)}
|
||||
>
|
||||
{column.label}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.data.map((row, rowIndex) => (
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className="border-t border-gray-100 hover:bg-gray-50"
|
||||
>
|
||||
{config.columns.map((column) => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={cn(
|
||||
"px-4 py-3 text-sm text-gray-700",
|
||||
getAlignClass(column.align)
|
||||
)}
|
||||
>
|
||||
{String(row[column.key] ?? '-')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 테이블 컴포넌트
|
||||
*/
|
||||
@@ -341,13 +492,22 @@ const TableSection = ({ config }: { config: TableConfig }) => {
|
||||
: formatCellValue(row[column.key], column.format);
|
||||
const isHighlighted = column.highlightValue && String(row[column.key]) === column.highlightValue;
|
||||
|
||||
// highlightColor 클래스 매핑
|
||||
const highlightColorClass = column.highlightColor ? {
|
||||
red: 'text-red-500',
|
||||
orange: 'text-orange-500',
|
||||
blue: 'text-blue-500',
|
||||
green: 'text-green-500',
|
||||
}[column.highlightColor] : '';
|
||||
|
||||
return (
|
||||
<td
|
||||
key={column.key}
|
||||
className={cn(
|
||||
"px-4 py-3 text-sm",
|
||||
getAlignClass(column.align),
|
||||
isHighlighted && "text-orange-500 font-medium"
|
||||
isHighlighted && "text-orange-500 font-medium",
|
||||
highlightColorClass
|
||||
)}
|
||||
>
|
||||
{cellValue}
|
||||
@@ -380,6 +540,24 @@ const TableSection = ({ config }: { config: TableConfig }) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 하단 다중 합계 섹션 */}
|
||||
{config.footerSummary && config.footerSummary.length > 0 && (
|
||||
<div className="mt-4 border rounded-lg bg-gray-50 p-4">
|
||||
<div className="space-y-2">
|
||||
{config.footerSummary.map((item, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">{item.label}</span>
|
||||
<span className="font-medium text-gray-900">
|
||||
{typeof item.value === 'number'
|
||||
? formatCurrency(item.value)
|
||||
: item.value}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -429,6 +607,16 @@ export function DetailModal({ isOpen, onClose, config }: DetailModalProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* VS 비교 섹션 영역 */}
|
||||
{config.comparisonSection && (
|
||||
<ComparisonSection config={config.comparisonSection} />
|
||||
)}
|
||||
|
||||
{/* 참조 테이블 영역 */}
|
||||
{config.referenceTable && (
|
||||
<ReferenceTableSection config={config.referenceTable} />
|
||||
)}
|
||||
|
||||
{/* 테이블 영역 */}
|
||||
{config.table && <TableSection key={config.title} config={config.table} />}
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,18 @@ import type { CardManagementData } from '../types';
|
||||
|
||||
interface CardManagementSectionProps {
|
||||
data: CardManagementData;
|
||||
onCardClick?: (cardId: string) => void;
|
||||
}
|
||||
|
||||
export function CardManagementSection({ data }: CardManagementSectionProps) {
|
||||
export function CardManagementSection({ data, onCardClick }: CardManagementSectionProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = () => {
|
||||
router.push('/ko/accounting/card-management');
|
||||
const handleClick = (cardId: string) => {
|
||||
if (onCardClick) {
|
||||
onCardClick(cardId);
|
||||
} else {
|
||||
router.push('/ko/accounting/card-management');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -32,7 +37,7 @@ export function CardManagementSection({ data }: CardManagementSectionProps) {
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={handleClick}
|
||||
onClick={() => handleClick(card.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -276,6 +276,7 @@ export interface TableColumnConfig {
|
||||
format?: 'number' | 'currency' | 'date' | 'text';
|
||||
width?: string;
|
||||
highlightValue?: string; // 이 값일 때 주황색으로 강조 표시
|
||||
highlightColor?: 'red' | 'orange' | 'blue' | 'green'; // 컬럼 전체에 적용할 색상
|
||||
}
|
||||
|
||||
// 테이블 필터 옵션 타입
|
||||
@@ -291,6 +292,13 @@ export interface TableFilterConfig {
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
// 테이블 하단 요약 항목 타입
|
||||
export interface FooterSummaryItem {
|
||||
label: string;
|
||||
value: string | number;
|
||||
format?: 'number' | 'currency';
|
||||
}
|
||||
|
||||
// 테이블 설정 타입
|
||||
export interface TableConfig {
|
||||
title: string;
|
||||
@@ -301,6 +309,37 @@ export interface TableConfig {
|
||||
totalLabel?: string;
|
||||
totalValue?: string | number;
|
||||
totalColumnKey?: string; // 합계가 들어갈 컬럼 키
|
||||
footerSummary?: FooterSummaryItem[]; // 하단 다중 합계 섹션
|
||||
}
|
||||
|
||||
// 비교 박스 아이템 타입
|
||||
export interface ComparisonBoxItem {
|
||||
label: string;
|
||||
value: string | number;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
// 비교 박스 설정 타입
|
||||
export interface ComparisonBoxConfig {
|
||||
title: string;
|
||||
items: ComparisonBoxItem[];
|
||||
borderColor: 'orange' | 'blue';
|
||||
}
|
||||
|
||||
// VS 비교 섹션 설정 타입
|
||||
export interface ComparisonSectionConfig {
|
||||
leftBox: ComparisonBoxConfig;
|
||||
rightBox: ComparisonBoxConfig;
|
||||
vsLabel: string;
|
||||
vsValue: string | number;
|
||||
vsSubLabel?: string;
|
||||
}
|
||||
|
||||
// 참조 테이블 설정 타입 (필터 없는 정보성 테이블)
|
||||
export interface ReferenceTableConfig {
|
||||
title: string;
|
||||
columns: TableColumnConfig[];
|
||||
data: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
// 상세 모달 전체 설정 타입
|
||||
@@ -310,6 +349,8 @@ export interface DetailModalConfig {
|
||||
barChart?: BarChartConfig;
|
||||
pieChart?: PieChartConfig;
|
||||
horizontalBarChart?: HorizontalBarChartConfig; // 가로 막대 차트 (도넛 차트 대신 사용)
|
||||
comparisonSection?: ComparisonSectionConfig; // VS 비교 섹션
|
||||
referenceTable?: ReferenceTableConfig; // 참조 테이블 (필터 없음)
|
||||
table?: TableConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { ChevronRight, Circle } from 'lucide-react';
|
||||
import type { MenuItem } from '@/store/menuStore';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
@@ -13,6 +13,230 @@ interface SidebarProps {
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
// 재귀적 메뉴 아이템 컴포넌트 Props
|
||||
interface MenuItemComponentProps {
|
||||
item: MenuItem;
|
||||
depth: number; // 0: 1depth, 1: 2depth, 2: 3depth
|
||||
activeMenu: string;
|
||||
expandedMenus: string[];
|
||||
sidebarCollapsed: boolean;
|
||||
isMobile: boolean;
|
||||
activeMenuRef: React.RefObject<HTMLDivElement | null>;
|
||||
onMenuClick: (menuId: string, path: string) => void;
|
||||
onToggleSubmenu: (menuId: string) => void;
|
||||
onCloseMobileSidebar?: () => void;
|
||||
}
|
||||
|
||||
// 재귀적 메뉴 아이템 컴포넌트 (3depth 이상 지원)
|
||||
function MenuItemComponent({
|
||||
item,
|
||||
depth,
|
||||
activeMenu,
|
||||
expandedMenus,
|
||||
sidebarCollapsed,
|
||||
isMobile,
|
||||
activeMenuRef,
|
||||
onMenuClick,
|
||||
onToggleSubmenu,
|
||||
onCloseMobileSidebar,
|
||||
}: MenuItemComponentProps) {
|
||||
const IconComponent = item.icon;
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedMenus.includes(item.id);
|
||||
const isActive = activeMenu === item.id;
|
||||
|
||||
const handleClick = () => {
|
||||
if (hasChildren) {
|
||||
onToggleSubmenu(item.id);
|
||||
} else {
|
||||
onMenuClick(item.id, item.path);
|
||||
if (isMobile && onCloseMobileSidebar) {
|
||||
onCloseMobileSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// depth별 스타일 설정
|
||||
// 1depth (depth=0): 아이콘 + 굵은 텍스트 + 배경색
|
||||
// 2depth (depth=1): 작은 아이콘 + 일반 텍스트 + 왼쪽 보더
|
||||
// 3depth (depth=2+): 점(dot) 아이콘 + 작은 텍스트 + 더 깊은 들여쓰기
|
||||
const is1Depth = depth === 0;
|
||||
const is2Depth = depth === 1;
|
||||
const is3DepthOrMore = depth >= 2;
|
||||
|
||||
// 1depth 메뉴 렌더링
|
||||
if (is1Depth) {
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
ref={isActive ? activeMenuRef : null}
|
||||
>
|
||||
{/* 메인 메뉴 버튼 */}
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={`w-full flex items-center rounded-xl transition-all duration-200 ease-out touch-manipulation group relative overflow-hidden sidebar-menu-item ${
|
||||
sidebarCollapsed ? 'p-3 justify-center' : 'space-x-2.5 p-3 md:p-3.5'
|
||||
} ${
|
||||
isActive
|
||||
? "text-white clean-shadow scale-[0.98]"
|
||||
: "text-foreground hover:bg-accent hover:scale-[1.02] active:scale-[0.98]"
|
||||
}`}
|
||||
style={isActive ? { backgroundColor: '#3B82F6' } : {}}
|
||||
title={sidebarCollapsed ? item.label : undefined}
|
||||
>
|
||||
<div className={`rounded-lg flex items-center justify-center transition-all duration-200 sidebar-menu-icon aspect-square ${
|
||||
sidebarCollapsed ? 'w-7' : 'w-8'
|
||||
} ${
|
||||
isActive
|
||||
? "bg-white/20"
|
||||
: "bg-primary/10 group-hover:bg-primary/20"
|
||||
}`}>
|
||||
{IconComponent && <IconComponent className={`transition-all duration-200 ${
|
||||
sidebarCollapsed ? 'h-4 w-4' : 'h-5 w-5'
|
||||
} ${
|
||||
isActive ? "text-white" : "text-primary"
|
||||
}`} />}
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="flex-1 font-medium transition-all duration-200 opacity-100 text-left text-sm">{item.label}</span>
|
||||
{hasChildren && (
|
||||
<div className={`transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90' : ''
|
||||
}`}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isActive && !sidebarCollapsed && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 자식 메뉴 (재귀) */}
|
||||
{hasChildren && isExpanded && !sidebarCollapsed && (
|
||||
<div className="mt-1.5 ml-3 space-y-1 border-l-2 border-primary/20 pl-3">
|
||||
{item.children?.map((child) => (
|
||||
<MenuItemComponent
|
||||
key={child.id}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
activeMenu={activeMenu}
|
||||
expandedMenus={expandedMenus}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
activeMenuRef={activeMenuRef}
|
||||
onMenuClick={onMenuClick}
|
||||
onToggleSubmenu={onToggleSubmenu}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2depth 메뉴 렌더링
|
||||
if (is2Depth) {
|
||||
return (
|
||||
<div ref={isActive ? activeMenuRef : null}>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={`w-full flex items-center rounded-lg transition-all duration-200 p-2.5 space-x-2.5 group ${
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{IconComponent && <IconComponent className="h-4 w-4 flex-shrink-0" />}
|
||||
<span className="flex-1 text-sm font-medium text-left">{item.label}</span>
|
||||
{hasChildren && (
|
||||
<div className={`transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90' : ''
|
||||
}`}>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 자식 메뉴 (3depth) */}
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="mt-1 ml-2 space-y-0.5 border-l border-border/50 pl-2">
|
||||
{item.children?.map((child) => (
|
||||
<MenuItemComponent
|
||||
key={child.id}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
activeMenu={activeMenu}
|
||||
expandedMenus={expandedMenus}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
activeMenuRef={activeMenuRef}
|
||||
onMenuClick={onMenuClick}
|
||||
onToggleSubmenu={onToggleSubmenu}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 3depth 이상 메뉴 렌더링 (점 아이콘 + 작은 텍스트)
|
||||
if (is3DepthOrMore) {
|
||||
return (
|
||||
<div ref={isActive ? activeMenuRef : null}>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={`w-full flex items-center rounded-md transition-all duration-200 p-2 space-x-2 group ${
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Circle className={`h-1.5 w-1.5 flex-shrink-0 ${
|
||||
isActive ? 'fill-primary text-primary' : 'fill-muted-foreground/50 text-muted-foreground/50'
|
||||
}`} />
|
||||
<span className="flex-1 text-xs text-left">{item.label}</span>
|
||||
{hasChildren && (
|
||||
<div className={`transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90' : ''
|
||||
}`}>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 자식 메뉴 (4depth 이상 - 재귀) */}
|
||||
{hasChildren && isExpanded && (
|
||||
<div className="mt-0.5 ml-1.5 space-y-0.5 border-l border-border/30 pl-1.5">
|
||||
{item.children?.map((child) => (
|
||||
<MenuItemComponent
|
||||
key={child.id}
|
||||
item={child}
|
||||
depth={depth + 1}
|
||||
activeMenu={activeMenu}
|
||||
expandedMenus={expandedMenus}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
activeMenuRef={activeMenuRef}
|
||||
onMenuClick={onMenuClick}
|
||||
onToggleSubmenu={onToggleSubmenu}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Sidebar({
|
||||
menuItems,
|
||||
activeMenu,
|
||||
@@ -24,9 +248,7 @@ export default function Sidebar({
|
||||
onCloseMobileSidebar,
|
||||
}: SidebarProps) {
|
||||
// 활성 메뉴 자동 스크롤을 위한 ref
|
||||
// eslint-disable-next-line no-undef
|
||||
const activeMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
// eslint-disable-next-line no-undef
|
||||
const menuContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// 활성 메뉴가 변경될 때 자동 스크롤
|
||||
@@ -39,18 +261,7 @@ export default function Sidebar({
|
||||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [activeMenu]); // activeMenu 변경 시에만 스크롤 (메뉴 클릭 시)
|
||||
|
||||
const handleMenuClick = (menuId: string, path: string, hasChildren: boolean) => {
|
||||
if (hasChildren) {
|
||||
onToggleSubmenu(menuId);
|
||||
} else {
|
||||
onMenuClick(menuId, path);
|
||||
if (isMobile && onCloseMobileSidebar) {
|
||||
onCloseMobileSidebar();
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [activeMenu]);
|
||||
|
||||
return (
|
||||
<div className={`h-full flex flex-col clean-glass rounded-2xl overflow-hidden transition-all duration-300 ${
|
||||
@@ -66,91 +277,21 @@ export default function Sidebar({
|
||||
<div className={`transition-all duration-300 ${
|
||||
sidebarCollapsed ? 'space-y-1.5 mt-4' : 'space-y-1.5 mt-3'
|
||||
}`}>
|
||||
{menuItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedMenus.includes(item.id);
|
||||
const isActive = activeMenu === item.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="relative"
|
||||
ref={isActive ? activeMenuRef : null}
|
||||
>
|
||||
{/* 메인 메뉴 버튼 */}
|
||||
<button
|
||||
onClick={() => handleMenuClick(item.id, item.path, !!hasChildren)}
|
||||
className={`w-full flex items-center rounded-xl transition-all duration-200 ease-out touch-manipulation group relative overflow-hidden sidebar-menu-item ${
|
||||
sidebarCollapsed ? 'p-3 justify-center' : 'space-x-2.5 p-3 md:p-3.5'
|
||||
} ${
|
||||
isActive
|
||||
? "text-white clean-shadow scale-[0.98]"
|
||||
: "text-foreground hover:bg-accent hover:scale-[1.02] active:scale-[0.98]"
|
||||
}`}
|
||||
style={isActive ? { backgroundColor: '#3B82F6' } : {}}
|
||||
title={sidebarCollapsed ? item.label : undefined}
|
||||
>
|
||||
<div className={`rounded-lg flex items-center justify-center transition-all duration-200 sidebar-menu-icon aspect-square ${
|
||||
sidebarCollapsed ? 'w-7' : 'w-8'
|
||||
} ${
|
||||
isActive
|
||||
? "bg-white/20"
|
||||
: "bg-primary/10 group-hover:bg-primary/20"
|
||||
}`}>
|
||||
{IconComponent && <IconComponent className={`transition-all duration-200 ${
|
||||
sidebarCollapsed ? 'h-4 w-4' : 'h-5 w-5'
|
||||
} ${
|
||||
isActive ? "text-white" : "text-primary"
|
||||
}`} />}
|
||||
</div>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="flex-1 font-medium transition-all duration-200 opacity-100 text-left text-sm">{item.label}</span>
|
||||
{hasChildren && (
|
||||
<div className={`transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-90' : ''
|
||||
}`}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isActive && !sidebarCollapsed && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 서브메뉴 */}
|
||||
{hasChildren && isExpanded && !sidebarCollapsed && (
|
||||
<div className="mt-1.5 ml-3 space-y-1.5 border-l-2 border-primary/20 pl-3">
|
||||
{item.children?.map((subItem) => {
|
||||
const SubIcon = subItem.icon;
|
||||
const isSubActive = activeMenu === subItem.id;
|
||||
return (
|
||||
<div
|
||||
key={subItem.id}
|
||||
ref={isSubActive ? activeMenuRef : null}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleMenuClick(subItem.id, subItem.path, false)}
|
||||
className={`w-full flex items-center rounded-lg transition-all duration-200 p-2.5 space-x-2.5 group ${
|
||||
isSubActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<SubIcon className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">{subItem.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{menuItems.map((item) => (
|
||||
<MenuItemComponent
|
||||
key={item.id}
|
||||
item={item}
|
||||
depth={0}
|
||||
activeMenu={activeMenu}
|
||||
expandedMenus={expandedMenus}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
isMobile={isMobile}
|
||||
activeMenuRef={activeMenuRef}
|
||||
onMenuClick={onMenuClick}
|
||||
onToggleSubmenu={onToggleSubmenu}
|
||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user