Project

General

Profile

Feature #105 » 工單下線-0707.html

和絹 傅, 07/13/2026 02:08 AM

 
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<title>工單下線與退料管制</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.editable-input {
padding: 6px 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
width: 100%;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
transition: border-color 0.15s;
}
.editable-input:focus {
border-color: #4c9aff;
outline: none;
}
.locked {
background-color: #f3f4f6;
cursor: not-allowed;
}
</style>
</head>
<body class="bg-gray-50 p-8">
<div id="app" class="min-h-screen max-w-7xl mx-auto">
<section class="bg-white shadow-lg rounded-2xl p-6">
<h2 class="text-3xl font-bold text-teal-800 mb-6 border-b-2 border-teal-200 pb-2">📅 工單下線作業區</h2>
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
<div class="relative w-full md:w-64">
<input v-model="searchQuery"
type="text"
placeholder="🔍 搜尋工單號碼..."
class="editable-input border-teal-300 focus:border-teal-500 py-2">
</div>
<p class="text-sm text-amber-600 font-medium">⚠️ 提示:若需修改已完成領料的工單,請先執行「退料並刪除」攔截重開。</p>
</div>

<h3 class="text-xl font-semibold text-gray-700 mb-3 border-b pb-2">
待開工單列表 (共 {{ filteredSchedules.length }} 筆)
</h3>
<table class="w-full border text-sm text-gray-700">
<thead class="bg-gray-100">
<tr>
<th class="p-2 border">工單號</th>
<th class="p-2 border">產品</th>
<th class="p-2 border w-1/6">生產線 (調度)</th>
<th class="p-2 border w-24">計劃數量</th>
<th class="p-2 border w-32">排程日期 (調度)</th>
<th class="p-2 border w-24">領料狀態</th>
<th class="p-2 border w-24">派發狀態</th>
<th class="p-2 border w-44">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(o,i) in filteredSchedules" :key="o.orderNo" class="hover:bg-teal-50">
<td class="p-2 border font-mono font-bold">{{o.orderNo}}</td>
<td class="p-2 border">{{o.product}}</td>
<td class="p-2 border">
<select v-model="o.line" class="editable-input bg-white" :class="{ 'locked': o.isDispatched }" :disabled="o.isDispatched">
<option v-for="l in linePool" :key="l" :value="l">{{ l }}</option>
</select>
</td>
<td class="p-2 border text-right font-semibold">
{{ o.planQty.toLocaleString() }}
</td>

<td class="p-2 border">
<input v-model="o.scheduleDate"
type="date"
class="editable-input"
:class="{ 'locked': o.isDispatched }"
:disabled="o.isDispatched">
</td>
<td class="p-2 border text-center">
<span class="px-2 py-1 rounded-full text-xs font-semibold"
:class="getMaterialStatusColor(o.materialReady)">
{{ getMaterialStatusText(o.materialReady) }}
</span>
</td>

<td class="p-2 border text-center">
<span class="px-2 py-1 rounded-full text-xs font-semibold"
:class="getDispatchStatusColor(o.isDispatched)">
{{ getDispatchStatusText(o.isDispatched) }}
</span>
</td>
<td class="p-2 border text-center">
<div class="flex items-center justify-center gap-1.5">
<!-- 派發按鈕邏輯 -->
<button v-if="!o.isDispatched"
@click="dispatchOrder(o)"
:disabled="!o.materialReady"
class="px-2 py-1 rounded text-xs font-medium text-white transition"
:class="o.materialReady ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-400 cursor-not-allowed'">
派發
</button>
<button v-else
@click="cancelDispatch(o)"
class="px-2 py-1 rounded text-xs font-medium bg-yellow-500 text-white hover:bg-yellow-600">
取消
</button>

<!-- ✨ 新增:下線前的安全「退料並刪除」按鈕 -->
<button @click="returnMaterialsAndDelete(o)"
class="px-2 py-1 rounded text-xs font-medium bg-red-600 text-white hover:bg-red-700 transition flex items-center gap-0.5">
退料刪除
</button>
</div>
</td>
</tr>
<tr v-if="filteredSchedules.length === 0">
<td colspan="8" class="p-4 text-center text-gray-500 border">目前沒有待下線或處理中的工單。</td>
</tr>
</tbody>
</table>
<div class="mt-8 p-4 bg-gray-100 rounded-lg">
<h4 class="font-bold text-gray-700 mb-2">💡 模擬即時開工 (下線監控測試)</h4>
<div class="flex flex-wrap gap-2">
<button v-for="o in schedules.filter(s => s.isDispatched && !s.isStarted)"
:key="o.orderNo"
@click="simulateStart(o.orderNo)"
class="px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600">
現場開工: {{o.orderNo}}
</button>
<span v-if="schedules.filter(s => s.isDispatched && !s.isStarted).length === 0" class="text-sm text-gray-400">目前無待開工單(需先完成領料並點擊「派發」)。</span>
</div>
</div>

</section>
</div>

<script>
const { createApp, ref, computed } = Vue;

createApp({
setup() {
const schedules = ref([
{ orderNo: 'WO20260707-001', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-紫', line: 'Line A', planQty: 5000, scheduleDate: '2026-07-07', materialReady: true, isDispatched: false, isStarted: false },
{ orderNo: 'WO20260707-002', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-藍', line: 'Line B', planQty: 10000, scheduleDate: '2026-07-08', materialReady: false, isDispatched: false, isStarted: false },
{ orderNo: 'WO20260707-003', product: '蔡司邁康Classic角膜塑形用硬性透氣接觸鏡-環曲-綠', line: 'Line C', planQty: 2000, scheduleDate: '2026-07-07', materialReady: true, isDispatched: true, isStarted: false },
]);
const linePool = ref(['Line A', 'Line B', 'Line C', 'Line D', 'Line E']);
const searchQuery = ref('');

const getMaterialStatusText = (isReady) => isReady ? '✔ 已完成' : '❌ 未完成';
const getMaterialStatusColor = (isReady) => isReady ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
const getDispatchStatusText = (isDispatched) => isDispatched ? '待開工' : '未派發';
const getDispatchStatusColor = (isDispatched) => isDispatched ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-700';
const filteredSchedules = computed(() => {
let records = schedules.value.filter(o => !o.isStarted);
if (searchQuery.value.trim()) {
const q = searchQuery.value.toLowerCase();
records = records.filter(o => o.orderNo.toLowerCase().includes(q));
}
return records;
});

const dispatchOrder = (order) => {
if (!order.materialReady) {
alert('錯誤:領料未完成!');
return;
}
if (confirm(`確定派發工單 ${order.orderNo}?`)) {
order.isDispatched = true;
}
};
const cancelDispatch = (order) => {
if (confirm(`確定取消派發?`)) {
order.isDispatched = false;
}
};

// ✨ 新增:核心退料與刪除攔截控制邏輯
const returnMaterialsAndDelete = (order) => {
let confirmMessage = `⚠️ 警告:您即將刪除未下線工單 [ ${order.orderNo} ]。\n\n`;
// 判斷是否需要跑退料流程
if (order.materialReady) {
confirmMessage += `偵測到【該工單已完成領料】!\n系統將會同步執行「退料入庫」程序,將已領物料與批號回補倉儲實體庫存。\n\n確定執行退料並刪除工單嗎?`;
} else {
confirmMessage += `該工單尚未領料,將直接取消並刪除。確定執行嗎?`;
}

if (confirm(confirmMessage)) {
if (order.materialReady) {
// 1. 執行退料:將領料狀態翻轉為未完成,並發送扣減/回補信號
order.materialReady = false;
console.log(`[ERP Sync] 工單 ${order.orderNo} 已生成退料單,庫存批號已回補。`);
alert(`📦 退料成功!已自動沖銷領料單據。\n接下來將完全刪除工單。`);
}
// 2. 執行刪除:從陣列中剔除該筆資料
const index = schedules.value.findIndex(s => s.orderNo === order.orderNo);
if (index !== -1) {
schedules.value.splice(index, 1);
}
alert(`🗑️ 工單 ${order.orderNo} 已成功攔截並徹底刪除。`);
}
};
const simulateStart = (orderNo) => {
const order = schedules.value.find(o => o.orderNo === orderNo);
if (order) {
order.isStarted = true;
alert(`工單 ${orderNo} 已現場開工下線,後續將無法在此處進行攔截與退料。`);
}
};

return {
schedules,
linePool,
searchQuery,
filteredSchedules,
getMaterialStatusText,
getMaterialStatusColor,
getDispatchStatusText,
getDispatchStatusColor,
dispatchOrder,
cancelDispatch,
simulateStart,
returnMaterialsAndDelete // ✨ 回傳新函數給前端視圖
};
}
}).mount('#app');
</script>
</body>
</html>
(1-1/2)