from typing import List, Optional from datetime import datetime, timedelta from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from sqlalchemy import func, and_ from pydantic import BaseModel from app.models import get_db from app.models.sample import SampleRecord from app.models.order import OrderRecord router = APIRouter(prefix="/lab", tags=["Lab"]) class LabKPI(BaseModel): avg_velocity: float # 平均轉換時間 (天) conversion_rate: float # 轉換比例 (%) orphan_count: int # 孤兒樣品總數 class ScatterPoint(BaseModel): customer: str pn: str sample_qty: int order_qty: int class OrphanSample(BaseModel): customer: str pn: str days_since_sent: int order_no: str date: str def parse_date(date_str: str) -> Optional[datetime]: try: return datetime.strptime(date_str, "%Y-%m-%d") except: return None @router.get("/kpi", response_model=LabKPI) def get_lab_kpi( start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), db: Session = Depends(get_db) ): # 1. 取得所有樣品與訂單 samples_query = db.query(SampleRecord) orders_query = db.query(OrderRecord) if start_date: samples_query = samples_query.filter(SampleRecord.date >= start_date) orders_query = orders_query.filter(OrderRecord.created_at >= start_date) # 訂單使用 created_at or date? OrderRecord 只有 created_at 欄位是 DateTime if end_date: samples_query = samples_query.filter(SampleRecord.date <= end_date) # Note: OrderRecord 只有 created_at samples = samples_query.all() orders = orders_query.all() # 建立群組 (ERP Code + PN) # ERP Code correspond to cust_id sample_groups = {} for s in samples: key = (s.cust_id, s.pn) if key not in sample_groups: sample_groups[key] = [] sample_groups[key].append(s) order_groups = {} for o in orders: key = (o.cust_id, o.pn) if key not in order_groups: order_groups[key] = [] order_groups[key].append(o) # 計算 Velocity 與 轉換率 velocities = [] converted_samples_count = 0 total_samples_count = len(samples) for key, group_samples in sample_groups.items(): if key in order_groups: # 轉換成功 converted_samples_count += len(group_samples) # 計算 Velocity: First Order Date - Earliest Sample Date earliest_sample_date = min([parse_date(s.date) for s in group_samples if s.date] or [datetime.max]) first_order_date = min([o.created_at for o in order_groups[key] if o.created_at] or [datetime.max]) if earliest_sample_date != datetime.max and first_order_date != datetime.max: diff = (first_order_date - earliest_sample_date).days if diff >= 0: velocities.append(diff) avg_velocity = sum(velocities) / len(velocities) if velocities else 0 conversion_rate = (converted_samples_count / total_samples_count * 100) if total_samples_count > 0 else 0 # 孤兒樣品: > 90天且無訂單 now = datetime.now() orphan_count = 0 for key, group_samples in sample_groups.items(): if key not in order_groups: for s in group_samples: s_date = parse_date(s.date) if s_date and (now - s_date).days > 90: orphan_count += 1 return LabKPI( avg_velocity=round(avg_velocity, 1), conversion_rate=round(conversion_rate, 1), orphan_count=orphan_count ) @router.get("/scatter", response_model=List[ScatterPoint]) def get_scatter_data( start_date: Optional[str] = Query(None), end_date: Optional[str] = Query(None), db: Session = Depends(get_db) ): samples_query = db.query(SampleRecord) orders_query = db.query(OrderRecord) if start_date: samples_query = samples_query.filter(SampleRecord.date >= start_date) if end_date: samples_query = samples_query.filter(SampleRecord.date <= end_date) samples = samples_query.all() orders = orders_query.all() # 聚合資料 data_map = {} # (cust_id, pn) -> {sample_qty, order_qty, customer_name} for s in samples: key = (s.cust_id, s.pn) if key not in data_map: data_map[key] = {"sample_qty": 0, "order_qty": 0, "customer": s.customer} data_map[key]["sample_qty"] += (s.qty or 0) for o in orders: key = (o.cust_id, o.pn) if key in data_map: data_map[key]["order_qty"] += (o.qty or 0) # 如果有訂單但沒樣品,我們在 ROI 分析中可能不顯示,或者顯示在 Y 軸上 X=0。 # 根據需求:分析「樣品寄送」與「訂單接收」的關聯,通常以有送樣的為基底。 return [ ScatterPoint( customer=v["customer"], pn=key[1], sample_qty=v["sample_qty"], order_qty=v["order_qty"] ) for key, v in data_map.items() ] @router.get("/orphans", response_model=List[OrphanSample]) def get_orphans(db: Session = Depends(get_db)): now = datetime.now() threshold_date = now - timedelta(days=90) # 找出所有樣品 samples = db.query(SampleRecord).all() # 找出有訂單的人 (cust_id, pn) orders_keys = set(db.query(OrderRecord.cust_id, OrderRecord.pn).distinct().all()) orphans = [] for s in samples: key = (s.cust_id, s.pn) s_date = parse_date(s.date) if key not in orders_keys: if s_date and s_date < threshold_date: orphans.append(OrphanSample( customer=s.customer, pn=s.pn, days_since_sent=(now - s_date).days, order_no=s.order_no, date=s.date )) return sorted(orphans, key=lambda x: x.days_since_sent, reverse=True)