Files
ewoooc/vendor_stockout_list.html.backup
ogt 1b4f3a7bbe
Some checks failed
CD Pipeline / deploy (push) Failing after 59s
feat: EwoooC 初始化 — 完整專案推版至 Gitea
- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml)
- 部署模式: rsync Python 檔案至 188 → docker restart (volume mount)
- Dockerfile/requirements 變動時自動重建 Docker image
- 部署通知: Telegram (開始/成功/失敗)
- 健康檢查: https://mo.wooo.work/health (最多 5 次重試)
- 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 01:21:13 +08:00

691 lines
28 KiB
Plaintext

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>缺貨清單 - 廠商缺貨系統</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
min-height: 100vh;
}
.navbar {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%) !important;
}
.table-container {
background: white;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
overflow: hidden;
}
.filter-card {
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.table {
margin-bottom: 0;
}
.table thead th {
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: white;
border: none;
font-weight: 600;
padding: 1rem;
position: sticky;
top: 0;
z-index: 10;
}
.table tbody tr {
transition: all 0.2s ease;
}
.table tbody tr:hover {
background-color: #f8f9fa;
transform: translateX(5px);
}
.badge {
padding: 0.5rem 0.8rem;
font-size: 0.85rem;
}
.status-pending {
background-color: #ffc107;
color: #000;
}
.status-sent {
background-color: #28a745;
}
.status-failed {
background-color: #dc3545;
}
.btn-action {
padding: 0.4rem 0.8rem;
font-size: 0.875rem;
}
.stats-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 1.5rem;
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-5px);
}
.stats-number {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stats-label {
color: #6c757d;
font-size: 0.9rem;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><i class="fas fa-home me-2"></i>MOMO 監控系統</a>
<div class="ms-auto">
<a href="/vendor-stockout" class="btn btn-outline-light">
<i class="fas fa-arrow-left me-2"></i>返回主頁
</a>
</div>
</div>
</nav>
<div class="container-fluid" style="padding: 2rem;">
<!-- 統計卡片 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="stats-card">
<div class="stats-number text-primary" id="totalCount">0</div>
<div class="stats-label">總缺貨筆數</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<div class="stats-number text-warning" id="pendingCount">0</div>
<div class="stats-label">待發送</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<div class="stats-number text-success" id="sentCount">0</div>
<div class="stats-label">已發送</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<div class="stats-number text-danger" id="vendorCount">0</div>
<div class="stats-label">涉及廠商數</div>
</div>
</div>
</div>
<!-- 篩選區域 -->
<div class="filter-card">
<h5 class="mb-3"><i class="fas fa-filter me-2 text-success"></i>篩選條件</h5>
<div class="row">
<div class="col-md-3">
<label class="form-label">批次選擇</label>
<select class="form-select" id="filterBatch">
<option value="">全部批次</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">廠商代碼/名稱</label>
<input type="text" class="form-control" id="filterVendor" placeholder="輸入廠商代碼或名稱">
</div>
<div class="col-md-2">
<label class="form-label">發送狀態</label>
<select class="form-select" id="filterStatus">
<option value="">全部狀態</option>
<option value="pending">待發送</option>
<option value="sent">已發送</option>
<option value="failed">發送失敗</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">排序方式</label>
<select class="form-select" id="sortBy">
<option value="created_at_desc">最新匯入</option>
<option value="created_at_asc">最早匯入</option>
<option value="vendor_code_asc">廠商代碼</option>
<option value="stockout_days_desc">缺貨天數</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<button class="btn btn-success w-100" id="applyFilter">
<i class="fas fa-search me-2"></i>套用篩選
</button>
</div>
</div>
</div>
<!-- 批次操作 -->
<div class="card mb-3" style="border-radius: 12px;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<input type="checkbox" id="selectAll" class="form-check-input me-2">
<label for="selectAll" class="form-check-label">全選 (<span id="selectedCount">0</span> 筆)</label>
</div>
<div>
<button class="btn btn-outline-danger btn-action me-2" id="batchDelete" disabled>
<i class="fas fa-trash me-1"></i>批次刪除
</button>
<button class="btn btn-outline-success btn-action" id="batchMarkSent" disabled>
<i class="fas fa-check me-1"></i>標記已發送
</button>
</div>
</div>
</div>
</div>
<!-- 資料表格 -->
<div class="table-container">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 50px;"></th>
<th>批次</th>
<th>廠商代碼</th>
<th>廠商名稱</th>
<th>商品ID</th>
<th>商品名稱</th>
<th>缺貨天數</th>
<th>日均銷量</th>
<th>可賣量</th>
<th>狀態</th>
<th>匯入時間</th>
<th>操作</th>
</tr>
</thead>
<tbody id="dataTable">
<tr>
<td colspan="12" class="text-center py-5">
<i class="fas fa-spinner fa-spin fa-2x text-muted mb-3"></i>
<p class="text-muted">載入中...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分頁 -->
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
顯示 <span id="pageInfo">0-0 / 0</span> 筆
</div>
<nav>
<ul class="pagination mb-0" id="pagination">
<li class="page-item disabled"><a class="page-link" href="#">上一頁</a></li>
<li class="page-item active"><a class="page-link" href="#">1</a></li>
<li class="page-item disabled"><a class="page-link" href="#">下一頁</a></li>
</ul>
</nav>
</div>
</div>
<!-- 編輯 Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-edit me-2"></i>編輯缺貨資料</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="editId">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">廠商代碼</label>
<input type="text" class="form-control" id="editVendorCode" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">廠商名稱</label>
<input type="text" class="form-control" id="editVendorName" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">商品ID</label>
<input type="text" class="form-control" id="editProductCode" readonly>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">商品名稱</label>
<input type="text" class="form-control" id="editProductName">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">缺貨天數</label>
<input type="number" class="form-control" id="editStockoutDays">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">日均銷量</label>
<input type="number" step="0.01" class="form-control" id="editDailyAvgSales">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">可賣量</label>
<input type="number" class="form-control" id="editCurrentStock">
</div>
<div class="col-md-12 mb-3">
<label class="form-label">備註</label>
<textarea class="form-control" id="editNotes" rows="3"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-success" id="saveEdit">
<i class="fas fa-save me-2"></i>儲存
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentPage = 1;
let pageSize = 50;
let totalCount = 0;
let allData = [];
let selectedIds = new Set();
let editModal;
// 取得 CSRF token
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
editModal = new bootstrap.Modal(document.getElementById('editModal'));
await loadBatches();
await loadData();
bindEvents();
});
// 載入批次清單
async function loadBatches() {
try {
const response = await fetch('/vendor-stockout/api/stockout/batches');
const result = await response.json();
if (result.success) {
const select = document.getElementById('filterBatch');
result.data.forEach(batch => {
const option = document.createElement('option');
option.value = batch.batch_number;
option.textContent = `${batch.batch_number} (${batch.count} 筆)`;
select.appendChild(option);
});
}
} catch (error) {
console.error('載入批次失敗:', error);
}
}
// 載入資料
async function loadData() {
try {
// 建立查詢參數
const params = new URLSearchParams({
page: currentPage,
page_size: pageSize
});
const batch = document.getElementById('filterBatch').value;
const vendor = document.getElementById('filterVendor').value;
const status = document.getElementById('filterStatus').value;
const sortBy = document.getElementById('sortBy').value;
if (batch) params.append('batch_number', batch);
if (vendor) params.append('vendor', vendor);
if (status) params.append('status', status);
if (sortBy) params.append('sort_by', sortBy);
const response = await fetch(`/vendor-stockout/api/stockout/list?${params}`);
const result = await response.json();
if (result.success) {
allData = result.data.records;
totalCount = result.data.total;
renderTable(allData);
updateStats(result.data.stats);
updatePagination();
}
} catch (error) {
console.error('載入資料失敗:', error);
document.getElementById('dataTable').innerHTML = `
<tr>
<td colspan="12" class="text-center py-5 text-danger">
<i class="fas fa-exclamation-circle fa-2x mb-3"></i>
<p>載入失敗: ${error.message}</p>
</td>
</tr>
`;
}
}
// 渲染表格
function renderTable(data) {
const tbody = document.getElementById('dataTable');
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="12" class="text-center py-5">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">暫無缺貨資料</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = data.map(record => `
<tr>
<td>
<input type="checkbox" class="form-check-input row-checkbox" value="${record.id}"
${selectedIds.has(record.id) ? 'checked' : ''}>
</td>
<td><span class="badge bg-info">${record.batch_number || '-'}</span></td>
<td><strong>${record.vendor_code}</strong></td>
<td>${record.vendor_name}</td>
<td><code>${record.product_code}</code></td>
<td>${record.product_name || '-'}</td>
<td><span class="badge bg-danger">${record.stockout_days || 0} 天</span></td>
<td>${record.daily_avg_sales ? record.daily_avg_sales.toFixed(1) : '-'}</td>
<td>${record.current_stock || 0}</td>
<td>${getStatusBadge(record.send_status)}</td>
<td><small>${formatDateTime(record.created_at)}</small></td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="editRecord(${record.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteRecord(${record.id})">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
// 綁定勾選事件
document.querySelectorAll('.row-checkbox').forEach(cb => {
cb.addEventListener('change', handleRowSelect);
});
}
// 更新統計
function updateStats(stats) {
document.getElementById('totalCount').textContent = stats.total || 0;
document.getElementById('pendingCount').textContent = stats.pending || 0;
document.getElementById('sentCount').textContent = stats.sent || 0;
document.getElementById('vendorCount').textContent = stats.vendor_count || 0;
}
// 更新分頁
function updatePagination() {
const totalPages = Math.ceil(totalCount / pageSize);
const start = (currentPage - 1) * pageSize + 1;
const end = Math.min(currentPage * pageSize, totalCount);
document.getElementById('pageInfo').textContent = `${start}-${end} / ${totalCount}`;
const pagination = document.getElementById('pagination');
let html = '';
// 上一頁
html += `<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage - 1}); return false;">上一頁</a>
</li>`;
// 頁碼
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
html += `<li class="page-item ${i === currentPage ? 'active' : ''}">
<a class="page-link" href="#" onclick="changePage(${i}); return false;">${i}</a>
</li>`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
html += `<li class="page-item disabled"><a class="page-link">...</a></li>`;
}
}
// 下一頁
html += `<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage + 1}); return false;">下一頁</a>
</li>`;
pagination.innerHTML = html;
}
// 切換頁面
function changePage(page) {
const totalPages = Math.ceil(totalCount / pageSize);
if (page < 1 || page > totalPages) return;
currentPage = page;
loadData();
}
// 綁定事件
function bindEvents() {
// 套用篩選
document.getElementById('applyFilter').addEventListener('click', () => {
currentPage = 1;
loadData();
});
// 全選
document.getElementById('selectAll').addEventListener('change', (e) => {
const checked = e.target.checked;
document.querySelectorAll('.row-checkbox').forEach(cb => {
cb.checked = checked;
if (checked) {
selectedIds.add(parseInt(cb.value));
} else {
selectedIds.delete(parseInt(cb.value));
}
});
updateBatchButtons();
});
// 批次刪除
document.getElementById('batchDelete').addEventListener('click', batchDelete);
// 批次標記已發送
document.getElementById('batchMarkSent').addEventListener('click', batchMarkSent);
// 儲存編輯
document.getElementById('saveEdit').addEventListener('click', saveEdit);
}
// 處理行勾選
function handleRowSelect(e) {
const id = parseInt(e.target.value);
if (e.target.checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
document.getElementById('selectAll').checked = false;
}
updateBatchButtons();
}
// 更新批次按鈕狀態
function updateBatchButtons() {
const count = selectedIds.size;
document.getElementById('selectedCount').textContent = count;
document.getElementById('batchDelete').disabled = count === 0;
document.getElementById('batchMarkSent').disabled = count === 0;
}
// 編輯記錄
async function editRecord(id) {
const record = allData.find(r => r.id === id);
if (!record) return;
document.getElementById('editId').value = record.id;
document.getElementById('editVendorCode').value = record.vendor_code;
document.getElementById('editVendorName').value = record.vendor_name;
document.getElementById('editProductCode').value = record.product_code;
document.getElementById('editProductName').value = record.product_name || '';
document.getElementById('editStockoutDays').value = record.stockout_days || 0;
document.getElementById('editDailyAvgSales').value = record.daily_avg_sales || 0;
document.getElementById('editCurrentStock').value = record.current_stock || 0;
document.getElementById('editNotes').value = record.notes || '';
editModal.show();
}
// 儲存編輯
async function saveEdit() {
const id = document.getElementById('editId').value;
const data = {
product_name: document.getElementById('editProductName').value,
stockout_days: parseInt(document.getElementById('editStockoutDays').value) || 0,
daily_avg_sales: parseFloat(document.getElementById('editDailyAvgSales').value) || 0,
current_stock: parseInt(document.getElementById('editCurrentStock').value) || 0,
notes: document.getElementById('editNotes').value
};
try {
const response = await fetch(`/vendor-stockout/api/stockout/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
editModal.hide();
await loadData();
alert('更新成功!');
} else {
alert('更新失敗: ' + result.message);
}
} catch (error) {
alert('更新失敗: ' + error.message);
}
}
// 刪除記錄
async function deleteRecord(id) {
if (!confirm('確定要刪除這筆記錄嗎?')) return;
try {
const response = await fetch(`/vendor-stockout/api/stockout/${id}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': csrfToken
}
});
const result = await response.json();
if (result.success) {
await loadData();
alert('刪除成功!');
} else {
alert('刪除失敗: ' + result.message);
}
} catch (error) {
alert('刪除失敗: ' + error.message);
}
}
// 批次刪除
async function batchDelete() {
if (selectedIds.size === 0) return;
if (!confirm(`確定要刪除選取的 ${selectedIds.size} 筆記錄嗎?`)) return;
try {
const response = await fetch('/vendor-stockout/api/stockout/batch/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
ids: Array.from(selectedIds)
})
});
const result = await response.json();
if (result.success) {
selectedIds.clear();
await loadData();
alert(`成功刪除 ${result.data.deleted_count} 筆記錄!`);
} else {
alert('刪除失敗: ' + result.message);
}
} catch (error) {
alert('刪除失敗: ' + error.message);
}
}
// 批次標記已發送
async function batchMarkSent() {
if (selectedIds.size === 0) return;
if (!confirm(`確定要將選取的 ${selectedIds.size} 筆記錄標記為已發送嗎?`)) return;
try {
const response = await fetch('/vendor-stockout/api/stockout/batch/mark-sent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
ids: Array.from(selectedIds)
})
});
const result = await response.json();
if (result.success) {
selectedIds.clear();
await loadData();
alert(`成功更新 ${result.data.updated_count} 筆記錄!`);
} else {
alert('更新失敗: ' + result.message);
}
} catch (error) {
alert('更新失敗: ' + error.message);
}
}
// 工具函數
function getStatusBadge(status) {
const badges = {
'pending': '<span class="badge status-pending"><i class="fas fa-clock me-1"></i>待發送</span>',
'sent': '<span class="badge status-sent"><i class="fas fa-check me-1"></i>已發送</span>',
'failed': '<span class="badge status-failed"><i class="fas fa-times me-1"></i>失敗</span>'
};
return badges[status] || badges['pending'];
}
function formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
</body>
</html>