|
<!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>
|