839 lines
39 KiB
HTML
839 lines
39 KiB
HTML
{% extends "ewoooc_base.html" %}
|
||
{% block title %}供應商窗口 · EwoooC{% endblock %}
|
||
{% block extra_css %}
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/page-vendor-tools.css') }}">
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<section class="vendor-tools-page" data-vendor-tool="management">
|
||
<header class="vendor-tools-header">
|
||
<div>
|
||
<span class="vendor-tools-kicker"><i class="fas fa-boxes-stacked"></i>供貨風險</span>
|
||
<h1>供應商窗口</h1>
|
||
<p>維護正確窗口,讓缺貨補救能快速送達。</p>
|
||
</div>
|
||
<div class="vendor-tools-nav" aria-label="廠商缺貨工具導覽">
|
||
<a href="/vendor-stockout/" ><i class="fas fa-table-columns"></i>總覽</a>
|
||
<a href="/vendor-stockout/list" ><i class="fas fa-list-check"></i>缺貨清單</a>
|
||
<a href="/vendor-stockout/import" ><i class="fas fa-file-arrow-up"></i>匯入</a>
|
||
<a href="/vendor-stockout/vendor-management" class="is-active" aria-current="page"><i class="fas fa-building"></i>廠商</a>
|
||
<a href="/vendor-stockout/send-email" ><i class="fas fa-paper-plane"></i>補貨通知</a>
|
||
<a href="/vendor-stockout/history" ><i class="fas fa-clock-rotate-left"></i>歷史</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="vendor-tools-body">
|
||
<div class="container-fluid" style="padding: 2rem;">
|
||
<h2 class="mb-4">
|
||
<i class="fas fa-building me-2" style="color: var(--momo-page-accent-dark);"></i>供應商窗口
|
||
</h2>
|
||
|
||
<!-- 分頁標籤 -->
|
||
<div class="section-tabs">
|
||
<ul class="nav nav-tabs" id="vendorTabs" 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="vendorTabContent">
|
||
<!-- 廠商清單 Tab -->
|
||
<div class="tab-pane fade show active" id="list" role="tabpanel">
|
||
<!-- 統計卡片 -->
|
||
<div class="row mb-4">
|
||
<div class="col-md-4">
|
||
<div class="stats-card">
|
||
<div class="stats-number text-primary" style="font-size: 2rem; font-weight: bold;" id="totalVendors">0</div>
|
||
<div class="stats-label text-muted">供應商數</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="stats-card">
|
||
<div class="stats-number" style="font-size: 2rem; font-weight: bold; color: var(--momo-page-accent-dark);" id="totalEmails">0</div>
|
||
<div class="stats-label text-muted">郵件地址總數</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="stats-card">
|
||
<div class="stats-number text-success" style="font-size: 2rem; font-weight: bold;" id="avgEmails">0</div>
|
||
<div class="stats-label text-muted">平均每廠商郵件數</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 篩選與操作 -->
|
||
<div class="card mb-3" style="border-radius: 12px;">
|
||
<div class="card-body">
|
||
<div class="row align-items-end">
|
||
<div class="col-md-4">
|
||
<label class="form-label">搜尋供應商</label>
|
||
<input type="text" class="form-control" id="searchVendor" placeholder="輸入供應商代碼或名稱">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<button class="btn btn-success" id="searchBtn">
|
||
<i class="fas fa-search me-2"></i>搜尋
|
||
</button>
|
||
<button class="btn btn-outline-secondary" id="resetBtn">
|
||
<i class="fas fa-redo me-2"></i>重置
|
||
</button>
|
||
</div>
|
||
<div class="col-md-5 text-end">
|
||
<button class="btn btn-primary" id="addVendorBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||
<i class="fas fa-plus me-2"></i>新增廠商
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 廠商列表 -->
|
||
<div class="table-container">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 150px;">廠商代碼</th>
|
||
<th>廠商名稱</th>
|
||
<th>郵件地址</th>
|
||
<th style="width: 120px;">郵件數量</th>
|
||
<th style="width: 180px;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="vendorTable">
|
||
<tr>
|
||
<td colspan="5" 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"></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">
|
||
<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,讓缺貨通知送到正確窗口。</p>
|
||
<p class="text-muted small">
|
||
<strong>必要資料:</strong>來源供應商編號、來源供應商名稱<br>
|
||
<strong>選填資料:</strong>Mail(支援多個郵件用逗號或分號分隔)
|
||
</p>
|
||
<button type="button" class="btn btn-primary btn-lg mt-3" id="selectFileBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||
<i class="fas fa-folder-open me-2"></i>選擇檔案
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mt-3 text-center">
|
||
<a href="/vendor-stockout/api/vendor/import/template" class="btn btn-outline-secondary">
|
||
<i class="fas fa-download me-2"></i>下載 Excel 範本
|
||
</a>
|
||
</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-lg" id="uploadBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||
<i class="fas fa-upload me-2"></i>開始匯入
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 進度條 -->
|
||
<div id="progressArea" class="mt-4" style="display: none;">
|
||
<div class="progress" style="height: 30px; border-radius: 15px;">
|
||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
||
role="progressbar" style="width: 100%; background: var(--momo-page-accent-dark);">
|
||
匯入中...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 匯入結果 -->
|
||
<div id="resultArea" style="display: none;">
|
||
<div class="card" style="border-radius: 16px; border-left: 5px solid var(--momo-page-accent-dark);">
|
||
<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="totalCount">0</h3>
|
||
<small class="text-muted">總筆數</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="text-center">
|
||
<h3 class="text-success" id="successCount">0</h3>
|
||
<small class="text-muted">新增廠商</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="text-center">
|
||
<h3 class="text-info" id="updatedCount">0</h3>
|
||
<small class="text-muted">更新廠商</small>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<div class="text-center">
|
||
<h3 style="color: var(--momo-page-accent-dark);" id="emailCount">0</h3>
|
||
<small class="text-muted">郵件地址</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="row mt-3">
|
||
<div class="col-md-12">
|
||
<div class="text-center">
|
||
<h3 class="text-danger" id="failedCount">0</h3>
|
||
<small class="text-muted">失敗</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mt-4 text-center">
|
||
<button type="button" class="btn btn-outline-secondary" id="importAgainBtn">
|
||
<i class="fas fa-redo me-2"></i>再次匯入
|
||
</button>
|
||
<button type="button" class="btn btn-primary" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);" onclick="refreshVendorList()">
|
||
<i class="fas fa-list 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="vendorModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header" style="background: var(--momo-page-accent-dark); color: white;">
|
||
<h5 class="modal-title" id="vendorModalTitle">
|
||
<i class="fas fa-building me-2"></i>新增廠商
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="editVendorId">
|
||
<div class="mb-3">
|
||
<label class="form-label">廠商代碼 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="vendorCode" placeholder="輸入廠商代碼">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">廠商名稱 <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="vendorName" placeholder="輸入廠商名稱">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">郵件地址 <small class="text-muted">(每行一個郵件地址)</small></label>
|
||
<textarea class="form-control" id="vendorEmails" rows="5" placeholder="輸入郵件地址,每行一個"></textarea>
|
||
<small class="text-muted">可貼上多個收件人,讓缺貨通知一次送到正確窗口。</small>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button type="button" class="btn btn-primary" id="saveVendorBtn" style="background: var(--momo-page-accent-dark); border-color: var(--momo-page-accent-dark);">
|
||
<i class="fas fa-save me-2"></i>儲存
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 管理郵件 Modal -->
|
||
<div class="modal fade" id="emailModal" tabindex="-1">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header" style="background: var(--momo-page-accent-dark); color: white;">
|
||
<h5 class="modal-title">
|
||
<i class="fas fa-envelope me-2"></i>管理郵件地址
|
||
</h5>
|
||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="emailVendorCode">
|
||
<h6 class="mb-3">
|
||
<strong id="emailVendorName"></strong> (<span id="emailVendorCode2"></span>)
|
||
</h6>
|
||
|
||
<!-- 新增郵件 -->
|
||
<div class="card mb-3">
|
||
<div class="card-body">
|
||
<h6><i class="fas fa-plus-circle me-2"></i>新增郵件地址</h6>
|
||
<div class="input-group">
|
||
<input type="email" class="form-control" id="newEmail" placeholder="輸入新的郵件地址">
|
||
<button class="btn btn-success" id="addEmailBtn">
|
||
<i class="fas fa-plus me-1"></i>新增
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 現有郵件列表 -->
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<h6><i class="fas fa-list me-2"></i>現有郵件地址</h6>
|
||
<div id="emailList" class="mt-3">
|
||
<p class="text-center text-muted">載入中...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">關閉</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// ====== Global Variables ======
|
||
const vendorCsrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||
let currentPage = 1;
|
||
let pageSize = 20;
|
||
let totalVendors = 0;
|
||
let searchKeyword = '';
|
||
let selectedFile = null;
|
||
let vendorModal, emailModal;
|
||
|
||
// ====== Initialize ======
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
vendorModal = new bootstrap.Modal(document.getElementById('vendorModal'));
|
||
emailModal = new bootstrap.Modal(document.getElementById('emailModal'));
|
||
loadVendorList();
|
||
bindEvents();
|
||
});
|
||
|
||
// ====== Bind Events ======
|
||
function bindEvents() {
|
||
// 廠商清單相關
|
||
document.getElementById('searchBtn').addEventListener('click', () => {
|
||
searchKeyword = document.getElementById('searchVendor').value;
|
||
currentPage = 1;
|
||
loadVendorList();
|
||
});
|
||
|
||
document.getElementById('resetBtn').addEventListener('click', () => {
|
||
document.getElementById('searchVendor').value = '';
|
||
searchKeyword = '';
|
||
currentPage = 1;
|
||
loadVendorList();
|
||
});
|
||
|
||
document.getElementById('addVendorBtn').addEventListener('click', () => {
|
||
openVendorModal();
|
||
});
|
||
|
||
document.getElementById('saveVendorBtn').addEventListener('click', saveVendor);
|
||
|
||
// 郵件管理相關
|
||
document.getElementById('addEmailBtn').addEventListener('click', addEmail);
|
||
|
||
// Excel 匯入相關
|
||
bindImportEvents();
|
||
}
|
||
|
||
// ====== Vendor List Functions ======
|
||
async function loadVendorList() {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: currentPage,
|
||
page_size: pageSize
|
||
});
|
||
|
||
if (searchKeyword) {
|
||
params.append('search', searchKeyword);
|
||
}
|
||
|
||
const response = await fetch(`/vendor-stockout/api/vendor/list?${params}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
renderVendorTable(result.data.vendors);
|
||
updateVendorStats(result.data.stats);
|
||
updatePagination(result.data.total);
|
||
totalVendors = result.data.total;
|
||
}
|
||
} catch (error) {
|
||
console.error('載入廠商清單失敗:', error);
|
||
document.getElementById('vendorTable').innerHTML = `
|
||
<tr>
|
||
<td colspan="5" 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 renderVendorTable(vendors) {
|
||
const tbody = document.getElementById('vendorTable');
|
||
|
||
if (vendors.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="5" 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 = vendors.map(vendor => `
|
||
<tr>
|
||
<td><strong>${vendor.vendor_code}</strong></td>
|
||
<td>${vendor.vendor_name}</td>
|
||
<td>
|
||
${vendor.emails.slice(0, 2).map(email =>
|
||
`<span class="email-badge">${email}</span>`
|
||
).join('')}
|
||
${vendor.email_count > 2 ? `<span class="badge bg-secondary">+${vendor.email_count - 2} 個</span>` : ''}
|
||
</td>
|
||
<td class="text-center">
|
||
<span class="badge bg-info">${vendor.email_count} 個</span>
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-outline-primary me-1" onclick="editVendor('${vendor.vendor_code}')">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-info me-1" onclick="manageEmails('${vendor.vendor_code}', '${vendor.vendor_name}')">
|
||
<i class="fas fa-envelope"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteVendor('${vendor.vendor_code}', '${vendor.vendor_name}')">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function updateVendorStats(stats) {
|
||
document.getElementById('totalVendors').textContent = stats.total_vendors || 0;
|
||
document.getElementById('totalEmails').textContent = stats.total_emails || 0;
|
||
document.getElementById('avgEmails').textContent = stats.avg_emails || '0.0';
|
||
}
|
||
|
||
function updatePagination(total) {
|
||
const totalPages = Math.ceil(total / pageSize);
|
||
const start = (currentPage - 1) * pageSize + 1;
|
||
const end = Math.min(currentPage * pageSize, total);
|
||
|
||
document.getElementById('pageInfo').textContent = `${start}-${end} / ${total}`;
|
||
|
||
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(totalVendors / pageSize);
|
||
if (page < 1 || page > totalPages) return;
|
||
currentPage = page;
|
||
loadVendorList();
|
||
}
|
||
|
||
// ====== Vendor CRUD Functions ======
|
||
function openVendorModal(vendorCode = null) {
|
||
document.getElementById('editVendorId').value = vendorCode || '';
|
||
document.getElementById('vendorModalTitle').innerHTML = vendorCode
|
||
? '<i class="fas fa-edit me-2"></i>編輯廠商'
|
||
: '<i class="fas fa-plus me-2"></i>新增廠商';
|
||
|
||
if (vendorCode) {
|
||
loadVendorDetail(vendorCode);
|
||
} else {
|
||
document.getElementById('vendorCode').value = '';
|
||
document.getElementById('vendorCode').readOnly = false;
|
||
document.getElementById('vendorName').value = '';
|
||
document.getElementById('vendorEmails').value = '';
|
||
}
|
||
|
||
vendorModal.show();
|
||
}
|
||
|
||
async function loadVendorDetail(vendorCode) {
|
||
try {
|
||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}`);
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const vendor = result.data;
|
||
document.getElementById('vendorCode').value = vendor.vendor_code;
|
||
document.getElementById('vendorCode').readOnly = true;
|
||
document.getElementById('vendorName').value = vendor.vendor_name;
|
||
document.getElementById('vendorEmails').value = vendor.emails.join('\n');
|
||
}
|
||
} catch (error) {
|
||
alert('載入廠商資料失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function editVendor(vendorCode) {
|
||
openVendorModal(vendorCode);
|
||
}
|
||
|
||
async function saveVendor() {
|
||
const vendorCode = document.getElementById('vendorCode').value.trim();
|
||
const vendorName = document.getElementById('vendorName').value.trim();
|
||
const emailsText = document.getElementById('vendorEmails').value;
|
||
|
||
if (!vendorCode || !vendorName) {
|
||
alert('廠商代碼和名稱必須填寫!');
|
||
return;
|
||
}
|
||
|
||
const emails = emailsText.split('\n')
|
||
.map(e => e.trim())
|
||
.filter(e => e && e.includes('@'));
|
||
|
||
const data = {
|
||
vendor_code: vendorCode,
|
||
vendor_name: vendorName,
|
||
emails: emails
|
||
};
|
||
|
||
try {
|
||
const isEdit = document.getElementById('editVendorId').value;
|
||
const url = isEdit
|
||
? `/vendor-stockout/api/vendor/${vendorCode}`
|
||
: '/vendor-stockout/api/vendor';
|
||
const method = isEdit ? 'PUT' : 'POST';
|
||
|
||
const response = await fetch(url, {
|
||
method: method,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': vendorCsrfToken
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
vendorModal.hide();
|
||
await loadVendorList();
|
||
alert(isEdit ? '更新成功!' : '新增成功!');
|
||
} else {
|
||
alert(result.message || '操作失敗');
|
||
}
|
||
} catch (error) {
|
||
alert('操作失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function deleteVendor(vendorCode, vendorName) {
|
||
if (!confirm(`確定要刪除廠商「${vendorName}」嗎?這將同時刪除所有相關郵件地址。`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'X-CSRFToken': vendorCsrfToken
|
||
}
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
await loadVendorList();
|
||
alert('刪除成功!');
|
||
} else {
|
||
alert(result.message || '刪除失敗');
|
||
}
|
||
} catch (error) {
|
||
alert('刪除失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// ====== Email Management Functions ======
|
||
async function manageEmails(vendorCode, vendorName) {
|
||
document.getElementById('emailVendorCode').value = vendorCode;
|
||
document.getElementById('emailVendorCode2').textContent = vendorCode;
|
||
document.getElementById('emailVendorName').textContent = vendorName;
|
||
document.getElementById('newEmail').value = '';
|
||
|
||
await loadEmailList(vendorCode);
|
||
emailModal.show();
|
||
}
|
||
|
||
async function loadEmailList(vendorCode) {
|
||
try {
|
||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}/emails`);
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
const emailList = document.getElementById('emailList');
|
||
if (result.data.length === 0) {
|
||
emailList.innerHTML = '<p class="text-muted text-center">暫無郵件地址</p>';
|
||
} else {
|
||
emailList.innerHTML = result.data.map(email => `
|
||
<div class="d-flex justify-content-between align-items-center mb-2 p-2 border rounded">
|
||
<div>
|
||
<i class="fas fa-envelope me-2" style="color: var(--momo-page-accent-dark);"></i>
|
||
<strong>${email.email}</strong>
|
||
<span class="badge bg-secondary ms-2">${email.email_type}</span>
|
||
</div>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="deleteEmail(${email.id})">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
document.getElementById('emailList').innerHTML =
|
||
'<p class="text-danger text-center">載入失敗: ' + error.message + '</p>';
|
||
}
|
||
}
|
||
|
||
async function addEmail() {
|
||
const vendorCode = document.getElementById('emailVendorCode').value;
|
||
const email = document.getElementById('newEmail').value.trim();
|
||
|
||
if (!email) {
|
||
alert('先填郵件地址。');
|
||
return;
|
||
}
|
||
|
||
if (!email.includes('@')) {
|
||
alert('郵件地址格式不正確。');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/vendor-stockout/api/vendor/${vendorCode}/emails`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': vendorCsrfToken
|
||
},
|
||
body: JSON.stringify({ email: email, email_type: 'primary' })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
document.getElementById('newEmail').value = '';
|
||
await loadEmailList(vendorCode);
|
||
await loadVendorList();
|
||
alert('新增成功!');
|
||
} else {
|
||
alert(result.message || '新增失敗');
|
||
}
|
||
} catch (error) {
|
||
alert('新增失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
async function deleteEmail(emailId) {
|
||
if (!confirm('確定要刪除這個郵件地址嗎?')) return;
|
||
|
||
const vendorCode = document.getElementById('emailVendorCode').value;
|
||
|
||
try {
|
||
const response = await fetch(`/vendor-stockout/api/vendor/email/${emailId}`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'X-CSRFToken': vendorCsrfToken
|
||
}
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
await loadEmailList(vendorCode);
|
||
await loadVendorList();
|
||
alert('刪除成功!');
|
||
} else {
|
||
alert(result.message || '刪除失敗');
|
||
}
|
||
} catch (error) {
|
||
alert('刪除失敗: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// ====== Excel Import Functions ======
|
||
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 檔。');
|
||
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/vendor/import/excel', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': vendorCsrfToken
|
||
},
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
document.getElementById('progressArea').style.display = 'none';
|
||
|
||
if (result.success) {
|
||
document.getElementById('totalCount').textContent = result.data.total_count;
|
||
document.getElementById('successCount').textContent = result.data.success_count;
|
||
document.getElementById('updatedCount').textContent = result.data.updated_count;
|
||
document.getElementById('emailCount').textContent = result.data.email_count;
|
||
document.getElementById('failedCount').textContent = result.data.failed_count;
|
||
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 refreshVendorList() {
|
||
const listTab = new bootstrap.Tab(document.getElementById('list-tab'));
|
||
listTab.show();
|
||
loadVendorList();
|
||
}
|
||
|
||
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';
|
||
}
|
||
</script>
|
||
{% endblock %}
|