4695de8783
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 14s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 23s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 9s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 10s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Frontend CI Pipeline / ci-frontend-8-steps (push) Failing after 3m3s
141 lines
5.6 KiB
Vue
141 lines
5.6 KiB
Vue
<!-- WmsInboundPickingView.vue -->
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import BaseStatusBadge from '../components/primitives/BaseStatusBadge.vue';
|
|
import BaseButton from '../components/primitives/BaseButton.vue';
|
|
import BarcodeInput from '../components/domain-fields/BarcodeInput.vue';
|
|
import LotField from '../components/domain-fields/LotField.vue';
|
|
import QuantityField from '../components/domain-fields/QuantityField.vue';
|
|
import { parseGS1Barcode, type OfflineCommand, type BarcodeScanResult } from '../modules/wms/domain/offlineCommand';
|
|
|
|
const isOnline = ref(true);
|
|
|
|
// WMS Scan & Picking State
|
|
const lastScanResult = ref<BarcodeScanResult | null>(null);
|
|
const scannedItemCode = ref('PROD-WMS-801');
|
|
const lotNumber = ref('LOT-20260726-A');
|
|
const pickQty = ref('15');
|
|
const totalPickedCount = ref(150);
|
|
|
|
// Offline Command Queue State (WMS-04)
|
|
const offlineQueue = ref<OfflineCommand[]>([]);
|
|
|
|
const handleBarcodeScanned = (input: BarcodeScanResult | string) => {
|
|
const result = typeof input === 'string' ? parseGS1Barcode(input) : input;
|
|
lastScanResult.value = result;
|
|
if (result.isValid && result.itemCode) {
|
|
scannedItemCode.value = result.itemCode;
|
|
if (result.lotNumber) lotNumber.value = result.lotNumber;
|
|
}
|
|
};
|
|
|
|
const handleConfirmPick = () => {
|
|
const cmdId = `CMD-${Date.now()}`;
|
|
const newCmd: OfflineCommand = {
|
|
commandId: cmdId,
|
|
idempotencyKey: `IDEM-${cmdId}`,
|
|
actionType: 'PICK',
|
|
payload: { itemCode: scannedItemCode.value, lot: lotNumber.value, qty: pickQty.value },
|
|
status: isOnline.value ? 'completed' : 'queued',
|
|
createdAt: new Date().toISOString(),
|
|
retryCount: 0
|
|
};
|
|
|
|
if (!isOnline.value) {
|
|
offlineQueue.value.unshift(newCmd);
|
|
} else {
|
|
totalPickedCount.value += Number(pickQty.value);
|
|
}
|
|
};
|
|
|
|
const syncOfflineQueue = () => {
|
|
offlineQueue.value.forEach(cmd => {
|
|
cmd.status = 'completed';
|
|
cmd.syncedAt = new Date().toISOString();
|
|
totalPickedCount.value += Number(cmd.payload.qty);
|
|
});
|
|
setTimeout(() => {
|
|
offlineQueue.value = [];
|
|
}, 600);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="wms-picking-container flex flex-col gap-4 p-6 bg-slate-100 min-h-full select-none text-left">
|
|
<!-- Header with Network Status Toggle -->
|
|
<div class="flex justify-between items-center bg-white p-4 rounded-lg shadow-sm border border-slate-200">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<BaseStatusBadge variant="info" label="Phase 6" />
|
|
<h1 class="text-xl font-bold text-slate-800">WMS 현장 입고·피킹 파일럿</h1>
|
|
<BaseStatusBadge
|
|
:variant="isOnline ? 'success' : 'warning'"
|
|
:label="isOnline ? 'ONLINE' : 'OFFLINE MODE'"
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-slate-500 mt-1">WMS-01 ~ 04: Touch Density (44px), 바코드 <100ms 파싱, FEFO 검증, OfflineCommand 오프라인 큐</p>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<BaseButton
|
|
:variant="isOnline ? 'outline' : 'secondary'"
|
|
density="touch"
|
|
@click="isOnline = !isOnline"
|
|
>
|
|
{{ isOnline ? '📡 온라인 상태' : '📶 오프라인 모드 전환' }}
|
|
</BaseButton>
|
|
<BaseButton
|
|
v-if="!isOnline && offlineQueue.length > 0"
|
|
variant="primary"
|
|
density="touch"
|
|
@click="syncOfflineQueue"
|
|
>
|
|
🔄 오프라인 큐 동기화 ({{ offlineQueue.length }})
|
|
</BaseButton>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scanner & Picking Main Action (Touch Friendly min-h 44px) -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- Left: Scanner Section -->
|
|
<div class="bg-white p-6 rounded-lg shadow-sm border border-slate-200 flex flex-col gap-4">
|
|
<h2 class="text-base font-bold text-slate-800">1. 스캐너 바코드 입력 (<100ms 판정)</h2>
|
|
<BarcodeInput @scan="handleBarcodeScanned" />
|
|
|
|
<div v-if="lastScanResult" class="p-3 bg-blue-50 border border-blue-200 rounded text-xs flex flex-col gap-1">
|
|
<div class="font-bold text-blue-800">최근 스캔 파싱 결과 (속도: {{ lastScanResult.parseTimeMs }}ms)</div>
|
|
<div>원본: {{ lastScanResult.rawBarcode }}</div>
|
|
<div>추출 품목: {{ lastScanResult.itemCode }}</div>
|
|
<div>추출 로트: {{ lastScanResult.lotNumber }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Touch Picking Action Form -->
|
|
<div class="bg-white p-6 rounded-lg shadow-sm border border-slate-200 flex flex-col gap-4">
|
|
<h2 class="text-base font-bold text-slate-800">2. 피킹 작업 확정 (누적: {{ totalPickedCount }} EA)</h2>
|
|
<div class="flex flex-col gap-3">
|
|
<LotField v-model="lotNumber" />
|
|
<QuantityField v-model="pickQty" />
|
|
</div>
|
|
|
|
<div class="pt-2">
|
|
<BaseButton variant="primary" density="touch" class="w-full text-lg" @click="handleConfirmPick">
|
|
{{ isOnline ? '✅ 피킹 작업 확정' : '💾 오프라인 큐 적재 (OfflineCommand)' }}
|
|
</BaseButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Offline Queue Log Table -->
|
|
<div v-if="offlineQueue.length > 0" class="bg-white p-4 rounded-lg shadow-sm border border-slate-200">
|
|
<h3 class="text-xs font-bold text-amber-700 mb-2">대기 중인 오프라인 명령 큐 (IndexedDB / LocalStorage)</h3>
|
|
<ul class="divide-y text-xs">
|
|
<li v-for="cmd in offlineQueue" :key="cmd.commandId" class="py-2 flex justify-between">
|
|
<span>[{{ cmd.actionType }}] {{ cmd.payload.itemCode }} ({{ cmd.payload.qty }} EA)</span>
|
|
<span class="font-mono text-amber-600 font-semibold">{{ cmd.status }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</template>
|