feat(WEB): CEO 대시보드 전 섹션 공통 컴포넌트 기반 리팩토링
- EnhancedSections 공통 컴포넌트 추출 (SectionCard, StatItem, StatusBadge 등) - 전 섹션(매출/매입/생산/출근/미출하/건설/캘린더/일보 등) 공통 패턴 적용 - components.tsx 공통 UI 컴포넌트 강화 - CLAUDE.md Git Workflow 섹션 추가 (develop/stage/main 플로우) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Check,
|
||||
AlertTriangle,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
AlertCircle,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
ChevronDown,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
@@ -18,18 +20,18 @@ import type { CheckPoint, CheckPointType, AmountCard, HighlightColor } from './t
|
||||
// 섹션별 컬러 테마 타입
|
||||
export type SectionColorTheme = 'blue' | 'purple' | 'orange' | 'green' | 'red' | 'amber' | 'cyan' | 'pink' | 'emerald' | 'indigo';
|
||||
|
||||
// 컬러 테마별 스타일
|
||||
export const SECTION_THEME_STYLES: Record<SectionColorTheme, { bg: string; border: string; iconBg: string; labelColor: string; accentColor: string }> = {
|
||||
blue: { bg: '#eff6ff', border: '#bfdbfe', iconBg: '#3b82f6', labelColor: '#1d4ed8', accentColor: '#3b82f6' },
|
||||
purple: { bg: '#faf5ff', border: '#e9d5ff', iconBg: '#a855f7', labelColor: '#7c3aed', accentColor: '#a855f7' },
|
||||
orange: { bg: '#fff7ed', border: '#fed7aa', iconBg: '#f97316', labelColor: '#ea580c', accentColor: '#f97316' },
|
||||
green: { bg: '#f0fdf4', border: '#bbf7d0', iconBg: '#22c55e', labelColor: '#16a34a', accentColor: '#22c55e' },
|
||||
red: { bg: '#fef2f2', border: '#fecaca', iconBg: '#ef4444', labelColor: '#dc2626', accentColor: '#ef4444' },
|
||||
amber: { bg: '#fffbeb', border: '#fde68a', iconBg: '#f59e0b', labelColor: '#d97706', accentColor: '#f59e0b' },
|
||||
cyan: { bg: '#ecfeff', border: '#a5f3fc', iconBg: '#06b6d4', labelColor: '#0891b2', accentColor: '#06b6d4' },
|
||||
pink: { bg: '#fdf2f8', border: '#fbcfe8', iconBg: '#ec4899', labelColor: '#db2777', accentColor: '#ec4899' },
|
||||
emerald: { bg: '#ecfdf5', border: '#a7f3d0', iconBg: '#10b981', labelColor: '#059669', accentColor: '#10b981' },
|
||||
indigo: { bg: '#eef2ff', border: '#c7d2fe', iconBg: '#6366f1', labelColor: '#4f46e5', accentColor: '#6366f1' },
|
||||
// 컬러 테마별 스타일 (다크모드 지원 Tailwind 클래스)
|
||||
export const SECTION_THEME_STYLES: Record<SectionColorTheme, { bgClass: string; borderClass: string; iconBg: string; labelClass: string; accentColor: string }> = {
|
||||
blue: { bgClass: 'bg-blue-50 dark:bg-blue-900/30', borderClass: 'border-blue-200 dark:border-blue-800', iconBg: '#3b82f6', labelClass: 'text-blue-700 dark:text-blue-300', accentColor: '#3b82f6' },
|
||||
purple: { bgClass: 'bg-purple-50 dark:bg-purple-900/30', borderClass: 'border-purple-200 dark:border-purple-800', iconBg: '#a855f7', labelClass: 'text-purple-700 dark:text-purple-300', accentColor: '#a855f7' },
|
||||
orange: { bgClass: 'bg-orange-50 dark:bg-orange-900/30', borderClass: 'border-orange-200 dark:border-orange-800', iconBg: '#f97316', labelClass: 'text-orange-700 dark:text-orange-300', accentColor: '#f97316' },
|
||||
green: { bgClass: 'bg-green-50 dark:bg-green-900/30', borderClass: 'border-green-200 dark:border-green-800', iconBg: '#22c55e', labelClass: 'text-green-700 dark:text-green-300', accentColor: '#22c55e' },
|
||||
red: { bgClass: 'bg-red-50 dark:bg-red-900/30', borderClass: 'border-red-200 dark:border-red-800', iconBg: '#ef4444', labelClass: 'text-red-700 dark:text-red-300', accentColor: '#ef4444' },
|
||||
amber: { bgClass: 'bg-amber-50 dark:bg-amber-900/30', borderClass: 'border-amber-200 dark:border-amber-800', iconBg: '#f59e0b', labelClass: 'text-amber-700 dark:text-amber-300', accentColor: '#f59e0b' },
|
||||
cyan: { bgClass: 'bg-cyan-50 dark:bg-cyan-900/30', borderClass: 'border-cyan-200 dark:border-cyan-800', iconBg: '#06b6d4', labelClass: 'text-cyan-700 dark:text-cyan-300', accentColor: '#06b6d4' },
|
||||
pink: { bgClass: 'bg-pink-50 dark:bg-pink-900/30', borderClass: 'border-pink-200 dark:border-pink-800', iconBg: '#ec4899', labelClass: 'text-pink-700 dark:text-pink-300', accentColor: '#ec4899' },
|
||||
emerald: { bgClass: 'bg-emerald-50 dark:bg-emerald-900/30', borderClass: 'border-emerald-200 dark:border-emerald-800', iconBg: '#10b981', labelClass: 'text-emerald-700 dark:text-emerald-300', accentColor: '#10b981' },
|
||||
indigo: { bgClass: 'bg-indigo-50 dark:bg-indigo-900/30', borderClass: 'border-indigo-200 dark:border-indigo-800', iconBg: '#6366f1', labelClass: 'text-indigo-700 dark:text-indigo-300', accentColor: '#6366f1' },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -249,31 +251,21 @@ export const AmountCardItem = ({
|
||||
return formatKoreanAmount(amount);
|
||||
};
|
||||
|
||||
// 테마 적용 시 스타일
|
||||
const cardStyle = themeStyle && !card.isHighlighted ? {
|
||||
backgroundColor: themeStyle.bg,
|
||||
borderColor: themeStyle.border,
|
||||
} : undefined;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'relative',
|
||||
'relative border',
|
||||
onClick && 'cursor-pointer hover:shadow-lg transition-all hover:scale-[1.02]',
|
||||
card.isHighlighted && 'border-red-300 bg-red-50',
|
||||
!themeStyle && !card.isHighlighted && 'border',
|
||||
card.isHighlighted && 'border-red-300 bg-red-50 dark:border-red-700 dark:bg-red-900/30',
|
||||
themeStyle && !card.isHighlighted && cn(themeStyle.bgClass, themeStyle.borderClass),
|
||||
className
|
||||
)}
|
||||
style={cardStyle}
|
||||
>
|
||||
{/* 건수 뱃지 (오른쪽 상단) */}
|
||||
{showCountBadge && card.subLabel && (
|
||||
<div
|
||||
className="absolute top-3 right-3 px-2 py-0.5 rounded-full text-xs font-bold"
|
||||
style={{
|
||||
backgroundColor: themeStyle ? themeStyle.iconBg : '#ef4444',
|
||||
color: '#ffffff'
|
||||
}}
|
||||
className="absolute top-3 right-3 px-2 py-0.5 rounded-full text-xs font-bold text-white"
|
||||
style={{ backgroundColor: themeStyle ? themeStyle.iconBg : '#ef4444' }}
|
||||
>
|
||||
{card.subLabel}
|
||||
</div>
|
||||
@@ -299,9 +291,8 @@ export const AmountCardItem = ({
|
||||
<p
|
||||
className={cn(
|
||||
"text-sm font-medium truncate",
|
||||
card.isHighlighted ? 'text-red-600' : 'text-muted-foreground'
|
||||
card.isHighlighted ? 'text-red-600' : themeStyle ? themeStyle.labelClass : 'text-muted-foreground'
|
||||
)}
|
||||
style={themeStyle && !card.isHighlighted ? { color: themeStyle.labelColor } : undefined}
|
||||
>
|
||||
{card.label}
|
||||
</p>
|
||||
@@ -309,7 +300,7 @@ export const AmountCardItem = ({
|
||||
|
||||
{/* 금액 */}
|
||||
<p className={cn(
|
||||
"text-2xl font-bold",
|
||||
"text-2xl font-bold text-foreground",
|
||||
card.isHighlighted && 'text-red-600'
|
||||
)}>
|
||||
{formatCardAmount(card.amount)}
|
||||
@@ -318,11 +309,12 @@ export const AmountCardItem = ({
|
||||
{/* 트렌드 표시 (pill 형태, 금액 아래에 배치) */}
|
||||
{showTrend && trendValue && (
|
||||
<div
|
||||
className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit"
|
||||
style={{
|
||||
backgroundColor: trendDirection === 'up' ? '#dcfce7' : '#fee2e2',
|
||||
color: trendDirection === 'up' ? '#16a34a' : '#dc2626'
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit",
|
||||
trendDirection === 'up'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||
)}
|
||||
>
|
||||
{trendDirection === 'up' ? (
|
||||
<TrendingUp className="h-3 w-3 shrink-0" />
|
||||
@@ -360,10 +352,12 @@ export const AmountCardItem = ({
|
||||
{card.subLabel && card.subAmount === undefined && !card.previousLabel && (
|
||||
subLabelAsBadge && themeStyle ? (
|
||||
<span
|
||||
className="text-xs font-medium px-2 py-0.5 rounded-full border truncate w-fit max-w-full"
|
||||
className={cn(
|
||||
"text-xs font-medium px-2 py-0.5 rounded-full border truncate w-fit max-w-full",
|
||||
themeStyle.labelClass
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `${themeStyle.iconBg}15`,
|
||||
color: themeStyle.labelColor,
|
||||
borderColor: `${themeStyle.iconBg}30`,
|
||||
}}
|
||||
>
|
||||
@@ -431,4 +425,69 @@ export const IssueCardItem = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 접기/펼치기 가능한 대시보드 카드
|
||||
* - 다크 헤더 + 흰색 바디 패턴의 공통 컴포넌트
|
||||
* - 헤더 클릭 시 바디 토글
|
||||
*/
|
||||
interface CollapsibleDashboardCardProps {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
rightElement?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
|
||||
export function CollapsibleDashboardCard({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
rightElement,
|
||||
children,
|
||||
defaultOpen = true,
|
||||
bodyClassName,
|
||||
}: CollapsibleDashboardCardProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border lg:overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'bg-slate-800 dark:bg-slate-700 px-6 py-4 sticky top-12 z-10 lg:static cursor-pointer select-none',
|
||||
isOpen ? 'rounded-t-xl' : 'rounded-xl'
|
||||
)}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3">
|
||||
<div className="bg-white/10 p-2 rounded-lg">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-center lg:text-left">
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
{subtitle && <p className="text-sm text-slate-300">{subtitle}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{rightElement}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 text-white/70 transition-transform duration-200',
|
||||
!isOpen && '-rotate-90'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className={cn('bg-card text-card-foreground', bodyClassName ?? 'p-6')}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
@@ -12,11 +11,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Plus, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Plus, ExternalLink, ChevronLeft, ChevronRight, CalendarDays } from 'lucide-react';
|
||||
import { ScheduleCalendar } from '@/components/common/ScheduleCalendar';
|
||||
import type { ScheduleEvent } from '@/components/common/ScheduleCalendar/types';
|
||||
import { getCalendarEventsForYear, type CalendarEvent } from '@/constants/calendarEvents';
|
||||
import { useCalendarScheduleStore } from '@/stores/useCalendarScheduleStore';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type {
|
||||
CalendarScheduleItem,
|
||||
CalendarViewType,
|
||||
@@ -46,16 +46,16 @@ const SCHEDULE_TYPE_COLORS: Record<string, string> = {
|
||||
|
||||
// 이슈 뱃지별 색상
|
||||
const ISSUE_BADGE_COLORS: Record<TodayIssueListBadgeType, string> = {
|
||||
'수주등록': 'bg-blue-100 text-blue-700',
|
||||
'추심이슈': 'bg-purple-100 text-purple-700',
|
||||
'안전재고': 'bg-orange-100 text-orange-700',
|
||||
'지출 승인대기': 'bg-green-100 text-green-700',
|
||||
'세금 신고': 'bg-red-100 text-red-700',
|
||||
'결재 요청': 'bg-yellow-100 text-yellow-700',
|
||||
'신규거래처': 'bg-emerald-100 text-emerald-700',
|
||||
'입금': 'bg-teal-100 text-teal-700',
|
||||
'출금': 'bg-pink-100 text-pink-700',
|
||||
'기타': 'bg-gray-100 text-gray-700',
|
||||
'수주등록': 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
||||
'추심이슈': 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
||||
'안전재고': 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
|
||||
'지출 승인대기': 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
||||
'세금 신고': 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
|
||||
'결재 요청': 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300',
|
||||
'신규거래처': 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
||||
'입금': 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300',
|
||||
'출금': 'bg-pink-100 text-pink-700 dark:bg-pink-900/40 dark:text-pink-300',
|
||||
'기타': 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
|
||||
};
|
||||
|
||||
// 부서 필터 옵션
|
||||
@@ -295,67 +295,15 @@ export function CalendarSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
{/* 모바일: 스티키 헤더 (타이틀+필터+월네비) */}
|
||||
<div className="lg:hidden sticky top-12 z-10 bg-white -mx-6 px-6 pt-2 pb-3 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||
<h3 className="text-lg font-semibold shrink-0">캘린더</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 부서 필터 */}
|
||||
<Select
|
||||
value={deptFilter}
|
||||
onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DEPT_FILTER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 업무 필터 */}
|
||||
<Select
|
||||
value={taskFilter}
|
||||
onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TASK_FILTER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 월 네비게이션 */}
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button variant="outline" size="sm" className="h-8 px-2" onClick={handleMobilePrevMonth}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm font-semibold whitespace-nowrap">
|
||||
{currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월
|
||||
</span>
|
||||
<Button variant="outline" size="sm" className="h-8 px-2" onClick={handleMobileNextMonth}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데스크탑: 일반 헤더 */}
|
||||
<div className="hidden lg:flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<h3 className="text-lg font-semibold shrink-0">캘린더</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<CalendarDays style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="캘린더"
|
||||
subtitle="일정 관리"
|
||||
>
|
||||
{/* 모바일: 필터+월네비 */}
|
||||
<div className="lg:hidden mb-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{/* 부서 필터 */}
|
||||
<Select
|
||||
value={deptFilter}
|
||||
onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)}
|
||||
@@ -372,6 +320,7 @@ export function CalendarSection({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 업무 필터 */}
|
||||
<Select
|
||||
value={taskFilter}
|
||||
onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)}
|
||||
@@ -388,32 +337,80 @@ export function CalendarSection({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 월 네비게이션 */}
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<Button variant="outline" size="sm" className="h-8 px-2" onClick={handleMobilePrevMonth}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-sm font-semibold text-foreground whitespace-nowrap">
|
||||
{currentDate.getFullYear()}년 {currentDate.getMonth() + 1}월
|
||||
</span>
|
||||
<Button variant="outline" size="sm" className="h-8 px-2" onClick={handleMobileNextMonth}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데스크탑: 필터 */}
|
||||
<div className="hidden lg:flex items-center justify-end mb-4 gap-2">
|
||||
<Select
|
||||
value={deptFilter}
|
||||
onValueChange={(value) => setDeptFilter(value as CalendarDeptFilterType)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DEPT_FILTER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={taskFilter}
|
||||
onValueChange={(value) => setTaskFilter(value as CalendarTaskFilterType)}
|
||||
>
|
||||
<SelectTrigger className="min-w-[80px] w-auto h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TASK_FILTER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 모바일: 리스트뷰 */}
|
||||
<div className="lg:hidden pt-3">
|
||||
|
||||
{/* 일별 리스트 */}
|
||||
<div className="space-y-1">
|
||||
<div className="divide-y divide-border">
|
||||
{monthDaysWithEvents.map((day) => {
|
||||
const hasEvents = day.events.length > 0;
|
||||
const isSelected = selectedDate && day.date.getTime() === selectedDate.getTime();
|
||||
return (
|
||||
<div
|
||||
key={day.dateStr}
|
||||
className={`rounded-lg px-3 py-2.5 cursor-pointer transition-colors ${
|
||||
isSelected ? 'bg-blue-50 ring-1 ring-blue-200' :
|
||||
day.isToday ? 'bg-amber-50' : ''
|
||||
className={`px-3 py-2.5 cursor-pointer transition-colors ${
|
||||
isSelected ? 'bg-blue-50 dark:bg-blue-900/30' :
|
||||
day.isToday ? 'bg-amber-50 dark:bg-amber-900/30' : ''
|
||||
} ${!hasEvents && !day.isToday && !isSelected ? 'opacity-50' : ''}`}
|
||||
onClick={() => handleDateClick(day.date)}
|
||||
>
|
||||
{/* 날짜 + 일정등록 버튼 */}
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className={`text-sm font-medium ${
|
||||
day.isWeekend ? 'text-red-500' : 'text-gray-900'
|
||||
day.isWeekend ? 'text-red-500' : 'text-foreground'
|
||||
} ${day.isToday ? 'font-bold' : ''}`}>
|
||||
{day.label}
|
||||
{day.isToday && <span className="ml-1 text-amber-600 text-xs font-semibold">(오늘)</span>}
|
||||
{day.isToday && <span className="ml-1 text-amber-600 dark:text-amber-400 text-xs font-semibold">(오늘)</span>}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Button
|
||||
@@ -440,7 +437,7 @@ export function CalendarSection({
|
||||
{/* 이벤트 목록 (날짜 아래) */}
|
||||
{hasEvents ? (
|
||||
<div className="space-y-1 pl-1">
|
||||
{day.events.slice(0, 3).map((ev) => {
|
||||
{(isSelected ? day.events : day.events.slice(0, 3)).map((ev) => {
|
||||
const evData = ev.data as Record<string, unknown>;
|
||||
const evType = evData?._type as string;
|
||||
const colorMap: Record<string, string> = {
|
||||
@@ -455,14 +452,14 @@ export function CalendarSection({
|
||||
const title = evData?.name as string || evData?.title as string || ev.title;
|
||||
const cleanTitle = title?.replace(/^[🔴🟠]\s*/, '') || '';
|
||||
return (
|
||||
<div key={ev.id} className="flex items-center gap-1.5 text-xs text-gray-700">
|
||||
<div key={ev.id} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<span className={`w-2 h-2 rounded-full shrink-0 ${dotColor}`} />
|
||||
<span className="truncate">{cleanTitle}</span>
|
||||
<span className={isSelected ? '' : 'truncate'}>{cleanTitle}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{day.events.length > 3 && (
|
||||
<div className="text-xs text-gray-400 pl-3.5">+{day.events.length - 3}건</div>
|
||||
{!isSelected && day.events.length > 3 && (
|
||||
<div className="text-xs text-muted-foreground pl-3.5">+{day.events.length - 3}건</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -486,14 +483,14 @@ export function CalendarSection({
|
||||
onMonthChange={handleMonthChange}
|
||||
maxEventsPerDay={4}
|
||||
weekStartsOn={1}
|
||||
className="[&_.weekend]:bg-yellow-50"
|
||||
className="[&_.weekend]:bg-yellow-50 dark:[&_.weekend]:bg-yellow-900/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 선택된 날짜 일정 + 이슈 목록 */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<div className="border border-border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-lg font-semibold">
|
||||
<h4 className="text-lg font-semibold text-foreground">
|
||||
{selectedDate ? formatSelectedDate(selectedDate) : '날짜를 선택하세요'}
|
||||
</h4>
|
||||
<Button
|
||||
@@ -537,13 +534,13 @@ export function CalendarSection({
|
||||
key={event.id}
|
||||
className={`p-3 rounded-lg ${
|
||||
isHoliday
|
||||
? 'bg-red-50 border border-red-200'
|
||||
: 'bg-orange-50 border border-orange-200'
|
||||
? 'bg-red-50 border border-red-200 dark:bg-red-900/30 dark:border-red-800'
|
||||
: 'bg-orange-50 border border-orange-200 dark:bg-orange-900/30 dark:border-orange-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{isHoliday ? '🔴' : '🟠'}</span>
|
||||
<span className="font-medium">{eventData.name}</span>
|
||||
<span className="font-medium text-foreground">{eventData.name}</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{isHoliday ? '공휴일' : '세금 신고 마감일'}
|
||||
@@ -555,10 +552,10 @@ export function CalendarSection({
|
||||
{selectedDateItems.schedules.map((schedule) => (
|
||||
<div
|
||||
key={schedule.id}
|
||||
className="p-3 border rounded-lg hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
className="p-3 border border-border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer"
|
||||
onClick={() => onScheduleClick?.(schedule)}
|
||||
>
|
||||
<div className="font-medium text-base mb-1">{schedule.title}</div>
|
||||
<div className="font-medium text-base text-foreground mb-1">{schedule.title}</div>
|
||||
<div className="text-sm text-muted-foreground">{formatScheduleDetail(schedule)}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -566,7 +563,7 @@ export function CalendarSection({
|
||||
{selectedDateItems.issues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="p-3 border border-red-200 rounded-lg hover:bg-red-50 transition-colors cursor-pointer"
|
||||
className="p-3 border border-red-200 dark:border-red-800 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors cursor-pointer"
|
||||
onClick={() => {
|
||||
if (issue.path) router.push(`/ko${issue.path}`);
|
||||
}}
|
||||
@@ -578,12 +575,12 @@ export function CalendarSection({
|
||||
>
|
||||
{issue.badge}
|
||||
</Badge>
|
||||
<span className="font-medium text-sm flex-1">{issue.content}</span>
|
||||
<span className="font-medium text-sm text-foreground flex-1">{issue.content}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{issue.time}</span>
|
||||
{issue.path && (
|
||||
<span className="flex items-center gap-1 text-blue-600 hover:underline">
|
||||
<span className="flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline">
|
||||
상세보기
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</span>
|
||||
@@ -595,7 +592,6 @@ export function CalendarSection({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { CreditCard, Wallet, Receipt, AlertTriangle } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem, type SectionColorTheme } from '../components';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard, type SectionColorTheme } from '../components';
|
||||
import type { CardManagementData } from '../types';
|
||||
|
||||
// 카드별 아이콘 매핑
|
||||
@@ -27,45 +26,40 @@ export function CardManagementSection({ data, onCardClick }: CardManagementSecti
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle
|
||||
title="카드/가지급금 관리"
|
||||
badge="warning"
|
||||
icon={CreditCard}
|
||||
colorTheme="blue"
|
||||
/>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<CreditCard style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="카드/가지급금 관리"
|
||||
subtitle="카드 및 가지급금 현황"
|
||||
>
|
||||
{data.warningBanner && (
|
||||
<div className="bg-red-500 text-white text-sm font-medium px-4 py-2 rounded-lg mb-4 flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{data.warningBanner}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.warningBanner && (
|
||||
<div className="bg-red-500 text-white text-sm font-medium px-4 py-2 rounded-lg mb-4 flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{data.warningBanner}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={() => handleClick(card.id)}
|
||||
icon={CARD_ICONS[idx] || CreditCard}
|
||||
colorTheme={CARD_THEMES[idx] || 'blue'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="up"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={() => handleClick(card.id)}
|
||||
icon={CARD_ICONS[idx] || CreditCard}
|
||||
colorTheme={CARD_THEMES[idx] || 'blue'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="up"
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Building2,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { ConstructionData } from '../types';
|
||||
|
||||
interface ConstructionSectionProps {
|
||||
@@ -14,75 +15,64 @@ interface ConstructionSectionProps {
|
||||
|
||||
export function ConstructionSection({ data }: ConstructionSectionProps) {
|
||||
return (
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<HardHat style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">시공 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">이달 시공 진행 현황</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#3b82f6', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
{data.thisMonth}건
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<HardHat style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="시공 현황"
|
||||
subtitle="이달 시공 진행 현황"
|
||||
rightElement={
|
||||
<Badge
|
||||
className="bg-blue-500 text-white border-none hover:opacity-90"
|
||||
>
|
||||
{data.thisMonth}건
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 시공 요약 카드 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
||||
<div className="rounded-lg border p-4" style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}>
|
||||
<div className="rounded-lg border p-4 bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<HardHat className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-blue-700">시공 진행 (7일 이내)</span>
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">시공 진행 (7일 이내)</span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">{data.thisMonth}건</span>
|
||||
<span className="text-2xl font-bold text-foreground">{data.thisMonth}건</span>
|
||||
</div>
|
||||
<div className="rounded-lg border p-4" style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}>
|
||||
<div className="rounded-lg border p-4 bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Building2 className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-green-700">시공 완료 (7일 이내)</span>
|
||||
<span className="text-sm font-medium text-green-700 dark:text-green-300">시공 완료 (7일 이내)</span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">{data.completed}건</span>
|
||||
<span className="text-2xl font-bold text-foreground">{data.completed}건</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 시공 상세 카드 목록 */}
|
||||
<div className="space-y-3">
|
||||
{data.items.map((item) => (
|
||||
<div key={item.id} className="border rounded-lg p-4 hover:bg-gray-50 transition-colors">
|
||||
<div key={item.id} className="border border-border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-900">{item.siteName}</span>
|
||||
<span className="text-sm font-semibold text-foreground">{item.siteName}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
item.status === '진행중'
|
||||
? 'text-blue-600 border-blue-200 bg-blue-50'
|
||||
? 'text-blue-600 border-blue-200 bg-blue-50 dark:text-blue-400 dark:border-blue-800 dark:bg-blue-900/30'
|
||||
: item.status === '예정'
|
||||
? 'text-gray-600 border-gray-200 bg-gray-50'
|
||||
: 'text-green-600 border-green-200 bg-green-50'
|
||||
? 'text-muted-foreground border-border bg-muted/50'
|
||||
: 'text-green-600 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/30'
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{item.client}</span>
|
||||
<span className="text-sm text-muted-foreground">{item.client}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 mb-2">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mb-2">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{item.startDate} ~ {item.endDate}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
@@ -91,12 +81,11 @@ export function ConstructionSection({ data }: ConstructionSectionProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-gray-600 min-w-[36px] text-right">{item.progress}%</span>
|
||||
<span className="text-xs font-medium text-muted-foreground min-w-[36px] text-right">{item.progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { DailyAttendanceData } from '../types';
|
||||
|
||||
interface DailyAttendanceSectionProps {
|
||||
@@ -35,79 +36,69 @@ export function DailyAttendanceSection({ data }: DailyAttendanceSectionProps) {
|
||||
|
||||
const statusBadgeClass = (status: string) => {
|
||||
switch (status) {
|
||||
case '출근': return 'text-green-600 border-green-200 bg-green-50';
|
||||
case '휴가': return 'text-blue-600 border-blue-200 bg-blue-50';
|
||||
case '지각': return 'text-orange-600 border-orange-200 bg-orange-50';
|
||||
case '결근': return 'text-red-600 border-red-200 bg-red-50';
|
||||
default: return 'text-gray-600 border-gray-200 bg-gray-50';
|
||||
case '출근': return 'text-green-600 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/30';
|
||||
case '휴가': return 'text-blue-600 border-blue-200 bg-blue-50 dark:text-blue-400 dark:border-blue-800 dark:bg-blue-900/30';
|
||||
case '지각': return 'text-orange-600 border-orange-200 bg-orange-50 dark:text-orange-400 dark:border-orange-800 dark:bg-orange-900/30';
|
||||
case '결근': return 'text-red-600 border-red-200 bg-red-50 dark:text-red-400 dark:border-red-800 dark:bg-red-900/30';
|
||||
default: return 'text-muted-foreground border-border bg-muted/50';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<Users style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">당일 근태 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">오늘의 출근 현황</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => router.push('/ko/hr/attendance')}
|
||||
className="text-white hover:bg-white/10 gap-1 text-xs"
|
||||
>
|
||||
근태관리
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Users style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="당일 근태 현황"
|
||||
subtitle="오늘의 출근 현황"
|
||||
rightElement={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); router.push('/ko/hr/attendance'); }}
|
||||
className="text-white hover:bg-white/10 gap-1 text-xs"
|
||||
>
|
||||
근태관리
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* 요약 카드 4개 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6">
|
||||
<div className="rounded-lg border p-3 text-center" style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}>
|
||||
<div className="rounded-lg border p-3 text-center bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<UserCheck className="h-3.5 w-3.5 text-green-500" />
|
||||
<span className="text-xs font-medium text-green-600">출근</span>
|
||||
<span className="text-xs font-medium text-green-600 dark:text-green-400">출근</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900">{data.present}명</span>
|
||||
<span className="text-xl font-bold text-foreground">{data.present}명</span>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3 text-center" style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}>
|
||||
<div className="rounded-lg border p-3 text-center bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<Palmtree className="h-3.5 w-3.5 text-blue-500" />
|
||||
<span className="text-xs font-medium text-blue-600">휴가</span>
|
||||
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">휴가</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900">{data.onLeave}명</span>
|
||||
<span className="text-xl font-bold text-foreground">{data.onLeave}명</span>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3 text-center" style={{ backgroundColor: '#fff7ed', borderColor: '#fed7aa' }}>
|
||||
<div className="rounded-lg border p-3 text-center bg-orange-50 dark:bg-orange-900/30 border-orange-200 dark:border-orange-800">
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<Clock className="h-3.5 w-3.5 text-orange-500" />
|
||||
<span className="text-xs font-medium text-orange-600">지각</span>
|
||||
<span className="text-xs font-medium text-orange-600 dark:text-orange-400">지각</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900">{data.late}명</span>
|
||||
<span className="text-xl font-bold text-foreground">{data.late}명</span>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3 text-center" style={{ backgroundColor: '#fef2f2', borderColor: '#fecaca' }}>
|
||||
<div className="rounded-lg border p-3 text-center bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800">
|
||||
<div className="flex items-center justify-center gap-1 mb-1">
|
||||
<UserX className="h-3.5 w-3.5 text-red-500" />
|
||||
<span className="text-xs font-medium text-red-600">결근</span>
|
||||
<span className="text-xs font-medium text-red-600 dark:text-red-400">결근</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900">{data.absent}명</span>
|
||||
<span className="text-xl font-bold text-foreground">{data.absent}명</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 border-b">
|
||||
<h4 className="text-sm font-semibold text-gray-700">직원 근태 목록</h4>
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">직원 근태 목록</h4>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-xs">
|
||||
<SelectTrigger className="w-full h-8 text-xs">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -122,21 +113,21 @@ export function DailyAttendanceSection({ data }: DailyAttendanceSectionProps) {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[400px]">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b">
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium w-12">No</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">부서</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">직급</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">이름</th>
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium">상태</th>
|
||||
<tr className="bg-muted/50 border-b border-border">
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium w-12">No</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">부서</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">직급</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">이름</th>
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredEmployees.map((emp, idx) => (
|
||||
<tr key={emp.id} className="border-b last:border-b-0 hover:bg-gray-50">
|
||||
<td className="px-4 py-2 text-center text-gray-500">{idx + 1}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{emp.department}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{emp.position}</td>
|
||||
<td className="px-4 py-2 text-gray-900 font-medium">{emp.name}</td>
|
||||
<tr key={emp.id} className="border-b border-border last:border-b-0 hover:bg-muted/50">
|
||||
<td className="px-4 py-2 text-center text-muted-foreground">{idx + 1}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{emp.department}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{emp.position}</td>
|
||||
<td className="px-4 py-2 text-foreground font-medium">{emp.name}</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<Badge variant="outline" className={statusBadgeClass(emp.status)}>
|
||||
{emp.status}
|
||||
@@ -148,7 +139,6 @@ export function DailyAttendanceSection({ data }: DailyAttendanceSectionProps) {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { formatKoreanAmount } from '@/lib/utils/amount';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { DailyProductionData } from '../types';
|
||||
|
||||
// 출고 현황 독립 섹션
|
||||
@@ -26,41 +27,30 @@ interface ShipmentSectionProps {
|
||||
|
||||
export function ShipmentSection({ data }: ShipmentSectionProps) {
|
||||
return (
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<Truck style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">출고 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">예상 출고 정보</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Truck style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="출고 현황"
|
||||
subtitle="예상 출고 정보"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="rounded-xl border p-4" style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}>
|
||||
<div className="rounded-xl border p-4 bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Package className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-gray-700">예상 출고 (7일 이내)</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">예상 출고 (7일 이내)</span>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.expectedAmount)}</p>
|
||||
<p className="text-xs text-gray-500">{data.shipment.expectedCount}건</p>
|
||||
<p className="text-xl font-bold text-foreground mb-1">{formatKoreanAmount(data.shipment.expectedAmount)}</p>
|
||||
<p className="text-xs text-muted-foreground">{data.shipment.expectedCount}건</p>
|
||||
</div>
|
||||
<div className="rounded-xl border p-4" style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}>
|
||||
<div className="rounded-xl border p-4 bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Truck className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-gray-700">예상 출고 (30일 이내)</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">예상 출고 (30일 이내)</span>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.actualAmount)}</p>
|
||||
<p className="text-xs text-gray-500">{data.shipment.actualCount}건</p>
|
||||
<p className="text-xl font-bold text-foreground mb-1">{formatKoreanAmount(data.shipment.actualAmount)}</p>
|
||||
<p className="text-xs text-muted-foreground">{data.shipment.actualCount}건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,29 +70,18 @@ export function DailyProductionSection({ data, showShipment = true }: DailyProdu
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<Factory style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">당일 생산 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">{data.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#8b5cf6', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
실시간
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Factory style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="당일 생산 현황"
|
||||
subtitle={data.date}
|
||||
rightElement={
|
||||
<Badge
|
||||
className="bg-violet-500 text-white border-none hover:opacity-90"
|
||||
>
|
||||
실시간
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 공정 탭 */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="mb-4">
|
||||
@@ -116,133 +95,133 @@ export function DailyProductionSection({ data, showShipment = true }: DailyProdu
|
||||
{data.processes.map((process) => (
|
||||
<TabsContent key={process.processName} value={process.processName}>
|
||||
{/* 요약 카드: 전체 작업 / 할일 / 작업중 / 완료 */}
|
||||
<div className="grid grid-cols-4 gap-3 mb-4">
|
||||
<div className="flex items-center gap-2 rounded-lg border p-3">
|
||||
<ClipboardList className="h-4 w-4 text-gray-500" />
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border p-3">
|
||||
<ClipboardList className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p className="text-[11px] text-gray-500">전체 작업</p>
|
||||
<p className="text-sm font-bold text-gray-900">{process.totalWork}건</p>
|
||||
<p className="text-[11px] text-muted-foreground">전체 작업</p>
|
||||
<p className="text-sm font-bold text-foreground">{process.totalWork}건</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-lg border p-3">
|
||||
<ListTodo className="h-4 w-4 text-orange-500" />
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border p-3">
|
||||
<ListTodo className="h-4 w-4 text-orange-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[11px] text-gray-500">할일</p>
|
||||
<p className="text-sm font-bold text-gray-900">{process.todo}건</p>
|
||||
<p className="text-[11px] text-muted-foreground">할일</p>
|
||||
<p className="text-sm font-bold text-foreground">{process.todo}건</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-lg border p-3">
|
||||
<Play className="h-4 w-4 text-blue-500" />
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border p-3">
|
||||
<Play className="h-4 w-4 text-blue-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[11px] text-gray-500">작업중</p>
|
||||
<p className="text-sm font-bold text-gray-900">{process.inProgress}건</p>
|
||||
<p className="text-[11px] text-muted-foreground">작업중</p>
|
||||
<p className="text-sm font-bold text-foreground">{process.inProgress}건</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 rounded-lg border p-3">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<div className="flex items-center gap-2 rounded-lg border border-border p-3">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<div>
|
||||
<p className="text-[11px] text-gray-500">완료</p>
|
||||
<p className="text-sm font-bold text-gray-900">{process.completed}건</p>
|
||||
<p className="text-[11px] text-muted-foreground">완료</p>
|
||||
<p className="text-sm font-bold text-foreground">{process.completed}건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4열 카드 레이아웃: 긴급 / 우선 / 일반 / 작업자 현황 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* 4열 카드 레이아웃: 긴급 / 우선 / 일반 / 작업자 현황 - 모바일 가로스크롤 */}
|
||||
<div className="-mx-6 px-6 flex gap-4 overflow-x-auto pb-2 lg:mx-0 lg:px-0 lg:grid lg:grid-cols-4 lg:overflow-visible lg:pb-0">
|
||||
{/* 긴급 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b" style={{ backgroundColor: '#fef2f2' }}>
|
||||
<div className="min-w-[200px] shrink-0 lg:min-w-0 lg:shrink border border-border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b border-border bg-red-50 dark:bg-red-900/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
|
||||
<span className="text-xs font-semibold text-red-600">긴급</span>
|
||||
<span className="text-xs font-semibold text-red-600 dark:text-red-400">긴급</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-900">{process.urgent}건</span>
|
||||
<span className="text-sm font-bold text-foreground">{process.urgent}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 space-y-2">
|
||||
<div className="p-2 space-y-2 max-h-[240px] overflow-y-auto">
|
||||
{process.workItems
|
||||
.filter((item) => item.status === '진행중')
|
||||
.slice(0, process.urgent)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="border rounded p-2 hover:bg-gray-50 cursor-pointer transition-colors">
|
||||
<p className="text-xs font-medium text-gray-900 truncate">{item.client}</p>
|
||||
<p className="text-[11px] text-gray-500">{item.product}</p>
|
||||
<p className="text-[11px] text-gray-400">{item.quantity}건</p>
|
||||
<div key={item.id} className="border border-border rounded p-2 hover:bg-muted/50 cursor-pointer transition-colors">
|
||||
<p className="text-xs font-medium text-foreground truncate">{item.client}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{item.product}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{item.quantity}건</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우선 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b" style={{ backgroundColor: '#fff7ed' }}>
|
||||
<div className="min-w-[200px] shrink-0 lg:min-w-0 lg:shrink border border-border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b border-border bg-orange-50 dark:bg-orange-900/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Star className="h-3.5 w-3.5 text-orange-500" />
|
||||
<span className="text-xs font-semibold text-orange-600">우선</span>
|
||||
<span className="text-xs font-semibold text-orange-600 dark:text-orange-400">우선</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-900">{process.subLine}건</span>
|
||||
<span className="text-sm font-bold text-foreground">{process.subLine}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 space-y-2">
|
||||
<div className="p-2 space-y-2 max-h-[240px] overflow-y-auto">
|
||||
{process.workItems
|
||||
.filter((item) => item.status === '대기')
|
||||
.slice(0, process.subLine)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="border rounded p-2 hover:bg-gray-50 cursor-pointer transition-colors">
|
||||
<p className="text-xs font-medium text-gray-900 truncate">{item.client}</p>
|
||||
<p className="text-[11px] text-gray-500">{item.product}</p>
|
||||
<p className="text-[11px] text-gray-400">{item.quantity}건</p>
|
||||
<div key={item.id} className="border border-border rounded p-2 hover:bg-muted/50 cursor-pointer transition-colors">
|
||||
<p className="text-xs font-medium text-foreground truncate">{item.client}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{item.product}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{item.quantity}건</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 일반 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b" style={{ backgroundColor: '#eff6ff' }}>
|
||||
<div className="min-w-[200px] shrink-0 lg:min-w-0 lg:shrink border border-border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b border-border bg-blue-50 dark:bg-blue-900/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Layers className="h-3.5 w-3.5 text-blue-500" />
|
||||
<span className="text-xs font-semibold text-blue-600">일반</span>
|
||||
<span className="text-xs font-semibold text-blue-600 dark:text-blue-400">일반</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-900">{process.regular}건</span>
|
||||
<span className="text-sm font-bold text-foreground">{process.regular}건</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 space-y-2">
|
||||
<div className="p-2 space-y-2 max-h-[240px] overflow-y-auto">
|
||||
{process.workItems
|
||||
.slice(0, process.regular)
|
||||
.map((item) => (
|
||||
<div key={item.id} className="border rounded p-2 hover:bg-gray-50 cursor-pointer transition-colors">
|
||||
<p className="text-xs font-medium text-gray-900 truncate">{item.client}</p>
|
||||
<p className="text-[11px] text-gray-500">{item.product}</p>
|
||||
<p className="text-[11px] text-gray-400">{item.quantity}건</p>
|
||||
<div key={item.id} className="border border-border rounded p-2 hover:bg-muted/50 cursor-pointer transition-colors">
|
||||
<p className="text-xs font-medium text-foreground truncate">{item.client}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{item.product}</p>
|
||||
<p className="text-[11px] text-muted-foreground">{item.quantity}건</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 작업자 현황 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b" style={{ backgroundColor: '#f0fdf4' }}>
|
||||
<div className="min-w-[200px] shrink-0 lg:min-w-0 lg:shrink border border-border rounded-lg overflow-hidden">
|
||||
<div className="p-3 border-b border-border bg-green-50 dark:bg-green-900/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users className="h-3.5 w-3.5 text-green-500" />
|
||||
<span className="text-xs font-semibold text-green-600">작업자 현황</span>
|
||||
<span className="text-xs font-semibold text-green-600 dark:text-green-400">작업자 현황</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-gray-900">{process.workerCount}명</span>
|
||||
<span className="text-sm font-bold text-foreground">{process.workerCount}명</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 space-y-2">
|
||||
<div className="p-2 space-y-2 max-h-[240px] overflow-y-auto">
|
||||
{process.workers.map((worker, idx) => (
|
||||
<div key={idx} className="border rounded p-2">
|
||||
<div key={idx} className="border border-border rounded p-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-medium text-gray-900">{worker.name}</span>
|
||||
<span className="text-[11px] text-gray-500">{worker.completed}/{worker.assigned}건</span>
|
||||
<span className="text-xs font-medium text-foreground">{worker.name}</span>
|
||||
<span className="text-[11px] text-muted-foreground">{worker.completed}/{worker.assigned}건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex-1 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full"
|
||||
style={{
|
||||
@@ -251,7 +230,7 @@ export function DailyProductionSection({ data, showShipment = true }: DailyProdu
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[10px] text-gray-500 min-w-[28px] text-right">{worker.rate}%</span>
|
||||
<span className="text-[10px] text-muted-foreground min-w-[28px] text-right">{worker.rate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -262,46 +241,34 @@ export function DailyProductionSection({ data, showShipment = true }: DailyProdu
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
|
||||
{/* 출고 현황 (별도 카드) */}
|
||||
{showShipment && (
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<Truck style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">출고 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">예상 출고 정보</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Truck style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="출고 현황"
|
||||
subtitle="예상 출고 정보"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="rounded-xl border p-4" style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}>
|
||||
<div className="rounded-xl border p-4 bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Package className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-gray-700">예상 출고 (7일 이내)</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">예상 출고 (7일 이내)</span>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.expectedAmount)}</p>
|
||||
<p className="text-xs text-gray-500">{data.shipment.expectedCount}건</p>
|
||||
<p className="text-xl font-bold text-foreground mb-1">{formatKoreanAmount(data.shipment.expectedAmount)}</p>
|
||||
<p className="text-xs text-muted-foreground">{data.shipment.expectedCount}건</p>
|
||||
</div>
|
||||
<div className="rounded-xl border p-4" style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}>
|
||||
<div className="rounded-xl border p-4 bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Truck className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-gray-700">예상 출고 (30일 이내)</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">예상 출고 (30일 이내)</span>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-gray-900 mb-1">{formatKoreanAmount(data.shipment.actualAmount)}</p>
|
||||
<p className="text-xs text-gray-500">{data.shipment.actualCount}건</p>
|
||||
<p className="text-xl font-bold text-foreground mb-1">{formatKoreanAmount(data.shipment.actualAmount)}</p>
|
||||
<p className="text-xs text-muted-foreground">{data.shipment.actualCount}건</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem } from '../components';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard } from '../components';
|
||||
import type { DailyReportData } from '../types';
|
||||
|
||||
interface DailyReportSectionProps {
|
||||
@@ -11,27 +11,24 @@ interface DailyReportSectionProps {
|
||||
|
||||
export function DailyReportSection({ data, onClick }: DailyReportSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<SectionTitle title="일일 일보" badge="info" />
|
||||
<span className="text-sm text-muted-foreground">{data.date}</span>
|
||||
</div>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<FileText style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="일일 일보"
|
||||
subtitle={data.date}
|
||||
>
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} onClick={onClick} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem key={card.id} card={card} onClick={onClick} />
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Gavel, FileWarning, AlertCircle, Scale } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem, type SectionColorTheme } from '../components';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard, type SectionColorTheme } from '../components';
|
||||
import type { DebtCollectionData } from '../types';
|
||||
|
||||
// 카드별 아이콘 매핑
|
||||
@@ -24,39 +23,34 @@ export function DebtCollectionSection({ data }: DebtCollectionSectionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle
|
||||
title="채권추심 현황"
|
||||
badge="error"
|
||||
icon={Gavel}
|
||||
colorTheme="red"
|
||||
/>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Gavel style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="채권추심 현황"
|
||||
subtitle="채권추심 관리 현황"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={data.detailButtonPath ? handleClick : undefined}
|
||||
icon={CARD_ICONS[idx] || Gavel}
|
||||
colorTheme={CARD_THEMES[idx] || 'red'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="down"
|
||||
subLabelAsBadge
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={data.detailButtonPath ? handleClick : undefined}
|
||||
icon={CARD_ICONS[idx] || Gavel}
|
||||
colorTheme={CARD_THEMES[idx] || 'red'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="down"
|
||||
subLabelAsBadge
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,12 @@ import {
|
||||
ArrowDownRight,
|
||||
Banknote,
|
||||
CircleDollarSign,
|
||||
LayoutGrid,
|
||||
} from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { formatKoreanAmount } from '@/lib/utils/amount';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { DailyReportData, MonthlyExpenseData, TodayIssueItem, TodayIssueSettings } from '../types';
|
||||
|
||||
// ============================================================
|
||||
@@ -46,10 +47,10 @@ interface EnhancedDailyReportSectionProps {
|
||||
}
|
||||
|
||||
const CARD_STYLES = [
|
||||
{ bg: '#ecfdf5', border: '#a7f3d0', iconBg: '#10b981', labelColor: '#047857', Icon: FileText },
|
||||
{ bg: '#eff6ff', border: '#bfdbfe', iconBg: '#3b82f6', labelColor: '#1d4ed8', Icon: Receipt },
|
||||
{ bg: '#f5f3ff', border: '#ddd6fe', iconBg: '#8b5cf6', labelColor: '#6d28d9', Icon: Briefcase },
|
||||
{ bg: '#fff7ed', border: '#fed7aa', iconBg: '#f97316', labelColor: '#c2410c', Icon: Clock },
|
||||
{ bgClass: 'bg-emerald-50 dark:bg-emerald-900/30', borderClass: 'border-emerald-200 dark:border-emerald-800', iconBg: '#10b981', labelClass: 'text-emerald-700 dark:text-emerald-300', Icon: FileText },
|
||||
{ bgClass: 'bg-blue-50 dark:bg-blue-900/30', borderClass: 'border-blue-200 dark:border-blue-800', iconBg: '#3b82f6', labelClass: 'text-blue-700 dark:text-blue-300', Icon: Receipt },
|
||||
{ bgClass: 'bg-purple-50 dark:bg-purple-900/30', borderClass: 'border-purple-200 dark:border-purple-800', iconBg: '#8b5cf6', labelClass: 'text-purple-700 dark:text-purple-300', Icon: Briefcase },
|
||||
{ bgClass: 'bg-orange-50 dark:bg-orange-900/30', borderClass: 'border-orange-200 dark:border-orange-800', iconBg: '#f97316', labelClass: 'text-orange-700 dark:text-orange-300', Icon: Clock },
|
||||
];
|
||||
|
||||
export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyReportSectionProps) {
|
||||
@@ -64,36 +65,19 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#1e293b' }}
|
||||
className="px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
style={{ backgroundColor: 'rgba(255,255,255,0.1)' }}
|
||||
className="p-2 rounded-lg"
|
||||
>
|
||||
<Wallet style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">자금현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">{data.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#10b981', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
실시간
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드 내용 */}
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Wallet style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="자금현황"
|
||||
subtitle={data.date}
|
||||
rightElement={
|
||||
<Badge
|
||||
style={{ backgroundColor: '#10b981', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
실시간
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{data.cards.map((card, idx) => {
|
||||
const style = CARD_STYLES[idx] || CARD_STYLES[0];
|
||||
@@ -101,20 +85,19 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
return (
|
||||
<div
|
||||
key={card.id}
|
||||
style={{ backgroundColor: style.bg, borderColor: style.border }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border min-h-[110px] flex flex-col"
|
||||
className={`rounded-xl p-4 cursor-pointer transition-all hover:shadow-lg border min-h-[110px] flex flex-col ${style.bgClass} ${style.borderClass}`}
|
||||
onClick={() => handleCardClick(card)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div style={{ backgroundColor: style.iconBg }} className="p-1.5 rounded-lg">
|
||||
<CardIcon style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<CardIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: style.labelColor }} className="text-sm font-medium">
|
||||
<span className={`text-sm font-medium ${style.labelClass}`}>
|
||||
{card.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-end gap-2">
|
||||
<span style={{ color: '#0f172a' }} className="text-2xl font-bold">
|
||||
<span className="text-2xl font-bold text-foreground">
|
||||
{card.displayValue
|
||||
? card.displayValue
|
||||
: card.currency === 'USD'
|
||||
@@ -123,8 +106,7 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
</span>
|
||||
{card.changeRate && (
|
||||
<span
|
||||
style={{ color: card.changeDirection === 'up' ? '#ef4444' : '#3b82f6' }}
|
||||
className="flex items-center text-xs font-medium mb-1"
|
||||
className={`flex items-center text-xs font-medium mb-1 ${card.changeDirection === 'up' ? 'text-red-500 dark:text-red-400' : 'text-blue-500 dark:text-blue-400'}`}
|
||||
>
|
||||
{card.changeDirection === 'up'
|
||||
? <ArrowUpRight className="h-3 w-3" />
|
||||
@@ -141,33 +123,27 @@ export function EnhancedDailyReportSection({ data, onClick }: EnhancedDailyRepor
|
||||
{/* 체크포인트 */}
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p style={{ color: '#64748b' }} className="text-xs font-medium uppercase tracking-wider mb-3">주요 알림</p>
|
||||
<p className="text-xs font-medium uppercase tracking-wider mb-3 text-muted-foreground">주요 알림</p>
|
||||
{data.checkPoints.map((cp, idx) => (
|
||||
<div
|
||||
key={cp.id}
|
||||
style={{
|
||||
backgroundColor: idx === 0 ? '#fffbeb' : '#f8fafc',
|
||||
borderColor: idx === 0 ? '#fde68a' : '#e2e8f0'
|
||||
}}
|
||||
className="flex items-start gap-3 p-3 rounded-lg border"
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border ${idx === 0 ? 'bg-amber-50 border-amber-200 dark:bg-amber-900/30 dark:border-amber-800' : 'bg-slate-50 border-slate-200 dark:bg-slate-800 dark:border-slate-700'}`}
|
||||
>
|
||||
<div
|
||||
style={{ backgroundColor: idx === 0 ? '#fef3c7' : '#f1f5f9' }}
|
||||
className="p-1 rounded-full shrink-0"
|
||||
className={`p-1 rounded-full shrink-0 ${idx === 0 ? 'bg-amber-100 dark:bg-amber-800' : 'bg-slate-100 dark:bg-slate-700'}`}
|
||||
>
|
||||
{idx === 0 ? (
|
||||
<AlertTriangle style={{ color: '#d97706' }} className="h-4 w-4" />
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||
) : (
|
||||
<CheckCircle2 style={{ color: '#16a34a' }} className="h-4 w-4" />
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
</div>
|
||||
<p style={{ color: '#475569' }} className="text-sm flex-1">{cp.message}</p>
|
||||
<p className="text-sm flex-1 text-muted-foreground">{cp.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,21 +165,21 @@ const LABEL_TO_SETTING_KEY: Record<string, keyof TodayIssueSettings> = {
|
||||
'결재 요청': 'approvalRequest',
|
||||
};
|
||||
|
||||
// 라벨별 스타일 매핑 (인라인 스타일용)
|
||||
const ITEM_STYLES: Record<string, { bg: string; border: string; iconBg: string; labelColor: string; Icon: React.ComponentType<{ style?: React.CSSProperties; className?: string }> }> = {
|
||||
'수주': { bg: '#eff6ff', border: '#bfdbfe', iconBg: '#3b82f6', labelColor: '#1d4ed8', Icon: ShoppingCart },
|
||||
'채권 추심': { bg: '#fef2f2', border: '#fecaca', iconBg: '#ef4444', labelColor: '#dc2626', Icon: AlertCircle },
|
||||
'안전 재고': { bg: '#fff7ed', border: '#fed7aa', iconBg: '#f97316', labelColor: '#ea580c', Icon: Receipt },
|
||||
'세금 신고': { bg: '#faf5ff', border: '#e9d5ff', iconBg: '#a855f7', labelColor: '#9333ea', Icon: FileText },
|
||||
'신규 업체 등록': { bg: '#ecfdf5', border: '#a7f3d0', iconBg: '#10b981', labelColor: '#059669', Icon: Building2 },
|
||||
'연차': { bg: '#ecfeff', border: '#a5f3fc', iconBg: '#06b6d4', labelColor: '#0891b2', Icon: Calendar },
|
||||
'지각': { bg: '#fffbeb', border: '#fde68a', iconBg: '#f59e0b', labelColor: '#d97706', Icon: Clock },
|
||||
'결근': { bg: '#fff1f2', border: '#fecdd3', iconBg: '#f43f5e', labelColor: '#e11d48', Icon: Users },
|
||||
'발주': { bg: '#eef2ff', border: '#c7d2fe', iconBg: '#6366f1', labelColor: '#4f46e5', Icon: Briefcase },
|
||||
'결재 요청': { bg: '#fdf2f8', border: '#fbcfe8', iconBg: '#ec4899', labelColor: '#db2777', Icon: CheckCircle2 },
|
||||
// 라벨별 스타일 매핑 (Tailwind 클래스 기반 - 다크모드 지원)
|
||||
const ITEM_STYLES: Record<string, { bgClass: string; borderClass: string; iconBg: string; labelClass: string; Icon: React.ComponentType<{ style?: React.CSSProperties; className?: string }> }> = {
|
||||
'수주': { bgClass: 'bg-blue-50 dark:bg-blue-900/30', borderClass: 'border-blue-200 dark:border-blue-800', iconBg: '#3b82f6', labelClass: 'text-blue-700 dark:text-blue-300', Icon: ShoppingCart },
|
||||
'채권 추심': { bgClass: 'bg-red-50 dark:bg-red-900/30', borderClass: 'border-red-200 dark:border-red-800', iconBg: '#ef4444', labelClass: 'text-red-700 dark:text-red-300', Icon: AlertCircle },
|
||||
'안전 재고': { bgClass: 'bg-orange-50 dark:bg-orange-900/30', borderClass: 'border-orange-200 dark:border-orange-800', iconBg: '#f97316', labelClass: 'text-orange-700 dark:text-orange-300', Icon: Receipt },
|
||||
'세금 신고': { bgClass: 'bg-purple-50 dark:bg-purple-900/30', borderClass: 'border-purple-200 dark:border-purple-800', iconBg: '#a855f7', labelClass: 'text-purple-700 dark:text-purple-300', Icon: FileText },
|
||||
'신규 업체 등록': { bgClass: 'bg-emerald-50 dark:bg-emerald-900/30', borderClass: 'border-emerald-200 dark:border-emerald-800', iconBg: '#10b981', labelClass: 'text-emerald-700 dark:text-emerald-300', Icon: Building2 },
|
||||
'연차': { bgClass: 'bg-cyan-50 dark:bg-cyan-900/30', borderClass: 'border-cyan-200 dark:border-cyan-800', iconBg: '#06b6d4', labelClass: 'text-cyan-700 dark:text-cyan-300', Icon: Calendar },
|
||||
'지각': { bgClass: 'bg-amber-50 dark:bg-amber-900/30', borderClass: 'border-amber-200 dark:border-amber-800', iconBg: '#f59e0b', labelClass: 'text-amber-700 dark:text-amber-300', Icon: Clock },
|
||||
'결근': { bgClass: 'bg-rose-50 dark:bg-rose-900/30', borderClass: 'border-rose-200 dark:border-rose-800', iconBg: '#f43f5e', labelClass: 'text-rose-700 dark:text-rose-300', Icon: Users },
|
||||
'발주': { bgClass: 'bg-indigo-50 dark:bg-indigo-900/30', borderClass: 'border-indigo-200 dark:border-indigo-800', iconBg: '#6366f1', labelClass: 'text-indigo-700 dark:text-indigo-300', Icon: Briefcase },
|
||||
'결재 요청': { bgClass: 'bg-pink-50 dark:bg-pink-900/30', borderClass: 'border-pink-200 dark:border-pink-800', iconBg: '#ec4899', labelClass: 'text-pink-700 dark:text-pink-300', Icon: CheckCircle2 },
|
||||
};
|
||||
|
||||
const DEFAULT_STYLE = { bg: '#f8fafc', border: '#e2e8f0', iconBg: '#64748b', labelColor: '#475569', Icon: FileText };
|
||||
const DEFAULT_STYLE = { bgClass: 'bg-slate-50 dark:bg-slate-800', borderClass: 'border-slate-200 dark:border-slate-700', iconBg: '#64748b', labelClass: 'text-slate-600 dark:text-slate-400', Icon: FileText };
|
||||
|
||||
interface EnhancedStatusBoardSectionProps {
|
||||
items: TodayIssueItem[];
|
||||
@@ -226,19 +202,18 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
|
||||
: items;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div style={{ backgroundColor: '#f59e0b' }} className="w-1.5 h-6 rounded-full" />
|
||||
<h3 style={{ color: '#0f172a' }} className="text-lg font-semibold">현황판</h3>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#fef3c7', color: '#b45309', borderColor: '#fde68a' }}
|
||||
>
|
||||
{filteredItems.length}개 항목
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CollapsibleDashboardCard
|
||||
icon={<LayoutGrid style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="현황판"
|
||||
subtitle="주요 현황 요약"
|
||||
rightElement={
|
||||
<Badge
|
||||
style={{ backgroundColor: '#f59e0b', color: '#ffffff', border: 'none' }}
|
||||
>
|
||||
{filteredItems.length}개 항목
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 카드 그리드 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||
{filteredItems.map((item) => {
|
||||
@@ -246,44 +221,38 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
|
||||
const style = ITEM_STYLES[item.label] || DEFAULT_STYLE;
|
||||
const Icon = style.Icon;
|
||||
|
||||
// 긴급 항목은 빨간 배경
|
||||
const bgColor = isHighlighted ? '#ef4444' : style.bg;
|
||||
const borderColor = isHighlighted ? '#ef4444' : style.border;
|
||||
const iconBgColor = isHighlighted ? 'rgba(255,255,255,0.2)' : style.iconBg;
|
||||
const labelColor = isHighlighted ? '#ffffff' : style.labelColor;
|
||||
const countColor = isHighlighted ? '#ffffff' : '#0f172a';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{ backgroundColor: bgColor, borderColor: borderColor }}
|
||||
className="relative p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] hover:shadow-md min-h-[130px] flex flex-col"
|
||||
className={`relative p-4 rounded-xl border cursor-pointer transition-all hover:scale-[1.02] hover:shadow-md min-h-[130px] flex flex-col ${isHighlighted ? 'bg-red-500 border-red-500 dark:bg-red-600 dark:border-red-600' : `${style.bgClass} ${style.borderClass}`}`}
|
||||
onClick={() => handleItemClick(item.path)}
|
||||
>
|
||||
{/* 아이콘 + 라벨 */}
|
||||
<div className="flex items-center gap-2 mb-3 min-w-0">
|
||||
<div style={{ backgroundColor: iconBgColor }} className="p-1.5 rounded-lg shrink-0">
|
||||
<Icon style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<div
|
||||
className="p-1.5 rounded-lg shrink-0"
|
||||
style={{ backgroundColor: isHighlighted ? 'rgba(255,255,255,0.2)' : style.iconBg }}
|
||||
>
|
||||
<Icon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: labelColor }} className="text-sm font-medium truncate flex-1 min-w-0">
|
||||
<span className={`text-sm font-medium truncate flex-1 min-w-0 ${isHighlighted ? 'text-white' : style.labelClass}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 숫자 */}
|
||||
<div style={{ color: countColor }} className="text-2xl font-bold">
|
||||
<div className={`text-2xl font-bold ${isHighlighted ? 'text-white' : 'text-foreground'}`}>
|
||||
{typeof item.count === 'number' ? `${item.count}건` : item.count}
|
||||
</div>
|
||||
|
||||
{/* 부가 정보 (최근 항목 외 N건) - pill 뱃지 스타일 */}
|
||||
{item.subLabel && (
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: isHighlighted ? 'rgba(255,255,255,0.2)' : `${style.iconBg}15`,
|
||||
color: isHighlighted ? '#ffffff' : style.labelColor,
|
||||
borderColor: isHighlighted ? 'rgba(255,255,255,0.3)' : `${style.iconBg}30`,
|
||||
}}
|
||||
className="text-xs font-medium mt-auto px-2 py-0.5 rounded-full border truncate w-fit max-w-full"
|
||||
className={`text-xs font-medium mt-auto px-2 py-0.5 rounded-full border truncate w-fit max-w-full ${isHighlighted ? 'bg-white/20 text-white border-white/30' : style.labelClass}`}
|
||||
style={!isHighlighted ? {
|
||||
backgroundColor: `${style.iconBg}15`,
|
||||
borderColor: `${style.iconBg}30`,
|
||||
} : undefined}
|
||||
>
|
||||
{item.subLabel}
|
||||
</span>
|
||||
@@ -292,8 +261,7 @@ export function EnhancedStatusBoardSection({ items, itemSettings }: EnhancedStat
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -311,40 +279,36 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
const totalAmount = data.cards.reduce((sum, card) => sum + (Number(card?.amount) || 0), 0);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div style={{ backgroundColor: '#f97316' }} className="w-1.5 h-6 rounded-full" />
|
||||
<h3 style={{ color: '#0f172a' }} className="text-lg font-semibold">당월 예상 지출 내역</h3>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#ffedd5', color: '#c2410c', borderColor: '#fed7aa' }}
|
||||
>
|
||||
전월 대비 +15%
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Receipt className="h-5 w-5 text-white" />}
|
||||
title="당월 예상 지출 내역"
|
||||
subtitle="이달 예상 지출 정보"
|
||||
rightElement={
|
||||
<Badge className="bg-orange-500 text-white border-none hover:opacity-90">
|
||||
전월 대비 +15%
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 카드 그리드 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{/* 카드 1: 매입 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#f5f3ff', borderColor: '#ddd6fe' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col bg-purple-50 border-purple-200 dark:bg-purple-900/30 dark:border-purple-800"
|
||||
onClick={() => onCardClick?.(data.cards[0]?.id || 'me1')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#8b5cf6' }} className="p-1.5 rounded-lg">
|
||||
<Receipt style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<Receipt className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#6d28d9' }} className="text-sm font-medium">
|
||||
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">
|
||||
{data.cards[0]?.label || '매입'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color: '#0f172a' }} className="text-2xl font-bold">
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.cards[0]?.amount || 0)}
|
||||
</div>
|
||||
{data.cards[0]?.previousLabel && (
|
||||
<div style={{ backgroundColor: '#dcfce7', color: '#16a34a' }} className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
{data.cards[0].previousLabel}
|
||||
</div>
|
||||
@@ -353,23 +317,22 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
|
||||
{/* 카드 2: 카드 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col bg-blue-50 border-blue-200 dark:bg-blue-900/30 dark:border-blue-800"
|
||||
onClick={() => onCardClick?.(data.cards[1]?.id || 'me2')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#3b82f6' }} className="p-1.5 rounded-lg">
|
||||
<CreditCard style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<CreditCard className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#1d4ed8' }} className="text-sm font-medium">
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
||||
{data.cards[1]?.label || '카드'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color: '#0f172a' }} className="text-2xl font-bold">
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.cards[1]?.amount || 0)}
|
||||
</div>
|
||||
{data.cards[1]?.previousLabel && (
|
||||
<div style={{ backgroundColor: '#dcfce7', color: '#16a34a' }} className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
{data.cards[1].previousLabel}
|
||||
</div>
|
||||
@@ -378,47 +341,45 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
|
||||
{/* 카드 3: 발행어음 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#fffbeb', borderColor: '#fde68a' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col bg-amber-50 border-amber-200 dark:bg-amber-900/30 dark:border-amber-800"
|
||||
onClick={() => onCardClick?.(data.cards[2]?.id || 'me3')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#f59e0b' }} className="p-1.5 rounded-lg">
|
||||
<Banknote style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<Banknote className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#b45309' }} className="text-sm font-medium">
|
||||
<span className="text-sm font-medium text-amber-700 dark:text-amber-300">
|
||||
{data.cards[2]?.label || '발행어음'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color: '#0f172a' }} className="text-2xl font-bold">
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.cards[2]?.amount || 0)}
|
||||
</div>
|
||||
{data.cards[2]?.previousLabel && (
|
||||
<div style={{ backgroundColor: '#dcfce7', color: '#16a34a' }} className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
{data.cards[2].previousLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 카드 4: 총 예상 지출 합계 (강조 - 인라인 스타일) */}
|
||||
{/* 카드 4: 총 예상 지출 합계 (강조) */}
|
||||
<div
|
||||
style={{ backgroundColor: '#f43f5e', borderColor: '#f43f5e' }}
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col"
|
||||
className="rounded-xl p-4 cursor-pointer transition-all hover:scale-[1.02] hover:shadow-lg border min-h-[130px] flex flex-col bg-rose-500 border-rose-500 dark:bg-rose-600 dark:border-rose-600"
|
||||
onClick={() => onCardClick?.(data.cards[3]?.id || 'me4')}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.2)' }} className="p-1.5 rounded-lg">
|
||||
<CircleDollarSign style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<div className="p-1.5 rounded-lg bg-white/20">
|
||||
<CircleDollarSign className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#ffe4e6' }} className="text-sm font-medium">
|
||||
<span className="text-sm font-medium text-rose-100">
|
||||
총 예상 지출 합계
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ color: '#ffffff' }} className="text-2xl font-bold">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{formatKoreanAmount(totalAmount)}
|
||||
</div>
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.2)', color: '#ffffff' }} className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit">
|
||||
<div className="flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold mt-auto w-fit bg-white/20 text-white">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
전월 대비 +10.5%
|
||||
</div>
|
||||
@@ -429,33 +390,31 @@ export function EnhancedMonthlyExpenseSection({ data, onCardClick }: EnhancedMon
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{data.checkPoints.map((cp, idx) => {
|
||||
const colors = [
|
||||
{ bg: '#fef2f2', border: '#fecaca', iconColor: '#ef4444' },
|
||||
{ bg: '#fffbeb', border: '#fde68a', iconColor: '#f59e0b' },
|
||||
{ bg: '#f0fdf4', border: '#bbf7d0', iconColor: '#22c55e' },
|
||||
const colorClasses = [
|
||||
{ bg: 'bg-red-50 dark:bg-red-900/30', border: 'border-red-200 dark:border-red-800', icon: 'text-red-500 dark:text-red-400' },
|
||||
{ bg: 'bg-amber-50 dark:bg-amber-900/30', border: 'border-amber-200 dark:border-amber-800', icon: 'text-amber-500 dark:text-amber-400' },
|
||||
{ bg: 'bg-green-50 dark:bg-green-900/30', border: 'border-green-200 dark:border-green-800', icon: 'text-green-500 dark:text-green-400' },
|
||||
];
|
||||
const color = colors[idx] || colors[2];
|
||||
const color = colorClasses[idx] || colorClasses[2];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={cp.id}
|
||||
style={{ backgroundColor: color.bg, borderColor: color.border }}
|
||||
className="p-3 rounded-lg border flex items-start gap-2"
|
||||
className={`p-3 rounded-lg border flex items-start gap-2 ${color.bg} ${color.border}`}
|
||||
>
|
||||
{idx === 0 ? (
|
||||
<AlertTriangle style={{ color: color.iconColor }} className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<AlertTriangle className={`h-4 w-4 mt-0.5 shrink-0 ${color.icon}`} />
|
||||
) : idx === 1 ? (
|
||||
<AlertCircle style={{ color: color.iconColor }} className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<AlertCircle className={`h-4 w-4 mt-0.5 shrink-0 ${color.icon}`} />
|
||||
) : (
|
||||
<CheckCircle2 style={{ color: color.iconColor }} className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<CheckCircle2 className={`h-4 w-4 mt-0.5 shrink-0 ${color.icon}`} />
|
||||
)}
|
||||
<p style={{ color: '#475569' }} className="text-sm">{cp.message}</p>
|
||||
<p className="text-sm text-muted-foreground">{cp.message}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Wine, Utensils, Users, CreditCard } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem, type SectionColorTheme } from '../components';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard, type SectionColorTheme } from '../components';
|
||||
import type { EntertainmentData } from '../types';
|
||||
|
||||
// 카드별 아이콘 매핑
|
||||
@@ -16,38 +15,33 @@ interface EntertainmentSectionProps {
|
||||
|
||||
export function EntertainmentSection({ data, onCardClick }: EntertainmentSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle
|
||||
title="접대비 현황"
|
||||
badge="warning"
|
||||
icon={Wine}
|
||||
colorTheme="pink"
|
||||
/>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Wine style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="접대비 현황"
|
||||
subtitle="접대비 사용 현황"
|
||||
>
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={() => onCardClick?.(card.id)}
|
||||
icon={CARD_ICONS[idx] || Wine}
|
||||
colorTheme={CARD_THEMES[idx] || 'pink'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="up"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={() => onCardClick?.(card.id)}
|
||||
icon={CARD_ICONS[idx] || Wine}
|
||||
colorTheme={CARD_THEMES[idx] || 'pink'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="up"
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem } from '../components';
|
||||
import { Receipt } from 'lucide-react';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard } from '../components';
|
||||
import type { MonthlyExpenseData } from '../types';
|
||||
|
||||
interface MonthlyExpenseSectionProps {
|
||||
@@ -11,28 +11,28 @@ interface MonthlyExpenseSectionProps {
|
||||
|
||||
export function MonthlyExpenseSection({ data, onCardClick }: MonthlyExpenseSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="당월 예상 지출 내역" badge="warning" />
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Receipt style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="당월 예상 지출 내역"
|
||||
subtitle="이달 예상 지출 정보"
|
||||
>
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onCardClick ? () => onCardClick(card.id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onCardClick ? () => onCardClick(card.id) : undefined}
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
import { formatKoreanAmount } from '@/lib/utils/amount';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { PurchaseStatusData } from '../types';
|
||||
|
||||
interface PurchaseStatusSectionProps {
|
||||
@@ -61,79 +62,56 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<ShoppingCart style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">매입 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">당월 매입 실적</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#f59e0b', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
당월
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<ShoppingCart className="h-5 w-5 text-white" />}
|
||||
title="매입 현황"
|
||||
subtitle="당월 매입 실적"
|
||||
rightElement={
|
||||
<Badge className="bg-amber-500 text-white border-none hover:opacity-90">
|
||||
당월
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 통계카드 3개 - 가로 배치 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
{/* 누적 매입 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#fff7ed', borderColor: '#fed7aa' }}
|
||||
className="rounded-xl p-4 border"
|
||||
>
|
||||
<div className="rounded-xl p-4 border bg-orange-50 border-orange-200 dark:bg-orange-900/30 dark:border-orange-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#f59e0b' }} className="p-1.5 rounded-lg">
|
||||
<DollarSign style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<DollarSign className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#b45309' }} className="text-sm font-medium">누적 매입</span>
|
||||
<span className="text-sm font-medium text-amber-700 dark:text-amber-300">누적 매입</span>
|
||||
</div>
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.cumulativePurchase)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 미결제 금액 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#fef2f2', borderColor: '#fecaca' }}
|
||||
className="rounded-xl p-4 border"
|
||||
>
|
||||
<div className="rounded-xl p-4 border bg-red-50 border-red-200 dark:bg-red-900/30 dark:border-red-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#ef4444' }} className="p-1.5 rounded-lg">
|
||||
<AlertCircle style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<AlertCircle className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#dc2626' }} className="text-sm font-medium">미결제 금액</span>
|
||||
<span className="text-sm font-medium text-red-700 dark:text-red-300">미결제 금액</span>
|
||||
</div>
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.unpaidAmount)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 전년 동기 대비 */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: data.yoyChange >= 0 ? '#fef2f2' : '#eff6ff',
|
||||
borderColor: data.yoyChange >= 0 ? '#fecaca' : '#bfdbfe',
|
||||
}}
|
||||
className="rounded-xl p-4 border"
|
||||
className={`rounded-xl p-4 border ${data.yoyChange >= 0 ? 'bg-red-50 border-red-200 dark:bg-red-900/30 dark:border-red-800' : 'bg-blue-50 border-blue-200 dark:bg-blue-900/30 dark:border-blue-800'}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: data.yoyChange >= 0 ? '#ef4444' : '#3b82f6' }} className="p-1.5 rounded-lg">
|
||||
<TrendingDown style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<TrendingDown className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: data.yoyChange >= 0 ? '#dc2626' : '#1d4ed8' }} className="text-sm font-medium">전년 동기 대비</span>
|
||||
<span className={`text-sm font-medium ${data.yoyChange >= 0 ? 'text-red-700 dark:text-red-300' : 'text-blue-700 dark:text-blue-300'}`}>전년 동기 대비</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{data.yoyChange >= 0 ? '+' : ''}{data.yoyChange}%
|
||||
</span>
|
||||
{data.yoyChange >= 0
|
||||
@@ -146,8 +124,8 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
{/* 차트 2열 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* 월별 매입 추이 */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">월별 매입 추이</h4>
|
||||
<div className="border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">월별 매입 추이</h4>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={data.monthlyTrend}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
||||
@@ -162,16 +140,16 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
</div>
|
||||
|
||||
{/* 자재 유형별 비율 (Donut) */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">자재 유형별 비율</h4>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<div className="border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">자재 유형별 비율</h4>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data.materialRatio.map((r) => ({ name: r.name, value: r.value, percentage: r.percentage, color: r.color }) as Record<string, unknown>)}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={80}
|
||||
cy="40%"
|
||||
innerRadius={40}
|
||||
outerRadius={65}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
>
|
||||
@@ -180,72 +158,67 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(value: number | undefined) => [formatKoreanAmount(value ?? 0), '금액']} />
|
||||
<Legend formatter={(value: string) => {
|
||||
const item = data.materialRatio.find((r) => r.name === value);
|
||||
return `${value} ${item?.percentage ?? 0}%`;
|
||||
}} />
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
wrapperStyle={{ fontSize: '12px', paddingTop: '8px' }}
|
||||
formatter={(value: string) => {
|
||||
const item = data.materialRatio.find((r) => r.name === value);
|
||||
return `${value} ${item?.percentage ?? 0}%`;
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
|
||||
{/* 당월 매입 내역 (별도 카드) */}
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<ShoppingCart style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">당월 매입 내역</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">당월 매입 거래 상세</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 border-b">
|
||||
<div className="text-sm text-gray-500">총 {filteredItems.length}건</div>
|
||||
<div className="flex gap-2">
|
||||
<MultiSelectCombobox
|
||||
options={suppliers.map((s) => ({ value: s, label: s }))}
|
||||
value={supplierFilter}
|
||||
onChange={setSupplierFilter}
|
||||
placeholder="전체 공급처"
|
||||
className="w-[160px] h-8 text-xs"
|
||||
/>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder}>
|
||||
<SelectTrigger className="w-[120px] h-8 text-xs">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date-desc">최신순</SelectItem>
|
||||
<SelectItem value="date-asc">오래된순</SelectItem>
|
||||
<SelectItem value="amount-desc">금액 높은순</SelectItem>
|
||||
<SelectItem value="amount-asc">금액 낮은순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<ShoppingCart className="h-5 w-5 text-white" />}
|
||||
title="당월 매입 내역"
|
||||
subtitle="당월 매입 거래 상세"
|
||||
bodyClassName="p-0"
|
||||
>
|
||||
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
|
||||
<div className="text-sm text-muted-foreground">총 {filteredItems.length}건</div>
|
||||
<MultiSelectCombobox
|
||||
options={suppliers.map((s) => ({ value: s, label: s }))}
|
||||
value={supplierFilter}
|
||||
onChange={setSupplierFilter}
|
||||
placeholder="전체 공급처"
|
||||
className="w-full h-8 text-xs"
|
||||
/>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder}>
|
||||
<SelectTrigger className="w-full h-8 text-xs">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date-desc">최신순</SelectItem>
|
||||
<SelectItem value="date-asc">오래된순</SelectItem>
|
||||
<SelectItem value="amount-desc">금액 높은순</SelectItem>
|
||||
<SelectItem value="amount-asc">금액 낮은순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[500px]">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b">
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">날짜</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">공급처</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">품목</th>
|
||||
<th className="px-4 py-2 text-right text-gray-600 font-medium">금액</th>
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium">상태</th>
|
||||
<tr className="bg-muted/50 border-b border-border">
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">날짜</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">공급처</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">품목</th>
|
||||
<th className="px-4 py-2 text-right text-muted-foreground font-medium">금액</th>
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredItems.map((item, idx) => (
|
||||
<tr key={idx} className="border-b last:border-b-0 hover:bg-gray-50">
|
||||
<td className="px-4 py-2 text-gray-700">{item.date}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.supplier}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.item}</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900 font-medium">
|
||||
<tr key={idx} className="border-b border-border last:border-b-0 hover:bg-muted/30">
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.date}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.supplier}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.item}</td>
|
||||
<td className="px-4 py-2 text-right text-foreground font-medium">
|
||||
{item.amount.toLocaleString()}원
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
@@ -253,10 +226,10 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
variant="outline"
|
||||
className={
|
||||
item.status === '결제완료'
|
||||
? 'text-green-600 border-green-200 bg-green-50'
|
||||
? 'text-green-600 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/30'
|
||||
: item.status === '미결제'
|
||||
? 'text-red-600 border-red-200 bg-red-50'
|
||||
: 'text-orange-600 border-orange-200 bg-orange-50'
|
||||
? 'text-red-600 border-red-200 bg-red-50 dark:text-red-400 dark:border-red-800 dark:bg-red-900/30'
|
||||
: 'text-orange-600 border-orange-200 bg-orange-50 dark:text-orange-400 dark:border-orange-800 dark:bg-orange-900/30'
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
@@ -266,9 +239,9 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="bg-gray-100 font-semibold">
|
||||
<td className="px-4 py-2 text-gray-700" colSpan={3}>합계</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900">
|
||||
<tr className="bg-muted font-semibold">
|
||||
<td className="px-4 py-2 text-muted-foreground" colSpan={3}>합계</td>
|
||||
<td className="px-4 py-2 text-right text-foreground">
|
||||
{filteredItems.reduce((sum, item) => sum + item.amount, 0).toLocaleString()}원
|
||||
</td>
|
||||
<td />
|
||||
@@ -276,7 +249,7 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Banknote, Clock, AlertTriangle, CircleDollarSign } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem, type SectionColorTheme } from '../components';
|
||||
import { Banknote, Clock, AlertTriangle, CircleDollarSign, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard, type SectionColorTheme } from '../components';
|
||||
import type { ReceivableData } from '../types';
|
||||
|
||||
// 카드별 아이콘 매핑 (미수금 합계, 30일 이내, 30~90일, 90일 초과)
|
||||
@@ -24,46 +24,46 @@ export function ReceivableSection({ data }: ReceivableSectionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle
|
||||
title="미수금 현황"
|
||||
badge="warning"
|
||||
icon={Banknote}
|
||||
colorTheme="amber"
|
||||
actionButton={
|
||||
data.detailButtonLabel
|
||||
? {
|
||||
label: data.detailButtonLabel,
|
||||
onClick: handleDetailClick,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Banknote style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="미수금 현황"
|
||||
subtitle="미수금 관리 현황"
|
||||
rightElement={
|
||||
data.detailButtonLabel ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => { e.stopPropagation(); handleDetailClick(); }}
|
||||
className="text-white hover:bg-white/10 gap-1 text-xs"
|
||||
>
|
||||
{data.detailButtonLabel}
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={handleDetailClick}
|
||||
icon={CARD_ICONS[idx] || Banknote}
|
||||
colorTheme={CARD_THEMES[idx] || 'amber'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection={idx === 3 ? 'down' : 'up'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={handleDetailClick}
|
||||
icon={CARD_ICONS[idx] || Banknote}
|
||||
colorTheme={CARD_THEMES[idx] || 'amber'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection={idx === 3 ? 'down' : 'up'}
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { formatKoreanAmount } from '@/lib/utils/amount';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { SalesStatusData } from '../types';
|
||||
|
||||
interface SalesStatusSectionProps {
|
||||
@@ -59,81 +60,58 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<BarChart3 style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">매출 현황</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">당월 매출 실적</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#3b82f6', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
당월
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<BarChart3 className="h-5 w-5 text-white" />}
|
||||
title="매출 현황"
|
||||
subtitle="당월 매출 실적"
|
||||
rightElement={
|
||||
<Badge className="bg-blue-500 text-white border-none hover:opacity-90">
|
||||
당월
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 통계카드 4개 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{/* 누적 매출 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#eff6ff', borderColor: '#bfdbfe' }}
|
||||
className="rounded-xl p-4 border"
|
||||
>
|
||||
<div className="rounded-xl p-4 border bg-blue-50 border-blue-200 dark:bg-blue-900/30 dark:border-blue-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#3b82f6' }} className="p-1.5 rounded-lg">
|
||||
<DollarSign style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<DollarSign className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#1d4ed8' }} className="text-sm font-medium">누적 매출</span>
|
||||
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">누적 매출</span>
|
||||
</div>
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.cumulativeSales)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 달성률 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#f0fdf4', borderColor: '#bbf7d0' }}
|
||||
className="rounded-xl p-4 border"
|
||||
>
|
||||
<div className="rounded-xl p-4 border bg-green-50 border-green-200 dark:bg-green-900/30 dark:border-green-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#22c55e' }} className="p-1.5 rounded-lg">
|
||||
<Target style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<Target className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#15803d' }} className="text-sm font-medium">달성률</span>
|
||||
<span className="text-sm font-medium text-green-700 dark:text-green-300">달성률</span>
|
||||
</div>
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{data.achievementRate}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 전년 동기 대비 */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: data.yoyChange >= 0 ? '#fef2f2' : '#eff6ff',
|
||||
borderColor: data.yoyChange >= 0 ? '#fecaca' : '#bfdbfe',
|
||||
}}
|
||||
className="rounded-xl p-4 border"
|
||||
className={`rounded-xl p-4 border ${data.yoyChange >= 0 ? 'bg-red-50 border-red-200 dark:bg-red-900/30 dark:border-red-800' : 'bg-blue-50 border-blue-200 dark:bg-blue-900/30 dark:border-blue-800'}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: data.yoyChange >= 0 ? '#ef4444' : '#3b82f6' }} className="p-1.5 rounded-lg">
|
||||
{data.yoyChange >= 0
|
||||
? <TrendingUp style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
: <TrendingDown style={{ color: '#ffffff' }} className="h-4 w-4" />}
|
||||
? <TrendingUp className="h-4 w-4 text-white" />
|
||||
: <TrendingDown className="h-4 w-4 text-white" />}
|
||||
</div>
|
||||
<span style={{ color: data.yoyChange >= 0 ? '#dc2626' : '#1d4ed8' }} className="text-sm font-medium">전년 동기 대비</span>
|
||||
<span className={`text-sm font-medium ${data.yoyChange >= 0 ? 'text-red-700 dark:text-red-300' : 'text-blue-700 dark:text-blue-300'}`}>전년 동기 대비</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{data.yoyChange >= 0 ? '+' : ''}{data.yoyChange}%
|
||||
</span>
|
||||
{data.yoyChange >= 0
|
||||
@@ -143,17 +121,14 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
</div>
|
||||
|
||||
{/* 당월 매출 */}
|
||||
<div
|
||||
style={{ backgroundColor: '#faf5ff', borderColor: '#e9d5ff' }}
|
||||
className="rounded-xl p-4 border"
|
||||
>
|
||||
<div className="rounded-xl p-4 border bg-purple-50 border-purple-200 dark:bg-purple-900/30 dark:border-purple-800">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div style={{ backgroundColor: '#a855f7' }} className="p-1.5 rounded-lg">
|
||||
<Calendar style={{ color: '#ffffff' }} className="h-4 w-4" />
|
||||
<Calendar className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<span style={{ color: '#7e22ce' }} className="text-sm font-medium">당월 매출</span>
|
||||
<span className="text-sm font-medium text-purple-700 dark:text-purple-300">당월 매출</span>
|
||||
</div>
|
||||
<span style={{ color: '#0f172a' }} className="text-xl font-bold">
|
||||
<span className="text-xl font-bold text-foreground">
|
||||
{formatKoreanAmount(data.monthlySales)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -162,8 +137,8 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
{/* 차트 2열 */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* 월별 매출 추이 */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">월별 매출 추이</h4>
|
||||
<div className="border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">월별 매출 추이</h4>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={data.monthlyTrend}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
||||
@@ -178,8 +153,8 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
</div>
|
||||
|
||||
{/* 거래처별 매출 (수평 Bar) */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">거래처별 매출</h4>
|
||||
<div className="border border-border rounded-lg p-4">
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3">거래처별 매출</h4>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={data.clientSales} layout="vertical">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
||||
@@ -194,63 +169,54 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
|
||||
{/* 당월 매출 내역 (별도 카드) */}
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<BarChart3 style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">당월 매출 내역</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">당월 매출 거래 상세</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 border-b">
|
||||
<div className="text-sm text-gray-500">총 {filteredItems.length}건</div>
|
||||
<div className="flex gap-2">
|
||||
<MultiSelectCombobox
|
||||
options={clients.map((c) => ({ value: c, label: c }))}
|
||||
value={clientFilter}
|
||||
onChange={setClientFilter}
|
||||
placeholder="전체 거래처"
|
||||
className="w-[160px] h-8 text-xs"
|
||||
/>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder}>
|
||||
<SelectTrigger className="w-[120px] h-8 text-xs">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date-desc">최신순</SelectItem>
|
||||
<SelectItem value="date-asc">오래된순</SelectItem>
|
||||
<SelectItem value="amount-desc">금액 높은순</SelectItem>
|
||||
<SelectItem value="amount-asc">금액 낮은순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<BarChart3 className="h-5 w-5 text-white" />}
|
||||
title="당월 매출 내역"
|
||||
subtitle="당월 매출 거래 상세"
|
||||
bodyClassName="p-0"
|
||||
>
|
||||
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
|
||||
<div className="text-sm text-muted-foreground">총 {filteredItems.length}건</div>
|
||||
<MultiSelectCombobox
|
||||
options={clients.map((c) => ({ value: c, label: c }))}
|
||||
value={clientFilter}
|
||||
onChange={setClientFilter}
|
||||
placeholder="전체 거래처"
|
||||
className="w-full h-8 text-xs"
|
||||
/>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder}>
|
||||
<SelectTrigger className="w-full h-8 text-xs">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date-desc">최신순</SelectItem>
|
||||
<SelectItem value="date-asc">오래된순</SelectItem>
|
||||
<SelectItem value="amount-desc">금액 높은순</SelectItem>
|
||||
<SelectItem value="amount-asc">금액 낮은순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[500px]">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b">
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">날짜</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">거래처</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">품목</th>
|
||||
<th className="px-4 py-2 text-right text-gray-600 font-medium">금액</th>
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium">상태</th>
|
||||
<tr className="bg-muted/50 border-b border-border">
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">날짜</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">거래처</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">품목</th>
|
||||
<th className="px-4 py-2 text-right text-muted-foreground font-medium">금액</th>
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium">상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredItems.map((item, idx) => (
|
||||
<tr key={idx} className="border-b last:border-b-0 hover:bg-gray-50">
|
||||
<td className="px-4 py-2 text-gray-700">{item.date}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.client}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.item}</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900 font-medium">
|
||||
<tr key={idx} className="border-b border-border last:border-b-0 hover:bg-muted/30">
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.date}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.client}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.item}</td>
|
||||
<td className="px-4 py-2 text-right text-foreground font-medium">
|
||||
{item.amount.toLocaleString()}원
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
@@ -258,10 +224,10 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
variant="outline"
|
||||
className={
|
||||
item.status === '입금완료'
|
||||
? 'text-green-600 border-green-200 bg-green-50'
|
||||
? 'text-green-600 border-green-200 bg-green-50 dark:text-green-400 dark:border-green-800 dark:bg-green-900/30'
|
||||
: item.status === '미입금'
|
||||
? 'text-red-600 border-red-200 bg-red-50'
|
||||
: 'text-orange-600 border-orange-200 bg-orange-50'
|
||||
? 'text-red-600 border-red-200 bg-red-50 dark:text-red-400 dark:border-red-800 dark:bg-red-900/30'
|
||||
: 'text-orange-600 border-orange-200 bg-orange-50 dark:text-orange-400 dark:border-orange-800 dark:bg-orange-900/30'
|
||||
}
|
||||
>
|
||||
{item.status}
|
||||
@@ -271,9 +237,9 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="bg-gray-100 font-semibold">
|
||||
<td className="px-4 py-2 text-gray-700" colSpan={3}>합계</td>
|
||||
<td className="px-4 py-2 text-right text-gray-900">
|
||||
<tr className="bg-muted font-semibold">
|
||||
<td className="px-4 py-2 text-muted-foreground" colSpan={3}>합계</td>
|
||||
<td className="px-4 py-2 text-right text-foreground">
|
||||
{filteredItems.reduce((sum, item) => sum + item.amount, 0).toLocaleString()}원
|
||||
</td>
|
||||
<td />
|
||||
@@ -281,7 +247,7 @@ export function SalesStatusSection({ data }: SalesStatusSectionProps) {
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, IssueCardItem } from '../components';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { IssueCardItem, CollapsibleDashboardCard } from '../components';
|
||||
import type { TodayIssueItem, TodayIssueSettings } from '../types';
|
||||
|
||||
// 라벨 → 설정키 매핑
|
||||
@@ -40,10 +40,11 @@ export function StatusBoardSection({ items, itemSettings }: StatusBoardSectionPr
|
||||
: items;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="현황판" badge="warning" />
|
||||
|
||||
<CollapsibleDashboardCard
|
||||
icon={<LayoutGrid style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="현황판"
|
||||
subtitle="주요 현황 요약"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{filteredItems.map((item) => (
|
||||
<IssueCardItem
|
||||
@@ -56,7 +57,6 @@ export function StatusBoardSection({ items, itemSettings }: StatusBoardSectionPr
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useMemo, useCallback, useRef, useLayoutEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@@ -31,6 +30,7 @@ import {
|
||||
Loader2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import { usePastIssue } from '@/hooks/useCEODashboard';
|
||||
import type { TodayIssueListItem, TodayIssueNotificationType } from '../types';
|
||||
|
||||
@@ -44,16 +44,16 @@ interface BadgeStyle {
|
||||
|
||||
// notification_type 코드 기반 스타일 매핑 (API 고정값 사용)
|
||||
const NOTIFICATION_STYLES: Record<TodayIssueNotificationType, BadgeStyle> = {
|
||||
sales_order: { bg: 'bg-blue-50', text: 'text-blue-700', iconBg: 'bg-blue-500', Icon: ShoppingCart },
|
||||
bad_debt: { bg: 'bg-purple-50', text: 'text-purple-700', iconBg: 'bg-purple-500', Icon: AlertCircle },
|
||||
safety_stock: { bg: 'bg-orange-50', text: 'text-orange-700', iconBg: 'bg-orange-500', Icon: Package },
|
||||
expected_expense: { bg: 'bg-green-50', text: 'text-green-700', iconBg: 'bg-green-500', Icon: Receipt },
|
||||
vat_report: { bg: 'bg-red-50', text: 'text-red-700', iconBg: 'bg-red-500', Icon: FileText },
|
||||
approval_request: { bg: 'bg-amber-50', text: 'text-amber-700', iconBg: 'bg-amber-500', Icon: CheckCircle2 },
|
||||
new_vendor: { bg: 'bg-emerald-50', text: 'text-emerald-700', iconBg: 'bg-emerald-500', Icon: Building2 },
|
||||
deposit: { bg: 'bg-cyan-50', text: 'text-cyan-700', iconBg: 'bg-cyan-500', Icon: TrendingUp },
|
||||
withdrawal: { bg: 'bg-pink-50', text: 'text-pink-700', iconBg: 'bg-pink-500', Icon: TrendingDown },
|
||||
other: { bg: 'bg-gray-50', text: 'text-gray-700', iconBg: 'bg-gray-500', Icon: Info },
|
||||
sales_order: { bg: 'bg-blue-50 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', iconBg: 'bg-blue-500', Icon: ShoppingCart },
|
||||
bad_debt: { bg: 'bg-purple-50 dark:bg-purple-900/30', text: 'text-purple-700 dark:text-purple-300', iconBg: 'bg-purple-500', Icon: AlertCircle },
|
||||
safety_stock: { bg: 'bg-orange-50 dark:bg-orange-900/30', text: 'text-orange-700 dark:text-orange-300', iconBg: 'bg-orange-500', Icon: Package },
|
||||
expected_expense: { bg: 'bg-green-50 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300', iconBg: 'bg-green-500', Icon: Receipt },
|
||||
vat_report: { bg: 'bg-red-50 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300', iconBg: 'bg-red-500', Icon: FileText },
|
||||
approval_request: { bg: 'bg-amber-50 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', iconBg: 'bg-amber-500', Icon: CheckCircle2 },
|
||||
new_vendor: { bg: 'bg-emerald-50 dark:bg-emerald-900/30', text: 'text-emerald-700 dark:text-emerald-300', iconBg: 'bg-emerald-500', Icon: Building2 },
|
||||
deposit: { bg: 'bg-cyan-50 dark:bg-cyan-900/30', text: 'text-cyan-700 dark:text-cyan-300', iconBg: 'bg-cyan-500', Icon: TrendingUp },
|
||||
withdrawal: { bg: 'bg-pink-50 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300', iconBg: 'bg-pink-500', Icon: TrendingDown },
|
||||
other: { bg: 'bg-muted/50', text: 'text-muted-foreground', iconBg: 'bg-gray-500', Icon: Info },
|
||||
};
|
||||
|
||||
// 신용등급 색상 매핑 (A=녹색, B=노랑, C=주황, D=빨강)
|
||||
@@ -277,11 +277,13 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
{/* 헤더 */}
|
||||
<CollapsibleDashboardCard
|
||||
icon={<AlertCircle style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="오늘의 이슈"
|
||||
subtitle="주요 알림 및 이슈 현황"
|
||||
>
|
||||
{/* 필터/탭 영역 */}
|
||||
<div className="flex items-center gap-3 mb-4 flex-wrap">
|
||||
<h2 className="text-lg font-semibold text-gray-900 shrink-0">오늘의 이슈</h2>
|
||||
|
||||
{/* 탭 */}
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="shrink-0">
|
||||
@@ -334,7 +336,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div className="flex items-center justify-between w-full gap-3">
|
||||
<span>{option.label}</span>
|
||||
<span className="bg-gray-100 text-gray-600 text-xs px-2 py-0.5 rounded-full font-medium">
|
||||
<span className="bg-muted text-muted-foreground text-xs px-2 py-0.5 rounded-full font-medium">
|
||||
{option.count}
|
||||
</span>
|
||||
</div>
|
||||
@@ -348,11 +350,11 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
<div ref={gridRef} className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-3 max-h-[400px] overflow-y-auto pr-1">
|
||||
{activeTab === 'past' && pastLoading ? (
|
||||
<div className="col-span-full flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-gray-400 mr-2" />
|
||||
<span className="text-sm text-gray-500">데이터를 불러오는 중...</span>
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground mr-2" />
|
||||
<span className="text-sm text-muted-foreground">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
) : filteredItems.length === 0 ? (
|
||||
<div className="col-span-full text-center py-8 text-gray-500">
|
||||
<div className="col-span-full text-center py-8 text-muted-foreground">
|
||||
{activeTab === 'past'
|
||||
? `${formatDateDisplay(pastDate)}에 이슈가 없습니다.`
|
||||
: '표시할 이슈가 없습니다.'}
|
||||
@@ -368,7 +370,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`flex items-center gap-2 p-3 border rounded-lg hover:shadow-md transition-all cursor-pointer ${badgeStyle.bg} border-transparent hover:border-gray-200`}
|
||||
className={`flex items-center gap-2 p-3 border rounded-lg hover:shadow-md transition-all cursor-pointer ${badgeStyle.bg} border-transparent hover:border-border`}
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
{/* 아이콘 + 뱃지 */}
|
||||
@@ -382,7 +384,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<span className="text-sm text-gray-800 truncate flex-1 min-w-0">
|
||||
<span className="text-sm text-foreground truncate flex-1 min-w-0">
|
||||
{item.content}
|
||||
</span>
|
||||
|
||||
@@ -404,7 +406,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
)}
|
||||
|
||||
{/* 시간 */}
|
||||
<span className="text-xs text-gray-500 whitespace-nowrap shrink-0">
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap shrink-0">
|
||||
{item.time}
|
||||
</span>
|
||||
|
||||
@@ -434,7 +436,7 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-6 px-2 text-xs text-gray-600 hover:text-green-600 hover:border-green-600 hover:bg-green-50"
|
||||
className="h-6 px-2 text-xs text-muted-foreground hover:text-green-600 hover:border-green-600 hover:bg-green-50"
|
||||
onClick={() => handleDismiss(item)}
|
||||
>
|
||||
확인
|
||||
@@ -447,7 +449,6 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { MultiSelectCombobox } from '@/components/ui/multi-select-combobox';
|
||||
import { CollapsibleDashboardCard } from '../components';
|
||||
import type { UnshippedData } from '../types';
|
||||
|
||||
interface UnshippedSectionProps {
|
||||
@@ -31,81 +32,68 @@ export function UnshippedSection({ data }: UnshippedSectionProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border overflow-hidden">
|
||||
{/* 다크 헤더 */}
|
||||
<div style={{ backgroundColor: '#1e293b' }} className="px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div style={{ backgroundColor: 'rgba(255,255,255,0.1)' }} className="p-2 rounded-lg">
|
||||
<PackageX style={{ color: '#ffffff' }} className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 style={{ color: '#ffffff' }} className="text-lg font-semibold">미출고 내역</h3>
|
||||
<p style={{ color: '#cbd5e1' }} className="text-sm">납기일 기준 미출고 현황</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
style={{ backgroundColor: '#ef4444', color: '#ffffff', border: 'none' }}
|
||||
className="hover:opacity-90"
|
||||
>
|
||||
{data.items.length}건
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ backgroundColor: '#ffffff' }} className="p-6">
|
||||
<CollapsibleDashboardCard
|
||||
icon={<PackageX style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="미출고 내역"
|
||||
subtitle="납기일 기준 미출고 현황"
|
||||
rightElement={
|
||||
<Badge
|
||||
className="bg-red-500 text-white border-none hover:opacity-90"
|
||||
>
|
||||
{data.items.length}건
|
||||
</Badge>
|
||||
}
|
||||
>
|
||||
{/* 미출고 테이블 */}
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 border-b">
|
||||
<h4 className="text-sm font-semibold text-gray-700">미출고 목록</h4>
|
||||
<div className="flex gap-2">
|
||||
<MultiSelectCombobox
|
||||
options={clients.map((c) => ({ value: c, label: c }))}
|
||||
value={clientFilter}
|
||||
onChange={setClientFilter}
|
||||
placeholder="전체 거래처"
|
||||
className="w-[160px] h-8 text-xs"
|
||||
/>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-xs">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="due-asc">납기일 가까운순</SelectItem>
|
||||
<SelectItem value="due-desc">납기일 먼순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
|
||||
<h4 className="text-sm font-semibold text-foreground">미출고 목록</h4>
|
||||
<MultiSelectCombobox
|
||||
options={clients.map((c) => ({ value: c, label: c }))}
|
||||
value={clientFilter}
|
||||
onChange={setClientFilter}
|
||||
placeholder="전체 거래처"
|
||||
className="w-full h-8 text-xs"
|
||||
/>
|
||||
<Select value={sortOrder} onValueChange={setSortOrder}>
|
||||
<SelectTrigger className="w-full h-8 text-xs">
|
||||
<SelectValue placeholder="정렬" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="due-asc">납기일 가까운순</SelectItem>
|
||||
<SelectItem value="due-desc">납기일 먼순</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm min-w-[550px]">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b">
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium w-12">No</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">포트번호</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">현장명</th>
|
||||
<th className="px-4 py-2 text-left text-gray-600 font-medium">수주처</th>
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium">납기일</th>
|
||||
<th className="px-4 py-2 text-center text-gray-600 font-medium">남은일</th>
|
||||
<tr className="bg-muted/50 border-b border-border">
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium w-12">No</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">포트번호</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">현장명</th>
|
||||
<th className="px-4 py-2 text-left text-muted-foreground font-medium">수주처</th>
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium">납기일</th>
|
||||
<th className="px-4 py-2 text-center text-muted-foreground font-medium">남은일</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredItems.map((item, idx) => (
|
||||
<tr key={item.id} className="border-b last:border-b-0 hover:bg-gray-50">
|
||||
<td className="px-4 py-2 text-center text-gray-500">{idx + 1}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.portNo}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.siteName}</td>
|
||||
<td className="px-4 py-2 text-gray-700">{item.orderClient}</td>
|
||||
<td className="px-4 py-2 text-center text-gray-700">{item.dueDate}</td>
|
||||
<tr key={item.id} className="border-b border-border last:border-b-0 hover:bg-muted/50">
|
||||
<td className="px-4 py-2 text-center text-muted-foreground">{idx + 1}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.portNo}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.siteName}</td>
|
||||
<td className="px-4 py-2 text-muted-foreground">{item.orderClient}</td>
|
||||
<td className="px-4 py-2 text-center text-muted-foreground">{item.dueDate}</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
item.daysLeft <= 3
|
||||
? 'text-red-600 border-red-200 bg-red-50'
|
||||
? 'text-red-600 border-red-200 bg-red-50 dark:text-red-400 dark:border-red-800 dark:bg-red-900/30'
|
||||
: item.daysLeft <= 7
|
||||
? 'text-orange-600 border-orange-200 bg-orange-50'
|
||||
: 'text-gray-600 border-gray-200 bg-gray-50'
|
||||
? 'text-orange-600 border-orange-200 bg-orange-50 dark:text-orange-400 dark:border-orange-800 dark:bg-orange-900/30'
|
||||
: 'text-muted-foreground border-border bg-muted/50'
|
||||
}
|
||||
>
|
||||
D-{item.daysLeft}
|
||||
@@ -117,7 +105,6 @@ export function UnshippedSection({ data }: UnshippedSectionProps) {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem } from '../components';
|
||||
import { Calculator } from 'lucide-react';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard } from '../components';
|
||||
import type { VatData } from '../types';
|
||||
|
||||
interface VatSectionProps {
|
||||
@@ -11,28 +11,28 @@ interface VatSectionProps {
|
||||
|
||||
export function VatSection({ data, onClick }: VatSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle title="부가세 현황" badge="warning" />
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Calculator style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="부가세 현황"
|
||||
subtitle="부가세 납부 정보"
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
{data.cards.map((card) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onClick}
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Heart, Gift, Coffee, Smile } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SectionTitle, AmountCardItem, CheckPointItem, type SectionColorTheme } from '../components';
|
||||
import { AmountCardItem, CheckPointItem, CollapsibleDashboardCard, type SectionColorTheme } from '../components';
|
||||
import type { WelfareData } from '../types';
|
||||
|
||||
// 카드별 아이콘 매핑
|
||||
@@ -16,38 +15,33 @@ interface WelfareSectionProps {
|
||||
|
||||
export function WelfareSection({ data, onCardClick }: WelfareSectionProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<SectionTitle
|
||||
title="복리후생비 현황"
|
||||
badge="info"
|
||||
icon={Heart}
|
||||
colorTheme="emerald"
|
||||
/>
|
||||
<CollapsibleDashboardCard
|
||||
icon={<Heart style={{ color: '#ffffff' }} className="h-5 w-5" />}
|
||||
title="복리후생비 현황"
|
||||
subtitle="복리후생비 사용 현황"
|
||||
>
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onCardClick ? () => onCardClick(card.id) : undefined}
|
||||
icon={CARD_ICONS[idx] || Heart}
|
||||
colorTheme={CARD_THEMES[idx] || 'emerald'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="up"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 xs:gap-4 mb-4">
|
||||
{data.cards.map((card, idx) => (
|
||||
<AmountCardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
onClick={onCardClick ? () => onCardClick(card.id) : undefined}
|
||||
icon={CARD_ICONS[idx] || Heart}
|
||||
colorTheme={CARD_THEMES[idx] || 'emerald'}
|
||||
showTrend={!!card.previousLabel}
|
||||
trendValue={card.previousLabel}
|
||||
trendDirection="up"
|
||||
/>
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{data.checkPoints.length > 0 && (
|
||||
<div className="border-t pt-4 space-y-1">
|
||||
{data.checkPoints.map((cp) => (
|
||||
<CheckPointItem key={cp.id} checkpoint={cp} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</CollapsibleDashboardCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user