Some checks failed
CD Pipeline / deploy (push) Failing after 59s
- 建立 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>
691 lines
28 KiB
Plaintext
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"> </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>
|