From c9247f7a79e6efbe2d37df71a5230f3c9d94737c Mon Sep 17 00:00:00 2001 From: OoO Date: Fri, 1 May 2026 00:06:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=96=B0=E5=A2=9E=E5=BB=A0?= =?UTF-8?q?=E5=95=86=E7=BC=BA=E8=B2=A8=20V2=20feature=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONSTITUTION.md | 4 +- app.py | 2 +- config.py | 2 +- docs/memory/history_logs.md | 3 + routes/vendor_routes.py | 15 +- tests/test_frontend_v2_assets.py | 15 ++ web/templates/vendor_stockout_index_v2.html | 246 ++++++++++++++++++++ 7 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 web/templates/vendor_stockout_index_v2.html diff --git a/CONSTITUTION.md b/CONSTITUTION.md index f0c1bb6..344564f 100644 --- a/CONSTITUTION.md +++ b/CONSTITUTION.md @@ -2,8 +2,8 @@ > 本文件定義專案開發的核心準則與不可違反的規範 > **建立日期**: 2026-01-12 -> **當前版本**: V10.36 (EDM dashboard v2 feature flag) -> **最後更新**: 2026-04-30 +> **當前版本**: V10.37 (Vendor stockout v2 feature flag) +> **最後更新**: 2026-05-01 --- diff --git a/app.py b/app.py index 3149bd7..9691c37 100644 --- a/app.py +++ b/app.py @@ -95,7 +95,7 @@ except Exception as e: sys_log.error(f"無法檢測磁碟空間: {e}") # 🚩 系統版本定義 (備份與顯示用) -# 🚩 2026-04-30 V10.37: Vendor stockout v2 feature flag +# 🚩 2026-05-01 V10.37: Vendor stockout v2 feature flag SYSTEM_VERSION = "V10.37" # ========================================== diff --git a/config.py b/config.py index 8364057..84ad79f 100644 --- a/config.py +++ b/config.py @@ -254,7 +254,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.36" +SYSTEM_VERSION = "V10.37" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 34ca92d..43d8b50 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -60,6 +60,9 @@ - **Frontend V2 static mount**: `momo-app` 補 `./web/static:/app/web/static:ro`,確保 Flask `url_for('static', filename='css/ewoooc-*.css')` 會從實際 `STATIC_DIR=/app/web/static` 讀到 V2 CSS;保留 `./static:/app/static:ro` 供 local-dev Nginx/舊資產相容。 - **EDM Dashboard V2 feature flag**: 五個活動看板入口預設仍走既有 `edm_dashboard.html`,加上 `?ui=v2` 才渲染 `edm_dashboard_v2.html`;新版頁沿用既有活動真實 `grouped_items/slot_stats/scheduler_stats`。 +### 2026-05-01:Frontend V2 營運頁推進 +- **Vendor Stockout V2 feature flag**: `/vendor-stockout` 預設仍走既有頁;`/vendor-stockout?ui=v2` 才渲染 `vendor_stockout_index_v2.html`,統計來自正式 `VendorStockout/VendorList/VendorEmail/EmailSendLog` 查詢,不建立假資料。 + ### 2026-04-28~29:Phase 3e 重構大戰 + daily_sales cache 隱形 bug 根除 - **app.py 縮減 -10.8%**: 7,386 → 6,590 行,11 commits 全綠零 502。 - **抽 Blueprint**: `/api/categories` → `category_routes.py` (8fce73b);`/api/test_url` + `/brand_assets` → `misc_routes.py` (e676840)。 diff --git a/routes/vendor_routes.py b/routes/vendor_routes.py index 8528499..90423d5 100644 --- a/routes/vendor_routes.py +++ b/routes/vendor_routes.py @@ -43,16 +43,16 @@ def _get_vendor_dashboard_stats(): total_stockouts = session.query(VendorStockout).count() pending_stockouts = session.query(VendorStockout).filter(or_( VendorStockout.status == 'pending', - VendorStockout.status == None + VendorStockout.status.is_(None) )).count() sent_stockouts = session.query(VendorStockout).filter(VendorStockout.status == 'sent').count() failed_stockouts = session.query(VendorStockout).filter(VendorStockout.status == 'failed').count() - duplicate_stockouts = session.query(VendorStockout).filter(VendorStockout.is_duplicate == True).count() + duplicate_stockouts = session.query(VendorStockout).filter(VendorStockout.is_duplicate.is_(True)).count() distinct_vendor_count = session.query(VendorStockout.vendor_code).distinct().count() - active_vendors = session.query(VendorList).filter(VendorList.is_active == True).count() - inactive_vendors = session.query(VendorList).filter(VendorList.is_active == False).count() - active_emails = session.query(VendorEmail).filter(VendorEmail.is_active == True).count() + active_vendors = session.query(VendorList).filter(VendorList.is_active.is_(True)).count() + inactive_vendors = session.query(VendorList).filter(VendorList.is_active.is_(False)).count() + active_emails = session.query(VendorEmail).filter(VendorEmail.is_active.is_(True)).count() email_total = session.query(EmailSendLog).count() email_success = session.query(EmailSendLog).filter(EmailSendLog.status == 'sent').count() @@ -62,11 +62,12 @@ def _get_vendor_dashboard_stats(): latest_import = session.query(VendorStockout).order_by(desc(VendorStockout.created_at)).first() latest_email = session.query(EmailSendLog).order_by(desc(EmailSendLog.created_at)).first() + latest_batch_time = func.max(VendorStockout.created_at).label('latest_date') latest_batch = session.query( VendorStockout.batch_id, func.count(VendorStockout.id).label('record_count'), - func.max(VendorStockout.created_at).label('latest_date') - ).group_by(VendorStockout.batch_id).order_by(desc('latest_date')).first() + latest_batch_time + ).group_by(VendorStockout.batch_id).order_by(desc(latest_batch_time)).first() return { 'total_stockouts': total_stockouts, diff --git a/tests/test_frontend_v2_assets.py b/tests/test_frontend_v2_assets.py index 4e55f25..0f92ecf 100644 --- a/tests/test_frontend_v2_assets.py +++ b/tests/test_frontend_v2_assets.py @@ -63,3 +63,18 @@ def test_edm_dashboard_v2_is_feature_flagged_and_uses_real_campaign_data(): assert "scheduler_stats.get(task_key, [])" in template assert "mock" not in template.lower() assert "假商品" not in template + + +def test_vendor_stockout_v2_is_feature_flagged_and_uses_real_vendor_data(): + route_source = (ROOT / "routes/vendor_routes.py").read_text(encoding="utf-8") + template = (ROOT / "web/templates/vendor_stockout_index_v2.html").read_text(encoding="utf-8") + + assert "request.args.get('ui') == 'v2'" in route_source + assert "vendor_stockout_index_v2.html" in route_source + assert "_get_vendor_dashboard_stats()" in route_source + assert "session.query(VendorStockout).count()" in route_source + assert "EmailSendLog" in route_source + assert "stats.get('pending_stockouts'" in template + assert "stats.get('email_success_rate'" in template + assert "mock" not in template.lower() + assert "假" not in template diff --git a/web/templates/vendor_stockout_index_v2.html b/web/templates/vendor_stockout_index_v2.html new file mode 100644 index 0000000..1f984cd --- /dev/null +++ b/web/templates/vendor_stockout_index_v2.html @@ -0,0 +1,246 @@ +{% extends 'ewoooc_base.html' %} + +{% block title %}EwoooC 廠商缺貨{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block ewooo_content %} +{% set stats = stats or {} %} +
+
+
+
+ OPERATIONS + Vendor Stockout Control +
+

廠商缺貨通知自動化系統

+

+ 串接正式缺貨匯入、廠商郵件與發送紀錄資料,集中追蹤待處理缺貨、通知成功率、廠商聯絡覆蓋與最近批次狀態。 +

+ +
+ + +
+ +
+ +
+
+
+
匯入缺貨資料
+
使用正式 Excel 匯入流程,建立批次缺貨紀錄並標記重複資料。
+ 前往匯入 +
+
+
+
缺貨清單
+
篩選、編輯、批次發信與追蹤缺貨商品處理狀態。
+ 查看清單 +
+
+
+
廠商管理
+
維護廠商主檔與多組收件、CC、BCC 郵件地址。
+ 管理廠商 +
+
+
+
發送歷史
+
檢查成功、失敗、待發送郵件紀錄與錯誤訊息。
+ 查看歷史 +
+
+
+ +
+ +
+
+
最近匯入
+
+ {% if stats.get('latest_import_time') %} +
時間{{ stats.get('latest_import_time').strftime('%Y-%m-%d %H:%M') }}
+
廠商{{ stats.get('latest_import_vendor') or '未提供' }}
+
商品{{ stats.get('latest_import_product') or '未提供' }}
+ {% else %} + 尚未讀到缺貨匯入紀錄。 + {% endif %} +
+
+
+
最近批次
+
+ {% if stats.get('latest_batch_id') %} +
批次{{ stats.get('latest_batch_id') }}
+
筆數{{ stats.get('latest_batch_count', 0) | number_format }}
+
時間{{ stats.get('latest_batch_time').strftime('%Y-%m-%d %H:%M') if stats.get('latest_batch_time') else '--' }}
+ {% else %} + 尚未讀到批次資訊。 + {% endif %} +
+
+
+
郵件狀態
+
+
成功{{ stats.get('email_success', 0) | number_format }}
+
失敗{{ stats.get('email_failed', 0) | number_format }}
+
待發送{{ stats.get('email_pending', 0) | number_format }}
+
活躍 Email{{ stats.get('active_emails', 0) | number_format }}
+
+
+
+
+
+{% endblock %}