Files
inventory/templates/types.html

427 lines
18 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-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仓库概览</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<header class="hero">
<div>
<h1>仓库概览</h1>
<p>先看关键指标与待补货,再进入对应分类处理</p>
</div>
<div class="hero-actions">
<a class="btn" href="{{ url_for('type_page', box_type='custom') }}#quick-add">添加容器</a>
<a class="btn btn-light" href="{{ url_for('search_page') }}">快速搜索</a>
<a class="btn btn-light" href="{{ url_for('stats_page') }}">统计页</a>
{% include '_account_menu.html' %}
</div>
</header>
<main class="container">
{% if error %}
<p class="alert">{{ error }}</p>
{% endif %}
{% if notice %}
<p class="notice">{{ notice }}</p>
{% endif %}
<div class="overview-layout">
<section class="overview-main">
<section class="metrics-grid">
<article class="metric-card">
<p class="metric-title">容器总数</p>
<p class="metric-value">{{ stats.box_count }}</p>
</article>
<article class="metric-card">
<p class="metric-title">启用元件</p>
<p class="metric-value">{{ stats.active_items }}</p>
</article>
<article class="metric-card">
<p class="metric-title">待补货元件</p>
<p class="metric-value">{{ stats.low_stock_count }}</p>
</article>
<article class="metric-card">
<p class="metric-title">近7天净变动</p>
<p class="metric-value">{% if stats.period_net_change_7d > 0 %}+{% endif %}{{ stats.period_net_change_7d }}</p>
</article>
</section>
<section class="metrics-grid">
{% for item in type_cards %}
<article class="metric-card type-card">
<div class="type-card-head">
<p class="metric-title">{{ item.label }}</p>
<a class="type-card-more" href="{{ url_for('edit_container_type', box_type=item.key) }}" aria-label="编辑容器属性" title="编辑容器属性">...</a>
</div>
<p class="metric-value">{{ item.count }}</p>
<p class="hint">{{ item.desc }}</p>
<p class="hint">容器 {{ item.count }} 个 | 启用元件 {{ item.item_count }} 种 | 总库存 {{ item.quantity }}</p>
<a class="btn" href="{{ item.url }}">进入分类</a>
</article>
{% endfor %}
</section>
<section class="panel side-low-stock" id="overview-low-stock">
<h2>低库存元器件</h2>
{% for group in low_stock_groups %}
<h3>{{ group['label'] }}{{ group['items']|length }}</h3>
<ul class="side-low-stock-list">
{% for item in group['items'] %}
<li>
<div>
<strong>{{ item.name }}</strong>
<p class="hint">{{ item.part_no }} | {{ item.box_name }} / {{ item.slot_code }} | 数量 {{ item.quantity }}</p>
</div>
<a class="btn btn-light" href="{{ item.edit_url }}">编辑</a>
</li>
{% else %}
<li class="muted">当前分类没有低库存元器件。</li>
{% endfor %}
</ul>
{% endfor %}
</section>
</section>
<aside class="overview-sidebar">
<section class="panel ai-panel" id="ai-panel">
<div class="ai-panel-head">
<h2>AI补货建议</h2>
<a class="btn btn-light" href="{{ url_for('ai_settings_page') }}">参数</a>
</div>
<p class="hint">根据低库存与近30天出库数据生成可执行补货建议。</p>
<button class="btn" id="ai-restock-btn" type="button">生成建议</button>
<p class="hint" id="ai-panel-status"></p>
<p class="hint" id="ai-panel-warning"></p>
<div id="ai-plan-groups" class="ai-plan-groups"></div>
<details class="box-overview" id="ai-raw-wrap" hidden>
<summary>查看原始 AI 文本</summary>
<pre id="ai-panel-content" class="ai-panel-content"></pre>
</details>
<hr class="ai-divider">
<div class="ai-panel-head">
<h2>AI重复物料巡检</h2>
</div>
<p class="hint">扫描疑似同料号、同参数、同立创编号记录,生成人工复核清单。</p>
<div class="actions">
<button class="btn" id="ai-duplicate-btn" type="button">开始巡检</button>
<button class="btn btn-light" id="ai-duplicate-export-current-btn" type="button">导出当前显示</button>
<button class="btn btn-light" id="ai-duplicate-export-all-btn" type="button">导出全量</button>
</div>
<p class="hint" id="ai-duplicate-status"></p>
<p class="hint" id="ai-duplicate-warning"></p>
<div id="ai-duplicate-groups" class="ai-plan-groups"></div>
</section>
</aside>
</div>
</main>
<script>
(function () {
var aiBtn = document.getElementById('ai-restock-btn');
var statusNode = document.getElementById('ai-panel-status');
var warningNode = document.getElementById('ai-panel-warning');
var contentNode = document.getElementById('ai-panel-content');
var planGroups = document.getElementById('ai-plan-groups');
var rawWrap = document.getElementById('ai-raw-wrap');
function clearPlan() {
if (planGroups) {
planGroups.innerHTML = '';
}
}
function renderPlan(plan) {
if (!planGroups) {
return;
}
clearPlan();
var groups = [
{ key: 'urgent', title: '紧急补货' },
{ key: 'this_week', title: '本周建议补货' },
{ key: 'defer', title: '暂缓补货' }
];
groups.forEach(function (groupMeta) {
var rows = (plan && plan[groupMeta.key]) || [];
var wrap = document.createElement('section');
wrap.className = 'ai-plan-group';
var title = document.createElement('h3');
title.textContent = groupMeta.title + '' + rows.length + '';
wrap.appendChild(title);
var list = document.createElement('ul');
list.className = 'side-low-stock-list';
if (!rows.length) {
var empty = document.createElement('li');
empty.className = 'muted';
empty.textContent = '暂无条目';
list.appendChild(empty);
} else {
rows.forEach(function (item) {
var li = document.createElement('li');
var content = document.createElement('div');
var strong = document.createElement('strong');
strong.textContent = (item.name || '未命名元件') + ' (' + (item.part_no || '-') + ')';
content.appendChild(strong);
var qty = document.createElement('p');
qty.className = 'hint';
qty.textContent = '建议补货: ' + (item.suggest_qty || '待确认');
content.appendChild(qty);
var reason = document.createElement('p');
reason.className = 'hint';
reason.textContent = '理由: ' + (item.reason || '无');
content.appendChild(reason);
li.appendChild(content);
list.appendChild(li);
});
}
wrap.appendChild(list);
planGroups.appendChild(wrap);
});
}
if (!aiBtn) {
return;
}
aiBtn.addEventListener('click', function () {
aiBtn.disabled = true;
statusNode.textContent = '正在生成建议,请稍候...';
if (warningNode) {
warningNode.textContent = '';
}
clearPlan();
if (contentNode) {
contentNode.textContent = '';
}
if (rawWrap) {
rawWrap.hidden = true;
}
fetch('{{ url_for('ai_restock_plan') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: '{}'
})
.then(function (resp) {
return resp.json().then(function (data) {
return { ok: resp.ok, data: data };
});
})
.then(function (result) {
var data = result.data || {};
if (!result.ok || !data.ok) {
statusNode.textContent = '生成失败';
if (warningNode) {
warningNode.textContent = data.message || 'AI服务暂时不可用';
}
if (data.plan) {
renderPlan(data.plan);
}
return;
}
statusNode.textContent = (data.plan && data.plan.summary) || '建议已生成';
if (warningNode) {
warningNode.textContent = data.parse_warning || '';
}
renderPlan(data.plan || {});
if (contentNode && data.suggestion) {
contentNode.textContent = data.suggestion;
if (rawWrap) {
rawWrap.hidden = false;
}
}
})
.catch(function () {
statusNode.textContent = '生成失败';
if (warningNode) {
warningNode.textContent = '请求失败,请稍后重试';
}
})
.finally(function () {
aiBtn.disabled = false;
});
});
})();
(function () {
var auditBtn = document.getElementById('ai-duplicate-btn');
var exportCurrentBtn = document.getElementById('ai-duplicate-export-current-btn');
var exportAllBtn = document.getElementById('ai-duplicate-export-all-btn');
var statusNode = document.getElementById('ai-duplicate-status');
var warningNode = document.getElementById('ai-duplicate-warning');
var groupsWrap = document.getElementById('ai-duplicate-groups');
var latestAuditGroups = [];
if (!auditBtn || !exportCurrentBtn || !exportAllBtn || !statusNode || !groupsWrap) {
return;
}
function clearGroups() {
latestAuditGroups = [];
groupsWrap.innerHTML = '';
}
function renderAuditGroups(groups) {
clearGroups();
if (!Array.isArray(groups) || !groups.length) {
var empty = document.createElement('p');
empty.className = 'muted';
empty.textContent = '未发现疑似重复物料';
groupsWrap.appendChild(empty);
return;
}
latestAuditGroups = groups.slice();
groups.forEach(function (group) {
var section = document.createElement('section');
section.className = 'ai-plan-group ai-audit-group';
var title = document.createElement('h3');
title.textContent = (group.reason || '疑似重复') + ' | ' + (group.key || '-') + '' + (group.member_count || 0) + '';
section.appendChild(title);
var suggestion = document.createElement('p');
suggestion.className = 'hint';
suggestion.textContent = '建议: ' + (group.suggestion || '请人工复核');
section.appendChild(suggestion);
var list = document.createElement('ul');
list.className = 'side-low-stock-list';
(group.members || []).forEach(function (member) {
var li = document.createElement('li');
var content = document.createElement('div');
var strong = document.createElement('strong');
strong.textContent = (member.name || '未命名元件') + ' (' + (member.part_no || '-') + ')';
content.appendChild(strong);
var spec = document.createElement('p');
spec.className = 'hint';
spec.textContent = '规格: ' + (member.specification || '-');
content.appendChild(spec);
var pos = document.createElement('p');
pos.className = 'hint';
pos.textContent = (member.box_name || '-') + ' / ' + (member.slot_code || '-') + ' | 数量 ' + (member.quantity || 0);
content.appendChild(pos);
if (member.lcsc_code) {
var lcsc = document.createElement('p');
lcsc.className = 'hint';
lcsc.textContent = '立创编号: ' + member.lcsc_code;
content.appendChild(lcsc);
}
li.appendChild(content);
if (member.edit_url) {
var editBtn = document.createElement('a');
editBtn.className = 'btn btn-light';
editBtn.href = member.edit_url;
editBtn.textContent = '编辑';
li.appendChild(editBtn);
}
list.appendChild(li);
});
section.appendChild(list);
groupsWrap.appendChild(section);
});
}
auditBtn.addEventListener('click', function () {
auditBtn.disabled = true;
statusNode.textContent = '正在巡检重复物料,请稍候...';
if (warningNode) {
warningNode.textContent = '';
}
clearGroups();
fetch('{{ url_for('ai_duplicate_audit') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: '{}'
})
.then(function (resp) {
return resp.json().then(function (data) {
return { ok: resp.ok, data: data };
});
})
.then(function (result) {
var data = result.data || {};
if (!result.ok || !data.ok) {
statusNode.textContent = '巡检失败';
if (warningNode) {
warningNode.textContent = data.message || '服务暂时不可用';
}
return;
}
statusNode.textContent = data.summary || '巡检已完成';
if (warningNode) {
warningNode.textContent = data.parse_warning || '';
}
renderAuditGroups((data.data && data.data.groups) || []);
})
.catch(function () {
statusNode.textContent = '巡检失败';
if (warningNode) {
warningNode.textContent = '请求失败,请稍后重试';
}
})
.finally(function () {
auditBtn.disabled = false;
});
});
exportCurrentBtn.addEventListener('click', function () {
if (!latestAuditGroups.length) {
statusNode.textContent = '请先运行巡检,再导出当前显示结果';
if (warningNode) {
warningNode.textContent = '';
}
return;
}
var params = new URLSearchParams();
params.set('limit', String(latestAuditGroups.length));
latestAuditGroups.forEach(function (group) {
params.append('group_id', (group.type || '') + '::' + (group.key || ''));
});
var downloadUrl = '{{ url_for('export_duplicate_audit_csv') }}?' + params.toString();
window.location.href = downloadUrl;
});
exportAllBtn.addEventListener('click', function () {
var downloadUrl = '{{ url_for('export_duplicate_audit_csv') }}?limit=1000';
window.location.href = downloadUrl;
});
})();
</script>
</body>
</html>