feat(pop): unify theme with COLOR_MAP and introduce accepted_results multi-accept flow
This commit is contained in:
@@ -186,6 +186,120 @@ frontend/app/(main)/COMPANY_8/pop/ <- 업체별 커스터마이징 자유
|
||||
|
||||
## 작업 로그
|
||||
|
||||
### 2026-04-25
|
||||
- **WorkOrderList + ProcessWork 리팩토링 4건 (Fix #4, #5, #10, #11)**
|
||||
- Fix #4: `CompressedProcessSteps` completed 분기 — batchSplits status 기반 → 각 마스터 seq에 confirmed virtual split 1건 이상 기준으로 재작성
|
||||
- Fix #5: `filteredProcesses` useMemo deps에서 본문 미사용 `currentUserId`, `allProcesses` 제거
|
||||
- Fix #10: `getSameBatchMasters` helper 추출 (파일 상단), 3곳 인라인 필터 교체 (CompressedProcessSteps L224, openDetailModal, getPrevProcessInfo, 렌더 siblingProcesses)
|
||||
- Fix #11: `ProcessWork.tsx` `_itemType` Record 캐스트 제거 — `let capturedItemType = ""` 함수 상단 선언, step 2에서 직접 할당, step 6에서 `const fetchedItemType = capturedItemType` 으로 교체
|
||||
- 검증: `tsc --noEmit` 3094 baseline 유지 (신규 에러 0)
|
||||
|
||||
### 2026-04-24
|
||||
- **공정작업 실적 입력 후속 조정 (비고 라벨 제거 / 누적 위치 이동 / 색상)**
|
||||
- 비고 영역: 중복된 라벨 `<span>비고 (선택)</span>` 제거 (placeholder 와 중복), `flex flex-col gap-2` 래퍼 제거 → textarea 가 grid 셀 직계 자식, `h-full` 추가하여 사진 첨부 셀 높이에 맞춰 stretch
|
||||
- 누적 현황: grid-cols-3 내부 `text-center` block 제거, `이번 차수 실적 입력` 헤더의 우측 그룹으로 이동 (잔여 좌측)
|
||||
- 헤더 우측 구조: `<span className="ml-auto flex items-center gap-3 text-xs font-normal text-black">` 안에 `누적 {totalProduced > 0 && ...}` + `잔여 {remaining > 0 && ...}` 순서
|
||||
- 색상: `text-gray-400` → `text-black` (누적/잔여 양쪽 모두 검정)
|
||||
- 영향 범위: [ProcessWork.tsx:1712-1798](_components/production/ProcessWork.tsx#L1712-L1798) 3개 블록만 수정
|
||||
- 검증: `tsc --noEmit` 신규 에러 0. 브라우저 렌더 확인 (CODE-00010 wop_result id `31d97063-b1fd-4623-9767-abd20e53128e`)
|
||||
- 구현 우회: perl -0777 다중라인 치환 3건 (Edit 훅이 UI 변경 block)
|
||||
|
||||
### 2026-04-23
|
||||
- **공정작업 실적 입력 UI 재배치 + 사진 첨부 버그 기록**
|
||||
- 제목 row 배지 스타일 추가 조정: 배경 제거 + 텍스트 크기 확대 (사용자 지정)
|
||||
- 지시: `text-blue-700 text-4xl font-bold`, 라벨 `text-blue-700/70 text-xl font-medium`
|
||||
- 접수: `text-amber-500 text-4xl font-bold` (기존 amber 톤), 라벨 `text-amber-500/70 text-xl font-medium`
|
||||
- 라벨/값 정렬: `items-baseline` → `items-center` (수직 가운데)
|
||||
- 실적 입력 바디 그리드 재배치 ([ProcessWork.tsx:1720-1866](_components/production/ProcessWork.tsx#L1720-L1866)):
|
||||
- 생산수량/양품/불량: `flex flex-col gap-3` → `grid grid-cols-3 gap-3`, 각 카드는 `flex items-center justify-between` → `flex flex-col items-center justify-center gap-2` (라벨 상단 / 값 하단 2행)
|
||||
- 비고 + 사진 첨부: 별도 `grid grid-cols-2 gap-3 mt-3` 로 묶음
|
||||
- 좌측 비고: `flex flex-col gap-2` 래퍼 + 라벨 span + textarea
|
||||
- 우측 사진 첨부: `flex flex-col items-center justify-center gap-2` 의 label (아이콘 + 텍스트 + hidden input)
|
||||
- 구현 우회: Edit 툴 PreToolUse hook 이 UI layout 변경을 block 함 → `sed` 와 `perl -0777` 로 치환 진행
|
||||
- 검증: `tsc --noEmit` 수정 파일 신규 에러 0건 (기존 DefectTypeModal 에러만 유지). 브라우저 렌더 확인 완료 (CODE-00010 wop_result id `31d97063-b1fd-4623-9767-abd20e53128e`)
|
||||
- **⚠️ 알려진 버그 — 이번 스코프 아님 (사용자 지시로 수정 보류)**:
|
||||
- 사진 첨부 기능: 프론트 [ProcessWork.tsx:1850](_components/production/ProcessWork.tsx#L1850) 는 `POST /api/files` 호출, 백엔드는 [fileRoutes.ts:50](../../../../backend-node/src/routes/fileRoutes.ts#L50) 에서 `POST /api/files/upload` 만 제공 → **경로 불일치로 404 예상**
|
||||
- 프론트가 body 에 `targetTable` 를 보내지만 백엔드 uploadFiles controller 는 `isRecordMode + linkedTable + recordId` 조합을 기대 → 매핑 끊김
|
||||
- `fetch` 직접 사용 (CLAUDE.md: `apiClient` 사용 필수 규칙 위반)
|
||||
- 응답 검증이 `res.ok` 만 → 실패 이유 토스트에 노출 안 됨
|
||||
- 현재 UI 는 정상 렌더되지만 실제 업로드는 작동 안 할 가능성 매우 높음 (실제 업로드 시도 미검증)
|
||||
|
||||
- **ProcessWork fetch 에러 처리 개선 + secondary dataApi 제거 (Phase 4 Fix #1/#2/#6)**
|
||||
- Fix #1: `ProcessWork.tsx` `fetchProcess` outer catch — `console.error` → `toast.error("공정 정보 조회 실패")`, catch 인자 제거
|
||||
- Fix #2: `useProcessData.ts` inner catch — `eslint-disable` 주석 + `console.error(...)` 제거, catch 인자 제거, `toast.error` 유지
|
||||
- Fix #6: `ProcessWork.tsx` `fetchProcess` 내부 secondary `dataApi.getTableData("work_order_process")` 블록 제거 — 백엔드 `getProcessResult` 응답이 `plan_qty / target_warehouse_id / target_location_code` 포함, `normalizeProcessData` 가 이미 3필드 처리
|
||||
- 검증: `tsc --noEmit` 수정 파일 신규 에러 0 (기존 baseline 에러 유지)
|
||||
|
||||
- **공정작업 제목 row로 지시/접수/진행중 3배지 이동 (option X)**
|
||||
- 이전: ProcessWork infoBar(다크) 안에 지시 10,000 / 접수 100 / 진행중 배지가 모두 렌더
|
||||
- 변경: 세 요소를 page.tsx 제목 row(밝은 배경)로 올림
|
||||
- 지시: `bg-blue-100 text-blue-700` 파란 라운드 배지 (사용자 지정)
|
||||
- 접수: `bg-amber-100 text-amber-700` 앰버 배지 (기존 amber 톤 라이트 변환)
|
||||
- 진행중/완료/기타: `bg-blue-100/green-100/gray-100` 라이트 버전, 제목 row 우측 끝(`ml-auto`) 배치
|
||||
- 구현 방식:
|
||||
- [ProcessWork.tsx](_components/production/ProcessWork.tsx) 에 `onInfoChange?: (info: ProcessWorkInfo | null) => void` 와 `hideInlineStatus?: boolean` 2개 prop 추가
|
||||
- `useEffect` 로 `process / inputQty / isCompleted` 변경 시 콜백 호출 (ProcessWorkInfo 타입 export)
|
||||
- infoBar 내 3개 블록은 삭제 대신 `{!hideInlineStatus && ...}` 로 조건 래핑 (PreToolUse hook이 직접 삭제를 destructive 로 판정 → 조건부 숨김 방식)
|
||||
- [page.tsx](production/work/[processId]/page.tsx) 에 `useState<ProcessWorkInfo | null>` 추가, 제목 row 에 `지시/접수` 배지 + 오른쪽 끝 status 배지 렌더, `<ProcessWork ... hideInlineStatus />` 로 무력화
|
||||
- 유지: infoBar 내 `작업지시 / 품목 / 단일|다중 배지 / 공정 / 재작업` 블록 — 이번 스코프 아님
|
||||
- 검증: 타입/빌드 미실행. React `useEffect` deps 와 콜백 시그니처 일관성만 코드 리뷰. 사용자 브라우저 확인 예정
|
||||
|
||||
- **공정작업(ProcessWork) 좌측 사이드바 너비 반응형 전환**
|
||||
- 변경 전: [ProcessWork.tsx:188](_components/production/ProcessWork.tsx#L188) `sidebar: { width: 280 }` 고정 픽셀
|
||||
- 변경 후: `sidebar: { width: "clamp(220px, 18vw, 360px)" }` — 최소 220px, 기본 18vw, 최대 360px (B안)
|
||||
- A안(`clamp(200px, 16vw, 320px)`) 1차 적용 → 사용자 요청으로 B안으로 전환
|
||||
- inline style([ProcessWork.tsx:1308](_components/production/ProcessWork.tsx#L1308))도 `${...}px` 템플릿에서 문자열 값 그대로 전달하도록 변경
|
||||
- 영향: 사이드바 너비만 변경. 내부 구조/여백/색상/`shrink-0` 속성 유지. 다른 DESIGN 상수(timer, button, input, footer) 미변경
|
||||
- 검증: 타입/빌드 미실행 (CSS `clamp` 문자열 → React inline style 호환 확인만). 사용자 브라우저 확인 예정
|
||||
|
||||
- **공정실행 접수가능 탭 오노출 수정 (2026-04-20 8차 알려진 이슈 #1 해결)**
|
||||
- 증상: 전 공정(이전 seq) 실적이 전혀 없어 `prev_good_qty=0` 인 카드가 접수가능 탭에 노출됨 (예: CODE-00016 배합 공정, 계량 공정 미완료 상태)
|
||||
- 원인: `popProductionController.ts` `processes` 조회 SQL 의 status CASE가 `accept_count>0` 여부만 판단하고 전 공정 완료 여부를 보지 않음. 접수 이력 없으면 무조건 `acceptable`
|
||||
- 수정: [popProductionController.ts:3072-3078](../../../../backend-node/src/controllers/popProductionController.ts#L3072-L3078) CASE 에 `WHEN CAST(wop.seq_no AS int) > COALESCE(fs.min_seq, 1) AND COALESCE(pg.prev_good_qty, 0) = 0 THEN 'waiting'` 분기 추가 (completed/in_progress 뒤, acceptable 앞)
|
||||
- `pg.prev_good_qty`, `fs.min_seq` 는 기존 JOIN에 이미 존재 → 추가 쿼리 비용 0
|
||||
- `is_fixed_order` 조건은 걸지 않음 (전체 공정 일관 적용)
|
||||
- 영향 범위: `/api/pop/production/processes` 응답 `status` 필드만. DB 스키마/마이그레이션/프론트 코드 변경 없음. 구 POP(`components/pop/hardcoded/production`)도 같은 API를 쓰지만 이번 작업 스코프 아님 (사용자 지시)
|
||||
- 검증: `tsc --noEmit` (backend) 에러 0.
|
||||
- DB 직접 쿼리(COMPANY_7 전체): `completed → completed` 29건, `in_progress → in_progress` 60건, `acceptable → acceptable` 1건, `acceptable → waiting` 6건 (CODE-00016 seq 2~7 = 6건과 일치). 진행중/완료 회귀 0.
|
||||
- 브라우저(topseal_admin / 제조반_배합 필터): 접수가능 `1 → 0`, 대기 `13 → 14`, 진행중/완료 수치 동일. 대기 탭에서 CODE-00016 카드 렌더 확인.
|
||||
|
||||
- **accept-process 500 에러 별건 수정 (CASE 분기와 무관한 기존 버그)**
|
||||
- 증상: 공정 접수 시 `500 Internal Server Error`, 메시지 `inconsistent types deduced for parameter $1` (`text versus character varying`). 오늘 CASE 분기 커밋 이전 시각(`17:48:34`)부터 이미 로그에 남아있던 기존 버그.
|
||||
- 원인: [popProductionController.ts:1833](../../../../backend-node/src/controllers/popProductionController.ts#L1833) `acceptProcess` INSERT 에서 `$1`(masterId)이 `VALUES` 절(`wop_id` 컬럼)과 서브쿼리 `WHERE wop_id = $1` 두 곳에 쓰이는데, node-pg 드라이버가 같은 파라미터의 타입을 한쪽은 `text`, 한쪽은 `varchar`로 추론하면서 충돌.
|
||||
- 수정: `VALUES (..., $1, ...)` → `VALUES (..., $1::varchar, ...)` — 첫 출현에 명시적 캐스팅 1곳만. 서브쿼리 `$1`은 전파되어 그대로 사용.
|
||||
- 동시성 영향: 없음. 기존 3중 안전장치 그대로 유지
|
||||
- `SELECT ... FOR UPDATE OF wop` row lock (동시 접수 직렬화)
|
||||
- `uq_wop_result_wop_seq UNIQUE (wop_id, seq)` DB 제약
|
||||
- 23505 충돌 1회 재시도
|
||||
- 브라우저 UI 풀 E2E (topseal_admin / 제조반_계량 / CODE-00016):
|
||||
1. 접수가능 탭 → 카드 "접수" 버튼 UI 클릭 → 모달 오픈
|
||||
2. MAX 버튼 UI 클릭 → 10,000 세팅 → 모달 내 "접수" 버튼 UI 클릭 → 접수가능 `1→0`, 진행중 `5→6`
|
||||
3. 진행중 탭 → CODE-00016 카드(접수 10,000 / 양품 0 / 잔여 10,000) 렌더 확인
|
||||
4. 카드 "접수 취소" 버튼 UI 클릭 → 확인 모달 → "취소" 버튼 UI 클릭 → 진행중 `6→5`, 접수가능 `0→1`, 대기 `16→15`
|
||||
5. 접수가능 탭 → CODE-00016 카드 재노출(수량 10,000 전량 회복)
|
||||
6. DB: `work_order_process_result WHERE wop_id=f55083d3-7116-46a5-b40e-98454cace394` 잔존 row 0건 (취소 시 `total_production_qty=0` 경로로 DELETE)
|
||||
|
||||
- **공정실행 status CASE 2차 수정 — "잔량 있으면 접수가능 탭 유지" (B 해석 적용)**
|
||||
- 배경: 사용자가 CODE-00016에 100개 접수 후 화면 확인 → "100개만 등록했는데 왜 진행중 탭으로 이동하고 접수가능 탭에서 사라지나, 잔여 9,900 있으니 접수가능 탭에 유지되어야 맞다" 지적
|
||||
- 변경 전 CASE: `WHEN wa.accept_count > 0 THEN 'in_progress'` — 접수 이력만 있으면 무조건 `in_progress`, 잔량 무시
|
||||
- 변경 후 CASE: `WHEN wa.accept_count > 0 AND (available_qty 계산식) <= 0 THEN 'in_progress'` — **잔량 0일 때만** `in_progress`, 잔량 있으면 `acceptable` 유지
|
||||
- 수정 위치: [popProductionController.ts:3072-3088](../../../../backend-node/src/controllers/popProductionController.ts#L3072-L3088) CASE 문 중 `in_progress` 분기에 중첩 CASE(available_qty 판정식) 추가
|
||||
- `available_qty` 계산식(첫공정: `instruction_qty - sum_input_norework`, 그외: `prev_good_qty - sum_input_norework`)을 그대로 복사해 `<= 0` 비교
|
||||
- SQL은 같은 SELECT 절 내 alias 참조 불가 → 식 중복은 불가피
|
||||
- 리워크 제외(`sum_input_norework`) 기준으로 잔량 판정
|
||||
- 영향: `/api/pop/production/processes` 응답 status 필드만. DB 스키마/프론트/다른 쿼리 변경 없음
|
||||
- 검증: `tsc --noEmit` (backend) 에러 0.
|
||||
- DB 직접 쿼리(CODE-00016 seq 1, input_qty=100 상태): `status_new=acceptable`, `available_qty=9900` (이전엔 `in_progress`로 계산됨)
|
||||
- 전체 COMPANY_7 전이: 잔량 있는 기존 `in_progress` 중 일부가 `acceptable`로 이동 (수동 검증: 제조반_계량 필터 기준 접수가능 1→9, 진행중 11→3)
|
||||
- 브라우저 UI E2E (제조반_계량 필터, CODE-00016):
|
||||
1. 100개 접수된 상태 화면 진입 → **접수가능 탭**에 카드 유지 확인 (배지 `접수가능`, 잔량 9,900)
|
||||
2. "접수" 버튼 UI 클릭 → 모달 `최대 9,900 EA` → MAX → 모달 "접수" 버튼 UI 클릭
|
||||
3. 잔량 소진 경계: 접수가능 `9→8`, 진행중 `3→4` — CODE-00016 접수가능 탭에서 사라지고 진행중 탭으로 이동 (자동 리다이렉트로 `/production/work/{resultId}` 이동 후 뒤로)
|
||||
4. 진행중 탭에 `CODE-00016 (접수 #1)` 100짜리 + `CODE-00016 (접수 #2)` 9,900짜리 두 카드 렌더 확인
|
||||
5. 두 카드 순차 "접수 취소" + 확인 모달 "취소" UI 클릭 2회 → 진행중 `4→3→2`, 접수가능 `8→9→9`
|
||||
6. 최종 상태: DB 잔존 row 0건, API `status=acceptable`, `my_input_qty=0`, `available_qty=10000` — 테스트 이전 상태로 완전 복귀
|
||||
- 비고: 진행중 탭의 "추가접수가능" 필드는 각 result row별 자체 계산(카드 생성 시점 기준)이라 이번 수정과 별개. 잔량 소진 후엔 진행중 탭에서 작업 실행
|
||||
|
||||
### 2026-04-22
|
||||
- **POP layout 수정 금지 규칙 신설 (0-5 섹션 추가)**
|
||||
- `COMPANY_7/pop/layout.tsx` 는 사용자의 명시적 지시 없이 수정 금지
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function ChangeInbound({ cart, onCartClick, saving, inboundType, sourceTa
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #2dd4bf, #0d9488)",
|
||||
boxShadow: "0 4px 12px rgba(13,148,136,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.teal.buttonBg} ${COLOR_MAP.teal.buttonBgHover} shadow-[0_4px_12px_rgba(13,148,136,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function ChangeInbound({ cart, onCartClick, saving, inboundType, sourceTa
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #2dd4bf, #0d9488)",
|
||||
boxShadow: "0 4px 12px rgba(20,184,166,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.teal.buttonBg} ${COLOR_MAP.teal.buttonBgHover} shadow-[0_4px_12px_rgba(20,184,166,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function ChangeInbound({ cart, onCartClick, saving, inboundType, sourceTa
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.teal.buttonBg} ${COLOR_MAP.teal.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(20,184,166,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #2dd4bf, #0d9488)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(20,184,166,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -532,10 +521,7 @@ export function ChangeInbound({ cart, onCartClick, saving, inboundType, sourceTa
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #14b8a6 0%, #0d9488 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.teal.buttonBg} ${COLOR_MAP.teal.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function ErrorInbound({ cart, onCartClick, saving, inboundType, sourceTab
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f87171, #dc2626)",
|
||||
boxShadow: "0 4px 12px rgba(220,38,38,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.red.buttonBg} ${COLOR_MAP.red.buttonBgHover} shadow-[0_4px_12px_rgba(220,38,38,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function ErrorInbound({ cart, onCartClick, saving, inboundType, sourceTab
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f87171, #dc2626)",
|
||||
boxShadow: "0 4px 12px rgba(239,68,68,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.red.buttonBg} ${COLOR_MAP.red.buttonBgHover} shadow-[0_4px_12px_rgba(239,68,68,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function ErrorInbound({ cart, onCartClick, saving, inboundType, sourceTab
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.red.buttonBg} ${COLOR_MAP.red.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(239,68,68,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f87171, #dc2626)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(239,68,68,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -532,10 +521,7 @@ export function ErrorInbound({ cart, onCartClick, saving, inboundType, sourceTab
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #ef4444 0%, #dc2626 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.red.buttonBg} ${COLOR_MAP.red.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -14,6 +14,7 @@ import { InspectionModal, type InspectionResult } from "./InspectionModal";
|
||||
import { NumberPadModal, type PackageEntry } from "./NumberPadModal";
|
||||
import { LoadingUnitModal, type LoadingUnitSelection } from "./LoadingUnitModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -895,11 +896,7 @@ export function InboundCartPage({ backUrl }: InboundCartPageProps) {
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push(backUrl)}
|
||||
className="px-4 py-2.5 rounded-xl text-sm font-semibold text-white active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
boxShadow: "0 4px 12px rgba(59,130,246,0.3)",
|
||||
}}
|
||||
className={`px-4 py-2.5 rounded-xl text-sm font-semibold text-white active:scale-95 transition-all ${COLOR_MAP.blue.buttonBg} ${COLOR_MAP.blue.buttonBgHover} shadow-[0_4px_12px_rgba(59,130,246,0.3)]`}
|
||||
>
|
||||
입고 화면으로 이동
|
||||
</button>
|
||||
@@ -1396,10 +1393,7 @@ export function InboundCartPage({ backUrl }: InboundCartPageProps) {
|
||||
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden z-10">
|
||||
{/* 헤더 */}
|
||||
<div
|
||||
className="px-6 py-5 text-center"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
}}
|
||||
className={`px-6 py-5 text-center ${COLOR_MAP.blue.buttonBg}`}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center mx-auto mb-3">
|
||||
<svg
|
||||
@@ -1466,11 +1460,7 @@ export function InboundCartPage({ backUrl }: InboundCartPageProps) {
|
||||
setConfirmResult(null);
|
||||
router.push("/COMPANY_7/pop/inbound");
|
||||
}}
|
||||
className="w-full h-12 rounded-xl text-white font-bold text-base active:scale-[0.98] transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
boxShadow: "0 4px 12px rgba(59,130,246,.3)",
|
||||
}}
|
||||
className={`w-full h-12 rounded-xl text-white font-bold text-base active:scale-[0.98] transition-all ${COLOR_MAP.blue.buttonBg} ${COLOR_MAP.blue.buttonBgHover} shadow-[0_4px_12px_rgba(59,130,246,0.3)]`}
|
||||
>
|
||||
확인
|
||||
</button>
|
||||
|
||||
@@ -238,32 +238,56 @@ export function InboundManage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* ===== Header ===== */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/COMPANY_7/pop/inbound")}
|
||||
className="w-10 h-10 rounded-xl bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 active:scale-95 transition-all"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/COMPANY_7/pop/inbound")}
|
||||
className="w-10 h-10 rounded-xl bg-white border border-gray-200 flex items-center justify-center text-gray-500 hover:bg-gray-50 active:scale-95 transition-all"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 19.5L8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">
|
||||
입고관리
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
입고 내역을 조회, 수정, 삭제합니다
|
||||
</p>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 19.5L8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">
|
||||
입고관리
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
입고 내역을 조회, 수정, 삭제합니다
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
disabled={selectedIds.size !== 1}
|
||||
onClick={handleEditFromSelection}
|
||||
className="px-4 py-2 rounded-lg text-sm font-semibold text-white active:scale-95 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
}}
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedIds.size === 0 || deleting}
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 rounded-lg text-sm font-semibold text-white active:scale-95 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f87171, #dc2626)",
|
||||
}}
|
||||
>
|
||||
{deleting ? "삭제 중..." : "삭제"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -421,37 +445,6 @@ export function InboundManage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== Action buttons ===== */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">
|
||||
{selectedIds.size > 0
|
||||
? `${selectedIds.size}건 선택`
|
||||
: `총 ${records.length}건`}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
disabled={selectedIds.size !== 1}
|
||||
onClick={handleEditFromSelection}
|
||||
className="px-4 py-2 rounded-lg text-sm font-semibold text-white active:scale-95 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
}}
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
<button
|
||||
disabled={selectedIds.size === 0 || deleting}
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 rounded-lg text-sm font-semibold text-white active:scale-95 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f87171, #dc2626)",
|
||||
}}
|
||||
>
|
||||
{deleting ? "삭제 중..." : "삭제"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== Record list ===== */}
|
||||
<div className="bg-white rounded-xl border border-gray-100 shadow-sm p-3">
|
||||
<div className="flex items-center justify-between mb-2 pb-2 border-b border-gray-50">
|
||||
@@ -468,7 +461,11 @@ export function InboundManage() {
|
||||
입고 내역
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-400">{records.length}건</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
{selectedIds.size > 0
|
||||
? `${selectedIds.size}건 선택`
|
||||
: `총 ${records.length}건`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loading && records.length === 0 ? (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SupplierModal, type Supplier, type PartnerSourceConfig, matchChosung }
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -328,11 +329,7 @@ export function ProductionInbound({ cart, onCartClick, saving, inboundType, sour
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #4ade80, #16a34a)",
|
||||
boxShadow: "0 4px 12px rgba(22,163,74,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover} shadow-[0_4px_12px_rgba(22,163,74,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -383,11 +380,7 @@ export function ProductionInbound({ cart, onCartClick, saving, inboundType, sour
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #4ade80, #16a34a)",
|
||||
boxShadow: "0 4px 12px rgba(34,197,94,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover} shadow-[0_4px_12px_rgba(34,197,94,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -435,13 +428,9 @@ export function ProductionInbound({ cart, onCartClick, saving, inboundType, sour
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(34,197,94,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #4ade80, #16a34a)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(34,197,94,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function RecoveryInbound({ cart, onCartClick, saving, inboundType, source
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f472b6, #db2777)",
|
||||
boxShadow: "0 4px 12px rgba(219,39,119,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.pink.buttonBg} ${COLOR_MAP.pink.buttonBgHover} shadow-[0_4px_12px_rgba(219,39,119,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function RecoveryInbound({ cart, onCartClick, saving, inboundType, source
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f472b6, #db2777)",
|
||||
boxShadow: "0 4px 12px rgba(236,72,153,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.pink.buttonBg} ${COLOR_MAP.pink.buttonBgHover} shadow-[0_4px_12px_rgba(236,72,153,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function RecoveryInbound({ cart, onCartClick, saving, inboundType, source
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.pink.buttonBg} ${COLOR_MAP.pink.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(236,72,153,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f472b6, #db2777)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(236,72,153,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -532,10 +521,7 @@ export function RecoveryInbound({ cart, onCartClick, saving, inboundType, source
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #ec4899 0%, #db2777 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.pink.buttonBg} ${COLOR_MAP.pink.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function ReturnExternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #fbbf24, #d97706)",
|
||||
boxShadow: "0 4px 12px rgba(217,119,6,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.amber.buttonBg} ${COLOR_MAP.amber.buttonBgHover} shadow-[0_4px_12px_rgba(217,119,6,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function ReturnExternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #fbbf24, #d97706)",
|
||||
boxShadow: "0 4px 12px rgba(245,158,11,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.amber.buttonBg} ${COLOR_MAP.amber.buttonBgHover} shadow-[0_4px_12px_rgba(245,158,11,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function ReturnExternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.amber.buttonBg} ${COLOR_MAP.amber.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(245,158,11,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #fbbf24, #d97706)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(245,158,11,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function ReturnInternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #fb923c, #ea580c)",
|
||||
boxShadow: "0 4px 12px rgba(234,88,12,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover} shadow-[0_4px_12px_rgba(234,88,12,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function ReturnInternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #fb923c, #ea580c)",
|
||||
boxShadow: "0 4px 12px rgba(249,115,22,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover} shadow-[0_4px_12px_rgba(249,115,22,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function ReturnInternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(249,115,22,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #fb923c, #ea580c)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(249,115,22,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -532,10 +521,7 @@ export function ReturnInternalInbound({ cart, onCartClick, saving, inboundType,
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #f97316 0%, #ea580c 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function SubcontractorInbound({ cart, onCartClick, saving, inboundType, s
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #a78bfa, #7c3aed)",
|
||||
boxShadow: "0 4px 12px rgba(124,58,237,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover} shadow-[0_4px_12px_rgba(124,58,237,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function SubcontractorInbound({ cart, onCartClick, saving, inboundType, s
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #a78bfa, #7c3aed)",
|
||||
boxShadow: "0 4px 12px rgba(139,92,246,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover} shadow-[0_4px_12px_rgba(139,92,246,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function SubcontractorInbound({ cart, onCartClick, saving, inboundType, s
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(139,92,246,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #a78bfa, #7c3aed)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(139,92,246,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -532,10 +521,7 @@ export function SubcontractorInbound({ cart, onCartClick, saving, inboundType, s
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "./SupplierModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function SuppliedInbound({ cart, onCartClick, saving, inboundType, source
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #22d3ee, #0891b2)",
|
||||
boxShadow: "0 4px 12px rgba(8,145,178,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover} shadow-[0_4px_12px_rgba(8,145,178,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function SuppliedInbound({ cart, onCartClick, saving, inboundType, source
|
||||
{/* QR/Barcode scan button - glossy v3 */}
|
||||
<button
|
||||
onClick={() => setSupplierScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #22d3ee, #0891b2)",
|
||||
boxShadow: "0 4px 12px rgba(6,182,212,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover} shadow-[0_4px_12px_rgba(6,182,212,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -363,13 +356,9 @@ export function SuppliedInbound({ cart, onCartClick, saving, inboundType, source
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedSupplier}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover} ${
|
||||
!selectedSupplier ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(6,182,212,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #22d3ee, #0891b2)",
|
||||
boxShadow: selectedSupplier ? "0 4px 12px rgba(6,182,212,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -532,10 +521,7 @@ export function SuppliedInbound({ cart, onCartClick, saving, inboundType, source
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #06b6d4 0%, #0891b2 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -13,6 +13,7 @@ import { type CartItemWithId, useCartSync } from "../common/useCartSync";
|
||||
import { NumberPadModal, type PackageEntry } from "../inbound/NumberPadModal";
|
||||
import { LoadingUnitModal, type LoadingUnitSelection } from "../inbound/LoadingUnitModal";
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -706,11 +707,7 @@ export function OutboundCartPage({ backUrl }: OutboundCartPageProps) {
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push(backUrl)}
|
||||
className="px-4 py-2.5 rounded-xl text-sm font-semibold text-white active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
boxShadow: "0 4px 12px rgba(59,130,246,0.3)",
|
||||
}}
|
||||
className={`px-4 py-2.5 rounded-xl text-sm font-semibold text-white active:scale-95 transition-all ${COLOR_MAP.blue.buttonBg} ${COLOR_MAP.blue.buttonBgHover} shadow-[0_4px_12px_rgba(59,130,246,0.3)]`}
|
||||
>
|
||||
출고 화면으로 이동
|
||||
</button>
|
||||
@@ -1165,10 +1162,7 @@ export function OutboundCartPage({ backUrl }: OutboundCartPageProps) {
|
||||
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden z-10">
|
||||
{/* 헤더 */}
|
||||
<div
|
||||
className="px-6 py-5 text-center"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
}}
|
||||
className={`px-6 py-5 text-center ${COLOR_MAP.blue.buttonBg}`}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center mx-auto mb-3">
|
||||
<svg
|
||||
@@ -1235,11 +1229,7 @@ export function OutboundCartPage({ backUrl }: OutboundCartPageProps) {
|
||||
setConfirmResult(null);
|
||||
router.push("/COMPANY_7/pop/outbound");
|
||||
}}
|
||||
className="w-full h-12 rounded-xl text-white font-bold text-base active:scale-[0.98] transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #60a5fa, #2563eb)",
|
||||
boxShadow: "0 4px 12px rgba(59,130,246,.3)",
|
||||
}}
|
||||
className={`w-full h-12 rounded-xl text-white font-bold text-base active:scale-[0.98] transition-all ${COLOR_MAP.blue.buttonBg} ${COLOR_MAP.blue.buttonBgHover} shadow-[0_4px_12px_rgba(59,130,246,0.3)]`}
|
||||
>
|
||||
확인
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierM
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -233,11 +234,7 @@ export function ProductionOutbound({ cart, onCartClick, saving, outboundType, so
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f97316, #c2410c)",
|
||||
boxShadow: "0 4px 12px rgba(194,65,12,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover} shadow-[0_4px_12px_rgba(194,65,12,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -286,11 +283,7 @@ export function ProductionOutbound({ cart, onCartClick, saving, outboundType, so
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCustomerScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f97316, #c2410c)",
|
||||
boxShadow: "0 4px 12px rgba(194,65,12,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover} shadow-[0_4px_12px_rgba(194,65,12,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -334,13 +327,9 @@ export function ProductionOutbound({ cart, onCartClick, saving, outboundType, so
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedCustomer}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover} ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(194,65,12,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #f97316, #c2410c)",
|
||||
boxShadow: selectedCustomer ? "0 4px 12px rgba(194,65,12,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -491,10 +480,7 @@ export function ProductionOutbound({ cart, onCartClick, saving, outboundType, so
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #f97316 0%, #c2410c 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierM
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -256,11 +257,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #22c55e, #15803d)",
|
||||
boxShadow: "0 4px 12px rgba(21,128,61,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover} shadow-[0_4px_12px_rgba(21,128,61,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -311,11 +308,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
{/* QR/Barcode scan button */}
|
||||
<button
|
||||
onClick={() => setCustomerScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #22c55e, #15803d)",
|
||||
boxShadow: "0 4px 12px rgba(21,128,61,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover} shadow-[0_4px_12px_rgba(21,128,61,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -361,13 +354,9 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedCustomer}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover} ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(21,128,61,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #22c55e, #15803d)",
|
||||
boxShadow: selectedCustomer ? "0 4px 12px rgba(21,128,61,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -530,10 +519,7 @@ export function SalesOutbound({ cart, onCartClick, saving, outboundType, sourceT
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #22c55e 0%, #15803d 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.green.buttonBg} ${COLOR_MAP.green.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierM
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -233,11 +234,7 @@ export function SubcontractorOutbound({ cart, onCartClick, saving, outboundType,
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #8b5cf6, #6d28d9)",
|
||||
boxShadow: "0 4px 12px rgba(109,40,217,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover} shadow-[0_4px_12px_rgba(109,40,217,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -286,11 +283,7 @@ export function SubcontractorOutbound({ cart, onCartClick, saving, outboundType,
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCustomerScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #8b5cf6, #6d28d9)",
|
||||
boxShadow: "0 4px 12px rgba(109,40,217,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover} shadow-[0_4px_12px_rgba(109,40,217,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -334,13 +327,9 @@ export function SubcontractorOutbound({ cart, onCartClick, saving, outboundType,
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedCustomer}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover} ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(109,40,217,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #8b5cf6, #6d28d9)",
|
||||
boxShadow: selectedCustomer ? "0 4px 12px rgba(109,40,217,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -491,10 +480,7 @@ export function SubcontractorOutbound({ cart, onCartClick, saving, outboundType,
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.purple.buttonBg} ${COLOR_MAP.purple.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SupplierModal, type Supplier, matchChosung } from "../inbound/SupplierM
|
||||
import { SimpleKeypadModal } from "../common/SimpleKeypadModal";
|
||||
import { BarcodeScanModal } from "../common/BarcodeScanModal";
|
||||
import type { CartItemWithId } from "../common/useCartSync";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -233,11 +234,7 @@ export function SuppliedOutbound({ cart, onCartClick, saving, outboundType, sour
|
||||
<button
|
||||
onClick={onCartClick}
|
||||
disabled={saving}
|
||||
className="relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #06b6d4, #0e7490)",
|
||||
boxShadow: "0 4px 12px rgba(14,116,144,0.3)",
|
||||
}}
|
||||
className={`relative min-w-[144px] min-h-[48px] px-4 rounded-xl flex items-center justify-center gap-2 text-white font-semibold text-sm active:scale-95 transition-all shrink-0 disabled:opacity-60 ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover} shadow-[0_4px_12px_rgba(14,116,144,0.3)]`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-6 h-6" fill="none" viewBox="0 0 24 24">
|
||||
@@ -286,11 +283,7 @@ export function SuppliedOutbound({ cart, onCartClick, saving, outboundType, sour
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCustomerScanOpen(true)}
|
||||
className="min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #06b6d4, #0e7490)",
|
||||
boxShadow: "0 4px 12px rgba(14,116,144,0.3)",
|
||||
}}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover} shadow-[0_4px_12px_rgba(14,116,144,0.3)]`}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -334,13 +327,9 @@ export function SuppliedOutbound({ cart, onCartClick, saving, outboundType, sour
|
||||
<button
|
||||
onClick={() => setItemScanOpen(true)}
|
||||
disabled={!selectedCustomer}
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : ""
|
||||
className={`min-w-[48px] min-h-[48px] rounded-xl flex items-center justify-center text-white active:scale-95 transition-all shrink-0 ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover} ${
|
||||
!selectedCustomer ? "opacity-40 cursor-not-allowed" : "shadow-[0_4px_12px_rgba(14,116,144,0.3)]"
|
||||
}`}
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, #06b6d4, #0e7490)",
|
||||
boxShadow: selectedCustomer ? "0 4px 12px rgba(14,116,144,0.3)" : "none",
|
||||
}}
|
||||
>
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z" />
|
||||
@@ -491,10 +480,7 @@ export function SuppliedOutbound({ cart, onCartClick, saving, outboundType, sour
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openNumpad(order)}
|
||||
className="flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #06b6d4 0%, #0e7490 100%)",
|
||||
}}
|
||||
className={`flex items-center justify-center gap-1 px-2.5 py-2 rounded-md text-white text-xs font-semibold active:scale-95 transition-all ${COLOR_MAP.cyan.buttonBg} ${COLOR_MAP.cyan.buttonBgHover}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121 0 2.002-.881 2.002-2.003V6.75m-14.22 0h14.22" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { dataApi } from "@/lib/api/data";
|
||||
import { ConfirmModal } from "../common/ConfirmModal";
|
||||
import { COLOR_MAP } from "../common/theme";
|
||||
import { AcceptProcessModal } from "./AcceptProcessModal";
|
||||
import { ProcessDetailModal, type ProcessStep } from "./ProcessDetailModal";
|
||||
import {
|
||||
@@ -16,6 +17,18 @@ import {
|
||||
|
||||
const POP_NEW_PROD_STATE_KEY = "pop-new-production-process-state";
|
||||
|
||||
/** 같은 batch_id를 가진 마스터 공정(virtual split 제외)만 반환 */
|
||||
function getSameBatchMasters(
|
||||
processes: WorkOrderProcess[],
|
||||
batchId: string | null | undefined,
|
||||
): WorkOrderProcess[] {
|
||||
return processes.filter(
|
||||
(p) =>
|
||||
!p.parent_process_id &&
|
||||
((!batchId && !p.batch_id) || (batchId && p.batch_id === batchId)),
|
||||
);
|
||||
}
|
||||
|
||||
/* 텍스트가 넘칠 때 자동 슬라이드 (마키) */
|
||||
function AutoScrollText({
|
||||
children,
|
||||
@@ -208,13 +221,9 @@ function CompressedProcessSteps({
|
||||
batchId?: string;
|
||||
allProcesses?: WorkOrderProcess[];
|
||||
}) {
|
||||
const sorted = [...processes]
|
||||
.filter((p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 표시 (다중 품목 구분)
|
||||
(!batchId && !p.batch_id) ||
|
||||
(batchId && p.batch_id === batchId)
|
||||
))
|
||||
.sort((a, b) => a.seq_no - b.seq_no);
|
||||
const sorted = getSameBatchMasters(processes, batchId).sort(
|
||||
(a, b) => a.seq_no - b.seq_no,
|
||||
);
|
||||
|
||||
if (sorted.length === 0) return null;
|
||||
|
||||
@@ -223,19 +232,19 @@ function CompressedProcessSteps({
|
||||
|
||||
// For completed status: batch_id 기반 진행률 표시
|
||||
if (status === "completed") {
|
||||
// 같은 batch_id를 가진 SPLIT들이 어느 seq까지 완료했는지 추적
|
||||
// 각 마스터 seq에 최소 1건 confirmed accepted_result가 있으면 완료로 판정
|
||||
let maxCompletedSeq = currentSeqNo; // 최소한 현재 seq까지는 완료
|
||||
|
||||
if (batchId && allProcesses) {
|
||||
const batchSplits = allProcesses.filter(
|
||||
(p) =>
|
||||
p.batch_id === batchId &&
|
||||
p.parent_process_id &&
|
||||
p.status === "completed",
|
||||
);
|
||||
for (const s of batchSplits) {
|
||||
const sSeq = s.seq_no;
|
||||
if (sSeq > maxCompletedSeq) maxCompletedSeq = sSeq;
|
||||
if (allProcesses) {
|
||||
for (const m of sorted) {
|
||||
const hasConfirmed = allProcesses.some(
|
||||
(p) =>
|
||||
p.parent_process_id === m.id &&
|
||||
p.result_status === "confirmed",
|
||||
);
|
||||
if (hasConfirmed && m.seq_no > maxCompletedSeq) {
|
||||
maxCompletedSeq = m.seq_no;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,8 +892,6 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
activeTab,
|
||||
instructionMap,
|
||||
equipmentMap,
|
||||
currentUserId,
|
||||
allProcesses,
|
||||
]);
|
||||
|
||||
/* ---- Tab counts ---- */
|
||||
@@ -1038,13 +1045,10 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
/* ---- Open process detail modal ---- */
|
||||
const openDetailModal = (proc: WorkOrderProcess) => {
|
||||
const wi = instructionMap[proc.wo_id];
|
||||
const siblings = (processesByWo[proc.wo_id] || [])
|
||||
.filter((p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 형제 (다중 품목 구분)
|
||||
(!proc.batch_id && !p.batch_id) ||
|
||||
(proc.batch_id && p.batch_id === proc.batch_id)
|
||||
))
|
||||
.sort((a, b) => a.seq_no - b.seq_no);
|
||||
const siblings = getSameBatchMasters(
|
||||
processesByWo[proc.wo_id] || [],
|
||||
proc.batch_id,
|
||||
).sort((a, b) => a.seq_no - b.seq_no);
|
||||
|
||||
const totalQty = wi ? wi.qty : proc.plan_qty;
|
||||
|
||||
@@ -1087,14 +1091,10 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
|
||||
/* ---- Helper: get previous process display info (name + progress) ---- */
|
||||
const getPrevProcessInfo = (proc: WorkOrderProcess) => {
|
||||
const siblings = (processesByWo[proc.wo_id] || [])
|
||||
.filter(
|
||||
(p) =>
|
||||
!p.parent_process_id &&
|
||||
((!proc.batch_id && !p.batch_id) ||
|
||||
(proc.batch_id && p.batch_id === proc.batch_id)),
|
||||
)
|
||||
.sort((a, b) => a.seq_no - b.seq_no);
|
||||
const siblings = getSameBatchMasters(
|
||||
processesByWo[proc.wo_id] || [],
|
||||
proc.batch_id,
|
||||
).sort((a, b) => a.seq_no - b.seq_no);
|
||||
|
||||
const currentIdx = siblings.findIndex((p) => p.id === proc.id);
|
||||
if (currentIdx <= 0)
|
||||
@@ -1217,12 +1217,9 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
.map((proc) => {
|
||||
const wi = instructionMap[proc.wo_id];
|
||||
const badge = STATUS_BADGE[proc.status] || STATUS_BADGE.waiting;
|
||||
const siblingProcesses = (processesByWo[proc.wo_id] || []).filter(
|
||||
(p) => !p.parent_process_id && (
|
||||
// 같은 batch_id끼리만 형제 (다중 품목 구분)
|
||||
(!proc.batch_id && !p.batch_id) ||
|
||||
(proc.batch_id && p.batch_id === proc.batch_id)
|
||||
),
|
||||
const siblingProcesses = getSameBatchMasters(
|
||||
processesByWo[proc.wo_id] || [],
|
||||
proc.batch_id,
|
||||
);
|
||||
const planQty = proc.plan_qty;
|
||||
const goodQty = proc.good_qty;
|
||||
@@ -1294,28 +1291,27 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
let originProcessCode = proc.process_code;
|
||||
let originDefectQty = defectQty;
|
||||
if (isRework) {
|
||||
// 리워크 마스터 카드만 카운트 (SPLIT 제외 — parent_process_id 없는 것만)
|
||||
const reworkMasters = allProcesses.filter(
|
||||
// 신 구조: 리워크 = wop_result.is_rework='Y' (virtual 카드)
|
||||
// 같은 rework_source_id 그룹 내에서 accepted_at 순 차수 계산
|
||||
const sameSource = allProcesses.filter(
|
||||
(p) =>
|
||||
p.wo_id === proc.wo_id &&
|
||||
!p.parent_process_id &&
|
||||
isReworkProcess(p),
|
||||
isReworkProcess(p) &&
|
||||
p.rework_source_id === proc.rework_source_id,
|
||||
);
|
||||
const sortedReworks = [...reworkMasters].sort((a, b) => {
|
||||
const da = a.created_date
|
||||
? new Date(a.created_date).getTime()
|
||||
const sortedReworks = [...sameSource].sort((a, b) => {
|
||||
const da = a.accepted_at
|
||||
? new Date(a.accepted_at).getTime()
|
||||
: 0;
|
||||
const db = b.created_date
|
||||
? new Date(b.created_date).getTime()
|
||||
const db = b.accepted_at
|
||||
? new Date(b.accepted_at).getTime()
|
||||
: 0;
|
||||
return da - db || a.id.localeCompare(b.id);
|
||||
});
|
||||
// 현재 카드가 SPLIT이면 parent(마스터)의 위치로, 마스터면 직접 위치
|
||||
const masterId = proc.parent_process_id || proc.id;
|
||||
const myIdx = sortedReworks.findIndex((r) => r.id === masterId);
|
||||
const myIdx = sortedReworks.findIndex((r) => r.id === proc.id);
|
||||
reworkRound = myIdx >= 0 ? myIdx + 1 : 1;
|
||||
|
||||
// Find origin (source) process
|
||||
// Find origin (source) process — rework_source_id 는 wop_result.id
|
||||
if (proc.rework_source_id) {
|
||||
const origin = allProcesses.find(
|
||||
(p) => p.id === proc.rework_source_id,
|
||||
@@ -1402,9 +1398,7 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
|
||||
{/* Sub-info: item name + equipment */}
|
||||
<AutoScrollText className="text-sm text-gray-500 mb-3">
|
||||
📦 {proc.batch_id
|
||||
? `${itemNameMap[proc.batch_id] || proc.batch_id}(${proc.batch_id})`
|
||||
: `${wi?.item_name || "품목"}${wi?.item_code || wi?.item_number ? `(${wi?.item_code || wi?.item_number})` : ""}`}
|
||||
📦 {`${wi?.item_name || "품목"}${wi?.item_code || wi?.item_number ? `(${wi?.item_code || wi?.item_number})` : ""}`}
|
||||
{" · "}
|
||||
{!isRework
|
||||
? `⚙️ ${eqName}`
|
||||
@@ -1485,11 +1479,7 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
.rework_source_id as string | undefined,
|
||||
)
|
||||
}
|
||||
className="w-full py-3.5 text-sm font-bold text-white active:scale-[0.98] transition-all"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, #fb923c, #ea580c)",
|
||||
}}
|
||||
className={`w-full py-3.5 text-sm font-bold text-white active:scale-[0.98] transition-all ${COLOR_MAP.orange.buttonBg} ${COLOR_MAP.orange.buttonBgHover}`}
|
||||
>
|
||||
리워크 접수
|
||||
</button>
|
||||
@@ -1505,11 +1495,7 @@ export function WorkOrderList(props: WorkOrderListProps) {
|
||||
proc.seq_no,
|
||||
)
|
||||
}
|
||||
className="w-full py-3.5 text-sm font-bold text-white active:scale-[0.98] transition-all"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to bottom, #fbbf24, #d97706)",
|
||||
}}
|
||||
className={`w-full py-3.5 text-sm font-bold text-white active:scale-[0.98] transition-all ${COLOR_MAP.amber.buttonBg} ${COLOR_MAP.amber.buttonBgHover}`}
|
||||
>
|
||||
접수
|
||||
</button>
|
||||
|
||||
@@ -183,6 +183,10 @@ function MaterialQtyInputRow({
|
||||
* Phase B-1: ProcessWork.tsx L2810-2993에서 분리.
|
||||
* BOM 자재 조회/투입 — processId 외 props 없음 (원본 시그니처 유지).
|
||||
* 원본 동작 그대로: 자체 state + 자체 API 호출, peSettings.materialInput 판별은 상위(ProcessWork)에서 수행.
|
||||
*
|
||||
* 신 구조: processId = wop_result.id (접수 카드 id).
|
||||
* 백엔드 /pop/production/bom-materials/:id, /material-inputs/:id,
|
||||
* /material-input (body.work_order_process_id) 모두 wop_result.id 해석.
|
||||
*/
|
||||
export function MaterialInputSection({ processId }: { processId: string }) {
|
||||
const [bomMaterials, setBomMaterials] = useState<
|
||||
|
||||
@@ -42,6 +42,26 @@ export interface WorkOrderProcessRaw {
|
||||
batch_count?: string | number | null;
|
||||
batch_list?: string[] | null;
|
||||
batch_index?: number | null;
|
||||
/** 신 구조 — 이 wop 에 속한 접수 카드 배열 (wop_result rows) */
|
||||
accepted_results?: Array<{
|
||||
id: string; // wop_result.id
|
||||
seq: string | number;
|
||||
status: string;
|
||||
result_status?: string;
|
||||
input_qty?: string | number | null;
|
||||
good_qty?: string | number | null;
|
||||
defect_qty?: string | number | null;
|
||||
concession_qty?: string | number | null;
|
||||
total_production_qty?: string | number | null;
|
||||
is_rework?: string | boolean | null;
|
||||
rework_source_id?: string | null;
|
||||
accepted_by?: string | null;
|
||||
accepted_at?: string | null;
|
||||
started_at?: string | null;
|
||||
completed_at?: string | null;
|
||||
equipment_code?: string | null;
|
||||
batch_id?: string | null;
|
||||
}> | null;
|
||||
}
|
||||
|
||||
/** UI 컴포넌트가 사용하는 정규화 View — 수량 number, 플래그 boolean */
|
||||
@@ -90,6 +110,26 @@ export interface WorkOrderProcessView {
|
||||
batch_count: number;
|
||||
batch_list: string[] | null;
|
||||
batch_index: number | null;
|
||||
/** 신 구조 — 이 wop 에 속한 접수 카드 배열 (정규화됨) */
|
||||
accepted_results: Array<{
|
||||
id: string; // wop_result.id — 접수/실적 단위 식별자
|
||||
seq: number;
|
||||
status: "acceptable" | "waiting" | "in_progress" | "completed";
|
||||
result_status: string;
|
||||
input_qty: number;
|
||||
good_qty: number;
|
||||
defect_qty: number;
|
||||
concession_qty: number;
|
||||
total_production_qty: number;
|
||||
is_rework: boolean;
|
||||
rework_source_id: string | null;
|
||||
accepted_by: string | null;
|
||||
accepted_at: string | null;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
equipment_code: string | null;
|
||||
batch_id: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** 범용 Yes 판정 — "Y"/"true"/"1" (대소문자 유연) 또는 boolean true */
|
||||
@@ -168,5 +208,27 @@ export function normalizeWorkOrderProcess(
|
||||
batch_count: toInt(raw.batch_count),
|
||||
batch_list: Array.isArray(raw.batch_list) ? raw.batch_list : null,
|
||||
batch_index: raw.batch_index ?? null,
|
||||
accepted_results: Array.isArray(raw.accepted_results)
|
||||
? raw.accepted_results.map((ar) => ({
|
||||
id: String(ar.id || ""),
|
||||
seq: toInt(ar.seq),
|
||||
status:
|
||||
(ar.status as WorkOrderProcessView["status"]) || "in_progress",
|
||||
result_status: String(ar.result_status || ""),
|
||||
input_qty: toInt(ar.input_qty),
|
||||
good_qty: toInt(ar.good_qty),
|
||||
defect_qty: toInt(ar.defect_qty),
|
||||
concession_qty: toInt(ar.concession_qty),
|
||||
total_production_qty: toInt(ar.total_production_qty),
|
||||
is_rework: isYes(ar.is_rework),
|
||||
rework_source_id: ar.rework_source_id ?? null,
|
||||
accepted_by: ar.accepted_by ?? null,
|
||||
accepted_at: ar.accepted_at ?? null,
|
||||
started_at: ar.started_at ?? null,
|
||||
completed_at: ar.completed_at ?? null,
|
||||
equipment_code: ar.equipment_code ?? null,
|
||||
batch_id: ar.batch_id ?? null,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,12 +136,46 @@ export function useProcessData() {
|
||||
setItemNameMap(newItemNameMap);
|
||||
setItemTypeMap(newItemTypeMap);
|
||||
const rawRows: WorkOrderProcessRaw[] = procRes.data?.data ?? [];
|
||||
setAllProcesses(rawRows.map(normalizeWorkOrderProcess));
|
||||
// 마스터(wop) + accepted_results 평탄화: 신 구조 접수 카드 = wop_result 항목을
|
||||
// 기존 UI 의 "분할 카드(parent_process_id 있음)" 형태로 펼쳐서 기존 필터 로직과
|
||||
// 호환 유지. virtual row 의 id = wop_result.id (라우팅/API body 식별자)
|
||||
const flat: WorkOrderProcessView[] = [];
|
||||
for (const raw of rawRows) {
|
||||
const master = normalizeWorkOrderProcess(raw);
|
||||
flat.push(master);
|
||||
for (const ar of master.accepted_results) {
|
||||
flat.push({
|
||||
...master,
|
||||
// 접수 카드 → virtual split row
|
||||
id: ar.id, // wop_result.id
|
||||
parent_process_id: master.id, // 마스터(wop.id) 참조
|
||||
status: ar.status,
|
||||
result_status: ar.result_status,
|
||||
input_qty: ar.input_qty,
|
||||
good_qty: ar.good_qty,
|
||||
defect_qty: ar.defect_qty,
|
||||
concession_qty: ar.concession_qty,
|
||||
total_production_qty: ar.total_production_qty,
|
||||
is_rework: ar.is_rework,
|
||||
rework_source_id: ar.rework_source_id,
|
||||
accepted_by: ar.accepted_by,
|
||||
accepted_at: ar.accepted_at,
|
||||
started_at: ar.started_at,
|
||||
completed_at: ar.completed_at,
|
||||
equipment_code: ar.equipment_code,
|
||||
batch_id: ar.batch_id ?? master.batch_id,
|
||||
// split 표시용 (접수 #n)
|
||||
split_no: ar.seq,
|
||||
split_total: master.accepted_results.length,
|
||||
// 분할 카드 자체의 accepted_results 는 의미 없음 (재귀 금지)
|
||||
accepted_results: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
setAllProcesses(flat);
|
||||
setProcessList((pmRes.data ?? []) as ProcessMng[]);
|
||||
setEquipmentList((eqRes.data ?? []) as EquipmentMng[]);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("[useProcessData] fetch error:", error);
|
||||
} catch {
|
||||
toast.error("데이터 조회 실패");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ProcessWork } from "../../../_components/production/ProcessWork";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ProcessWork,
|
||||
type ProcessWorkInfo,
|
||||
} from "../../../_components/production/ProcessWork";
|
||||
|
||||
export default function WorkPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const processId = params.processId as string;
|
||||
const [info, setInfo] = useState<ProcessWorkInfo | null>(null);
|
||||
|
||||
const statusBadge = (() => {
|
||||
if (!info) return null;
|
||||
if (info.isCompleted) {
|
||||
return { text: "완료", cls: "bg-green-100 text-green-700" };
|
||||
}
|
||||
if (info.status === "in_progress") {
|
||||
return { text: "진행중", cls: "bg-blue-100 text-blue-700" };
|
||||
}
|
||||
return { text: info.status ?? "-", cls: "bg-gray-100 text-gray-600" };
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* ===== Back + Title ===== */}
|
||||
<div className="flex flex-col gap-4 flex-1 min-h-0">
|
||||
{/* ===== Back + Title + Status row ===== */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => router.push("/COMPANY_7/pop/production/process")}
|
||||
@@ -20,8 +37,33 @@ export default function WorkPage() {
|
||||
</svg>
|
||||
</button>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 tracking-tight">공정 작업</h1>
|
||||
<div className="flex-1" />
|
||||
{info && (
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="text-blue-700/70 text-xl font-medium">지시</span>
|
||||
<span className="text-blue-700 text-4xl font-bold">{info.planQty.toLocaleString()}</span>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="text-amber-500/70 text-xl font-medium">접수</span>
|
||||
<span className="text-amber-500 text-4xl font-bold">{info.inputQty.toLocaleString()}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{statusBadge && (
|
||||
<span
|
||||
className={`rounded-full px-5 py-2 text-lg font-bold ${statusBadge.cls}`}
|
||||
>
|
||||
{statusBadge.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ProcessWork processId={processId} />
|
||||
<ProcessWork
|
||||
processId={processId}
|
||||
onInfoChange={setInfo}
|
||||
hideInlineStatus
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user