Files
ewoooc/web/templates/vendor_stockout/list.html
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

1794 lines
75 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;
padding-top: 70px;
}
/* Custom Dark Gray Navbar */
.navbar.bg-custom-dark {
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.navbar.bg-custom-dark .navbar-brand {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.85);
font-weight: 500;
}
.navbar.bg-custom-dark .navbar-nav .nav-link:hover {
color: #ffffff;
}
.navbar.bg-custom-dark .navbar-nav .nav-link.active {
color: #ffffff;
font-weight: 600;
}
.navbar.bg-custom-dark .navbar-text {
color: rgba(255, 255, 255, 0.75);
}
/* 主要標籤樣式 */
.main-tabs {
background: white;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
margin-bottom: 1.5rem;
overflow: hidden;
}
.main-tabs .nav-tabs {
border-bottom: none;
}
.main-tabs .nav-link {
border: none;
border-radius: 0;
padding: 1rem 2rem;
font-weight: 600;
color: #6c757d;
transition: all 0.3s;
position: relative;
}
.main-tabs .nav-link.active {
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
color: white;
}
.main-tabs .nav-link:hover:not(.active) {
background: #f8f9fa;
}
.main-tabs .nav-link .badge {
position: absolute;
top: 5px;
right: 5px;
font-size: 0.7rem;
}
/* 狀態子標籤 */
.status-tabs {
background: #f8f9fa;
border-radius: 12px;
padding: 0.5rem;
margin-bottom: 1.5rem;
}
.status-tabs .nav-link {
border: none;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-weight: 500;
color: #6c757d;
transition: all 0.3s;
margin-right: 0.5rem;
}
.status-tabs .nav-link.active {
background: white;
color: #2c3e50;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.status-tabs .nav-link.tab-pending.active {
border-left: 3px solid #f39c12;
}
.status-tabs .nav-link.tab-sent.active {
border-left: 3px solid #27ae60;
}
.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 thead th.th-pending {
background: linear-gradient(135deg, #f39c12 0%, #f1c40f 100%);
}
.table thead th.th-sent {
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
}
.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;
border-top: 4px solid #3498db;
}
.stats-card:hover {
transform: translateY(-5px);
}
.stats-card.card-total { border-top-color: #3498db; }
.stats-card.card-pending { border-top-color: #f39c12; }
.stats-card.card-sent { border-top-color: #27ae60; }
.stats-card.card-vendor { border-top-color: #9b59b6; }
.stats-number {
font-size: 2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.stats-label {
color: #6c757d;
font-size: 0.9rem;
}
.upload-area {
border: 3px dashed #3498db;
border-radius: 20px;
padding: 3rem;
text-align: center;
background: white;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #2980b9;
background: #f8f9fa;
transform: scale(1.02);
}
.upload-area.dragover {
border-color: #27ae60;
background: #d5f4e6;
}
.upload-icon {
font-size: 5rem;
color: #3498db;
margin-bottom: 1rem;
}
.btn-upload {
padding: 1rem 3rem;
font-size: 1.1rem;
border-radius: 50px;
font-weight: 600;
}
.progress {
height: 30px;
border-radius: 15px;
}
.result-card {
border-left: 5px solid #3498db;
}
/* 流程提示樣式 */
.workflow-guide {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.workflow-guide h5 {
margin-bottom: 1rem;
}
.workflow-steps {
display: flex;
justify-content: space-between;
align-items: center;
}
.workflow-step {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
.workflow-step .step-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.5rem;
font-size: 1.2rem;
}
.workflow-step .step-icon.active {
background: white;
color: #667eea;
}
.workflow-step .step-text {
font-size: 0.85rem;
}
.workflow-arrow {
font-size: 1.5rem;
opacity: 0.5;
}
/* 重送確認 Modal 特殊樣式 */
.resend-warning {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.resend-warning i {
color: #856404;
}
/* 批次操作區塊改進 */
.batch-actions-card {
background: white;
border-radius: 12px;
padding: 1rem 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.batch-actions-card.pending-actions {
border-left: 4px solid #f39c12;
}
.batch-actions-card.sent-actions {
border-left: 4px solid #27ae60;
}
/* 使用者下拉選單 */
.user-dropdown .dropdown-toggle::after {
display: none;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
/* 空狀態提示 */
.empty-state {
padding: 4rem 2rem;
text-align: center;
}
.empty-state i {
font-size: 4rem;
color: #dee2e6;
margin-bottom: 1rem;
}
.empty-state h5 {
color: #6c757d;
margin-bottom: 0.5rem;
}
.empty-state p {
color: #adb5bd;
}
</style>
</head>
<body>
<!-- 導航列 (使用共用組件) -->
{% include 'components/_navbar.html' %}
<div class="container-fluid" style="padding: 2rem;">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="fas fa-box-open me-2" style="color: #27ae60;"></i>缺貨管理
</h2>
<a href="/vendor-stockout" class="btn btn-outline-secondary">
<i class="fas fa-home me-2"></i>返回主頁
</a>
</div>
<!-- 流程提示 -->
<div class="workflow-guide" id="workflowGuide">
<h5><i class="fas fa-route me-2"></i>操作流程</h5>
<div class="workflow-steps">
<div class="workflow-step">
<div class="step-icon" id="step1Icon"><i class="fas fa-file-upload"></i></div>
<div class="step-text">1. 匯入 Excel</div>
</div>
<div class="workflow-arrow"><i class="fas fa-chevron-right"></i></div>
<div class="workflow-step">
<div class="step-icon" id="step2Icon"><i class="fas fa-check-square"></i></div>
<div class="step-text">2. 勾選待發送</div>
</div>
<div class="workflow-arrow"><i class="fas fa-chevron-right"></i></div>
<div class="workflow-step">
<div class="step-icon" id="step3Icon"><i class="fas fa-paper-plane"></i></div>
<div class="step-text">3. 發送郵件</div>
</div>
<div class="workflow-arrow"><i class="fas fa-chevron-right"></i></div>
<div class="workflow-step">
<div class="step-icon" id="step4Icon"><i class="fas fa-history"></i></div>
<div class="step-text">4. 查看記錄</div>
</div>
</div>
</div>
<!-- 主分頁標籤 -->
<div class="main-tabs">
<ul class="nav nav-tabs" id="mainTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="list-tab" data-bs-toggle="tab" data-bs-target="#list" type="button">
<i class="fas fa-list me-2"></i>缺貨清單
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="import-tab" data-bs-toggle="tab" data-bs-target="#import" type="button">
<i class="fas fa-file-upload me-2"></i>Excel 匯入
</button>
</li>
</ul>
</div>
<div class="tab-content" id="mainTabContent">
<!-- 缺貨清單 Tab -->
<div class="tab-pane fade show active" id="list" role="tabpanel">
<!-- 統計卡片 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="stats-card card-total">
<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 card-pending">
<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 card-sent">
<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 card-vendor">
<div class="stats-number text-purple" id="vendorCount" style="color: #9b59b6;">0</div>
<div class="stats-label">涉及廠商數</div>
</div>
</div>
</div>
<!-- 狀態子標籤(待發送/已發送) -->
<div class="status-tabs">
<ul class="nav" id="statusTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link tab-pending active" id="pending-tab" data-status="pending" type="button">
<i class="fas fa-clock me-2"></i>待發送
<span class="badge bg-warning text-dark ms-2" id="pendingBadge">0</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link tab-sent" id="sent-tab" data-status="sent" type="button">
<i class="fas fa-check-circle me-2"></i>已發送
<span class="badge bg-success ms-2" id="sentBadge">0</span>
</button>
</li>
</ul>
</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-3">
<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-3">
<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="batch-actions-card pending-actions" id="pendingActions">
<div class="d-flex justify-content-between align-items-center">
<div>
<input type="checkbox" id="selectAllPending" class="form-check-input me-2">
<label for="selectAllPending" class="form-check-label">
全選待發送 (<span id="selectedPendingCount">0</span> 筆)
</label>
</div>
<div>
<button class="btn btn-outline-secondary btn-action me-2" onclick="openEmailTemplateModal()">
<i class="fas fa-cog me-1"></i>郵件模板
</button>
<button class="btn btn-primary btn-action me-2" id="batchSendGrouped" disabled>
<i class="fas fa-paper-plane me-1"></i>按廠商發送
</button>
<button class="btn btn-info btn-action me-2" id="batchSendSingle" disabled>
<i class="fas fa-envelope me-1"></i>按商品發送
</button>
<button class="btn btn-outline-success btn-action me-2" id="batchMarkSent" disabled>
<i class="fas fa-check me-1"></i>標記已發送
</button>
<button class="btn btn-outline-danger btn-action" id="batchDeletePending" disabled>
<i class="fas fa-trash me-1"></i>刪除
</button>
</div>
</div>
</div>
<!-- 已發送批次操作(預設隱藏) -->
<div class="batch-actions-card sent-actions" id="sentActions" style="display: none;">
<div class="d-flex justify-content-between align-items-center">
<div>
<input type="checkbox" id="selectAllSent" class="form-check-input me-2">
<label for="selectAllSent" class="form-check-label">
全選已發送 (<span id="selectedSentCount">0</span> 筆)
</label>
</div>
<div>
<button class="btn btn-warning btn-action me-2" id="batchResend" disabled>
<i class="fas fa-redo me-1"></i>重新發送
</button>
<button class="btn btn-outline-warning btn-action me-2" id="batchMarkPending" disabled>
<i class="fas fa-undo me-1"></i>重設為待發送
</button>
<button class="btn btn-outline-danger btn-action" id="batchDeleteSent" disabled>
<i class="fas fa-trash me-1"></i>刪除
</button>
</div>
</div>
</div>
<!-- 資料表格 -->
<div class="table-container">
<div class="table-responsive">
<table class="table table-hover">
<thead id="tableHead">
<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>
<!-- Excel 匯入 Tab -->
<div class="tab-pane fade" id="import" role="tabpanel">
<div class="row justify-content-center">
<div class="col-md-10">
<!-- 頁面說明 -->
<div class="card mb-4" style="border-radius: 16px;">
<div class="card-body">
<h4 class="mb-3">
<i class="fas fa-file-excel text-primary me-3"></i>Excel 匯入
</h4>
<p class="text-muted mb-0">上傳包含缺貨商品資料的 Excel 檔案,系統會自動檢測並匯入</p>
</div>
</div>
<!-- 上傳區域 -->
<div class="card mb-4" style="border-radius: 16px;">
<div class="card-body">
<div class="upload-area" id="uploadArea">
<input type="file" id="fileInput" accept=".xlsx,.xls" style="display: none;">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h4 class="mb-3">拖曳檔案到此處或點擊選擇檔案</h4>
<p class="text-muted">支援格式: Excel (.xlsx, .xls)</p>
<button type="button" class="btn btn-primary btn-upload mt-3" id="selectFileBtn">
<i class="fas fa-folder-open me-2"></i>選擇檔案
</button>
</div>
<!-- 檔案資訊 -->
<div id="fileInfo" class="mt-4" style="display: none;">
<div class="alert alert-info">
<i class="fas fa-file me-2"></i>
<strong>已選擇檔案:</strong> <span id="fileName"></span>
<span class="badge bg-primary ms-2" id="fileSize"></span>
</div>
<button type="button" class="btn btn-success w-100 btn-upload" id="uploadBtn">
<i class="fas fa-upload me-2"></i>開始匯入
</button>
</div>
<!-- 進度條 -->
<div id="progressArea" class="mt-4" style="display: none;">
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 100%">
匯入中...
</div>
</div>
</div>
</div>
</div>
<!-- 匯入結果 -->
<div id="resultArea" style="display: none;">
<div class="card result-card" style="border-radius: 16px;">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-check-circle text-success me-2"></i>匯入完成
</h5>
<div class="row mt-4">
<div class="col-md-3">
<div class="text-center">
<h3 class="text-primary" id="importTotalCount">0</h3>
<small class="text-muted">總筆數</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h3 class="text-success" id="importSuccessCount">0</h3>
<small class="text-muted">成功匯入</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h3 class="text-warning" id="importDuplicateCount">0</h3>
<small class="text-muted">重複項目</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h3 class="text-danger" id="importFailedCount">0</h3>
<small class="text-muted">失敗</small>
</div>
</div>
</div>
<div class="mt-4">
<p class="mb-2">
<i class="fas fa-barcode me-2"></i>
<strong>批次編號:</strong> <code id="batchId"></code>
</p>
</div>
<div class="mt-4 text-center">
<button type="button" class="btn btn-primary me-2" onclick="switchToListTab()">
<i class="fas fa-list me-2"></i>查看缺貨清單
</button>
<button type="button" class="btn btn-outline-secondary" id="importAgainBtn">
<i class="fas fa-redo me-2"></i>再次匯入
</button>
</div>
</div>
</div>
</div>
<!-- 錯誤訊息 -->
<div id="errorArea" style="display: none;">
<div class="alert alert-danger">
<h5><i class="fas fa-exclamation-circle me-2"></i>匯入失敗</h5>
<p class="mb-0" id="errorMessage"></p>
</div>
</div>
</div>
</div>
</div>
</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>
<!-- 郵件模板設定 Modal -->
<div class="modal fade" id="emailTemplateModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title"><i class="fas fa-cog me-2"></i>郵件模板設定</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- 說明 -->
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
設定郵件模板內容,之後所有廠商缺貨通知郵件都會套用此模板。
</div>
<!-- 可用變數提示 -->
<div class="mb-3 p-3 bg-light rounded">
<div class="mb-2">
<i class="fas fa-lightbulb text-warning me-1"></i>
<strong>可用變數(會自動替換為實際值):</strong>
</div>
<div class="row">
<div class="col-md-4">
<code class="text-primary">{{vendor_name}}</code>
<small class="text-muted ms-1">廠商名稱</small>
</div>
<div class="col-md-4">
<code class="text-primary">{{vendor_code}}</code>
<small class="text-muted ms-1">廠商代碼</small>
</div>
<div class="col-md-4">
<code class="text-primary">{{item_count}}</code>
<small class="text-muted ms-1">缺貨商品數量</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<!-- 郵件主旨 -->
<div class="mb-3">
<label class="form-label fw-bold">
<i class="fas fa-heading me-1"></i>郵件主旨
</label>
<input type="text" class="form-control" id="emailSubjectInput"
oninput="refreshEmailPreview()">
</div>
<!-- 郵件內容 -->
<div class="mb-3">
<label class="form-label fw-bold">
<i class="fas fa-file-alt me-1"></i>郵件內容 (HTML)
</label>
<textarea class="form-control" id="emailBodyInput" rows="12"
oninput="refreshEmailPreview()"></textarea>
<div class="form-text">支援 HTML 格式標籤,如 &lt;p&gt;&lt;br&gt;&lt;b&gt;</div>
</div>
</div>
<div class="col-md-6">
<!-- 預覽區 -->
<div class="mb-3">
<label class="form-label fw-bold">
<i class="fas fa-eye me-1"></i>郵件預覽(範例)
</label>
<div class="border rounded p-3 bg-white" style="min-height: 350px; max-height: 400px; overflow-y: auto;">
<div class="mb-2 pb-2 border-bottom">
<strong>主旨:</strong>
<span id="emailPreviewSubject" class="text-primary"></span>
</div>
<div id="emailPreviewBody"></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-warning" onclick="resetEmailTemplate()">
<i class="fas fa-undo me-2"></i>恢復預設
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveEmailTemplate()">
<i class="fas fa-save me-2"></i>儲存模板
</button>
</div>
</div>
</div>
</div>
<!-- 重新發送確認 Modal -->
<div class="modal fade" id="resendConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title"><i class="fas fa-exclamation-triangle me-2"></i>確認重新發送</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="resend-warning">
<i class="fas fa-info-circle me-2"></i>
<strong>注意:</strong>這些項目已經發送過郵件通知給廠商。
</div>
<p>您確定要重新發送嗎?</p>
<ul class="mb-3">
<li>選取的 <strong id="resendCount">0</strong> 筆記錄將會重新發送郵件</li>
<li>廠商將會收到重複的通知</li>
</ul>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmResend">
<label class="form-check-label" for="confirmResend">
我了解並確認要重新發送
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="confirmResendBtn" disabled>
<i class="fas fa-paper-plane 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>
// ====== Global Variables ======
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
let currentPage = 1;
let pageSize = 50;
let totalCount = 0;
let allData = [];
let selectedIds = new Set();
let editModal;
let emailTemplateModal;
let resendConfirmModal;
let selectedFile = null;
let currentStatus = 'pending'; // 當前檢視的狀態pending 或 sent
// ====== Initialize ======
document.addEventListener('DOMContentLoaded', async () => {
editModal = new bootstrap.Modal(document.getElementById('editModal'));
emailTemplateModal = new bootstrap.Modal(document.getElementById('emailTemplateModal'));
resendConfirmModal = new bootstrap.Modal(document.getElementById('resendConfirmModal'));
await loadBatches();
await loadData();
bindEvents();
updateWorkflowGuide();
});
// ====== Bind Events ======
function bindEvents() {
// 狀態標籤切換
document.querySelectorAll('#statusTabs .nav-link').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll('#statusTabs .nav-link').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
currentStatus = e.target.dataset.status;
currentPage = 1;
selectedIds.clear();
loadData();
updateUIForStatus();
});
});
// 篩選與全選
document.getElementById('applyFilter').addEventListener('click', () => {
currentPage = 1;
loadData();
});
// 待發送全選
document.getElementById('selectAllPending').addEventListener('change', (e) => {
handleSelectAll(e.target.checked);
});
// 已發送全選
document.getElementById('selectAllSent').addEventListener('change', (e) => {
handleSelectAll(e.target.checked);
});
// 批次操作按鈕
document.getElementById('batchDeletePending').addEventListener('click', batchDelete);
document.getElementById('batchDeleteSent').addEventListener('click', batchDelete);
document.getElementById('batchMarkSent').addEventListener('click', batchMarkSent);
document.getElementById('batchMarkPending').addEventListener('click', batchMarkPending);
document.getElementById('batchSendGrouped').addEventListener('click', batchSendGrouped);
document.getElementById('batchSendSingle').addEventListener('click', batchSendSingle);
document.getElementById('batchResend').addEventListener('click', openResendConfirm);
document.getElementById('saveEdit').addEventListener('click', saveEdit);
// 重送確認
document.getElementById('confirmResend').addEventListener('change', (e) => {
document.getElementById('confirmResendBtn').disabled = !e.target.checked;
});
document.getElementById('confirmResendBtn').addEventListener('click', executeResend);
// Excel 匯入相關
bindImportEvents();
}
// 更新 UI 根據當前狀態
function updateUIForStatus() {
const isPending = currentStatus === 'pending';
// 顯示/隱藏對應的操作區塊
document.getElementById('pendingActions').style.display = isPending ? 'block' : 'none';
document.getElementById('sentActions').style.display = isPending ? 'none' : 'block';
// 更新表頭顏色
const headers = document.querySelectorAll('#tableHead th');
headers.forEach(th => {
th.classList.remove('th-pending', 'th-sent');
th.classList.add(isPending ? 'th-pending' : 'th-sent');
});
// 重置選取
document.getElementById('selectAllPending').checked = false;
document.getElementById('selectAllSent').checked = false;
updateBatchButtons();
}
// 全選處理
function handleSelectAll(checked) {
document.querySelectorAll('.row-checkbox').forEach(cb => {
cb.checked = checked;
if (checked) {
selectedIds.add(parseInt(cb.value));
} else {
selectedIds.delete(parseInt(cb.value));
}
});
updateBatchButtons();
}
// ====== 缺貨清單功能 ======
// 載入批次清單
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,
status: currentStatus
});
const batch = document.getElementById('filterBatch').value;
const vendor = document.getElementById('filterVendor').value;
const sortBy = document.getElementById('sortBy').value;
if (batch) params.append('batch_number', batch);
if (vendor) params.append('vendor', vendor);
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();
updateUIForStatus();
}
} 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');
const isPending = currentStatus === 'pending';
if (data.length === 0) {
const emptyMessage = isPending
? '暫無待發送的缺貨資料<br><small class="text-muted">請先匯入 Excel 檔案或從「已發送」重設項目</small>'
: '暫無已發送的記錄<br><small class="text-muted">發送郵件後會顯示在此處</small>';
tbody.innerHTML = `
<tr>
<td colspan="12">
<div class="empty-state">
<i class="fas ${isPending ? 'fa-inbox' : 'fa-paper-plane'}"></i>
<h5>${emptyMessage}</h5>
</div>
</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;
// 更新標籤上的徽章
document.getElementById('pendingBadge').textContent = stats.pending || 0;
document.getElementById('sentBadge').textContent = stats.sent || 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 = totalCount > 0 ? `${start}-${end} / ${totalCount}` : '0 筆';
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 || totalPages === 0 ? '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 handleRowSelect(e) {
const id = parseInt(e.target.value);
if (e.target.checked) {
selectedIds.add(id);
} else {
selectedIds.delete(id);
// 取消全選勾選
if (currentStatus === 'pending') {
document.getElementById('selectAllPending').checked = false;
} else {
document.getElementById('selectAllSent').checked = false;
}
}
updateBatchButtons();
}
// 更新批次按鈕狀態
function updateBatchButtons() {
const count = selectedIds.size;
if (currentStatus === 'pending') {
document.getElementById('selectedPendingCount').textContent = count;
document.getElementById('batchDeletePending').disabled = count === 0;
document.getElementById('batchMarkSent').disabled = count === 0;
document.getElementById('batchSendGrouped').disabled = count === 0;
document.getElementById('batchSendSingle').disabled = count === 0;
} else {
document.getElementById('selectedSentCount').textContent = count;
document.getElementById('batchDeleteSent').disabled = count === 0;
document.getElementById('batchMarkPending').disabled = count === 0;
document.getElementById('batchResend').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} 筆記錄標記為已發送嗎?\n(這不會發送實際郵件)`)) 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);
}
}
// 批次標記為待發送(從已發送重設)
async function batchMarkPending() {
if (selectedIds.size === 0) return;
if (!confirm(`確定要將選取的 ${selectedIds.size} 筆記錄重設為「待發送」狀態嗎?`)) return;
try {
const response = await fetch('/vendor-stockout/api/stockout/batch/mark-pending', {
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 openResendConfirm() {
if (selectedIds.size === 0) return;
document.getElementById('resendCount').textContent = selectedIds.size;
document.getElementById('confirmResend').checked = false;
document.getElementById('confirmResendBtn').disabled = true;
resendConfirmModal.show();
}
// 執行重新發送
async function executeResend() {
resendConfirmModal.hide();
await batchSendGrouped();
}
// 按廠商分組發送郵件
async function batchSendGrouped() {
if (selectedIds.size === 0) return;
const vendorCount = new Set(allData.filter(r => selectedIds.has(r.id)).map(r => r.vendor_code)).size;
// 如果是從已發送重送,不需要再次確認(已經在 Modal 確認過)
if (currentStatus === 'pending') {
if (!confirm(`確定要按廠商分組發送嗎?\n\n將發送給 ${vendorCount} 個廠商\n選取的 ${selectedIds.size} 個商品將按廠商整合為一封郵件`)) return;
}
try {
const response = await fetch('/vendor-stockout/api/email/send/grouped', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
stockout_ids: Array.from(selectedIds)
})
});
const result = await response.json();
if (result.success) {
selectedIds.clear();
await loadData();
// 顯示詳細結果
let message = result.message + '\n\n';
if (result.data.success_vendors && result.data.success_vendors.length > 0) {
message += '✅ 成功發送:\n';
result.data.success_vendors.forEach(v => {
message += ` - ${v.vendor_name} (${v.vendor_code}): ${v.item_count} 個商品\n`;
});
}
if (result.data.failed_vendors && result.data.failed_vendors.length > 0) {
message += '\n❌ 發送失敗:\n';
result.data.failed_vendors.forEach(v => {
message += ` - ${v.vendor_name} (${v.vendor_code}): ${v.reason}\n`;
});
}
alert(message);
updateWorkflowGuide();
} else {
alert('發送失敗: ' + result.message);
}
} catch (error) {
alert('發送失敗: ' + error.message);
}
}
// 按商品單筆發送郵件
async function batchSendSingle() {
if (selectedIds.size === 0) return;
if (!confirm(`確定要按商品單筆發送嗎?\n\n將為 ${selectedIds.size} 個商品各發送一封獨立郵件`)) return;
try {
const response = await fetch('/vendor-stockout/api/email/send/single', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
stockout_ids: Array.from(selectedIds)
})
});
const result = await response.json();
if (result.success) {
selectedIds.clear();
await loadData();
// 顯示詳細結果
let message = result.message + '\n\n';
if (result.data.success_items && result.data.success_items.length > 0) {
message += `✅ 成功發送 ${result.data.success_items.length} 封郵件\n`;
}
if (result.data.failed_items && result.data.failed_items.length > 0) {
message += '\n❌ 發送失敗:\n';
result.data.failed_items.forEach(item => {
message += ` - ${item.product_name} (${item.product_code}): ${item.reason}\n`;
});
}
alert(message);
updateWorkflowGuide();
} else {
alert('發送失敗: ' + result.message);
}
} catch (error) {
alert('發送失敗: ' + error.message);
}
}
// 更新流程提示
function updateWorkflowGuide() {
// 根據目前狀態更新流程圖標
const pendingCount = parseInt(document.getElementById('pendingCount').textContent) || 0;
const sentCount = parseInt(document.getElementById('sentCount').textContent) || 0;
// Step 1: 匯入
document.getElementById('step1Icon').classList.toggle('active', true);
// Step 2: 勾選
document.getElementById('step2Icon').classList.toggle('active', pendingCount > 0);
// Step 3: 發送
document.getElementById('step3Icon').classList.toggle('active', sentCount > 0);
// Step 4: 查看記錄
document.getElementById('step4Icon').classList.toggle('active', sentCount > 0);
}
// ====== Excel 匯入功能 ======
function bindImportEvents() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const selectFileBtn = document.getElementById('selectFileBtn');
uploadArea.addEventListener('click', () => fileInput.click());
selectFileBtn.addEventListener('click', (e) => {
e.stopPropagation();
fileInput.click();
});
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
handleFile(e.dataTransfer.files[0]);
});
document.getElementById('uploadBtn').addEventListener('click', uploadExcel);
document.getElementById('importAgainBtn').addEventListener('click', resetImport);
}
function handleFile(file) {
if (!file) return;
if (!file.name.match(/\.(xlsx|xls)$/i)) {
alert('請選擇 Excel 檔案 (.xlsx 或 .xls)');
return;
}
selectedFile = file;
document.getElementById('fileName').textContent = file.name;
document.getElementById('fileSize').textContent = formatFileSize(file.size);
document.getElementById('fileInfo').style.display = 'block';
document.getElementById('resultArea').style.display = 'none';
document.getElementById('errorArea').style.display = 'none';
}
async function uploadExcel() {
if (!selectedFile) return;
const formData = new FormData();
formData.append('file', selectedFile);
document.getElementById('fileInfo').style.display = 'none';
document.getElementById('progressArea').style.display = 'block';
document.getElementById('errorArea').style.display = 'none';
document.getElementById('resultArea').style.display = 'none';
try {
const response = await fetch('/vendor-stockout/api/import/excel', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
},
body: formData
});
const result = await response.json();
document.getElementById('progressArea').style.display = 'none';
if (result.success) {
// 顯示成功結果
document.getElementById('importTotalCount').textContent = result.data.total_count;
document.getElementById('importSuccessCount').textContent = result.data.success_count;
document.getElementById('importDuplicateCount').textContent = result.data.duplicate_count;
document.getElementById('importFailedCount').textContent = result.data.failed_count;
document.getElementById('batchId').textContent = result.data.batch_id;
document.getElementById('resultArea').style.display = 'block';
} else {
// 顯示錯誤
document.getElementById('errorMessage').textContent = result.message;
document.getElementById('errorArea').style.display = 'block';
}
} catch (error) {
document.getElementById('progressArea').style.display = 'none';
document.getElementById('errorMessage').textContent = '網路錯誤: ' + error.message;
document.getElementById('errorArea').style.display = 'block';
}
}
function resetImport() {
selectedFile = null;
document.getElementById('fileInput').value = '';
document.getElementById('fileInfo').style.display = 'none';
document.getElementById('resultArea').style.display = 'none';
document.getElementById('errorArea').style.display = 'none';
}
function switchToListTab() {
const listTab = new bootstrap.Tab(document.getElementById('list-tab'));
listTab.show();
loadData();
}
// ====== 工具函數 ======
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'
});
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// ====== 郵件模板設定功能 ======
// 開啟郵件模板設定 Modal
async function openEmailTemplateModal() {
await loadEmailTemplate();
emailTemplateModal.show();
}
// 載入郵件模板
async function loadEmailTemplate() {
try {
const response = await fetch('/vendor-stockout/api/email/template');
const result = await response.json();
if (result.success) {
document.getElementById('emailSubjectInput').value = result.data.subject || '';
document.getElementById('emailBodyInput').value = result.data.body || '';
refreshEmailPreview();
} else {
alert('載入模板失敗: ' + result.message);
}
} catch (error) {
alert('載入模板失敗: ' + error.message);
}
}
// 儲存郵件模板
async function saveEmailTemplate() {
const subject = document.getElementById('emailSubjectInput').value.trim();
const body = document.getElementById('emailBodyInput').value.trim();
if (!subject || !body) {
alert('請填寫郵件主旨和內容');
return;
}
try {
const response = await fetch('/vendor-stockout/api/email/template', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ subject, body })
});
const result = await response.json();
if (result.success) {
alert('郵件模板已儲存!之後發送的郵件都會套用此模板。');
emailTemplateModal.hide();
} else {
alert('儲存失敗: ' + result.message);
}
} catch (error) {
alert('儲存失敗: ' + error.message);
}
}
// 恢復預設模板
async function resetEmailTemplate() {
if (!confirm('確定要恢復為預設郵件模板嗎?')) return;
try {
const response = await fetch('/vendor-stockout/api/email/template/reset', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
});
const result = await response.json();
if (result.success) {
document.getElementById('emailSubjectInput').value = result.data.subject || '';
document.getElementById('emailBodyInput').value = result.data.body || '';
refreshEmailPreview();
alert('已恢復為預設模板');
} else {
alert('恢復失敗: ' + result.message);
}
} catch (error) {
alert('恢復失敗: ' + error.message);
}
}
// 重新整理郵件預覽
function refreshEmailPreview() {
const subject = document.getElementById('emailSubjectInput').value;
const body = document.getElementById('emailBodyInput').value;
// 範例資料
const sampleData = {
vendor_name: '範例廠商',
vendor_code: 'V001',
item_count: '5'
};
// 替換變數
let previewSubject = subject;
let previewBody = body;
for (const [key, value] of Object.entries(sampleData)) {
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
previewSubject = previewSubject.replace(regex, value);
previewBody = previewBody.replace(regex, value);
}
document.getElementById('emailPreviewSubject').textContent = previewSubject;
document.getElementById('emailPreviewBody').innerHTML = previewBody;
}
</script>
<script>
// 更新導航列時鐘
function updateNavTime() {
const now = new Date();
const elem = document.getElementById('nav-time');
if (elem) {
elem.textContent = now.toLocaleString('zh-TW', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
}
}
updateNavTime();
setInterval(updateNavTime, 1000);
</script>
</body>
</html>