feat: 모바일 반응형 UI 개선 및 공휴일/일정 시스템 통합
- MobileCard 접기/펼치기(collapsible) 기능 추가 및 반응형 레이아웃 개선 - DatePicker 공휴일/세무일정 색상 코딩 통합, DateTimePicker 신규 추가 - useCalendarScheduleInit 훅으로 전역 공휴일/일정 데이터 캐싱 - 전 도메인 날짜 필드 DatePicker 표준화 - 생산대시보드/작업지시/견적서/주문관리 모바일 호환성 강화 - 회계 모듈 기능 개선 (매입상세 결재연동, 미수금현황 조회조건 등) - 달력 일정 관리 API 연동 및 대량 등록 다이얼로그 개선 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,27 +143,27 @@ export default function ProductionDashboard() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 overflow-x-hidden pb-20">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
||||
<Factory className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">생산 현황판</h1>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl sm:text-2xl font-bold">생산 현황판</h1>
|
||||
<p className="text-sm text-muted-foreground">공장별 작업 현황을 확인합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{screenPerm.canView && (
|
||||
<Button variant="outline" onClick={handleWorkerScreenClick}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
<Button variant="outline" size="sm" onClick={handleWorkerScreenClick}>
|
||||
<Users className="mr-1 h-4 w-4" />
|
||||
작업자 화면
|
||||
</Button>
|
||||
)}
|
||||
{workOrderPerm.canView && (
|
||||
<Button variant="outline" onClick={handleWorkOrderListClick}>
|
||||
<Button variant="outline" size="sm" onClick={handleWorkOrderListClick}>
|
||||
작업지시 목록
|
||||
</Button>
|
||||
)}
|
||||
@@ -171,19 +171,21 @@ export default function ProductionDashboard() {
|
||||
</div>
|
||||
|
||||
{/* 공정별 탭 (동적 생성) */}
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabsList>
|
||||
{isLoadingProcesses ? (
|
||||
<TabsTrigger value="all" className="px-6">전체</TabsTrigger>
|
||||
) : (
|
||||
tabOptions.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value} className="px-6">
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))
|
||||
)}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="overflow-x-auto -mx-3 px-3 md:mx-0 md:px-0">
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabsList className="w-max">
|
||||
{isLoadingProcesses ? (
|
||||
<TabsTrigger value="all" className="px-6">전체</TabsTrigger>
|
||||
) : (
|
||||
tabOptions.map((tab) => (
|
||||
<TabsTrigger key={tab.value} value={tab.value} className="px-4 sm:px-6">
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))
|
||||
)}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 로딩 상태 */}
|
||||
{isLoading && <StatCardGridSkeleton count={6} />}
|
||||
@@ -395,41 +397,40 @@ function WorkOrderCard({ order, onClick, showDelay, showCompleted }: WorkOrderCa
|
||||
onClick={onClick}
|
||||
className="p-3 border rounded-lg hover:bg-accent cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{order.orderNo}
|
||||
</span>
|
||||
{!showCompleted && (
|
||||
<Badge className={`text-xs ${statusColors[order.status]}`}>
|
||||
{STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium mt-1 truncate">{order.productName}</p>
|
||||
<p className="text-xs text-muted-foreground truncate">{order.client}</p>
|
||||
{order.assignees.length > 0 && (
|
||||
<p className="text-xs text-blue-600 truncate">담당: {order.assignees.join(', ')}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{order.processName}
|
||||
{/* 1행: 작업번호 + 상태뱃지 + 공정뱃지 */}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{order.orderNo}
|
||||
</span>
|
||||
{!showCompleted && (
|
||||
<Badge className={`text-xs ${statusColors[order.status]}`}>
|
||||
{STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
{showDelay && order.delayDays && (
|
||||
<p className="text-xs text-orange-600 mt-1">+{order.delayDays}일 지연</p>
|
||||
)}
|
||||
{showCompleted && (
|
||||
<p className="text-xs text-green-600 mt-1">{formatDate(order.createdAt)}</p>
|
||||
)}
|
||||
{order.isUrgent && !showDelay && !showCompleted && (
|
||||
<p className="text-xs text-muted-foreground mt-1">순위 {order.priority}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{order.processName}
|
||||
</Badge>
|
||||
</div>
|
||||
{/* 2행: 품명 + 우측 정보 */}
|
||||
<div className="flex items-center justify-between gap-2 mt-1">
|
||||
<p className="text-sm font-medium truncate min-w-0">{order.productName}</p>
|
||||
{showDelay && order.delayDays && (
|
||||
<span className="text-xs text-orange-600 font-medium shrink-0">+{order.delayDays}일 지연</span>
|
||||
)}
|
||||
{showCompleted && (
|
||||
<span className="text-xs text-green-600 shrink-0">{formatDate(order.createdAt)}</span>
|
||||
)}
|
||||
{order.isUrgent && !showDelay && !showCompleted && (
|
||||
<span className="text-xs text-muted-foreground shrink-0">순위 {order.priority}</span>
|
||||
)}
|
||||
</div>
|
||||
{/* 3행: 거래처 */}
|
||||
<p className="text-xs text-muted-foreground truncate">{order.client}</p>
|
||||
{order.assignees.length > 0 && (
|
||||
<p className="text-xs text-blue-600 truncate">담당: {order.assignees.join(', ')}</p>
|
||||
)}
|
||||
{order.instruction && !showCompleted && (
|
||||
<p className="text-xs text-muted-foreground mt-2 truncate">
|
||||
<p className="text-xs text-muted-foreground mt-1 truncate">
|
||||
{order.instruction}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,7 @@ function ProcessStepPills({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex flex-col sm:flex-row sm:flex-wrap gap-2">
|
||||
{steps.map((step, index) => {
|
||||
const isCompleted = index < currentStep;
|
||||
|
||||
@@ -384,83 +384,85 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
|
||||
{/* 기본 정보 (기획서 4열 구성) */}
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 md:p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-4 md:gap-x-6 gap-y-4">
|
||||
{/* 1행: 작업번호 | 수주일 | 공정 | 구분 */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">작업번호</p>
|
||||
<p className="font-medium truncate">{order.workOrderNo}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">수주일</p>
|
||||
<p className="font-medium">{order.salesOrderDate || '-'}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">공정</p>
|
||||
<p className="font-medium">{order.processName}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">구분</p>
|
||||
<p className="font-medium">{order.processName !== '-' ? order.processName : '-'}</p>
|
||||
</div>
|
||||
<div className="max-h-[360px] overflow-y-auto md:max-h-none md:overflow-visible">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-4 md:gap-x-6 gap-y-4">
|
||||
{/* 1행: 작업번호 | 수주일 | 공정 | 구분 */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">작업번호</p>
|
||||
<p className="font-medium text-xs sm:text-sm break-all">{order.workOrderNo}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">수주일</p>
|
||||
<p className="font-medium">{order.salesOrderDate || '-'}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">공정</p>
|
||||
<p className="font-medium">{order.processName}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">구분</p>
|
||||
<p className="font-medium">{order.processName !== '-' ? order.processName : '-'}</p>
|
||||
</div>
|
||||
|
||||
{/* 2행: 로트번호 | 수주처 | 현장명 | 수주 담당자 */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">로트번호</p>
|
||||
<p className="font-medium truncate">{order.lotNo}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주처</p>
|
||||
<p className="font-medium">{order.client}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">현장명</p>
|
||||
<p className="font-medium">{order.projectName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주 담당자</p>
|
||||
<p className="font-medium">{order.salesOrderWriter || '-'}</p>
|
||||
</div>
|
||||
{/* 2행: 로트번호 | 수주처 | 현장명 | 수주 담당자 */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">로트번호</p>
|
||||
<p className="font-medium text-xs sm:text-sm break-all">{order.lotNo}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">수주처</p>
|
||||
<p className="font-medium break-words">{order.client}</p>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-muted-foreground mb-1">현장명</p>
|
||||
<p className="font-medium break-words">{order.projectName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">수주 담당자</p>
|
||||
<p className="font-medium">{order.salesOrderWriter || '-'}</p>
|
||||
</div>
|
||||
|
||||
{/* 3행: 담당자 연락처 | 출고예정일 | 틀수 | 우선순위 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">담당자 연락처</p>
|
||||
<p className="font-medium">{order.clientContact || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">출고예정일</p>
|
||||
<p className="font-medium">{order.shipmentDate || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">틀수 (개소)</p>
|
||||
<p className="font-medium">{order.shutterCount != null ? `${Math.floor(order.shutterCount)}개소` : '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">우선순위</p>
|
||||
<p className="font-medium">{order.priorityLabel || '-'}</p>
|
||||
</div>
|
||||
{/* 3행: 담당자 연락처 | 출고예정일 | 틀수 | 우선순위 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">담당자 연락처</p>
|
||||
<p className="font-medium">{order.clientContact || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">출고예정일</p>
|
||||
<p className="font-medium">{order.shipmentDate || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">틀수 (개소)</p>
|
||||
<p className="font-medium">{order.shutterCount != null ? `${Math.floor(order.shutterCount)}개소` : '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">우선순위</p>
|
||||
<p className="font-medium">{order.priorityLabel || '-'}</p>
|
||||
</div>
|
||||
|
||||
{/* 4행: 부서 | 생산 담당자 | 상태 | 비고 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">부서</p>
|
||||
<p className="font-medium">{order.department || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">생산 담당자</p>
|
||||
<p className="font-medium">
|
||||
{order.assignees && order.assignees.length > 0
|
||||
? order.assignees.map(a => a.name).join(', ')
|
||||
: order.assignee}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">상태</p>
|
||||
<Badge className={`${WORK_ORDER_STATUS_COLORS[order.status]} border-0`}>
|
||||
{WORK_ORDER_STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">비고</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{order.note || '-'}</p>
|
||||
{/* 4행: 부서 | 생산 담당자 | 상태 | 비고 */}
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">부서</p>
|
||||
<p className="font-medium">{order.department || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">생산 담당자</p>
|
||||
<p className="font-medium">
|
||||
{order.assignees && order.assignees.length > 0
|
||||
? order.assignees.map(a => a.name).join(', ')
|
||||
: order.assignee}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">상태</p>
|
||||
<Badge className={`${WORK_ORDER_STATUS_COLORS[order.status]} border-0`}>
|
||||
{WORK_ORDER_STATUS_LABELS[order.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">비고</p>
|
||||
<p className="font-medium whitespace-pre-wrap">{order.note || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { SquarePen, Trash2 } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -374,9 +374,10 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
)}
|
||||
|
||||
{/* 기본 정보 (기획서 4열 구성) */}
|
||||
<section className="bg-amber-50 border border-amber-200 rounded-lg p-6">
|
||||
<section className="bg-amber-50 border border-amber-200 rounded-lg p-4 md:p-6">
|
||||
<h3 className="font-semibold mb-4">기본 정보</h3>
|
||||
<div className="grid grid-cols-4 gap-x-6 gap-y-4">
|
||||
<div className="max-h-[400px] overflow-y-auto md:max-h-none md:overflow-visible">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-x-4 md:gap-x-6 gap-y-3 md:gap-y-4">
|
||||
{/* 1행: 작업번호(읽기) | 수주일(읽기) | 공정(셀렉트) | 구분(읽기) */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-sm text-muted-foreground">작업번호</Label>
|
||||
@@ -500,6 +501,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 공정 진행 섹션 */}
|
||||
@@ -524,7 +526,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
</p>
|
||||
|
||||
{/* 공정 단계 토글 버튼 */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex flex-col sm:flex-row sm:flex-wrap gap-2">
|
||||
{processSteps.map((step) => {
|
||||
const isCompleted = stepStatus[step.key] || false;
|
||||
return (
|
||||
@@ -609,7 +611,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
|
||||
onClick={() => handleItemEditStart(item.id)}
|
||||
disabled={item.isEditing}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<SquarePen className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -405,7 +405,7 @@ export function WorkOrderList() {
|
||||
headerBadges={
|
||||
<>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
#{globalIndex}
|
||||
{globalIndex}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.workOrderNo}
|
||||
|
||||
@@ -331,7 +331,7 @@ export function WorkResultList() {
|
||||
headerBadges={
|
||||
<>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
#{globalIndex}
|
||||
{globalIndex}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{item.lotNo}
|
||||
|
||||
@@ -323,9 +323,9 @@ export function MaterialInputModal({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="!max-w-3xl p-0 gap-0">
|
||||
<DialogContent className="!max-w-3xl p-0 gap-0 max-h-[85vh] flex flex-col">
|
||||
{/* 헤더 */}
|
||||
<DialogHeader className="p-6 pb-4">
|
||||
<DialogHeader className="p-6 pb-4 shrink-0">
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
자재 투입{workOrderItemName ? ` - ${workOrderItemName}` : ''}
|
||||
</DialogTitle>
|
||||
@@ -334,7 +334,7 @@ export function MaterialInputModal({
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="px-6 pb-6 space-y-4">
|
||||
<div className="px-6 pb-6 space-y-4 flex-1 min-h-0 flex flex-col">
|
||||
{/* 자재 목록 */}
|
||||
{isLoading ? (
|
||||
<ContentSkeleton type="table" rows={4} />
|
||||
@@ -343,7 +343,7 @@ export function MaterialInputModal({
|
||||
이 공정에 배정된 자재가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
<div className="space-y-4 flex-1 overflow-y-auto min-h-0">
|
||||
{materialGroups.map((group) => {
|
||||
const groupAllocated = group.lots.reduce(
|
||||
(sum, lot) => sum + (allocations.get(getLotKey(lot)) || 0),
|
||||
@@ -355,8 +355,8 @@ export function MaterialInputModal({
|
||||
return (
|
||||
<div key={group.groupKey} className="border rounded-lg overflow-hidden">
|
||||
{/* 품목 그룹 헤더 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-1.5 px-4 py-2.5 bg-gray-50 border-b">
|
||||
<div className="flex items-center gap-2 flex-wrap min-w-0">
|
||||
{group.category && (
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
||||
group.category === 'guideRail' ? 'bg-blue-100 text-blue-700' :
|
||||
@@ -381,7 +381,7 @@ export function MaterialInputModal({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.alreadyInputted > 0 ? (
|
||||
<>
|
||||
@@ -433,6 +433,7 @@ export function MaterialInputModal({
|
||||
</div>
|
||||
|
||||
{/* 로트 테이블 */}
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -509,6 +510,7 @@ export function MaterialInputModal({
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -516,7 +518,7 @@ export function MaterialInputModal({
|
||||
)}
|
||||
|
||||
{/* 버튼 영역 */}
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-3 shrink-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={resetAndClose}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, memo } from 'react';
|
||||
import { ChevronDown, ChevronUp, Pencil, Trash2, ImageIcon } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp, SquarePen, Trash2, ImageIcon } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -89,11 +89,12 @@ export const WorkItemCard = memo(function WorkItemCard({
|
||||
|
||||
{/* 제작 사이즈 (재공품은 숨김) */}
|
||||
{!item.isWip && (
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-700">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-sm text-gray-700">
|
||||
<span className="text-gray-500">제작 사이즈</span>
|
||||
<span className="font-medium">
|
||||
{formatNumber(item.width)} X {formatNumber(item.height)} mm
|
||||
</span>
|
||||
<span className="text-gray-500">수량</span>
|
||||
<span className="font-medium text-gray-900">{item.quantity}개</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -216,7 +217,7 @@ export const WorkItemCard = memo(function WorkItemCard({
|
||||
className="h-7 w-7 p-0"
|
||||
onClick={() => onEditMaterial(item.id, mat)}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5 text-gray-500" />
|
||||
<SquarePen className="h-3.5 w-3.5 text-gray-500" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -264,7 +265,7 @@ function SlatExtraInfo({
|
||||
jointBar?: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{length != null && length > 0 && (
|
||||
<Badge variant="outline" className="text-xs px-2.5 py-1 border-gray-300">
|
||||
길이 {formatNumber(length)}mm
|
||||
|
||||
@@ -1383,7 +1383,7 @@ export default function WorkerScreen() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="pb-20">
|
||||
<div className="pb-20 overflow-x-hidden">
|
||||
{/* 완료 토스트 */}
|
||||
{toastInfo && <CompletionToast info={toastInfo} />}
|
||||
|
||||
@@ -1403,21 +1403,23 @@ export default function WorkerScreen() {
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v)}
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
{processTabs.length > 0 ? (
|
||||
processTabs.map((proc) => (
|
||||
<TabsTrigger key={proc.id} value={proc.id} className="flex-1">
|
||||
{proc.processName}
|
||||
</TabsTrigger>
|
||||
))
|
||||
) : (
|
||||
(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
|
||||
<TabsTrigger key={tab} value={tab} className="flex-1">
|
||||
{PROCESS_TAB_LABELS[tab]}
|
||||
</TabsTrigger>
|
||||
))
|
||||
)}
|
||||
</TabsList>
|
||||
<div className="overflow-x-auto -mx-3 px-3 md:mx-0 md:px-0">
|
||||
<TabsList className="w-max md:w-full">
|
||||
{processTabs.length > 0 ? (
|
||||
processTabs.map((proc) => (
|
||||
<TabsTrigger key={proc.id} value={proc.id} className="px-4 md:flex-1">
|
||||
{proc.processName}
|
||||
</TabsTrigger>
|
||||
))
|
||||
) : (
|
||||
(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => (
|
||||
<TabsTrigger key={tab} value={tab} className="px-4 md:flex-1">
|
||||
{PROCESS_TAB_LABELS[tab]}
|
||||
</TabsTrigger>
|
||||
))
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{(processTabs.length > 0
|
||||
? processTabs.map((p) => p.id)
|
||||
@@ -1508,7 +1510,7 @@ export default function WorkerScreen() {
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">수주 정보</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-3">
|
||||
<InfoField label="수주일" value={orderInfo?.orderDate} />
|
||||
<InfoField label="수주로트" value={orderInfo?.salesOrderNo} />
|
||||
<InfoField label="현장명" value={orderInfo?.siteName} />
|
||||
@@ -1631,14 +1633,14 @@ export default function WorkerScreen() {
|
||||
|
||||
{/* 하단 고정 버튼 */}
|
||||
{(hasWipItems || activeProcessSettings.needsWorkLog || activeProcessSettings.hasDocumentTemplate) && (
|
||||
<div className={`fixed bottom-4 left-4 right-4 px-4 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:px-6 md:right-[24px] ${sidebarCollapsed ? 'md:left-[113px]' : 'md:left-[304px]'}`}>
|
||||
<div className="flex gap-3">
|
||||
<div className={`fixed bottom-4 left-3 right-3 px-3 py-3 bg-background/95 backdrop-blur rounded-xl border shadow-lg z-50 transition-all duration-300 md:bottom-6 md:left-auto md:right-[24px] md:px-6 ${sidebarCollapsed ? 'md:left-[113px]' : 'md:left-[304px]'}`}>
|
||||
<div className="flex gap-2 md:gap-3">
|
||||
{hasWipItems ? (
|
||||
<Button
|
||||
onClick={handleInspection}
|
||||
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
작업일지 및 검사성적서 보기
|
||||
작업일지 및 검사성적서
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
@@ -1646,7 +1648,7 @@ export default function WorkerScreen() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleWorkLog}
|
||||
className="flex-1 py-6 text-base font-medium"
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium"
|
||||
>
|
||||
작업일지 보기
|
||||
</Button>
|
||||
@@ -1654,7 +1656,7 @@ export default function WorkerScreen() {
|
||||
{activeProcessSettings.hasDocumentTemplate && (
|
||||
<Button
|
||||
onClick={handleInspection}
|
||||
className="flex-1 py-6 text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
className="flex-1 py-5 md:py-6 text-sm md:text-base font-medium bg-gray-900 hover:bg-gray-800"
|
||||
>
|
||||
검사성적서 보기
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user