575 lines
18 KiB
HTML
575 lines
18 KiB
HTML
{% extends 'ewoooc_base.html' %}
|
||
|
||
{% block title %}EwoooC 缺貨匯入{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.stockout-import-stack {
|
||
display: grid;
|
||
gap: 22px;
|
||
}
|
||
|
||
.stockout-import-header,
|
||
.stockout-upload-panel,
|
||
.stockout-import-card {
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.stockout-import-header {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.7fr);
|
||
gap: 18px;
|
||
padding: 22px;
|
||
}
|
||
|
||
.stockout-import-eyebrow {
|
||
display: inline-flex;
|
||
width: fit-content;
|
||
align-items: center;
|
||
gap: 7px;
|
||
margin-bottom: 10px;
|
||
padding: 4px 10px;
|
||
color: var(--momo-accent-strong);
|
||
background: var(--momo-accent-soft);
|
||
border-radius: var(--momo-radius-pill);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.stockout-import-title {
|
||
margin: 0;
|
||
color: var(--momo-text-primary);
|
||
font-size: 30px;
|
||
font-weight: 800;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.stockout-import-subtitle {
|
||
max-width: 760px;
|
||
margin: 8px 0 0;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.stockout-import-actions,
|
||
.stockout-result-grid {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stockout-action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
min-height: 36px;
|
||
padding: 8px 14px;
|
||
color: var(--momo-text-primary);
|
||
background: var(--momo-bg-paper);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 800;
|
||
text-decoration: none;
|
||
transition: var(--momo-transition-base);
|
||
}
|
||
|
||
.stockout-action:hover {
|
||
color: var(--momo-text-primary);
|
||
border-color: var(--momo-border-strong);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.stockout-action.is-primary {
|
||
color: var(--momo-text-inverse);
|
||
background: var(--momo-ink);
|
||
border-color: var(--momo-ink);
|
||
}
|
||
|
||
.stockout-import-note {
|
||
display: grid;
|
||
align-content: center;
|
||
gap: 8px;
|
||
padding: 16px;
|
||
color: var(--momo-text-inverse);
|
||
background: var(--momo-ink);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.stockout-import-note-label {
|
||
color: rgba(250, 247, 240, 0.62);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.stockout-import-note-value {
|
||
color: var(--momo-text-inverse);
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.stockout-upload-panel {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
|
||
gap: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stockout-dropzone {
|
||
display: grid;
|
||
min-height: 330px;
|
||
cursor: pointer;
|
||
place-items: center;
|
||
padding: 30px;
|
||
background: var(--momo-bg-surface);
|
||
border: 0;
|
||
border-right: 1px solid var(--momo-border-light);
|
||
transition: var(--momo-transition-base);
|
||
}
|
||
|
||
.stockout-dropzone:hover,
|
||
.stockout-dropzone.is-dragover {
|
||
background: var(--momo-bg-paper);
|
||
}
|
||
|
||
.stockout-dropzone-inner {
|
||
max-width: 520px;
|
||
text-align: center;
|
||
}
|
||
|
||
.stockout-upload-icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 58px;
|
||
height: 58px;
|
||
color: var(--momo-accent-strong);
|
||
background: var(--momo-accent-soft);
|
||
border-radius: 8px;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.stockout-dropzone-title {
|
||
margin: 18px 0 8px;
|
||
color: var(--momo-text-primary);
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.stockout-dropzone-subtitle {
|
||
margin: 0;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 13px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.stockout-import-side {
|
||
display: grid;
|
||
gap: 12px;
|
||
align-content: start;
|
||
padding: 18px;
|
||
background: var(--momo-bg-paper);
|
||
}
|
||
|
||
.stockout-import-card {
|
||
padding: 16px;
|
||
}
|
||
|
||
.stockout-card-label {
|
||
color: var(--momo-text-secondary);
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.stockout-card-title {
|
||
margin-top: 8px;
|
||
color: var(--momo-text-primary);
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.stockout-card-meta {
|
||
margin-top: 8px;
|
||
color: var(--momo-text-tertiary);
|
||
font-size: 12px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.stockout-file-panel,
|
||
.stockout-progress-panel,
|
||
.stockout-result-panel,
|
||
.stockout-error-panel {
|
||
display: none;
|
||
padding: 18px;
|
||
background: var(--momo-bg-surface);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.stockout-file-panel.is-visible,
|
||
.stockout-progress-panel.is-visible,
|
||
.stockout-result-panel.is-visible,
|
||
.stockout-error-panel.is-visible {
|
||
display: block;
|
||
}
|
||
|
||
.stockout-file-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
}
|
||
|
||
.stockout-file-name {
|
||
color: var(--momo-text-primary);
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.stockout-file-size {
|
||
color: var(--momo-text-tertiary);
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.stockout-progress-track {
|
||
height: 8px;
|
||
overflow: hidden;
|
||
background: var(--momo-bg-paper);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: var(--momo-radius-pill);
|
||
}
|
||
|
||
.stockout-progress-bar {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: var(--momo-accent);
|
||
animation: stockout-pulse 1.1s ease-in-out infinite alternate;
|
||
}
|
||
|
||
.stockout-result-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 10px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.stockout-result-cell {
|
||
padding: 14px;
|
||
background: var(--momo-bg-paper);
|
||
border: 1px solid var(--momo-border-light);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.stockout-result-value {
|
||
color: var(--momo-text-primary);
|
||
font-size: 26px;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
}
|
||
|
||
.stockout-result-label {
|
||
margin-top: 8px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.stockout-batch-id {
|
||
margin-top: 14px;
|
||
color: var(--momo-text-secondary);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.stockout-error-panel {
|
||
color: var(--momo-danger);
|
||
background: rgba(214, 83, 68, 0.08);
|
||
border-color: rgba(214, 83, 68, 0.22);
|
||
}
|
||
|
||
@keyframes stockout-pulse {
|
||
from { opacity: 0.48; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.stockout-import-header,
|
||
.stockout-upload-panel {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.stockout-dropzone {
|
||
border-right: 0;
|
||
border-bottom: 1px solid var(--momo-border-light);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.stockout-import-title {
|
||
font-size: 26px;
|
||
}
|
||
|
||
.stockout-file-row {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.stockout-result-grid {
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block ewooo_content %}
|
||
<div class="stockout-import-stack">
|
||
<section class="stockout-import-header">
|
||
<div>
|
||
<div class="stockout-import-eyebrow momo-mono">
|
||
<i class="fas fa-file-import"></i>
|
||
STOCKOUT IMPORT
|
||
</div>
|
||
<h1 class="stockout-import-title">Excel 匯入</h1>
|
||
<p class="stockout-import-subtitle">
|
||
上傳正式缺貨 Excel 後,系統會使用既有匯入 API 寫入 vendor_stockout,完成後直接回傳批次編號與成功、重複、失敗筆數。
|
||
</p>
|
||
<div class="stockout-import-actions mt-3">
|
||
<a class="stockout-action" href="{{ url_for('vendor.index') }}">
|
||
<i class="fas fa-arrow-left"></i>
|
||
回總覽
|
||
</a>
|
||
<a class="stockout-action" href="{{ url_for('vendor.list_page') }}">
|
||
<i class="fas fa-table-list"></i>
|
||
缺貨清單
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<aside class="stockout-import-note">
|
||
<div class="stockout-import-note-label momo-mono">資料原則</div>
|
||
<div class="stockout-import-note-value">只匯入使用者上傳的正式 Excel,不預填任何資料列。</div>
|
||
</aside>
|
||
</section>
|
||
|
||
<section class="stockout-upload-panel" aria-label="Excel 匯入">
|
||
<button class="stockout-dropzone" id="stockoutDropzone" type="button">
|
||
<input id="stockoutFileInput" type="file" accept=".xlsx,.xls" hidden>
|
||
<span class="stockout-dropzone-inner">
|
||
<span class="stockout-upload-icon">
|
||
<i class="fas fa-cloud-arrow-up"></i>
|
||
</span>
|
||
<span class="stockout-dropzone-title d-block">選擇或拖曳 Excel 檔案</span>
|
||
<span class="stockout-dropzone-subtitle d-block">
|
||
支援 .xlsx / .xls。檔案送出後會寫入正式缺貨資料表,請確認內容是可匯入的正式清單。
|
||
</span>
|
||
<span class="stockout-action is-primary mt-3">
|
||
<i class="fas fa-folder-open"></i>
|
||
選擇檔案
|
||
</span>
|
||
</span>
|
||
</button>
|
||
|
||
<aside class="stockout-import-side">
|
||
<div class="stockout-import-card">
|
||
<div class="stockout-card-label momo-mono">必要欄位</div>
|
||
<div class="stockout-card-title">來源供應商編號、來源供應商名稱、商品ID、商品名稱</div>
|
||
<div class="stockout-card-meta momo-mono">缺少必要欄位時,API 會拒絕匯入並回傳錯誤原因。</div>
|
||
</div>
|
||
<div class="stockout-import-card">
|
||
<div class="stockout-card-label momo-mono">欄位範本</div>
|
||
<div class="stockout-card-title">下載只有正式欄位標題的空白範本</div>
|
||
<div class="stockout-card-meta">
|
||
<a class="stockout-action" href="{{ url_for('vendor.api_import_template') }}">
|
||
<i class="fas fa-download"></i>
|
||
下載範本
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</section>
|
||
|
||
<section class="stockout-file-panel" id="stockoutFilePanel">
|
||
<div class="stockout-file-row">
|
||
<div>
|
||
<div class="stockout-card-label momo-mono">已選擇檔案</div>
|
||
<div class="stockout-file-name" id="stockoutFileName"></div>
|
||
<div class="stockout-file-size momo-mono" id="stockoutFileSize"></div>
|
||
</div>
|
||
<button class="stockout-action is-primary" id="stockoutUploadButton" type="button">
|
||
<i class="fas fa-upload"></i>
|
||
開始匯入
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="stockout-progress-panel" id="stockoutProgressPanel">
|
||
<div class="stockout-card-label momo-mono mb-2">匯入中</div>
|
||
<div class="stockout-progress-track">
|
||
<div class="stockout-progress-bar"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="stockout-result-panel" id="stockoutResultPanel">
|
||
<div class="stockout-card-label momo-mono">匯入結果</div>
|
||
<div class="stockout-result-grid">
|
||
<div class="stockout-result-cell">
|
||
<div class="stockout-result-value momo-mono" id="stockoutTotalCount">0</div>
|
||
<div class="stockout-result-label">總筆數</div>
|
||
</div>
|
||
<div class="stockout-result-cell">
|
||
<div class="stockout-result-value momo-mono" id="stockoutSuccessCount">0</div>
|
||
<div class="stockout-result-label">成功匯入</div>
|
||
</div>
|
||
<div class="stockout-result-cell">
|
||
<div class="stockout-result-value momo-mono" id="stockoutDuplicateCount">0</div>
|
||
<div class="stockout-result-label">重複項目</div>
|
||
</div>
|
||
<div class="stockout-result-cell">
|
||
<div class="stockout-result-value momo-mono" id="stockoutFailedCount">0</div>
|
||
<div class="stockout-result-label">失敗</div>
|
||
</div>
|
||
</div>
|
||
<div class="stockout-batch-id momo-mono">批次編號 <span id="stockoutBatchId"></span></div>
|
||
<div class="stockout-import-actions mt-3">
|
||
<a class="stockout-action is-primary" href="{{ url_for('vendor.list_page') }}">
|
||
<i class="fas fa-table-list"></i>
|
||
查看缺貨清單
|
||
</a>
|
||
<button class="stockout-action" id="stockoutImportAgainButton" type="button">
|
||
<i class="fas fa-rotate-right"></i>
|
||
再次匯入
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="stockout-error-panel" id="stockoutErrorPanel">
|
||
<div class="stockout-card-label momo-mono">匯入失敗</div>
|
||
<div class="stockout-card-title" id="stockoutErrorMessage"></div>
|
||
</section>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
(function () {
|
||
const dropzone = document.getElementById('stockoutDropzone');
|
||
const fileInput = document.getElementById('stockoutFileInput');
|
||
const filePanel = document.getElementById('stockoutFilePanel');
|
||
const fileName = document.getElementById('stockoutFileName');
|
||
const fileSize = document.getElementById('stockoutFileSize');
|
||
const uploadButton = document.getElementById('stockoutUploadButton');
|
||
const progressPanel = document.getElementById('stockoutProgressPanel');
|
||
const resultPanel = document.getElementById('stockoutResultPanel');
|
||
const errorPanel = document.getElementById('stockoutErrorPanel');
|
||
const errorMessage = document.getElementById('stockoutErrorMessage');
|
||
const importAgainButton = document.getElementById('stockoutImportAgainButton');
|
||
let selectedFile = null;
|
||
|
||
function show(panel) {
|
||
panel.classList.add('is-visible');
|
||
}
|
||
|
||
function hide(panel) {
|
||
panel.classList.remove('is-visible');
|
||
}
|
||
|
||
function resetTransientPanels() {
|
||
hide(progressPanel);
|
||
hide(resultPanel);
|
||
hide(errorPanel);
|
||
}
|
||
|
||
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';
|
||
}
|
||
|
||
function handleFile(file) {
|
||
if (!file) return;
|
||
if (!file.name.match(/\.(xlsx|xls)$/i)) {
|
||
errorMessage.textContent = '請選擇 Excel 檔案 (.xlsx 或 .xls)';
|
||
show(errorPanel);
|
||
return;
|
||
}
|
||
selectedFile = file;
|
||
fileName.textContent = file.name;
|
||
fileSize.textContent = formatFileSize(file.size);
|
||
resetTransientPanels();
|
||
show(filePanel);
|
||
}
|
||
|
||
dropzone.addEventListener('click', function () {
|
||
fileInput.click();
|
||
});
|
||
|
||
fileInput.addEventListener('change', function (event) {
|
||
handleFile(event.target.files[0]);
|
||
});
|
||
|
||
dropzone.addEventListener('dragover', function (event) {
|
||
event.preventDefault();
|
||
dropzone.classList.add('is-dragover');
|
||
});
|
||
|
||
dropzone.addEventListener('dragleave', function () {
|
||
dropzone.classList.remove('is-dragover');
|
||
});
|
||
|
||
dropzone.addEventListener('drop', function (event) {
|
||
event.preventDefault();
|
||
dropzone.classList.remove('is-dragover');
|
||
handleFile(event.dataTransfer.files[0]);
|
||
});
|
||
|
||
uploadButton.addEventListener('click', async function () {
|
||
if (!selectedFile) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', selectedFile);
|
||
hide(filePanel);
|
||
hide(resultPanel);
|
||
hide(errorPanel);
|
||
show(progressPanel);
|
||
|
||
try {
|
||
const response = await fetchWithCSRF('/vendor-stockout/api/import/excel', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const result = await response.json();
|
||
hide(progressPanel);
|
||
|
||
if (result.success) {
|
||
document.getElementById('stockoutTotalCount').textContent = result.data.total_count;
|
||
document.getElementById('stockoutSuccessCount').textContent = result.data.success_count;
|
||
document.getElementById('stockoutDuplicateCount').textContent = result.data.duplicate_count;
|
||
document.getElementById('stockoutFailedCount').textContent = result.data.failed_count;
|
||
document.getElementById('stockoutBatchId').textContent = result.data.batch_id;
|
||
show(resultPanel);
|
||
} else {
|
||
errorMessage.textContent = result.message || '匯入失敗';
|
||
show(errorPanel);
|
||
}
|
||
} catch (error) {
|
||
hide(progressPanel);
|
||
errorMessage.textContent = '網路錯誤:' + error.message;
|
||
show(errorPanel);
|
||
}
|
||
});
|
||
|
||
importAgainButton.addEventListener('click', function () {
|
||
selectedFile = null;
|
||
fileInput.value = '';
|
||
hide(filePanel);
|
||
resetTransientPanels();
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|