feat: 회계/결재/생산/출하/대시보드 다수 개선 및 QA 수정
- BadDebtCollection, BillManagement, CardTransaction, TaxInvoice 회계 개선 - VendorManagement/VendorDetailClient 소폭 추가 - DocumentCreate/DraftBox 결재 기능 개선 - WorkOrder Create/Detail/Edit, ShipmentEdit 생산/출하 개선 - CEO 대시보드: PurchaseStatusSection, receivable/status-issue transformer 정비 - dashboard types/invalidation 확장 - LoginPage, Sidebar, HeaderFavoritesBar 레이아웃 수정 - QMS 페이지, StockStatusDetail, OrderRegistration 소폭 수정 - AttendanceManagement, VacationManagement HR 수정 - ConstructionDetailClient 건설 상세 개선 - claudedocs: 주간 구현내역, 대시보드 QA/수정계획, 결재/품질/생산/출하 문서 추가
This commit is contained in:
@@ -229,7 +229,7 @@ export default function QualityInspectionPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-auto">
|
||||
{/* 헤더 (설정 버튼 포함) */}
|
||||
<Header
|
||||
rightContent={<SettingsButton onClick={() => setSettingsOpen(true)} />}
|
||||
@@ -283,9 +283,9 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{activeDay === 1 ? (
|
||||
// ===== 기준/매뉴얼 심사 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-0">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-[500px]">
|
||||
{/* 좌측: 점검표 항목 */}
|
||||
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection && displaySettings.showDocumentViewer
|
||||
? 'lg:col-span-3'
|
||||
: displaySettings.showDocumentSection || displaySettings.showDocumentViewer
|
||||
@@ -303,7 +303,7 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{/* 중앙: 기준 문서화 */}
|
||||
{displaySettings.showDocumentSection && (
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentViewer ? 'lg:col-span-4' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentSection
|
||||
@@ -318,7 +318,7 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{/* 우측: 문서 뷰어 */}
|
||||
{displaySettings.showDocumentViewer && (
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection ? 'lg:col-span-5' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentViewer document={selectedStandardDoc} />
|
||||
@@ -327,8 +327,8 @@ export default function QualityInspectionPage() {
|
||||
</div>
|
||||
) : (
|
||||
// ===== 로트 추적 심사 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-0">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-[500px]">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<ReportList
|
||||
reports={filteredReports}
|
||||
selectedId={selectedReport?.id || null}
|
||||
@@ -336,7 +336,7 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<RouteList
|
||||
routes={currentRoutes}
|
||||
selectedId={selectedRoute?.id || null}
|
||||
@@ -346,7 +346,7 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<DocumentList
|
||||
documents={currentDocuments}
|
||||
routeCode={selectedRoute?.code || null}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
@@ -137,12 +138,14 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
if (isNewMode) {
|
||||
const result = await createBadDebt(formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '등록에 실패했습니다.' };
|
||||
} else {
|
||||
const result = await updateBadDebt(recordId!, formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '수정에 실패했습니다.' };
|
||||
@@ -159,6 +162,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
try {
|
||||
const result = await deleteBadDebt(String(id));
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '삭제에 실패했습니다.' };
|
||||
|
||||
@@ -14,6 +14,7 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2';
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -176,6 +177,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteBadDebt(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
setData((prev) => prev.filter((item) => item.id !== id));
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { useDeleteDialog } from '@/hooks/useDeleteDialog';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import {
|
||||
@@ -90,25 +88,6 @@ export function BillManagementClient({
|
||||
const [currentPage, setCurrentPage] = useState(initialPagination.currentPage);
|
||||
const itemsPerPage = initialPagination.perPage;
|
||||
|
||||
// 삭제 다이얼로그
|
||||
const deleteDialog = useDeleteDialog({
|
||||
onDelete: async (id) => {
|
||||
const result = await deleteBill(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('bill');
|
||||
// 서버에서 재조회 (로컬 필터링 대신 - 페이지네이션 정합성 보장)
|
||||
await loadData(currentPage);
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
entityName: '어음',
|
||||
});
|
||||
|
||||
// 날짜 범위 상태
|
||||
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
|
||||
|
||||
@@ -337,6 +316,25 @@ export function BillManagementClient({
|
||||
totalCount: pagination.total,
|
||||
};
|
||||
},
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteBill(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('bill');
|
||||
await loadData(currentPage);
|
||||
setSelectedItems(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
},
|
||||
},
|
||||
|
||||
// 삭제 확인 메시지
|
||||
deleteConfirmMessage: {
|
||||
title: '어음 삭제',
|
||||
description: '이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
|
||||
},
|
||||
|
||||
// 테이블 컬럼
|
||||
@@ -448,6 +446,7 @@ export function BillManagementClient({
|
||||
isLoading,
|
||||
router,
|
||||
loadData,
|
||||
currentPage,
|
||||
handleSave,
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
@@ -474,14 +473,6 @@ export function BillManagementClient({
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialog.single.isOpen}
|
||||
onOpenChange={deleteDialog.single.onOpenChange}
|
||||
onConfirm={deleteDialog.single.confirm}
|
||||
title="어음 삭제"
|
||||
description="이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
loading={deleteDialog.isPending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const tableColumns = [
|
||||
{ key: 'deductionType', label: '공제', className: 'min-w-[95px]', sortable: false },
|
||||
{ key: 'businessNumber', label: '사업자번호', className: 'min-w-[110px]' },
|
||||
{ key: 'merchantName', label: '가맹점명', className: 'min-w-[100px]' },
|
||||
{ key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[130px]', sortable: false },
|
||||
{ key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[160px]', sortable: false },
|
||||
{ key: 'description', label: '내역', className: 'min-w-[120px]', sortable: false },
|
||||
{ key: 'totalAmount', label: '합계금액', className: 'min-w-[100px] text-right' },
|
||||
{ key: 'supplyAmount', label: '공급가액', className: 'min-w-[110px] text-right', sortable: false },
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { FormField } from '@/components/molecules/FormField';
|
||||
import { BusinessNumberInput } from '@/components/ui/business-number-input';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -199,12 +200,14 @@ export function ManualEntryModal({
|
||||
onChange={(value) => handleChange('vendorName', value)}
|
||||
placeholder="공급자명"
|
||||
/>
|
||||
<FormField
|
||||
label="사업자번호"
|
||||
value={formData.vendorBusinessNumber}
|
||||
onChange={(value) => handleChange('vendorBusinessNumber', value)}
|
||||
placeholder="사업자번호"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">사업자번호</Label>
|
||||
<BusinessNumberInput
|
||||
value={formData.vendorBusinessNumber}
|
||||
onChange={(value) => handleChange('vendorBusinessNumber', value)}
|
||||
placeholder="000-00-00000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -257,10 +257,14 @@ export function transformFrontendToApi(data: ManualEntryFormData): Record<string
|
||||
const isSales = data.division === 'sales';
|
||||
return {
|
||||
direction: isSales ? 'sales' : 'purchases',
|
||||
issue_type: 'normal',
|
||||
issue_date: data.writeDate,
|
||||
...(isSales
|
||||
? { buyer_corp_name: data.vendorName, buyer_corp_num: data.vendorBusinessNumber }
|
||||
: { supplier_corp_name: data.vendorName, supplier_corp_num: data.vendorBusinessNumber }),
|
||||
// 매출: 거래처=공급받는자(buyer), 매입: 거래처=공급자(supplier)
|
||||
// DB 컬럼이 NOT NULL이므로 빈 문자열로 전송
|
||||
supplier_corp_name: isSales ? '' : data.vendorName,
|
||||
supplier_corp_num: isSales ? '' : data.vendorBusinessNumber,
|
||||
buyer_corp_name: isSales ? data.vendorName : '',
|
||||
buyer_corp_num: isSales ? data.vendorBusinessNumber : '',
|
||||
supply_amount: data.supplyAmount,
|
||||
tax_amount: data.taxAmount,
|
||||
total_amount: data.totalAmount,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { Plus, Trash2, Upload } from 'lucide-react';
|
||||
@@ -194,6 +195,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
return { success: false, error: result.message || '저장에 실패했습니다.' };
|
||||
}
|
||||
|
||||
invalidateDashboard('client');
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -214,6 +216,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
return { success: false, error: result.message || '삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
invalidateDashboard('client');
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
@@ -129,6 +130,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteClient(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('client');
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect, useTransition, useRef, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { format } from 'date-fns';
|
||||
@@ -50,7 +51,7 @@ import { getClients } from '@/components/accounting/VendorManagement/actions';
|
||||
|
||||
// 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정
|
||||
const getInitialBasicInfo = (): BasicInfo => ({
|
||||
drafter: '홍길동',
|
||||
drafter: '', // 클라이언트에서 currentUser로 설정
|
||||
draftDate: '', // 클라이언트에서 설정
|
||||
documentNo: '',
|
||||
documentType: 'proposal',
|
||||
@@ -118,14 +119,22 @@ export function DocumentCreate() {
|
||||
const today = format(new Date(), 'yyyy-MM-dd');
|
||||
const now = format(new Date(), 'yyyy-MM-dd HH:mm');
|
||||
|
||||
setBasicInfo(prev => ({ ...prev, draftDate: prev.draftDate || now }));
|
||||
// localStorage 'user' 키에서 사용자 이름 가져오기 (로그인 시 저장됨)
|
||||
const userDataStr = typeof window !== 'undefined' ? localStorage.getItem('user') : null;
|
||||
const userName = userDataStr ? JSON.parse(userDataStr).name : currentUser?.name || '';
|
||||
|
||||
setBasicInfo(prev => ({
|
||||
...prev,
|
||||
drafter: prev.drafter || userName,
|
||||
draftDate: prev.draftDate || now,
|
||||
}));
|
||||
setProposalData(prev => ({ ...prev, vendorPaymentDate: prev.vendorPaymentDate || today }));
|
||||
setExpenseReportData(prev => ({
|
||||
...prev,
|
||||
requestDate: prev.requestDate || today,
|
||||
paymentDate: prev.paymentDate || today,
|
||||
}));
|
||||
}, []);
|
||||
}, [currentUser?.name]);
|
||||
|
||||
// 미리보기 모달 상태
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
@@ -172,6 +181,7 @@ export function DocumentCreate() {
|
||||
setBasicInfo(prev => ({
|
||||
...prev,
|
||||
...mockData.basicInfo,
|
||||
drafter: currentUserName || prev.drafter,
|
||||
draftDate: prev.draftDate || mockData.basicInfo.draftDate,
|
||||
documentType: (mockData.basicInfo.documentType || prev.documentType) as BasicInfo['documentType'],
|
||||
}));
|
||||
@@ -343,6 +353,7 @@ export function DocumentCreate() {
|
||||
try {
|
||||
const result = await deleteApproval(parseInt(documentId));
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('문서가 삭제되었습니다.');
|
||||
router.back();
|
||||
} else {
|
||||
@@ -375,6 +386,7 @@ export function DocumentCreate() {
|
||||
if (isEditMode && documentId) {
|
||||
const result = await updateAndSubmitApproval(parseInt(documentId), formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('수정 및 상신 완료', {
|
||||
description: `문서번호: ${result.data?.documentNo}`,
|
||||
});
|
||||
@@ -386,6 +398,7 @@ export function DocumentCreate() {
|
||||
// 새 문서: 생성 후 상신
|
||||
const result = await createAndSubmitApproval(formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('상신 완료', {
|
||||
description: `문서번호: ${result.data?.documentNo}`,
|
||||
});
|
||||
@@ -411,6 +424,7 @@ export function DocumentCreate() {
|
||||
if (isEditMode && documentId) {
|
||||
const result = await updateApproval(parseInt(documentId), formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('저장 완료', {
|
||||
description: `문서번호: ${result.data?.documentNo}`,
|
||||
});
|
||||
@@ -421,6 +435,7 @@ export function DocumentCreate() {
|
||||
// 새 문서: 임시저장
|
||||
const result = await createApproval(formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('임시저장 완료', {
|
||||
description: `문서번호: ${result.data?.documentNo}`,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
@@ -175,6 +176,7 @@ export function DraftBox() {
|
||||
try {
|
||||
const result = await submitDrafts(ids);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success(`${ids.length}건의 문서를 상신했습니다.`);
|
||||
loadData();
|
||||
loadSummary();
|
||||
@@ -200,6 +202,7 @@ export function DraftBox() {
|
||||
try {
|
||||
const result = await deleteDrafts(ids);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success(`${ids.length}건의 문서를 삭제했습니다.`);
|
||||
loadData();
|
||||
loadSummary();
|
||||
@@ -222,6 +225,7 @@ export function DraftBox() {
|
||||
try {
|
||||
const result = await deleteDraft(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('문서를 삭제했습니다.');
|
||||
loadData();
|
||||
loadSummary();
|
||||
@@ -298,6 +302,7 @@ export function DraftBox() {
|
||||
try {
|
||||
const result = await submitDraft(selectedDocument.id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('approval');
|
||||
toast.success('문서를 상신했습니다.');
|
||||
setIsModalOpen(false);
|
||||
setSelectedDocument(null);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -268,7 +269,11 @@ export function LoginPage() {
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{t('rememberMe')}</span>
|
||||
</label>
|
||||
<button type="button" className="text-sm text-primary hover:underline">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-primary hover:underline"
|
||||
onClick={() => toast.info('비밀번호 초기화는 시스템 관리자에게 요청해 주세요.')}
|
||||
>
|
||||
{t('forgotPassword')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -47,12 +47,7 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
<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>
|
||||
}
|
||||
subtitle="매입 실적"
|
||||
>
|
||||
{/* 통계카드 3개 - 가로 배치 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
@@ -158,8 +153,8 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
{/* 당월 매입 내역 (별도 카드) */}
|
||||
<CollapsibleDashboardCard
|
||||
icon={<ShoppingCart className="h-5 w-5 text-white" />}
|
||||
title="당월 매입 내역"
|
||||
subtitle="당월 매입 거래 상세"
|
||||
title="최근 매입 내역"
|
||||
subtitle="매입 거래 상세"
|
||||
bodyClassName="p-0"
|
||||
>
|
||||
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
|
||||
|
||||
@@ -725,13 +725,13 @@ export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = {
|
||||
orders: true,
|
||||
debtCollection: true,
|
||||
safetyStock: true,
|
||||
taxReport: false,
|
||||
newVendor: false,
|
||||
taxReport: true,
|
||||
newVendor: true,
|
||||
annualLeave: true,
|
||||
vehicle: false,
|
||||
equipment: false,
|
||||
purchase: false,
|
||||
approvalRequest: false,
|
||||
approvalRequest: true,
|
||||
fundStatus: true,
|
||||
},
|
||||
},
|
||||
@@ -774,13 +774,13 @@ export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = {
|
||||
orders: true,
|
||||
debtCollection: true,
|
||||
safetyStock: true,
|
||||
taxReport: false,
|
||||
newVendor: false,
|
||||
taxReport: true,
|
||||
newVendor: true,
|
||||
annualLeave: true,
|
||||
vehicle: false,
|
||||
equipment: false,
|
||||
purchase: false,
|
||||
approvalRequest: false,
|
||||
approvalRequest: true,
|
||||
fundStatus: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getTodayString, formatDate } from '@/lib/utils/date';
|
||||
@@ -243,6 +244,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
|
||||
try {
|
||||
const result = await updateConstructionManagementDetail(id, formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('construction');
|
||||
toast.success('저장되었습니다.');
|
||||
return { success: true };
|
||||
} else {
|
||||
@@ -265,6 +267,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
|
||||
try {
|
||||
const result = await completeConstruction(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('construction');
|
||||
toast.success('시공이 완료되었습니다.');
|
||||
router.push('/ko/construction/project/construction-management');
|
||||
} else {
|
||||
|
||||
@@ -248,12 +248,14 @@ export function generatePurchaseApprovalData(options: GeneratePurchaseApprovalDa
|
||||
const { vendors = SAMPLE_VENDORS, documentType = 'proposal' } = options;
|
||||
const vendor = randomPick(vendors);
|
||||
|
||||
// 현재 사용자를 결재선에 추가 (기본값: 홍길동)
|
||||
// 현재 사용자를 결재선에 추가 (기본값: 로그인 사용자 정보)
|
||||
const userDataStr = typeof window !== 'undefined' ? localStorage.getItem('user') : null;
|
||||
const userData = userDataStr ? JSON.parse(userDataStr) : null;
|
||||
const currentUser: ApprovalPerson = options.currentUser || {
|
||||
id: 'user-1',
|
||||
department: '개발팀',
|
||||
position: '사원',
|
||||
name: '홍길동',
|
||||
id: userData?.id || 'user-1',
|
||||
department: userData?.department || '',
|
||||
position: userData?.position || '',
|
||||
name: userData?.name || '',
|
||||
};
|
||||
|
||||
// 경리/회계/재무 직원 중 랜덤으로 1명 참조 추가
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
Clock,
|
||||
@@ -310,6 +311,7 @@ export function AttendanceManagement() {
|
||||
if (attendanceDialogMode === 'create') {
|
||||
const result = await createAttendance(data);
|
||||
if (result.success && result.data) {
|
||||
invalidateDashboard('attendance');
|
||||
setAttendanceRecords(prev => [result.data!, ...prev]);
|
||||
} else {
|
||||
console.error('Create failed:', result.error);
|
||||
@@ -317,6 +319,7 @@ export function AttendanceManagement() {
|
||||
} else if (selectedAttendance) {
|
||||
const result = await updateAttendance(selectedAttendance.id, data);
|
||||
if (result.success && result.data) {
|
||||
invalidateDashboard('attendance');
|
||||
setAttendanceRecords(prev =>
|
||||
prev.map(r => r.id === selectedAttendance.id ? result.data! : r)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { format } from 'date-fns';
|
||||
import { useDateRange } from '@/hooks';
|
||||
import {
|
||||
@@ -312,6 +313,7 @@ export function VacationManagement() {
|
||||
const ids = Array.from(selectedItems).map((id) => parseInt(id, 10));
|
||||
const result = await approveLeavesMany(ids);
|
||||
if (result.success) {
|
||||
invalidateDashboard('leave');
|
||||
await fetchLeaveRequests();
|
||||
await fetchUsageData(); // 휴가 사용현황도 갱신
|
||||
} else {
|
||||
@@ -340,6 +342,7 @@ export function VacationManagement() {
|
||||
const ids = Array.from(selectedItems).map((id) => parseInt(id, 10));
|
||||
const result = await rejectLeavesMany(ids, '관리자에 의해 반려됨');
|
||||
if (result.success) {
|
||||
invalidateDashboard('leave');
|
||||
await fetchLeaveRequests();
|
||||
} else {
|
||||
console.error('[VacationManagement] 반려 실패:', result.error);
|
||||
@@ -750,6 +753,7 @@ export function VacationManagement() {
|
||||
reason: data.reason,
|
||||
});
|
||||
if (result.success) {
|
||||
invalidateDashboard('leave');
|
||||
await fetchGrantData();
|
||||
await fetchUsageData();
|
||||
} else {
|
||||
@@ -780,6 +784,7 @@ export function VacationManagement() {
|
||||
days: data.vacationDays,
|
||||
});
|
||||
if (result.success) {
|
||||
invalidateDashboard('leave');
|
||||
await fetchLeaveRequests();
|
||||
await fetchUsageData();
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Bookmark, MoreHorizontal } from 'lucide-react';
|
||||
import { Pin, MoreHorizontal } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -51,7 +51,7 @@ function StarDropdown({
|
||||
className={`p-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center ${className ?? 'w-10 h-10'}`}
|
||||
title="즐겨찾기"
|
||||
>
|
||||
<Bookmark className="h-4 w-4 fill-white" />
|
||||
<Pin className="h-4 w-4 fill-white" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bookmark, ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle } from 'lucide-react';
|
||||
import { Pin, ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle } from 'lucide-react';
|
||||
import type { MenuItem } from '@/stores/menuStore';
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useFavoritesStore, MAX_FAVORITES } from '@/stores/favoritesStore';
|
||||
@@ -159,7 +159,7 @@ function MenuItemComponent({
|
||||
}`}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
<Bookmark className={`h-3.5 w-3.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
<Pin className={`h-3.5 w-3.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@ function MenuItemComponent({
|
||||
}`}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
<Bookmark className={`h-3 w-3 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
<Pin className={`h-3 w-3 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -291,7 +291,7 @@ function MenuItemComponent({
|
||||
}`}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
<Bookmark className={`h-2.5 w-2.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
<Pin className={`h-2.5 w-2.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -133,6 +134,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
|
||||
const result = await updateStock(id, formData);
|
||||
|
||||
if (result.success) {
|
||||
invalidateDashboard('stock');
|
||||
toast.success('재고 정보가 저장되었습니다.');
|
||||
// 상세 데이터 업데이트
|
||||
setDetail((prev) =>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useDaumPostcode } from "@/hooks/useDaumPostcode";
|
||||
import { useClientList } from "@/hooks/useClientList";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -504,6 +505,7 @@ export function OrderRegistration({
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await onSave(form);
|
||||
invalidateDashboard('order');
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : '저장 중 오류가 발생했습니다.';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Plus, Trash2, ChevronDown, Search } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -305,6 +306,7 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || '출고 수정에 실패했습니다.' };
|
||||
}
|
||||
invalidateDashboard('shipment');
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
if (isNextRedirectError(err)) throw err;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -291,6 +292,7 @@ export function WorkOrderCreate() {
|
||||
if (!result.success) {
|
||||
return { success: false, error: result.error || '작업지시 등록에 실패했습니다.' };
|
||||
}
|
||||
invalidateDashboard('production');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FileText, Play, CheckCircle2, Loader2, Undo2, ClipboardCheck } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -272,6 +273,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
try {
|
||||
const result = await updateWorkOrderStatus(orderId, newStatus);
|
||||
if (result.success && result.data) {
|
||||
invalidateDashboard('production');
|
||||
setOrder(result.data);
|
||||
const statusLabels = {
|
||||
waiting: '작업대기',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SquarePen, Trash2 } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -239,6 +240,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
invalidateDashboard('production');
|
||||
toast.success('작업지시가 수정되었습니다.');
|
||||
router.push(`/production/work-orders/${orderId}?mode=view`);
|
||||
return { success: true };
|
||||
|
||||
@@ -93,31 +93,11 @@ function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거
|
||||
// 채권추심 카드별 더미 서브라벨 (회사명 + 건수)
|
||||
const DEBT_COLLECTION_FALLBACK_SUB_LABELS: Record<string, { company: string; count: number }> = {
|
||||
dc1: { company: '(주)부산화학 외', count: 5 },
|
||||
dc2: { company: '(주)삼성테크 외', count: 3 },
|
||||
dc3: { company: '(주)대한전자 외', count: 2 },
|
||||
dc4: { company: '(주)한국정밀 외', count: 3 },
|
||||
};
|
||||
|
||||
/**
|
||||
* 채권추심 subLabel 생성 헬퍼
|
||||
* dc1(누적)은 API client_count 사용, 나머지는 더미값
|
||||
* 채권추심 subLabel: 백엔드 sub_labels 필드 직접 사용
|
||||
*/
|
||||
function buildDebtSubLabel(cardId: string, clientCount?: number): string | undefined {
|
||||
const fallback = DEBT_COLLECTION_FALLBACK_SUB_LABELS[cardId];
|
||||
if (!fallback) return undefined;
|
||||
|
||||
const count = cardId === 'dc1' && clientCount !== undefined ? clientCount : fallback.count;
|
||||
if (count <= 0) return undefined;
|
||||
|
||||
const remaining = count - 1;
|
||||
if (remaining > 0) {
|
||||
return `${fallback.company} ${remaining}건`;
|
||||
}
|
||||
return fallback.company.replace(/ 외$/, '');
|
||||
function buildDebtSubLabel(cardId: string, subLabels?: Record<string, string | null>): string | undefined {
|
||||
return subLabels?.[cardId] || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,25 +110,25 @@ export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCo
|
||||
id: 'dc1',
|
||||
label: '누적 악성채권',
|
||||
amount: api.total_amount,
|
||||
subLabel: buildDebtSubLabel('dc1', api.client_count),
|
||||
subLabel: buildDebtSubLabel('dc1', api.sub_labels),
|
||||
},
|
||||
{
|
||||
id: 'dc2',
|
||||
label: '추심중',
|
||||
amount: api.collecting_amount,
|
||||
subLabel: buildDebtSubLabel('dc2'),
|
||||
subLabel: buildDebtSubLabel('dc2', api.sub_labels),
|
||||
},
|
||||
{
|
||||
id: 'dc3',
|
||||
label: '법적조치',
|
||||
amount: api.legal_action_amount,
|
||||
subLabel: buildDebtSubLabel('dc3'),
|
||||
subLabel: buildDebtSubLabel('dc3', api.sub_labels),
|
||||
},
|
||||
{
|
||||
id: 'dc4',
|
||||
label: '회수완료',
|
||||
amount: api.recovered_amount,
|
||||
subLabel: buildDebtSubLabel('dc4'),
|
||||
subLabel: buildDebtSubLabel('dc4', api.sub_labels),
|
||||
},
|
||||
],
|
||||
checkPoints: generateDebtCollectionCheckPoints(api),
|
||||
|
||||
@@ -14,42 +14,26 @@ import { normalizePath } from './common';
|
||||
// ============================================
|
||||
// 현황판 (StatusBoard)
|
||||
// ============================================
|
||||
|
||||
// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거
|
||||
// API id 기준: orders, bad_debts, safety_stock, tax_deadline, new_clients, leaves, purchases, approvals
|
||||
const STATUS_BOARD_FALLBACK_SUB_LABELS: Record<string, string> = {
|
||||
orders: '(주)삼성전자 외',
|
||||
bad_debts: '주식회사 부산화학 외',
|
||||
safety_stock: '',
|
||||
tax_deadline: '',
|
||||
new_clients: '대한철강 외',
|
||||
leaves: '',
|
||||
// purchases: '(유)한국정밀 외', // [2026-03-03] 비활성화 — 백엔드 path 오류 + 데이터 정합성 이슈 (N4 참조)
|
||||
approvals: '구매 결재 외',
|
||||
};
|
||||
//
|
||||
// [대시보드 vs 원본 페이지 쿼리 조건 차이 — 건수 불일치는 버그 아님]
|
||||
//
|
||||
// | 항목 | 대시보드 조건 | 원본 페이지 |
|
||||
// |----------------|---------------------------------------------------|------------------------------------------|
|
||||
// | 수주 현황 | 오늘 날짜 + status=confirmed만 | /sales/order-management-sales (전체 기간) |
|
||||
// | 채권 추심 | status=collecting + is_active=true만 | /accounting/bad-debt-collection (전체) |
|
||||
// | 안전 재고 | safety_stock>0 && stock_qty<safety_stock (날짜무관) | /material/stock-status (날짜 필터 적용) |
|
||||
// | 세금 신고 | 가장 가까운 tax 일정 D-day | /accounting/tax-invoices |
|
||||
// | 신규 업체 | 최근 7일 이내 등록된 거래처만 | /accounting/vendors (전체 목록) |
|
||||
// | 연차 현황 | 오늘 기준 approved 휴가만 | /hr/vacation-management (전체 기간) |
|
||||
// | 발주 현황 | status=draft(임시저장)만 | /construction/order (전체 상태) |
|
||||
// | 결재 요청 | 현재 로그인 사용자의 pending 결재만 | /approval/inbox (필터 조건 다름) |
|
||||
//
|
||||
|
||||
/**
|
||||
* 현황판 subLabel 생성 헬퍼
|
||||
* API sub_label이 있으면 사용, 없으면 더미값 + 건수로 조합
|
||||
* 현황판 subLabel: 백엔드 sub_label 필드 직접 사용
|
||||
*/
|
||||
function buildStatusSubLabel(item: { id: string; count: number | string; sub_label?: string }): string | undefined {
|
||||
// API에서 sub_label 제공 시 우선 사용
|
||||
if (item.sub_label) return item.sub_label;
|
||||
|
||||
// 건수가 0이거나 문자열이면 subLabel 불필요
|
||||
const count = typeof item.count === 'number' ? item.count : parseInt(String(item.count), 10);
|
||||
if (isNaN(count) || count <= 0) return undefined;
|
||||
|
||||
const fallback = STATUS_BOARD_FALLBACK_SUB_LABELS[item.id];
|
||||
if (!fallback) return undefined;
|
||||
|
||||
// "대한철강 외" + 나머지 건수
|
||||
const remaining = count - 1;
|
||||
if (remaining > 0) {
|
||||
return `${fallback} ${remaining}건`;
|
||||
}
|
||||
// 1건이면 "외" 제거하고 이름만
|
||||
return fallback.replace(/ 외$/, '');
|
||||
function buildStatusSubLabel(item: { sub_label?: string }): string | undefined {
|
||||
return item.sub_label || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface BadDebtApiResponse {
|
||||
recovered_amount: number; // 회수완료
|
||||
bad_debt_amount: number; // 대손처리
|
||||
client_count?: number; // 거래처 수
|
||||
sub_labels?: Record<string, string | null>; // 카드별 거래처 sub_label (dc1~dc4)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -31,7 +31,17 @@ type DomainKey =
|
||||
| 'expectedExpense'
|
||||
| 'bill'
|
||||
| 'giftCertificate'
|
||||
| 'journalEntry';
|
||||
| 'journalEntry'
|
||||
| 'order'
|
||||
| 'stock'
|
||||
| 'schedule'
|
||||
| 'client'
|
||||
| 'leave'
|
||||
| 'approval'
|
||||
| 'attendance'
|
||||
| 'production'
|
||||
| 'shipment'
|
||||
| 'construction';
|
||||
|
||||
const DOMAIN_SECTION_MAP: Record<DomainKey, DashboardSectionKey[]> = {
|
||||
deposit: ['dailyReport', 'receivable'],
|
||||
@@ -43,6 +53,16 @@ const DOMAIN_SECTION_MAP: Record<DomainKey, DashboardSectionKey[]> = {
|
||||
bill: ['dailyReport', 'receivable'],
|
||||
giftCertificate: ['entertainment', 'cardManagement'],
|
||||
journalEntry: ['entertainment', 'welfare', 'monthlyExpense'],
|
||||
order: ['statusBoard', 'salesStatus'],
|
||||
stock: ['statusBoard'],
|
||||
schedule: ['statusBoard'],
|
||||
client: ['statusBoard'],
|
||||
leave: ['statusBoard', 'dailyAttendance'],
|
||||
approval: ['statusBoard'],
|
||||
attendance: ['statusBoard', 'dailyAttendance'],
|
||||
production: ['statusBoard', 'dailyProduction'],
|
||||
shipment: ['statusBoard', 'unshipped'],
|
||||
construction: ['statusBoard', 'construction'],
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'dashboard:stale-sections';
|
||||
|
||||
Reference in New Issue
Block a user