feat: 레이아웃/출하/생산/회계/대시보드 전반 개선
- HeaderFavoritesBar 대폭 개선 - Sidebar/AuthenticatedLayout 소폭 수정 - ShipmentCreate, VehicleDispatch 출하 관련 개선 - WorkOrderCreate/Edit, WorkerScreen 생산 관련 개선 - InspectionCreate 자재 입고검사 개선 - DailyReport, VendorDetail 회계 수정 - CEO 대시보드: CardManagement/DailyProduction/DailyAttendance 섹션 개선 - useCEODashboard, expense transformer 정비 - DocumentViewer, PDF generate route 소폭 수정 - bill-prototype 개발 페이지 추가 - mockData 불필요 데이터 제거
This commit is contained in:
@@ -20,6 +20,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { formatNumber as formatAmount } from '@/lib/utils/amount';
|
||||
import { printElement } from '@/lib/print-utils';
|
||||
import type { NoteReceivableItem, DailyAccountItem } from './types';
|
||||
import { getNoteReceivables, getDailyAccounts, getDailyReportSummary } from './actions';
|
||||
import { toast } from 'sonner';
|
||||
@@ -204,9 +205,19 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
}, []);
|
||||
|
||||
// ===== 인쇄 =====
|
||||
const printAreaRef = useRef<HTMLDivElement>(null);
|
||||
const handlePrint = useCallback(() => {
|
||||
window.print();
|
||||
}, []);
|
||||
if (printAreaRef.current) {
|
||||
printElement(printAreaRef.current, {
|
||||
title: `일일일보_${startDate}`,
|
||||
styles: `
|
||||
.print-container { font-size: 11px; }
|
||||
table { width: 100%; margin-bottom: 12px; }
|
||||
h3 { margin-bottom: 8px; }
|
||||
`,
|
||||
});
|
||||
}
|
||||
}, [startDate]);
|
||||
|
||||
// ===== USD 금액 포맷 =====
|
||||
const formatUsd = useCallback((value: number) => `$ ${formatAmount(value)}`, []);
|
||||
@@ -299,6 +310,8 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 인쇄 영역 */}
|
||||
<div ref={printAreaRef} className="print-area space-y-4 md:space-y-6">
|
||||
{/* 일자별 입출금 합계 */}
|
||||
<Card>
|
||||
<CardContent className="px-3 pt-4 pb-3 md:px-6 md:pt-6 md:pb-6">
|
||||
@@ -660,6 +673,7 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,6 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai
|
||||
import { vendorConfig } from './vendorConfig';
|
||||
import { CreditAnalysisModal, MOCK_CREDIT_DATA } from './CreditAnalysisModal';
|
||||
|
||||
// 필드명 매핑
|
||||
const FIELD_NAME_MAP: Record<string, string> = {
|
||||
businessNumber: '사업자등록번호',
|
||||
vendorName: '거래처명',
|
||||
category: '거래처 유형',
|
||||
};
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -29,7 +23,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
// 새 입력 컴포넌트
|
||||
import { PhoneInput } from '@/components/ui/phone-input';
|
||||
import { BusinessNumberInput } from '@/components/ui/business-number-input';
|
||||
@@ -186,13 +179,25 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
}
|
||||
|
||||
setValidationErrors(errors);
|
||||
if (Object.keys(errors).length > 0) {
|
||||
const firstError = Object.values(errors)[0];
|
||||
toast.error(firstError);
|
||||
}
|
||||
return Object.keys(errors).length === 0;
|
||||
}, [formData.businessNumber, formData.vendorName, formData.category]);
|
||||
|
||||
// 필드 변경 핸들러
|
||||
const handleChange = useCallback((field: string, value: string | number | boolean) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
}, []);
|
||||
// 에러 클리어
|
||||
if (validationErrors[field]) {
|
||||
setValidationErrors(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[field];
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [validationErrors]);
|
||||
|
||||
// 파일 검증 및 추가
|
||||
const validateAndAddFiles = useCallback((files: FileList | File[]) => {
|
||||
@@ -265,7 +270,6 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
// 저장 핸들러 (IntegratedDetailTemplate용)
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!validateForm()) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
return { success: false, error: '입력 내용을 확인해주세요.' };
|
||||
}
|
||||
|
||||
@@ -326,8 +330,9 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
onChange={(e) => handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={isViewMode || disabled}
|
||||
className="bg-white"
|
||||
className={`bg-white ${validationErrors[field] ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
{validationErrors[field] && <p className="text-sm text-red-500">{validationErrors[field]}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -350,7 +355,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
onValueChange={(val) => handleChange(field, val)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<SelectTrigger className="bg-white">
|
||||
<SelectTrigger className={`bg-white ${validationErrors[field] ? 'border-red-500' : ''}`}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -361,6 +366,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{validationErrors[field] && <p className="text-sm text-red-500">{validationErrors[field]}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -368,35 +374,6 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
// 폼 콘텐츠 렌더링 (View/Edit 공통)
|
||||
const renderFormContent = () => (
|
||||
<div className="space-y-6">
|
||||
{/* Validation 에러 표시 */}
|
||||
{Object.keys(validationErrors).length > 0 && (
|
||||
<Alert className="bg-red-50 border-red-200">
|
||||
<AlertDescription className="text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-lg">⚠️</span>
|
||||
<div className="flex-1">
|
||||
<strong className="block mb-2">
|
||||
입력 내용을 확인해주세요 ({Object.keys(validationErrors).length}개 오류)
|
||||
</strong>
|
||||
<ul className="space-y-1 text-sm">
|
||||
{Object.entries(validationErrors).map(([field, message]) => {
|
||||
const fieldName = FIELD_NAME_MAP[field] || field;
|
||||
return (
|
||||
<li key={field} className="flex items-start gap-1">
|
||||
<span>•</span>
|
||||
<span>
|
||||
<strong>{fieldName}</strong>: {message}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 기본 정보 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -496,6 +473,7 @@ export function VendorDetail({ mode, vendorId, openModal }: VendorDetailProps) {
|
||||
showValidation={!isViewMode}
|
||||
error={!!validationErrors.businessNumber}
|
||||
/>
|
||||
{validationErrors.businessNumber && <p className="text-sm text-red-500">{validationErrors.businessNumber}</p>}
|
||||
</div>
|
||||
{renderField('거래처코드', 'vendorCode', formData.vendorCode, { placeholder: '자동생성', disabled: true })}
|
||||
{renderField('거래처명', 'vendorName', formData.vendorName, { required: true })}
|
||||
|
||||
Reference in New Issue
Block a user