공사관리 리스트 공통화: - 입찰/계약/견적/인수인계/이슈/품목/노무/현장/파트너/단가/기성/현장브리핑/구조검토/유틸리티/작업자현황 리스트 공통 포맷터 적용 - 중복 포맷팅 로직 제거 (-530줄) 캘린더 기능 개선: - CEODashboard CalendarSection 기능 확장 - ScheduleCalendar DayCell/MonthView/WeekView 개선 - ui/calendar 컴포넌트 기능 추가 유틸리티 개선: - date.ts 날짜 유틸 함수 추가 - formatAmount.ts 금액 포맷 함수 추가 신규 추가: - useListHandlers 훅 추가 - src/constants/ 디렉토리 추가 - 포맷터 공통화 계획 문서 추가 - SAM ERP/MES 정체성 분석 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
608 lines
23 KiB
TypeScript
608 lines
23 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* 발주관리 - UniversalListPage 마이그레이션
|
|
*
|
|
* 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환
|
|
* - 클라이언트 사이드 필터링 (검색, 필터, 정렬)
|
|
* - ScheduleCalendar (beforeTableContent)
|
|
* - 달력 전용 필터 (siteFilters, workTeamFilters - 달력 이벤트 필터링용)
|
|
* - 달력 날짜 선택 필터링 (selectedCalendarDate)
|
|
* - DateRangeSelector + 등록 버튼 (dateRangeSelector + createButton)
|
|
* - filterConfig (multi 7개 + single 2개)
|
|
* - 삭제 기능 (deleteConfirmMessage)
|
|
*/
|
|
|
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
import { Package, Pencil, Trash2 } from 'lucide-react';
|
|
import { useListHandlers } from '@/hooks/useListHandlers';
|
|
import { Button } from '@/components/ui/button';
|
|
import { TableCell, TableRow } from '@/components/ui/table';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
|
import { MobileCard } from '@/components/organisms/MobileCard';
|
|
import { ScheduleCalendar, ScheduleEvent, DayBadge } from '@/components/common/ScheduleCalendar';
|
|
import {
|
|
UniversalListPage,
|
|
type UniversalListConfig,
|
|
type SelectionHandlers,
|
|
type RowClickHandlers,
|
|
} from '@/components/templates/UniversalListPage';
|
|
import { format, parseISO, isSameDay, startOfDay } from 'date-fns';
|
|
import type { Order, OrderStats } from './types';
|
|
import {
|
|
ORDER_STATUS_OPTIONS,
|
|
ORDER_SORT_OPTIONS,
|
|
ORDER_STATUS_STYLES,
|
|
ORDER_STATUS_LABELS,
|
|
ORDER_TYPE_OPTIONS,
|
|
ORDER_TYPE_LABELS,
|
|
MOCK_PARTNERS,
|
|
MOCK_SITES,
|
|
MOCK_CONSTRUCTION_PM,
|
|
MOCK_ORDER_MANAGERS,
|
|
MOCK_ORDER_COMPANIES,
|
|
MOCK_WORK_TEAM_LEADERS,
|
|
getScheduleColorByManager,
|
|
} from './types';
|
|
import {
|
|
getOrderList,
|
|
getOrderStats,
|
|
deleteOrder,
|
|
deleteOrders,
|
|
} from './actions';
|
|
|
|
// 테이블 컬럼 정의
|
|
const tableColumns = [
|
|
{ key: 'no', label: '번호', className: 'w-[50px] text-center' },
|
|
{ key: 'contractNumber', label: '계약번호', className: 'w-[100px]' },
|
|
{ key: 'partnerName', label: '거래처', className: 'w-[80px]' },
|
|
{ key: 'siteName', label: '현장명', className: 'min-w-[100px]' },
|
|
{ key: 'name', label: '명칭', className: 'w-[80px]' },
|
|
{ key: 'constructionPM', label: '공사PM', className: 'w-[70px]' },
|
|
{ key: 'orderManager', label: '발주담당자', className: 'w-[80px]' },
|
|
{ key: 'orderNumber', label: '발주번호', className: 'w-[100px]' },
|
|
{ key: 'orderCompany', label: '발주처명', className: 'w-[80px]' },
|
|
{ key: 'workTeamLeader', label: '작업반장', className: 'w-[70px]' },
|
|
{ key: 'constructionStartDate', label: '시공투입일', className: 'w-[90px]' },
|
|
{ key: 'orderType', label: '구분', className: 'w-[80px] text-center' },
|
|
{ key: 'item', label: '품목', className: 'w-[80px]' },
|
|
{ key: 'quantity', label: '수량', className: 'w-[60px] text-right' },
|
|
{ key: 'orderDate', label: '발주일', className: 'w-[90px]' },
|
|
{ key: 'plannedDeliveryDate', label: '계획인수일', className: 'w-[90px]' },
|
|
{ key: 'actualDeliveryDate', label: '실제인수일', className: 'w-[90px]' },
|
|
{ key: 'status', label: '상태', className: 'w-[80px] text-center' },
|
|
{ key: 'actions', label: '작업', className: 'w-[80px] text-center' },
|
|
];
|
|
|
|
interface OrderManagementListClientProps {
|
|
initialData?: Order[];
|
|
initialStats?: OrderStats;
|
|
}
|
|
|
|
export default function OrderManagementListClient({
|
|
initialData = [],
|
|
initialStats,
|
|
}: OrderManagementListClientProps) {
|
|
// ===== 공통 핸들러 Hook =====
|
|
const { handleRowClick, handleEdit, router } = useListHandlers<Order>(
|
|
'construction/order/order-management'
|
|
);
|
|
|
|
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
|
const [startDate, setStartDate] = useState<string>('');
|
|
const [endDate, setEndDate] = useState<string>('');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
// 달력 관련 상태
|
|
const [selectedCalendarDate, setSelectedCalendarDate] = useState<Date | null>(null);
|
|
const [calendarDate, setCalendarDate] = useState<Date>(new Date());
|
|
// 달력 전용 필터 (테이블 필터와 별도)
|
|
const [calendarSiteFilters, setCalendarSiteFilters] = useState<string[]>([]);
|
|
const [calendarWorkTeamFilters, setCalendarWorkTeamFilters] = useState<string[]>([]);
|
|
|
|
// 전체 데이터 (달력 이벤트용)
|
|
const [allOrders, setAllOrders] = useState<Order[]>(initialData);
|
|
|
|
// 필터 옵션 (memo)
|
|
const siteOptions: MultiSelectOption[] = useMemo(() => MOCK_SITES, []);
|
|
const workTeamOptions: MultiSelectOption[] = useMemo(() => MOCK_WORK_TEAM_LEADERS, []);
|
|
const partnerOptions: MultiSelectOption[] = useMemo(() => MOCK_PARTNERS, []);
|
|
const constructionPMOptions: MultiSelectOption[] = useMemo(() => MOCK_CONSTRUCTION_PM, []);
|
|
const orderManagerOptions: MultiSelectOption[] = useMemo(() => MOCK_ORDER_MANAGERS, []);
|
|
const orderCompanyOptions: MultiSelectOption[] = useMemo(() => MOCK_ORDER_COMPANIES, []);
|
|
const orderTypeOptions: MultiSelectOption[] = useMemo(() => ORDER_TYPE_OPTIONS, []);
|
|
|
|
// 달력 이벤트 데이터 (달력 전용 필터 적용)
|
|
const calendarEvents: ScheduleEvent[] = useMemo(() => {
|
|
return allOrders
|
|
.filter((order) => {
|
|
// 유효한 날짜가 있는 항목만 달력에 표시
|
|
// periodStart/periodEnd가 빈 문자열이면 parseISO가 Invalid Date를 반환하여
|
|
// 모든 이벤트가 일요일(0번 컬럼)에 표시되는 버그 방지
|
|
if (!order.periodStart || !order.periodEnd) {
|
|
return false;
|
|
}
|
|
|
|
// 현장 필터 (달력용)
|
|
if (calendarSiteFilters.length > 0) {
|
|
const matchingSite = MOCK_SITES.find((s) => order.siteName.includes(s.label.split(' ')[0]));
|
|
if (!matchingSite || !calendarSiteFilters.includes(matchingSite.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
// 작업반장 필터 (달력용)
|
|
if (calendarWorkTeamFilters.length > 0) {
|
|
const matchingLeader = MOCK_WORK_TEAM_LEADERS.find((l) => order.orderManager.includes(l.label.replace('반장', '')));
|
|
if (!matchingLeader || !calendarWorkTeamFilters.includes(matchingLeader.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
})
|
|
.map((order) => ({
|
|
id: order.id,
|
|
title: `${order.orderManager} - ${order.siteName} / ${order.orderNumber}`,
|
|
startDate: order.periodStart,
|
|
endDate: order.periodEnd,
|
|
color: getScheduleColorByManager(order.orderManager),
|
|
status: order.status,
|
|
data: order,
|
|
}));
|
|
}, [allOrders, calendarSiteFilters, calendarWorkTeamFilters]);
|
|
|
|
// 달력 뱃지 (사용 안 함)
|
|
const calendarBadges: DayBadge[] = [];
|
|
|
|
// 날짜 포맷
|
|
const formatDate = useCallback((dateStr: string | null) => {
|
|
if (!dateStr) return '-';
|
|
return dateStr.split('T')[0];
|
|
}, []);
|
|
|
|
// ===== 추가 핸들러 =====
|
|
const handleCreate = useCallback(() => {
|
|
router.push('/ko/construction/order/order-management?mode=new');
|
|
}, [router]);
|
|
|
|
// 달력 이벤트 핸들러
|
|
const handleCalendarDateClick = useCallback((date: Date) => {
|
|
if (selectedCalendarDate && isSameDay(selectedCalendarDate, date)) {
|
|
setSelectedCalendarDate(null);
|
|
} else {
|
|
setSelectedCalendarDate(date);
|
|
}
|
|
}, [selectedCalendarDate]);
|
|
|
|
const handleCalendarEventClick = useCallback((event: ScheduleEvent) => {
|
|
if (event.data) {
|
|
router.push(`/ko/construction/order/order-management/${event.id}?mode=view`);
|
|
}
|
|
}, [router]);
|
|
|
|
const handleCalendarMonthChange = useCallback((date: Date) => {
|
|
setCalendarDate(date);
|
|
}, []);
|
|
|
|
// 달력 필터 슬롯
|
|
const calendarFilterSlot = useMemo(() => (
|
|
<div className="flex items-center gap-2">
|
|
<MultiSelectCombobox
|
|
options={siteOptions}
|
|
value={calendarSiteFilters}
|
|
onChange={setCalendarSiteFilters}
|
|
placeholder="현장"
|
|
searchPlaceholder="현장 검색..."
|
|
className="w-[160px]"
|
|
/>
|
|
<MultiSelectCombobox
|
|
options={workTeamOptions}
|
|
value={calendarWorkTeamFilters}
|
|
onChange={setCalendarWorkTeamFilters}
|
|
placeholder="작업반장"
|
|
searchPlaceholder="작업반장 검색..."
|
|
className="w-[130px]"
|
|
/>
|
|
</div>
|
|
), [siteOptions, workTeamOptions, calendarSiteFilters, calendarWorkTeamFilters]);
|
|
|
|
// ===== UniversalListPage Config =====
|
|
const config: UniversalListConfig<Order> = useMemo(
|
|
() => ({
|
|
// 페이지 기본 정보
|
|
title: '발주관리',
|
|
description: '발주 스케줄 및 목록을 관리합니다',
|
|
icon: Package,
|
|
basePath: '/construction/order/order-management',
|
|
|
|
// ID 추출
|
|
idField: 'id',
|
|
|
|
// API 액션
|
|
actions: {
|
|
getList: async () => {
|
|
const result = await getOrderList({
|
|
size: 1000,
|
|
startDate: startDate || undefined,
|
|
endDate: endDate || undefined,
|
|
});
|
|
if (result.success && result.data) {
|
|
return {
|
|
success: true,
|
|
data: result.data.items,
|
|
totalCount: result.data.total,
|
|
};
|
|
}
|
|
return { success: false, error: result.error };
|
|
},
|
|
deleteItem: async (id: string) => {
|
|
const result = await deleteOrder(id);
|
|
return { success: result.success, error: result.error };
|
|
},
|
|
deleteBulk: async (ids: string[]) => {
|
|
const result = await deleteOrders(ids);
|
|
return { success: result.success, error: result.error };
|
|
},
|
|
},
|
|
|
|
// 테이블 컬럼
|
|
columns: tableColumns,
|
|
|
|
// 클라이언트 사이드 필터링
|
|
clientSideFiltering: true,
|
|
itemsPerPage: 20,
|
|
|
|
// 데이터 변경 콜백 (달력 이벤트용)
|
|
onDataChange: (data) => setAllOrders(data),
|
|
|
|
// 검색 필터
|
|
searchPlaceholder: '발주번호, 거래처, 현장명, 발주담당 검색',
|
|
searchFilter: (item, searchValue) => {
|
|
const search = searchValue.toLowerCase();
|
|
return (
|
|
item.orderNumber.toLowerCase().includes(search) ||
|
|
item.partnerName.toLowerCase().includes(search) ||
|
|
item.siteName.toLowerCase().includes(search) ||
|
|
item.orderManager.toLowerCase().includes(search)
|
|
);
|
|
},
|
|
|
|
// 필터 설정
|
|
filterConfig: [
|
|
{ key: 'partners', label: '거래처', type: 'multi', options: partnerOptions },
|
|
{ key: 'sites', label: '현장명', type: 'multi', options: siteOptions },
|
|
{ key: 'constructionPMs', label: '공사PM', type: 'multi', options: constructionPMOptions },
|
|
{ key: 'orderManagers', label: '발주담당자', type: 'multi', options: orderManagerOptions },
|
|
{ key: 'orderCompanies', label: '발주처', type: 'multi', options: orderCompanyOptions },
|
|
{ key: 'workTeamLeaders', label: '작업반장', type: 'multi', options: workTeamOptions },
|
|
{ key: 'orderTypes', label: '구분', type: 'multi', options: orderTypeOptions },
|
|
{ key: 'status', label: '상태', type: 'single', options: ORDER_STATUS_OPTIONS.filter((o) => o.value !== 'all') },
|
|
{ key: 'sortBy', label: '정렬', type: 'single', options: ORDER_SORT_OPTIONS },
|
|
],
|
|
initialFilters: {
|
|
partners: [],
|
|
sites: [],
|
|
constructionPMs: [],
|
|
orderManagers: [],
|
|
orderCompanies: [],
|
|
workTeamLeaders: [],
|
|
orderTypes: [],
|
|
status: 'all',
|
|
sortBy: 'latest',
|
|
},
|
|
filterTitle: '발주 필터',
|
|
|
|
// 커스텀 필터 함수
|
|
customFilterFn: (items, filterValues) => {
|
|
if (!items || items.length === 0) return items;
|
|
return items.filter((item) => {
|
|
// 거래처 필터 (다중선택)
|
|
const partnerFilters = filterValues.partners as string[];
|
|
if (partnerFilters?.length > 0) {
|
|
const matchingPartner = MOCK_PARTNERS.find((p) => p.label === item.partnerName);
|
|
if (!matchingPartner || !partnerFilters.includes(matchingPartner.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 현장명 필터 (다중선택)
|
|
const siteFilters = filterValues.sites as string[];
|
|
if (siteFilters?.length > 0) {
|
|
const matchingSite = MOCK_SITES.find((s) => s.label === item.siteName);
|
|
if (!matchingSite || !siteFilters.includes(matchingSite.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 공사PM 필터 (다중선택)
|
|
const pmFilters = filterValues.constructionPMs as string[];
|
|
if (pmFilters?.length > 0) {
|
|
const matchingPM = MOCK_CONSTRUCTION_PM.find((p) => p.label === item.constructionPM);
|
|
if (!matchingPM || !pmFilters.includes(matchingPM.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 발주담당자 필터 (다중선택)
|
|
const managerFilters = filterValues.orderManagers as string[];
|
|
if (managerFilters?.length > 0) {
|
|
const matchingManager = MOCK_ORDER_MANAGERS.find((m) => m.label === item.orderManager);
|
|
if (!matchingManager || !managerFilters.includes(matchingManager.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 발주처 필터 (다중선택)
|
|
const companyFilters = filterValues.orderCompanies as string[];
|
|
if (companyFilters?.length > 0) {
|
|
const matchingCompany = MOCK_ORDER_COMPANIES.find((c) => c.label === item.orderCompany);
|
|
if (!matchingCompany || !companyFilters.includes(matchingCompany.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 작업반장 필터 (다중선택)
|
|
const teamLeaderFilters = filterValues.workTeamLeaders as string[];
|
|
if (teamLeaderFilters?.length > 0) {
|
|
const matchingLeader = MOCK_WORK_TEAM_LEADERS.find((l) => l.label === item.workTeamLeader);
|
|
if (!matchingLeader || !teamLeaderFilters.includes(matchingLeader.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 구분 필터 (다중선택)
|
|
const typeFilters = filterValues.orderTypes as string[];
|
|
if (typeFilters?.length > 0 && !typeFilters.includes(item.orderType)) {
|
|
return false;
|
|
}
|
|
|
|
// 상태 필터 (단일선택)
|
|
const statusFilter = filterValues.status as string;
|
|
if (statusFilter && statusFilter !== 'all' && item.status !== statusFilter) {
|
|
return false;
|
|
}
|
|
|
|
// 달력 날짜 필터
|
|
if (selectedCalendarDate) {
|
|
// periodStart/periodEnd가 빈 문자열이면 필터링에서 제외
|
|
if (!item.periodStart || !item.periodEnd) {
|
|
return false;
|
|
}
|
|
const orderStart = startOfDay(parseISO(item.periodStart));
|
|
const orderEnd = startOfDay(parseISO(item.periodEnd));
|
|
const selected = startOfDay(selectedCalendarDate);
|
|
if (selected < orderStart || selected > orderEnd) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
},
|
|
|
|
// 커스텀 정렬 함수
|
|
customSortFn: (items, filterValues) => {
|
|
const sorted = [...items];
|
|
const sortBy = (filterValues.sortBy as string) || 'latest';
|
|
|
|
switch (sortBy) {
|
|
case 'oldest':
|
|
sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
break;
|
|
case 'partnerNameAsc':
|
|
sorted.sort((a, b) => a.partnerName.localeCompare(b.partnerName, 'ko'));
|
|
break;
|
|
case 'partnerNameDesc':
|
|
sorted.sort((a, b) => b.partnerName.localeCompare(a.partnerName, 'ko'));
|
|
break;
|
|
case 'siteNameAsc':
|
|
sorted.sort((a, b) => a.siteName.localeCompare(b.siteName, 'ko'));
|
|
break;
|
|
case 'siteNameDesc':
|
|
sorted.sort((a, b) => b.siteName.localeCompare(a.siteName, 'ko'));
|
|
break;
|
|
case 'deliveryDateAsc':
|
|
sorted.sort((a, b) => a.plannedDeliveryDate.localeCompare(b.plannedDeliveryDate));
|
|
break;
|
|
case 'deliveryDateDesc':
|
|
sorted.sort((a, b) => b.plannedDeliveryDate.localeCompare(a.plannedDeliveryDate));
|
|
break;
|
|
default: // latest
|
|
sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
break;
|
|
}
|
|
return sorted;
|
|
},
|
|
|
|
// 검색창 (공통 컴포넌트에서 자동 생성)
|
|
hideSearch: true,
|
|
searchValue: searchQuery,
|
|
onSearchChange: setSearchQuery,
|
|
|
|
// 공통 헤더 옵션
|
|
dateRangeSelector: {
|
|
enabled: true,
|
|
startDate,
|
|
endDate,
|
|
onStartDateChange: setStartDate,
|
|
onEndDateChange: setEndDate,
|
|
},
|
|
createButton: {
|
|
label: '발주 등록',
|
|
onClick: handleCreate,
|
|
},
|
|
|
|
// 삭제 확인 메시지
|
|
deleteConfirmMessage: {
|
|
title: '발주 삭제',
|
|
description: '선택한 발주를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.',
|
|
},
|
|
|
|
// 테이블 헤더 액션 (총건 + 달력 날짜 필터 해제)
|
|
tableHeaderActions: ({ totalCount }) => (
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
|
총 {totalCount}건
|
|
{selectedCalendarDate && (
|
|
<span className="ml-2 text-primary">
|
|
({format(selectedCalendarDate, 'M/d')} 필터 적용중)
|
|
</span>
|
|
)}
|
|
</span>
|
|
{selectedCalendarDate && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setSelectedCalendarDate(null)}
|
|
>
|
|
날짜 필터 해제
|
|
</Button>
|
|
)}
|
|
</div>
|
|
),
|
|
|
|
// 달력 섹션 (beforeTableContent)
|
|
beforeTableContent: (
|
|
<div className="w-full flex-shrink-0 mb-6">
|
|
<ScheduleCalendar
|
|
events={calendarEvents}
|
|
badges={calendarBadges}
|
|
currentDate={calendarDate}
|
|
selectedDate={selectedCalendarDate}
|
|
onDateClick={handleCalendarDateClick}
|
|
onEventClick={handleCalendarEventClick}
|
|
onMonthChange={handleCalendarMonthChange}
|
|
titleSlot="발주 스케줄"
|
|
filterSlot={calendarFilterSlot}
|
|
maxEventsPerDay={5}
|
|
weekStartsOn={0}
|
|
isLoading={false}
|
|
/>
|
|
</div>
|
|
),
|
|
|
|
// 테이블 행 렌더링
|
|
renderTableRow: (
|
|
order: Order,
|
|
index: number,
|
|
globalIndex: number,
|
|
handlers: SelectionHandlers & RowClickHandlers<Order>
|
|
) => (
|
|
<TableRow
|
|
key={order.id}
|
|
className="cursor-pointer hover:bg-muted/50"
|
|
onClick={() => handleRowClick(order)}
|
|
>
|
|
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
|
<Checkbox checked={handlers.isSelected} onCheckedChange={handlers.onToggle} />
|
|
</TableCell>
|
|
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
|
|
<TableCell>{order.contractNumber}</TableCell>
|
|
<TableCell>{order.partnerName}</TableCell>
|
|
<TableCell>{order.siteName}</TableCell>
|
|
<TableCell>{order.name}</TableCell>
|
|
<TableCell>{order.constructionPM}</TableCell>
|
|
<TableCell>{order.orderManager}</TableCell>
|
|
<TableCell>{order.orderNumber}</TableCell>
|
|
<TableCell>{order.orderCompany}</TableCell>
|
|
<TableCell>{order.workTeamLeader}</TableCell>
|
|
<TableCell>{formatDate(order.constructionStartDate)}</TableCell>
|
|
<TableCell className="text-center">
|
|
<span className="px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
{ORDER_TYPE_LABELS[order.orderType]}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>{order.item}</TableCell>
|
|
<TableCell className="text-right">{order.quantity}</TableCell>
|
|
<TableCell>{formatDate(order.orderDate)}</TableCell>
|
|
<TableCell>{formatDate(order.plannedDeliveryDate)}</TableCell>
|
|
<TableCell>{formatDate(order.actualDeliveryDate)}</TableCell>
|
|
<TableCell className="text-center">
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${ORDER_STATUS_STYLES[order.status]}`}>
|
|
{ORDER_STATUS_LABELS[order.status]}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
{handlers.isSelected && (
|
|
<div className="flex items-center justify-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleEdit(order);
|
|
}}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handlers.onDelete?.(order);
|
|
}}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
|
|
// 모바일 카드 렌더링
|
|
renderMobileCard: (
|
|
order: Order,
|
|
index: number,
|
|
globalIndex: number,
|
|
handlers: SelectionHandlers & RowClickHandlers<Order>
|
|
) => (
|
|
<MobileCard
|
|
key={order.id}
|
|
title={order.siteName}
|
|
subtitle={order.orderNumber}
|
|
badge={ORDER_STATUS_LABELS[order.status]}
|
|
badgeVariant="secondary"
|
|
isSelected={handlers.isSelected}
|
|
onToggle={handlers.onToggle}
|
|
onClick={() => handleRowClick(order)}
|
|
details={[
|
|
{ label: '거래처', value: order.partnerName },
|
|
{ label: '발주담당', value: order.orderManager },
|
|
{ label: '계획인수일', value: formatDate(order.plannedDeliveryDate) },
|
|
]}
|
|
/>
|
|
),
|
|
}),
|
|
[
|
|
startDate,
|
|
endDate,
|
|
searchQuery,
|
|
selectedCalendarDate,
|
|
calendarEvents,
|
|
calendarBadges,
|
|
calendarDate,
|
|
calendarFilterSlot,
|
|
partnerOptions,
|
|
siteOptions,
|
|
constructionPMOptions,
|
|
orderManagerOptions,
|
|
orderCompanyOptions,
|
|
workTeamOptions,
|
|
orderTypeOptions,
|
|
handleRowClick,
|
|
handleEdit,
|
|
handleCreate,
|
|
handleCalendarDateClick,
|
|
handleCalendarEventClick,
|
|
handleCalendarMonthChange,
|
|
formatDate,
|
|
]
|
|
);
|
|
|
|
return <UniversalListPage config={config} initialData={initialData} onSearchChange={setSearchQuery} />;
|
|
}
|