zlwu hace 5 meses
padre
commit
3f122da803
Se han modificado 55 ficheros con 2675 adiciones y 1669 borrados
  1. 1 1
      backend/app.pid
  2. 42 0
      backend/app/crud/pilot_application.py
  3. 18 0
      backend/app/crud/pilot_infusion_schedule_mgmt.py
  4. 12 3
      backend/app/crud/production_task.py
  5. 14 2
      backend/app/crud/production_work_order.py
  6. 15 6
      backend/app/crud/qc_task.py
  7. 52 15
      backend/app/crud/qc_work_order.py
  8. 0 0
      backend/app/graphify-out/cache/ast/0060ed896e7585c41aba383d817dcd3ce1c0f9228bfd29ef32f3e6dc272721ad.json
  9. 0 0
      backend/app/graphify-out/cache/ast/5b4f71997decb7f914e4cb0f0eb56f4b45a18866d713e754531c2989e1165891.json
  10. 0 0
      backend/app/graphify-out/cache/ast/5cdc06fea3802a54b68af9c54bbb1cfdad87d8bc003e36e8c1d707dff1749b4e.json
  11. 0 0
      backend/app/graphify-out/cache/ast/6f255aaef818389e593f30b6b1278694f465b7be2d13dfe8b5f4c43abb592910.json
  12. 0 0
      backend/app/graphify-out/cache/ast/7c97cce00c2187f390f9dfdddc25ebda970104c93d0fda9cea437602d8790a53.json
  13. 0 0
      backend/app/graphify-out/cache/ast/7f9f24a147dbaa56b977c477cc98948f3bc52ac6bc6e8c8c250ea7dc10df7ab1.json
  14. 0 0
      backend/app/graphify-out/cache/ast/9b3398699e409edf104f1f2add83b2969f5257f7d36b8c11f984bc45520abba7.json
  15. 0 0
      backend/app/graphify-out/cache/ast/9e53f1feb9c0fc87b2049387a14c6e8f764fd906066aa13fa6677bee9823530d.json
  16. 0 0
      backend/app/graphify-out/cache/ast/acf5d67ebfb739c476c87d2f41c7c9652af9271dbc884d0c5b809a71b1b52bd2.json
  17. 0 0
      backend/app/graphify-out/cache/ast/b408dc2f7734497949a08ae95636c49dc8595fc200dc9fbaff7e9c81709981c4.json
  18. 0 0
      backend/app/graphify-out/cache/ast/bfb71d93ffaef4ead493c053ffbc5ad32c4244d27c91d7c830d684400e543887.json
  19. 0 0
      backend/app/graphify-out/cache/ast/e970a910b8209e25684cf80c124cca9fa4b58a476e8231b8d0ce807a8cf3c8e3.json
  20. 0 0
      backend/app/graphify-out/cache/ast/eda6273f0d9170d4185ea8f1228977347a63e253fba25c68b261e7de04fcf63f.json
  21. 0 0
      backend/app/graphify-out/cache/ast/f16092b374c7eee1ee1b640fb45c759cb86741b1e4dcbcf096ea5cfb8c55b8ce.json
  22. 0 0
      backend/app/graphify-out/cache/ast/fcff3a3c5848dc81f500c4c1f40eb5337a9d3e9dd7ba411014d40395bc6bd514.json
  23. 491 271
      backend/graphify-out/.graphify_analysis.json
  24. 312 137
      backend/graphify-out/.graphify_extract.json
  25. 139 114
      backend/graphify-out/GRAPH_REPORT.md
  26. 1 1
      backend/graphify-out/graph.html
  27. 112 112
      backend/graphify-out/graph.json
  28. 11 11
      backend/graphify-out/manifest.json
  29. 441 546
      web-admin/graphify-out/.graphify_analysis.json
  30. 308 107
      web-admin/graphify-out/.graphify_extract.json
  31. 155 79
      web-admin/graphify-out/GRAPH_REPORT.md
  32. 1 1
      web-admin/graphify-out/graph.html
  33. 9 8
      web-admin/graphify-out/manifest.json
  34. 31 1
      web-admin/src/api/production-work-order.ts
  35. 1 0
      web-admin/src/api/qc-work-order.ts
  36. 1 0
      web-admin/src/graphify-out/cache/ast/616b119c1a80f8cdacafd192567f17c1c5846fa57a3a28a90057f45209c294fe.json
  37. 1 0
      web-admin/src/graphify-out/cache/ast/63f687ead14f5ec06abc4f6f2ae1711a348da572e823991ff348959971f80857.json
  38. 0 0
      web-admin/src/graphify-out/cache/ast/af36c0616309a30527735013d4a1e21d9b9955d5fe1cfc1f72c53cfe11b06b3d.json
  39. 0 0
      web-admin/src/graphify-out/cache/ast/b21735e02675abdb668df78cc46d34886c8a0c385368d8ccabedce17cb5a1bfa.json
  40. 0 0
      web-admin/src/graphify-out/cache/ast/b856bf1fd075ba90f88669187c1597ae44335897a9257834779fe0b68e1180d9.json
  41. 0 0
      web-admin/src/graphify-out/cache/ast/b9c8ba969bfdcefada06da2d9277f2c4417f6483df2ad89a4d71ceb66dc01337.json
  42. 1 0
      web-admin/src/graphify-out/cache/ast/c25128d9313e66e30de5f1f9d17605997aee20440f66f785d97e7a5a8299e6a3.json
  43. 0 0
      web-admin/src/graphify-out/cache/ast/d4ceca96746d6d9130d9dcec1b4d45387075d38d605ad4dd5e8cd154f7e8acdb.json
  44. 0 0
      web-admin/src/graphify-out/cache/ast/e11551776d4da80d9182d11a804734b02bca62af70772362866c499f0b664178.json
  45. 0 0
      web-admin/src/graphify-out/cache/ast/f8bd6783f6511ccf88be9968362c3a06d13a3f561107218111633b38addf990a.json
  46. 0 0
      web-admin/src/graphify-out/cache/ast/fc9840e32c212a7150970562c0406e8bddb8f5f92f5ca086e01c261debe7d8f6.json
  47. 120 0
      web-admin/src/utils/outbound-qc-pilot-view-sections.ts
  48. 33 0
      web-admin/src/utils/production-labels.ts
  49. 15 1
      web-admin/src/utils/status-tag-registry.ts
  50. 14 36
      web-admin/src/views/production/production-task/index.vue
  51. 2 1
      web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue
  52. 143 59
      web-admin/src/views/production/production-work-order/index.vue
  53. 150 24
      web-admin/src/views/quality-inspection/qc-task/index.vue
  54. 26 130
      web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue
  55. 3 3
      web-admin/src/views/quality-inspection/qc-work-order/index.vue

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-96950
+10238

+ 42 - 0
backend/app/crud/pilot_application.py

@@ -19,6 +19,7 @@ from app.models.models import (
     BizPilotInfusionScheduleProduct,
     BizPilotPatient,
     BizProductionWorkOrder,
+    BizQcWorkOrder,
 )
 from app.crud import pilot_checkup
 
@@ -1007,6 +1008,25 @@ def auto_generate_schedules(
             del_flag=0,
         )
         db.add(sched)
+        from app.crud import production_work_order as pwo_svc
+
+        pwo_svc.create_outbound_production_work_order(
+            db,
+            schedule_id=sched.id,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            create_dept_id=dept_id,
+            biz_type="pilot_service",
+        )
+        from app.crud import qc_work_order as qc_svc
+
+        qc_svc.create_outbound_qc_work_order(
+            db,
+            schedule_id=sched.id,
+            tenant_no=tenant_no,
+            user_id=user_id,
+            create_dept_id=dept_id,
+        )
 
     db.commit()
     return {"generatedCount": total}
@@ -1103,6 +1123,16 @@ def create_schedule(
         create_dept_id=dept_id,
         biz_type="pilot_service",
     )
+    # 自动创建出库质检工单
+    from app.crud import qc_work_order as qc_svc
+
+    qc_svc.create_outbound_qc_work_order(
+        db,
+        schedule_id=schedule_id,
+        tenant_no=tenant_no,
+        user_id=user_id,
+        create_dept_id=dept_id,
+    )
     if commit:
         db.commit()
         db.refresh(row)
@@ -1439,6 +1469,18 @@ def soft_delete_schedule(
         outbound_wo.work_order_status = "yzf"
         outbound_wo.update_time = now
 
+    for qc_wo in (
+        db.query(BizQcWorkOrder)
+        .filter(
+            BizQcWorkOrder.infusion_schedule_id == schedule_id,
+            BizQcWorkOrder.work_order_status.notin_(["ywc", "yzf"]),
+            BizQcWorkOrder.del_flag == 0,
+        )
+        .all()
+    ):
+        qc_wo.work_order_status = "yzf"
+        qc_wo.update_time = now
+
     db.query(BizPilotInfusionScheduleProduct).filter(
         BizPilotInfusionScheduleProduct.infusion_schedule_id == schedule_id,
         BizPilotInfusionScheduleProduct.tenant_no == tenant_no,

+ 18 - 0
backend/app/crud/pilot_infusion_schedule_mgmt.py

@@ -20,6 +20,7 @@ from app.models.models import (
     BizPilotInfusionSchedule,
     BizPilotInfusionScheduleProduct,
     BizPilotPatient,
+    BizProductionWorkOrder,
     BizQcWorkOrder,
 )
 from app.crud.pilot_service import (
@@ -496,6 +497,23 @@ def update_infusion_status(
     if infusion_status == "yqx":
         row.cancel_reason = cancel_reason or ""
 
+        # 自动作废关联的未完成出库生产工单
+        outbound_wo = (
+            db.query(BizProductionWorkOrder)
+            .filter(
+                BizProductionWorkOrder.infusion_schedule_id == schedule_id,
+                BizProductionWorkOrder.tenant_no == tenant_no,
+                BizProductionWorkOrder.del_flag == 0,
+                BizProductionWorkOrder.work_order_type == "outbound",
+                BizProductionWorkOrder.work_order_status.notin_(["ywc", "yzf"]),
+            )
+            .first()
+        )
+        if outbound_wo:
+            outbound_wo.work_order_status = "yzf"
+            outbound_wo.update_by = user_id
+            outbound_wo.update_time = now
+
         # 自动作废关联的未完成出库质检工单
         pending_qc_orders = (
             db.query(BizQcWorkOrder)

+ 12 - 3
backend/app/crud/production_task.py

@@ -417,8 +417,17 @@ def create_production_task(
             )
             .first()
         )
-        if wo and (getattr(wo, "work_order_type", None) or "inbound") == "temporary":
-            order_source = "temporary"
+        if wo:
+            wo_type = getattr(wo, "work_order_type", None) or "inbound"
+            if wo_type == "temporary":
+                order_source = "temporary"
+            elif wo_type == "outbound":
+                order_source = "outbound"
+
+    task_type = (data.get("task_type") or "").strip() or "cell_storage"
+    if wo and (getattr(wo, "work_order_type", None) or "inbound") == "outbound":
+        if not (data.get("task_type") or "").strip():
+            task_type = "outbound"
 
     task_no = _generate_task_no(db, tenant_no)
     now = datetime.now()
@@ -427,7 +436,7 @@ def create_production_task(
         task_no=task_no,
         work_order_id=data.get("work_order_id", ""),
         order_source=order_source,
-        task_type=data.get("task_type") or "cell_storage",
+        task_type=task_type,
         task_name=(data.get("task_name") or "").strip() or None,
         description=(data.get("description") or "").strip() or None,
         route_id=data.get("route_id") or None,

+ 14 - 2
backend/app/crud/production_work_order.py

@@ -223,6 +223,19 @@ def _attach_outbound_detail(
     db: Session, wo: BizProductionWorkOrder, out: Dict[str, Any]
 ) -> None:
     """outbound 工单详情附加申请单 + 输注计划信息。"""
+    from app.crud.qc_work_order import build_outbound_pilot_context_by_schedule
+
+    out["reception"] = {}
+    schedule_id = getattr(wo, "infusion_schedule_id", None) or ""
+    biz_type = getattr(wo, "biz_type", None) or ""
+
+    if biz_type == "pilot_service" and schedule_id:
+        pilot_ctx = build_outbound_pilot_context_by_schedule(
+            db, schedule_id, wo.tenant_no or ""
+        )
+        out.update(pilot_ctx)
+        return
+
     from app.models.models import (
         BizPilotApplication,
         BizPilotInfusionPlan,
@@ -233,7 +246,7 @@ def _attach_outbound_detail(
 
     sched = (
         db.query(BizPilotInfusionSchedule)
-        .filter(BizPilotInfusionSchedule.id == wo.infusion_schedule_id, BizPilotInfusionSchedule.del_flag == 0)
+        .filter(BizPilotInfusionSchedule.id == schedule_id, BizPilotInfusionSchedule.del_flag == 0)
         .first()
     )
     plan = None
@@ -243,7 +256,6 @@ def _attach_outbound_detail(
     if plan and plan.pilot_application_id:
         app = db.query(BizPilotApplication).filter(BizPilotApplication.id == plan.pilot_application_id).first()
 
-    out["reception"] = {}
     out["pilotApplication"] = {
         "applicationNo": app.application_no if app else "",
         "applicationName": app.application_name if app else "",

+ 15 - 6
backend/app/crud/qc_task.py

@@ -17,6 +17,7 @@ from app.crud.qc_work_order import (
     _reception_source_label,
     _sample_type_name_for_reception,
     _work_order_qc_biz_type,
+    _OUTBOUND_BIZ_TYPE_LABELS,
 )
 from app.models.models import (
     BizCollectionProtocol,
@@ -91,20 +92,28 @@ def _task_to_dict(
 
 def _task_work_order_context(wo: BizQcWorkOrder, t: BizQcTask) -> Dict[str, Any]:
     biz = _work_order_qc_biz_type(wo)
-    order_source = getattr(t, "order_source", "inbound") or "inbound"
     wo_type = getattr(wo, "work_order_type", None) or "inbound"
-    if order_source == "temporary" or wo_type == "temporary":
+    if wo_type == "temporary":
+        order_source = "temporary"
         recv_source = ""
+        recv_label = ""
+    elif wo_type == "outbound":
+        order_source = "outbound"
+        recv_source = getattr(wo, "biz_type", "") or ""
+        recv_label = _OUTBOUND_BIZ_TYPE_LABELS.get(recv_source, recv_source) if recv_source else ""
     else:
+        order_source = getattr(t, "order_source", "inbound") or "inbound"
         recv_source = getattr(wo, "reception_source", "cell_storage") or "cell_storage"
+        recv_label = _reception_source_label(recv_source)
     return {
+        "orderSource": order_source,
         "receptionId": wo.reception_id,
         "protocolNo": wo.protocol_no or "",
         "sampleCode": wo.sample_code or t.sample_code or "",
         "qcBizType": biz,
         "qcBizTypeLabel": _qc_biz_type_label(biz),
         "receptionSource": recv_source,
-        "receptionSourceLabel": _reception_source_label(recv_source) if recv_source else "",
+        "receptionSourceLabel": recv_label,
         "cellProductId": getattr(wo, "cell_product_id", None) or "",
         "cellProductCode": "",
         "cellProductName": "",
@@ -196,9 +205,9 @@ def _enrich_tasks_reception_fields(
         ):
             donation_status_map[d.id] = d.donation_status or ""
     for item in items:
-        if item.get("orderSource") == "temporary":
-            item["receptionSource"] = ""
-            item["receptionSourceLabel"] = ""
+        if item.get("orderSource") in ("temporary", "outbound"):
+            item.setdefault("receptionSource", "")
+            item.setdefault("receptionSourceLabel", "")
             continue
         rec = rec_map.get(item.get("receptionId") or "")
         if not rec:

+ 52 - 15
backend/app/crud/qc_work_order.py

@@ -34,6 +34,7 @@ from app.models.models import (
     BizQcWorkOrderLabItem,
     BizQcWorkOrderThirdItem,
     BizSampleReception,
+    SysUser,
 )
 from app.models.qc_param import BizQcTaskResult
 from app.models.research_donation import BizResearchDonation, BizResearchDonationSample
@@ -41,6 +42,7 @@ from app.models.research_donation import BizResearchDonation, BizResearchDonatio
 _RECEPTION_SOURCE_LABELS = {
     "cell_storage": "细胞存储",
     "research_donation": "科研捐赠",
+    "outbound": "出库",
 }
 _QC_BIZ_TYPE_LABELS = {
     "inbound": "入库质检",
@@ -353,10 +355,19 @@ def _gen_daily_no(db: Session, tenant_no: str, prefix: str) -> str:
     return f"{prefix}{day}{cnt + 1:04d}"
 
 
+def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
+    ids = list({x for x in user_ids if x})
+    if not ids:
+        return {}
+    rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
+    return {u.id: (u.nick_name or "").strip() for u in rows}
+
+
 def _wo_to_dict(
     wo: BizQcWorkOrder,
     *,
     outbound_info: Optional[Dict[str, Any]] = None,
+    configured_by_name: str = "",
 ) -> Dict[str, Any]:
     wo_type = getattr(wo, "work_order_type", None) or "inbound"
     recv_source_raw = getattr(wo, "reception_source", None)
@@ -404,6 +415,7 @@ def _wo_to_dict(
         "hasThirdParty": wo.has_third_party == "1",
         "configuredTime": _fmt_dt(wo.configured_time),
         "configuredBy": wo.configured_by or "",
+        "configuredByName": configured_by_name or wo.configured_by or "",
         "thirdPartyFeeTotal": _dec(wo.third_party_fee_total),
         "remark": wo.remark or "",
         "createTime": _fmt_dt(wo.create_time),
@@ -844,7 +856,13 @@ def configure_work_order(
         raise ValueError("工单已完成,无法调整检测项目")
     if wo.work_order_status not in ("dpz", "ypz"):
         raise ValueError("当前工单状态不可配置检测项目")
-    order_source = "temporary" if (getattr(wo, "work_order_type", None) or "inbound") == "temporary" else "inbound"
+    wo_type = getattr(wo, "work_order_type", None) or "inbound"
+    if wo_type == "temporary":
+        order_source = "temporary"
+    elif wo_type == "outbound":
+        order_source = "outbound"
+    else:
+        order_source = "inbound"
     if wo.work_order_status == "ypz":
         _soft_delete_pending_work_order_tasks(db, wo.id, tenant_no)
 
@@ -1512,29 +1530,18 @@ def delete_temp_qc_work_order(
     db.commit()
 
 
-def get_outbound_qc_detail(
-    db: Session, work_order_id: str, tenant_no: str
+def build_outbound_pilot_context_by_schedule(
+    db: Session, schedule_id: str, tenant_no: str
 ) -> Dict[str, Any]:
-    wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
-    if not wo:
-        raise ValueError("工单不存在")
-    wo_type = getattr(wo, "work_order_type", None) or "inbound"
-    if wo_type != "outbound":
-        raise ValueError("该工单不是出库质检类型")
-
+    """按输注计划 ID 组装出库中试服务上下文(申请单/患者/方案/计划/产品),供质检与生产工单详情复用。"""
     out: Dict[str, Any] = {
-        "workOrder": _wo_to_dict(wo),
         "application": None,
         "patient": None,
         "infusionPlan": None,
         "infusionSchedule": None,
         "products": [],
-        "tasks": [],
     }
-
-    schedule_id = getattr(wo, "infusion_schedule_id", None) or ""
     if not schedule_id:
-        out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
         return out
 
     sched = (
@@ -1637,6 +1644,36 @@ def get_outbound_qc_detail(
         }
         for p in products
     ]
+    return out
+
+
+def get_outbound_qc_detail(
+    db: Session, work_order_id: str, tenant_no: str
+) -> Dict[str, Any]:
+    wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
+    if not wo:
+        raise ValueError("工单不存在")
+    wo_type = getattr(wo, "work_order_type", None) or "inbound"
+    if wo_type != "outbound":
+        raise ValueError("该工单不是出库质检类型")
+
+    configured_by = wo.configured_by or ""
+    configured_by_name = ""
+    if configured_by:
+        configured_by_name = _user_nick_name_map(db, [configured_by]).get(configured_by, "")
+
+    schedule_id = getattr(wo, "infusion_schedule_id", None) or ""
+    pilot_ctx = build_outbound_pilot_context_by_schedule(db, schedule_id, tenant_no)
+
+    out: Dict[str, Any] = {
+        "workOrder": _wo_to_dict(wo, configured_by_name=configured_by_name),
+        **pilot_ctx,
+        "tasks": [],
+    }
+
+    if not schedule_id:
+        out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
+        return out
 
     out["tasks"] = task_svc.list_tasks_by_work_order(db, wo.id, tenant_no)
     return out

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/0060ed896e7585c41aba383d817dcd3ce1c0f9228bfd29ef32f3e6dc272721ad.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/5b4f71997decb7f914e4cb0f0eb56f4b45a18866d713e754531c2989e1165891.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/5cdc06fea3802a54b68af9c54bbb1cfdad87d8bc003e36e8c1d707dff1749b4e.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/6f255aaef818389e593f30b6b1278694f465b7be2d13dfe8b5f4c43abb592910.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/7c97cce00c2187f390f9dfdddc25ebda970104c93d0fda9cea437602d8790a53.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/7f9f24a147dbaa56b977c477cc98948f3bc52ac6bc6e8c8c250ea7dc10df7ab1.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/9b3398699e409edf104f1f2add83b2969f5257f7d36b8c11f984bc45520abba7.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/9e53f1feb9c0fc87b2049387a14c6e8f764fd906066aa13fa6677bee9823530d.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/acf5d67ebfb739c476c87d2f41c7c9652af9271dbc884d0c5b809a71b1b52bd2.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/b408dc2f7734497949a08ae95636c49dc8595fc200dc9fbaff7e9c81709981c4.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/bfb71d93ffaef4ead493c053ffbc5ad32c4244d27c91d7c830d684400e543887.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/e970a910b8209e25684cf80c124cca9fa4b58a476e8231b8d0ce807a8cf3c8e3.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/eda6273f0d9170d4185ea8f1228977347a63e253fba25c68b261e7de04fcf63f.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/f16092b374c7eee1ee1b640fb45c759cb86741b1e4dcbcf096ea5cfb8c55b8ce.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
backend/app/graphify-out/cache/ast/fcff3a3c5848dc81f500c4c1f40eb5337a9d3e9dd7ba411014d40395bc6bd514.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 491 - 271
backend/graphify-out/.graphify_analysis.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 312 - 137
backend/graphify-out/.graphify_extract.json


+ 139 - 114
backend/graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - app  (2026-05-26)
 
 ## Corpus Check
-- 160 files · ~165,399 words
+- 160 files · ~167,784 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 3062 nodes · 13205 edges · 55 communities detected
-- Extraction: 39% EXTRACTED · 61% INFERRED · 0% AMBIGUOUS · INFERRED: 8095 edges (avg confidence: 0.62)
+- 3093 nodes · 13377 edges · 60 communities detected
+- Extraction: 39% EXTRACTED · 61% INFERRED · 0% AMBIGUOUS · INFERRED: 8198 edges (avg confidence: 0.62)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -59,24 +59,29 @@
 - [[_COMMUNITY_Community 46|Community 46]]
 - [[_COMMUNITY_Community 47|Community 47]]
 - [[_COMMUNITY_Community 48|Community 48]]
+- [[_COMMUNITY_Community 49|Community 49]]
+- [[_COMMUNITY_Community 50|Community 50]]
 - [[_COMMUNITY_Community 51|Community 51]]
 - [[_COMMUNITY_Community 52|Community 52]]
 - [[_COMMUNITY_Community 53|Community 53]]
-- [[_COMMUNITY_Community 54|Community 54]]
-- [[_COMMUNITY_Community 55|Community 55]]
 - [[_COMMUNITY_Community 56|Community 56]]
+- [[_COMMUNITY_Community 57|Community 57]]
+- [[_COMMUNITY_Community 58|Community 58]]
+- [[_COMMUNITY_Community 59|Community 59]]
+- [[_COMMUNITY_Community 60|Community 60]]
+- [[_COMMUNITY_Community 61|Community 61]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `success_response()` - 501 edges
-2. `error_response()` - 400 edges
-3. `SysUser` - 315 edges
-4. `BizQcTaskResult` - 245 edges
+1. `success_response()` - 502 edges
+2. `error_response()` - 401 edges
+3. `SysUser` - 323 edges
+4. `BizQcTaskResult` - 246 edges
 5. `BizQcMaterialUsage` - 244 edges
 6. `BizMaterialStockLog` - 240 edges
 7. `BizQcMaterial` - 240 edges
 8. `BizMaterial` - 237 edges
 9. `BizQcItemParam` - 235 edges
-10. `generate_uuid()` - 167 edges
+10. `generate_uuid()` - 168 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog`  [INFERRED]
@@ -94,264 +99,284 @@
 
 ### Community 0 - "Community 0"
 Cohesion: 0.02
-Nodes (546): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+538 more)
+Nodes (553): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+545 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.01
-Nodes (463): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+455 more)
+Nodes (473): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+465 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.04
-Nodes (382): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+374 more)
+Nodes (363): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+355 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.05
-Nodes (165): generate_uuid(), _deduct_fee_on_delivery(), 中试交付管理 CRUD(交付信息、交付物料、交付方式 + 费用扣减), 创建交付时扣减费用。      流程:     1. schedule → plan → application     2. application.chan, List all deliveries for an application (through plan → schedule → delivery)., Bulk save: soft-delete existing + insert new., Create or update delivery method (upsert pattern)., 中试输注计划管理 CRUD(输注计划列表、详情、状态变更、编辑) (+157 more)
+Cohesion: 0.02
+Nodes (252): api_save_application(), delete(), get_multi_with_filters(), _format_attachment_file_size(), get_protocol_nos_used_in_protocols(), urls_to_attachment_list(), create_dict_type(), dict_type_code_exists() (+244 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.02
-Nodes (142): get_designer_data(), ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, delete() (+134 more)
+Cohesion: 0.06
+Nodes (156): generate_uuid(), activate_channel_agreement(), _apply_gmcp_order_quotas(), _apply_purchase_txn_holdings(), _apply_unsubscribe_txn_holdings(), _calc_gmcp_purchase_total(), cell_product_price_code_exists(), cell_product_spec_code_exists() (+148 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.02
-Nodes (116): get_by_schedule(), get_method(), list_by_application(), api_detail(), api_list(), generate_id(), camel_to_snake(), CRUDBase (+108 more)
+Cohesion: 0.03
+Nodes (105): _leave_local_callback(), submit_leave(), get_designer_data(), ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody (+97 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.03
-Nodes (128): create_collection_protocol(), delete_collection_protocol(), _ensure_customer_for_mother(), get_collection_protocol_detail(), _get_current_user_id(), _get_tenant_no(), list_collection_protocol_audits(), list_collection_protocols() (+120 more)
+Cohesion: 0.04
+Nodes (121): create_collection_protocol(), delete_collection_protocol(), _ensure_customer_for_mother(), get_collection_protocol_detail(), _get_current_user_id(), _get_tenant_no(), list_collection_protocol_audits(), list_collection_protocols() (+113 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.03
-Nodes (60): _error_json(), 数据库异常:返回 JSON 并保留 CORS 头,避免浏览器只报跨域错误, 未捕获异常:统一 JSON 500,确保跨域场景下仍有 Access-Control-Allow-Origin, sqlalchemy_exception_handler(), unhandled_exception_handler(), BaseHTTPMiddleware, ApiCryptoError, _b64url_decode() (+52 more)
+Nodes (93): get_role_menus(), _avatar_url_field(), _build_user_info_payload(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+85 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.04
-Nodes (102): create_lab_item(), get_lab_item(), item_code_exists(), list_active_lab_items(), list_lab_items(), _row_to_dict(), soft_delete_lab_item(), update_lab_item() (+94 more)
+Cohesion: 0.05
+Nodes (79): get_next_cell_product_code(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_products_batch(), create_cell_products_by_quantity(), delete_cell_product() (+71 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.06
-Nodes (69): _format_attachment_file_size(), urls_to_attachment_list(), _production_conclusion_label(), _accept_conclusion_label(), _attach_outbound_detail(), _attach_reception_detail(), complete_production_work_order(), configure_temp_production_work_order() (+61 more)
+Cohesion: 0.07
+Nodes (67): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, OccupancyCreateBody, 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+59 more)
 
 ### Community 10 - "Community 10"
 Cohesion: 0.35
 Nodes (61): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+53 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.05
-Nodes (59): list_roles_for_select(), _avatar_url_field(), _build_user_info_payload(), get_web_auth_menu_codes(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。 (+51 more)
+Cohesion: 0.07
+Nodes (13): BaseStorage, convert_heic_to_jpeg(), get_storage(), LocalStorage, MinIOStorage, OSSStorage, process_image_content(), QiniuStorage (+5 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.09
-Nodes (44): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), _resolve_receiver_purpose(), revert_step_stock_changes(), _row_to_dict(), _user_nick_name_map() (+36 more)
+Cohesion: 0.11
+Nodes (40): create_collection_protocol(), _active_link_query(), assert_biz_no_available(), create_convert_link(), get_convert_partner(), get_partner_sample_codes(), has_active_convert_link(), upsert_convert_link_for_to_storage() (+32 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.11
-Nodes (43): create_collection_protocol(), _active_link_query(), assert_biz_no_available(), create_convert_link(), get_convert_partner(), get_partner_sample_codes(), has_active_convert_link(), upsert_convert_link_for_to_storage() (+35 more)
+Cohesion: 0.12
+Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.1
-Nodes (34): get_next_cell_product_code(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_products_batch(), create_cell_products_by_quantity(), delete_cell_product() (+26 more)
+Cohesion: 0.13
+Nodes (22): _accept_conclusion_label(), _base_query(), _ensure_notification(), _fmt_dt(), generate_sms_content(), list_notifications(), _normalize_date_bound(), resolve_notification() (+14 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.13
-Nodes (33): api_save_application(), auto_generate_schedules(), complete_application(), create_application(), create_infusion_plan(), create_schedule(), _create_schedule_products(), _format_yyyy_mm_dd() (+25 more)
+Cohesion: 0.18
+Nodes (24): add_step(), copy_step(), create_route(), _create_step_consumable(), _create_step_data_collection(), _create_step_output(), _create_step_reagent(), get_route() (+16 more)
 
 ### Community 16 - "Community 16"
-Cohesion: 0.12
-Nodes (11): get_wechat_pay_client(), get_wechat_pay_v2(), 微信支付工具模块  支持微信支付 V3 API 文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.ht, 统一下单接口 (V3)          Args:             out_trade_no: 商户订单号             total, 申请退款          Args:             out_trade_no: 商户订单号             total_amount, 小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致), 统一下单 (V2 API)          Args:             out_trade_no: 商户订单号             tot, 生成签名 V3 (SHA256 with RSA) (+3 more)
+Cohesion: 0.17
+Nodes (12): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+4 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.16
-Nodes (26): add_step(), copy_step(), create_route(), _create_step_consumable(), _create_step_data_collection(), _create_step_output(), _create_step_reagent(), get_route() (+18 more)
-
-### Community 18 - "Community 18"
 Cohesion: 0.21
 Nodes (19): _consumable_row_to_dict(), create_process(), _create_sub_consumable(), _create_sub_data_collection(), _create_sub_output(), _create_sub_reagent(), _data_collection_row_to_dict(), get_process() (+11 more)
 
+### Community 18 - "Community 18"
+Cohesion: 0.22
+Nodes (18): _create_settlement_txn(), _deduct_fee_on_complete(), _find_existing_settlement_txn(), _find_service_order_product(), get_infusion_schedule_detail(), _get_plan_schedule_course_meta(), _get_products_for_schedule(), list_infusion_schedules_mgmt() (+10 more)
+
 ### Community 19 - "Community 19"
-Cohesion: 0.25
-Nodes (14): batch_stock_in(), batch_stock_out(), create_reagent(), _dec(), get_reagent(), _is_enabled_status(), list_reagents(), _norm_status() (+6 more)
+Cohesion: 0.23
+Nodes (14): batch_stock_in(), batch_stock_out(), consumable_code_exists(), create_consumable(), _dec(), get_consumable(), _is_enabled_status(), list_consumables() (+6 more)
 
 ### Community 20 - "Community 20"
-Cohesion: 0.21
-Nodes (13): create_collection_region(), create_hospital(), get_collection_region(), get_hospital(), _hospital_names_by_region_ids(), _hospital_to_dict(), list_collection_regions(), list_hospitals() (+5 more)
+Cohesion: 0.25
+Nodes (14): batch_stock_in(), batch_stock_out(), create_reagent(), _dec(), get_reagent(), _is_enabled_status(), list_reagents(), _norm_status() (+6 more)
 
 ### Community 21 - "Community 21"
-Cohesion: 0.25
-Nodes (13): batch_issue_protocol_nos(), batch_revoke_protocol_nos(), create_protocol_no(), get_protocol_no(), list_protocol_nos(), _parse_issued_time(), protocol_no_exists(), 采集协议号池 CRUD(biz_collection_protocol_no) (+5 more)
+Cohesion: 0.21
+Nodes (13): get_method(), create_delivery(), _deduct_fee_on_delivery(), _generate_delivery_no(), get_delivery_by_schedule(), get_delivery_method(), list_deliveries_by_application(), list_delivery_materials() (+5 more)
 
 ### Community 22 - "Community 22"
-Cohesion: 0.26
-Nodes (11): _base_query(), _ensure_notification(), _fmt_dt(), generate_sms_content(), list_notifications(), _normalize_date_bound(), resolve_notification(), _row_to_dict() (+3 more)
+Cohesion: 0.25
+Nodes (13): code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), _norm_status(), options_list() (+5 more)
 
 ### Community 23 - "Community 23"
-Cohesion: 0.26
-Nodes (12): batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), 样本编码池 CRUD(biz_sample_code), _resolve_issued_user_display_name() (+4 more)
+Cohesion: 0.25
+Nodes (13): batch_issue_protocol_nos(), batch_revoke_protocol_nos(), create_protocol_no(), get_protocol_no(), list_protocol_nos(), _parse_issued_time(), protocol_no_exists(), 采集协议号池 CRUD(biz_collection_protocol_no) (+5 more)
 
 ### Community 24 - "Community 24"
-Cohesion: 0.27
-Nodes (11): _accept_conclusion_label(), _base_query(), _ensure_notification(), _fmt_dt(), generate_sms_content(), list_notifications(), _normalize_date_bound(), resolve_notification() (+3 more)
+Cohesion: 0.26
+Nodes (12): batch_issue_sample_codes(), batch_revoke_sample_codes(), create_sample_code(), get_sample_code(), list_sample_codes(), _parse_issued_time(), 样本编码池 CRUD(biz_sample_code), _resolve_issued_user_display_name() (+4 more)
 
 ### Community 25 - "Community 25"
+Cohesion: 0.23
+Nodes (12): create_collection_region(), create_hospital(), get_collection_region(), get_hospital(), _hospital_names_by_region_ids(), _hospital_to_dict(), list_collection_regions(), list_hospitals() (+4 more)
+
+### Community 26 - "Community 26"
 Cohesion: 0.24
 Nodes (9): 微信消息通知服务  支持微信小程序模板消息推送  使用方式:     from app.core.wechat_message import Wech, 发送模板消息                  Args:             openid: 用户openid             templ, send_delivery_notice(), send_delivery_warning(), send_order_status(), send_payment_success(), send_refund_result(), send_urge_notification() (+1 more)
 
-### Community 26 - "Community 26"
+### Community 27 - "Community 27"
 Cohesion: 0.23
 Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
 
-### Community 27 - "Community 27"
+### Community 28 - "Community 28"
 Cohesion: 0.31
 Nodes (12): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), options_list(), _row_to_dict() (+4 more)
 
-### Community 28 - "Community 28"
-Cohesion: 0.3
-Nodes (10): create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists(), _row_to_dict() (+2 more)
-
 ### Community 29 - "Community 29"
-Cohesion: 0.27
-Nodes (11): _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree(), 保留匹配节点及其全部上级,供关键词检索后仍能正确组树。 (+3 more)
+Cohesion: 0.24
+Nodes (12): list_depts(), _build_tree(), _cascade_soft_delete(), _collect_with_ancestors(), create_dept(), get_dept(), list_depts(), list_depts_tree() (+4 more)
 
 ### Community 30 - "Community 30"
-Cohesion: 0.29
-Nodes (9): _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict(), soft_delete_menu() (+1 more)
+Cohesion: 0.26
+Nodes (10): list_menus(), _build_tree(), _cascade_soft_delete(), create_menu(), get_menu(), list_menus(), list_menus_tree(), _row_to_dict() (+2 more)
 
 ### Community 31 - "Community 31"
-Cohesion: 0.31
-Nodes (8): create_express_company(), express_code_exists(), get_express_company(), list_express_companies(), 快递公司 CRUD(biz_express_company), _row_to_dict(), soft_delete_express_company(), update_express_company()
+Cohesion: 0.3
+Nodes (10): create_consumable_pack(), get_consumable_pack(), get_pack_detail(), _item_row_to_dict(), list_all_packs(), list_consumable_packs(), pack_code_exists(), _row_to_dict() (+2 more)
 
 ### Community 32 - "Community 32"
-Cohesion: 0.38
-Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
+Cohesion: 0.24
+Nodes (9): dict_data_options(), create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), 字典数据 CRUD(sys_dict_data), _row_to_dict(), soft_delete_dict_data() (+1 more)
 
 ### Community 33 - "Community 33"
-Cohesion: 0.27
-Nodes (8): create_dict_data(), get_dict_data(), list_dict_data(), list_dict_data_options(), 字典数据 CRUD(sys_dict_data), _row_to_dict(), soft_delete_dict_data(), update_dict_data()
+Cohesion: 0.31
+Nodes (8): create_sop_document(), get_sop_document(), list_sop_documents(), SOP文档 CRUD(biz_sop_document), _row_to_dict(), soft_delete_sop_document(), sop_code_exists(), update_sop_document()
 
 ### Community 34 - "Community 34"
+Cohesion: 0.36
+Nodes (9): deduct_for_task(), _display_remark(), list_usages(), list_usages_by_task(), _parse_receiver_purpose_from_remark(), _resolve_receiver_purpose(), revert_task_material_usages(), _row_to_dict() (+1 more)
+
+### Community 35 - "Community 35"
+Cohesion: 0.31
+Nodes (8): create_express_company(), express_code_exists(), get_express_company(), list_express_companies(), 快递公司 CRUD(biz_express_company), _row_to_dict(), soft_delete_express_company(), update_express_company()
+
+### Community 36 - "Community 36"
+Cohesion: 0.38
+Nodes (9): create_check_item(), get_check_item(), item_code_exists(), list_active_check_items(), list_check_items(), _order_query(), _row_to_dict(), soft_delete_check_item() (+1 more)
+
+### Community 37 - "Community 37"
 Cohesion: 0.31
 Nodes (8): category_code_exists(), create_process_category(), get_process_category(), list_process_categories(), 工艺工序分类 CRUD(biz_process_category), _row_to_dict(), soft_delete_process_category(), update_process_category()
 
-### Community 35 - "Community 35"
+### Community 38 - "Community 38"
+Cohesion: 0.31
+Nodes (8): create_item_unit(), get_item_unit(), list_item_units(), 物品单位 CRUD(biz_item_unit), _row_to_dict(), soft_delete_item_unit(), unit_code_exists(), update_item_unit()
+
+### Community 39 - "Community 39"
 Cohesion: 0.31
 Nodes (8): create_cell_sample_type(), get_cell_sample_type(), list_cell_sample_types(), 细胞样本类型 CRUD(biz_cell_sample_type), _row_to_dict(), soft_delete_cell_sample_type(), type_code_exists(), update_cell_sample_type()
 
-### Community 36 - "Community 36"
+### Community 40 - "Community 40"
 Cohesion: 0.31
 Nodes (8): create_output_type(), get_output_type(), list_output_types(), 工艺产出物类型 CRUD(biz_output_type), _row_to_dict(), soft_delete_output_type(), type_code_exists(), update_output_type()
 
-### Community 37 - "Community 37"
+### Community 41 - "Community 41"
 Cohesion: 0.36
 Nodes (7): create_post(), get_post(), list_posts(), post_code_exists(), _row_to_dict(), soft_delete_post(), update_post()
 
-### Community 38 - "Community 38"
+### Community 42 - "Community 42"
 Cohesion: 0.36
 Nodes (7): create_tenant(), get_tenant(), list_tenants(), _row_to_dict(), soft_delete_tenant(), tenant_no_exists(), update_tenant()
 
-### Community 39 - "Community 39"
+### Community 43 - "Community 43"
 Cohesion: 0.36
 Nodes (7): create_site(), get_site(), list_sites(), _row_to_dict(), site_code_exists(), soft_delete_site(), update_site()
 
-### Community 40 - "Community 40"
+### Community 44 - "Community 44"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 41 - "Community 41"
+### Community 45 - "Community 45"
 Cohesion: 0.43
 Nodes (7): confirm_infusion(), create_infusion_confirm(), delete_infusion_confirm(), get_confirm_by_id(), list_confirms_by_application(), _row_to_dict(), update_infusion_confirm()
 
-### Community 42 - "Community 42"
+### Community 46 - "Community 46"
+Cohesion: 0.43
+Nodes (7): _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), _resolve_receiver_purpose(), revert_step_stock_changes(), _row_to_dict(), _user_nick_name_map()
+
+### Community 47 - "Community 47"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 43 - "Community 43"
+### Community 48 - "Community 48"
 Cohesion: 0.53
 Nodes (5): create_release_review(), delete_release_review(), list_reviews_by_schedule(), _row_to_dict(), update_review_conclusion()
 
-### Community 44 - "Community 44"
+### Community 49 - "Community 49"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 45 - "Community 45"
+### Community 50 - "Community 50"
 Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
-### Community 46 - "Community 46"
+### Community 51 - "Community 51"
 Cohesion: 0.67
 Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)
 
-### Community 47 - "Community 47"
+### Community 52 - "Community 52"
 Cohesion: 1.0
 Nodes (1): 请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
 
-### Community 48 - "Community 48"
+### Community 53 - "Community 53"
 Cohesion: 1.0
 Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
 
-### Community 51 - "Community 51"
+### Community 56 - "Community 56"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 52 - "Community 52"
+### Community 57 - "Community 57"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 53 - "Community 53"
+### Community 58 - "Community 58"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 54 - "Community 54"
+### Community 59 - "Community 59"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 55 - "Community 55"
+### Community 60 - "Community 60"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 56 - "Community 56"
+### Community 61 - "Community 61"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
 - **90 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+85 more)
   These have ≤1 connection - possible missing edges or undocumented components.
-- **Thin community `Community 40`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
+- **Thin community `Community 44`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 45`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
+- **Thin community `Community 50`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 46`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
+- **Thin community `Community 51`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 47`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
+- **Thin community `Community 52`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 48`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
+- **Thin community `Community 53`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 56`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 52`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 57`** (1 nodes): `发送支付成功通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 53`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
+- **Thin community `Community 58`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 59`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 55`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 60`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 56`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 61`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `SysUser` connect `Community 1` to `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 10`, `Community 11`, `Community 21`, `Community 23`?**
-  _High betweenness centrality (0.101) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 4`, `Community 5`, `Community 6`, `Community 11`, `Community 46`, `Community 15`, `Community 14`?**
+- **Why does `SysUser` connect `Community 1` to `Community 2`, `Community 4`, `Community 5`, `Community 7`, `Community 9`, `Community 10`, `Community 23`, `Community 24`?**
   _High betweenness centrality (0.087) - this node is a cross-community bridge._
-- **Why does `generate_uuid()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 8`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 24`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 41`, `Community 43`?**
-  _High betweenness centrality (0.077) - this node is a cross-community bridge._
-- **Are the 500 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
-  _`success_response()` has 500 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 473 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `extract_login_credentials()`) actually correct?**
-  _`str` has 473 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 399 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
-  _`error_response()` has 399 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 313 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
-  _`SysUser` has 313 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `generate_uuid()` connect `Community 4` to `Community 0`, `Community 1`, `Community 2`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 12`, `Community 14`, `Community 15`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 24`, `Community 25`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 40`, `Community 41`, `Community 42`, `Community 43`, `Community 45`, `Community 48`?**
+  _High betweenness centrality (0.083) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 32`, `Community 1`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 51`, `Community 21`, `Community 29`, `Community 30`?**
+  _High betweenness centrality (0.078) - this node is a cross-community bridge._
+- **Are the 501 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
+  _`success_response()` has 501 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 477 inferred relationships involving `str` (e.g. with `unhandled_exception_handler()` and `extract_login_credentials()`) actually correct?**
+  _`str` has 477 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 400 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
+  _`error_response()` has 400 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 321 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
+  _`SysUser` has 321 INFERRED edges - model-reasoned connections that need verification._

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
backend/graphify-out/graph.html


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 112 - 112
backend/graphify-out/graph.json


+ 11 - 11
backend/graphify-out/manifest.json

@@ -25,7 +25,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/core/wechat_message.py": 1778578258.0458734,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/material.py": 1779283385.245847,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/qc_param.py": 1779022399.0175831,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1779791079.2238667,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1779792400.2401285,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/__init__.py": 1779418058.876012,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/research_donation.py": 1779177348.5945773,
   "/Users/wuzilong/work/00code/zkxw/backend/app/models/qc_material.py": 1779284411.5179992,
@@ -57,14 +57,14 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/category_parameter.py": 1778578258.0944862,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/common.py": 1778578258.094703,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/unit.py": 1778578258.0996897,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/qc_work_order.py": 1779614955.9010859,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/qc_work_order.py": 1779791557.200459,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product.py": 1778578258.0968297,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/workflow.py": 1778633845.5937889,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_review.py": 1778578258.098316,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/category.py": 1778578258.0941947,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/leave.py": 1778666088.7680702,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_release_review.py": 1779616246.7074304,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1779612097.0528247,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1779800623.8611279,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_route.py": 1779587424.6859367,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_code.py": 1779279460.690872,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task_result.py": 1779331731.6720955,
@@ -72,7 +72,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/post.py": 1779265648.38155,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_result.py": 1779255909.8554008,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/user.py": 1778775532.8870275,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779789790.655923,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779801387.303171,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/material.py": 1779283386.3879013,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/dict_type.py": 1779265621.5212462,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_occupancy.py": 1779418213.0446064,
@@ -81,19 +81,19 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/tenant.py": 1779265611.0532296,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/consumable.py": 1779512620.86979,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sop_document.py": 1779265721.9928973,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779692903.0248797,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779801612.283878,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_third_party_item.py": 1779265661.482352,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/reagent.py": 1779512669.9847593,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_protocol.py": 1779692218.216856,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_material_usage.py": 1779332036.2049248,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_infusion_confirm.py": 1779657020.847086,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/reagent_stock_log.py": 1779515479.6599197,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_infusion_schedule_mgmt.py": 1779787101.7686176,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_infusion_schedule_mgmt.py": 1779794445.3921702,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/clean_room.py": 1778775238.2993922,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/__init__.py": 1778605691.3816392,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_entry_sms_notification.py": 1779691734.0972729,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/express_company.py": 1779265659.3453207,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_application.py": 1779789964.587889,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_application.py": 1779794392.4726036,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_lab_test_item.py": 1779265673.3888667,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/role.py": 1779265616.3701816,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_product.py": 1779600789.9863036,
@@ -109,9 +109,9 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_checkup.py": 1779767080.896167,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process_category.py": 1779265700.0606413,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/item_unit.py": 1779265653.7504313,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_work_order.py": 1779791349.4892187,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_work_order.py": 1779801381.700379,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process.py": 1779309113.0447981,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779713509.1784575,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779792399.6927543,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer.py": 1779686149.7508218,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/workflow.py": 1779265993.677624,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_sms_notification.py": 1779691673.794283,
@@ -133,14 +133,14 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/outbound_release_review.py": 1779656994.641828,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/public.py": 1779313409.0685387,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/material.py": 1779526648.041865,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779791336.1861324,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779791675.3703022,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1779692218.217267,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1779686149.7501295,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_confirm.py": 1779657043.1389203,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_infusion_schedule_mgmt.py": 1779781585.2199733,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1779771972.6553016,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_entry_sms_notification.py": 1779691738.9150457,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_application.py": 1779760994.8401182,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_application.py": 1779791571.100721,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/operations.py": 1779685679.1959367,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/menu.py": 1779263225.4964793,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/pilot_delivery.py": 1779655584.820777,

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 441 - 546
web-admin/graphify-out/.graphify_analysis.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 308 - 107
web-admin/graphify-out/.graphify_extract.json


+ 155 - 79
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
 # Graph Report - src  (2026-05-26)
 
 ## Corpus Check
-- Large corpus: 631 files · ~389,603 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 632 files · ~389,944 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 1809 nodes · 2342 edges · 36 communities detected
-- Extraction: 77% EXTRACTED · 23% INFERRED · 0% AMBIGUOUS · INFERRED: 527 edges (avg confidence: 0.8)
+- 1813 nodes · 2357 edges · 48 communities detected
+- Extraction: 77% EXTRACTED · 23% INFERRED · 0% AMBIGUOUS · INFERRED: 538 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -25,6 +25,7 @@
 - [[_COMMUNITY_Community 13|Community 13]]
 - [[_COMMUNITY_Community 14|Community 14]]
 - [[_COMMUNITY_Community 15|Community 15]]
+- [[_COMMUNITY_Community 16|Community 16]]
 - [[_COMMUNITY_Community 17|Community 17]]
 - [[_COMMUNITY_Community 18|Community 18]]
 - [[_COMMUNITY_Community 19|Community 19]]
@@ -33,22 +34,33 @@
 - [[_COMMUNITY_Community 22|Community 22]]
 - [[_COMMUNITY_Community 23|Community 23]]
 - [[_COMMUNITY_Community 24|Community 24]]
+- [[_COMMUNITY_Community 25|Community 25]]
 - [[_COMMUNITY_Community 26|Community 26]]
-- [[_COMMUNITY_Community 27|Community 27]]
+- [[_COMMUNITY_Community 28|Community 28]]
 - [[_COMMUNITY_Community 29|Community 29]]
 - [[_COMMUNITY_Community 30|Community 30]]
-- [[_COMMUNITY_Community 31|Community 31]]
 - [[_COMMUNITY_Community 32|Community 32]]
 - [[_COMMUNITY_Community 33|Community 33]]
+- [[_COMMUNITY_Community 34|Community 34]]
+- [[_COMMUNITY_Community 35|Community 35]]
 - [[_COMMUNITY_Community 36|Community 36]]
 - [[_COMMUNITY_Community 37|Community 37]]
 - [[_COMMUNITY_Community 38|Community 38]]
-- [[_COMMUNITY_Community 40|Community 40]]
+- [[_COMMUNITY_Community 39|Community 39]]
+- [[_COMMUNITY_Community 41|Community 41]]
 - [[_COMMUNITY_Community 42|Community 42]]
+- [[_COMMUNITY_Community 43|Community 43]]
+- [[_COMMUNITY_Community 44|Community 44]]
+- [[_COMMUNITY_Community 46|Community 46]]
+- [[_COMMUNITY_Community 47|Community 47]]
+- [[_COMMUNITY_Community 48|Community 48]]
+- [[_COMMUNITY_Community 49|Community 49]]
+- [[_COMMUNITY_Community 51|Community 51]]
+- [[_COMMUNITY_Community 53|Community 53]]
 
 ## God Nodes (most connected - your core abstractions)
 1. `get()` - 193 edges
-2. `push()` - 32 edges
+2. `push()` - 33 edges
 3. `WebSocketClient` - 26 edges
 4. `withFullscreenLoading()` - 16 edges
 5. `MenuProcessor` - 16 edges
@@ -56,202 +68,266 @@
 7. `emit()` - 15 edges
 8. `handleDynamicRoutes()` - 15 edges
 9. `TableCache` - 14 edges
-10. `StorageCompatibilityManager` - 12 edges
+10. `buildOutboundPilotContextSections()` - 12 edges
 
 ## Surprising Connections (you probably didn't know these)
-- `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
-  views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
-- `handleBatchDelete()` --calls--> `withFullscreenLoading()`  [INFERRED]
-  views/basic-info/collection-region/index.vue → utils/fullscreen-loading.ts
 - `handleAttachmentDownload()` --calls--> `downloadAttachmentFile()`  [INFERRED]
-  views/cell-storage/payment-confirm/index.vue → utils/download-attachment.ts
+  views/cell-storage/refund-confirm/index.vue → utils/download-attachment.ts
 - `subscribeTokenRefresh()` --calls--> `push()`  [INFERRED]
   utils/http/index.ts → views/operations/storage-facility/index.vue
 - `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()`  [INFERRED]
   utils/http/index.ts → api/auth.ts
+- `fetchLeaveList()` --calls--> `get()`  [INFERRED]
+  api/leave.ts → views/production/process-route/index.vue
+- `fetchLeaveDetail()` --calls--> `get()`  [INFERRED]
+  api/leave.ts → views/production/process-route/index.vue
 
 ## Communities
 
 ### Community 0 - "Community 0"
 Cohesion: 0.01
-Nodes (141): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList() (+133 more)
+Nodes (93): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList() (+85 more)
 
 ### Community 1 - "Community 1"
-Cohesion: 0.01
-Nodes (6): fetchDeleteProjectSystemItem(), fetchGetRoleList(), fetchNotDeletedRegions(), fetchRegionList(), loadRoleList(), if()
+Cohesion: 0.03
+Nodes (62): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+54 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.03
-Nodes (60): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+52 more)
+Nodes (39): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+31 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.03
-Nodes (40): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+32 more)
+Nodes (61): deletePilotCheckupActualItem(), fetchFullmoonTodoList(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail(), fetchPilotCheckupReserveItems(), fetchUpdateFullmoonTodoInsurance(), markPilotCheckupChecked(), savePilotCheckupActualItems() (+53 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.04
-Nodes (67): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchCheckupInstitutionProjects(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail() (+59 more)
+Cohesion: 0.03
+Nodes (22): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllReagentStockLogs() (+14 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.04
-Nodes (38): fetchUploadAttachment(), FireworkSystem, reload(), handleReportUpload(), handleReportUpload(), handleAttachmentChange(), handleMenuJump(), openExternalLink() (+30 more)
+Cohesion: 0.05
+Nodes (45): batchSavePilotCheckup(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchCheckupInstitutionProjects(), savePilotCheckup(), savePilotCheckupReserveItems(), appendCheckupReserveItems() (+37 more)
 
 ### Community 6 - "Community 6"
 Cohesion: 0.04
-Nodes (33): handleBlur(), handleFocus(), handleInput(), fetchEthnicList(), auditorDisplay(), dash(), expressName(), formatYm() (+25 more)
+Nodes (35): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchCreateMenu(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchDeleteMenu(), fetchHospitalList() (+27 more)
 
 ### Community 7 - "Community 7"
 Cohesion: 0.05
 Nodes (35): fetchServiceOrderTxnList(), fetchCollectionProtocolAuditList(), fetchCollectionProtocolList(), fetchPaymentDetailList(), fetchReceiptConfirmedList(), fetchReceiptConfirmList(), fetchReceiptConfirmSubmit(), fetchRefundConfirmedList() (+27 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.06
-Nodes (11): fetchGetLatestUpgradeLog(), getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager (+3 more)
-
-### Community 9 - "Community 9"
 Cohesion: 0.07
 Nodes (8): updateInfusionScheduleStatus(), handleSubmit(), handleReset(), handleSearch(), WebSocketClient, TableCache, emit(), handleSubmit()
 
-### Community 10 - "Community 10"
+### Community 9 - "Community 9"
 Cohesion: 0.05
-Nodes (29): fetchCreateCustomer(), fetchCreateMenu(), fetchCustomerList(), fetchDeleteCustomer(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateCustomer(), fetchUpdateMenu() (+21 more)
+Nodes (25): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleDownloadAttachment() (+17 more)
 
-### Community 11 - "Community 11"
+### Community 10 - "Community 10"
 Cohesion: 0.06
-Nodes (39): completePilotApplication(), deletePilotApplication(), fetchFullmoonTodoList(), fetchPilotApplicationList(), fetchUpdateFullmoonTodoInsurance(), startPilotApplication(), voidPilotApplication(), fetchCollectionProtocolDetail() (+31 more)
+Nodes (33): fetchCreateSite(), fetchCreateUser(), fetchDeleteSite(), fetchDeleteUser(), fetchResetPassword(), fetchSiteList(), fetchUpdateSite(), fetchUpdateUser() (+25 more)
+
+### Community 11 - "Community 11"
+Cohesion: 0.07
+Nodes (21): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+13 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.08
-Nodes (32): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+24 more)
+Cohesion: 0.07
+Nodes (22): handleBlur(), handleFocus(), handleInput(), fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport() (+14 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.07
-Nodes (19): fetchAttachmentDownloadUrl(), handleDownloadAttachment(), handleReceptionAttachmentDownload(), attachmentFullUrl(), handleAttachmentDownload(), handleAttachmentPreview(), formatDetectMethods(), fullUrl() (+11 more)
+Cohesion: 0.08
+Nodes (32): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+24 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.1
-Nodes (19): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+11 more)
+Cohesion: 0.07
+Nodes (21): for(), set(), walk(), annotateForkEdges(), chainApiToElements(), nextAncestorLink(), if(), ensureCategoryPathDataLoaded() (+13 more)
 
 ### Community 15 - "Community 15"
-Cohesion: 0.1
-Nodes (23): fetchCreateUser(), fetchDeleteUser(), fetchDeptTree(), fetchPostBoundUserIds(), fetchResetPassword(), fetchRoleOptions(), fetchUpdateUser(), fetchUserDetail() (+15 more)
+Cohesion: 0.09
+Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
+
+### Community 16 - "Community 16"
+Cohesion: 0.09
+Nodes (9): fetchGetAllUpgradeLogs(), fetchGetChinaMapData(), fetchGetComments(), fetchGetCurrentVersion(), fetchGetLatestUpgradeLog(), fetchGetSystemConfig(), fetchGetUpgradeLogs(), systemUpgrade() (+1 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.11
-Nodes (18): fetchCreateSite(), fetchDeleteSite(), fetchDictDataOptions(), fetchSiteList(), fetchUpdateSite(), fetchUserList(), formatContactUserLabel(), handleAdd() (+10 more)
+Cohesion: 0.09
+Nodes (11): maskIdCard(), buildOutboundPilotContextSections(), applicationStatusLabel(), applicationStatusTagType(), infusionMethodLabel(), infusionStatusLabel(), infusionStatusTagType(), qcStatusLabel() (+3 more)
 
 ### Community 18 - "Community 18"
+Cohesion: 0.13
+Nodes (17): completePilotApplication(), deletePilotApplication(), fetchPilotApplicationList(), startPilotApplication(), voidPilotApplication(), handleBatchComplete(), handleBatchStart(), handleBatchVoid() (+9 more)
+
+### Community 19 - "Community 19"
 Cohesion: 0.12
 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
-### Community 19 - "Community 19"
+### Community 20 - "Community 20"
 Cohesion: 0.1
 Nodes (6): fetchProductionTaskDetail(), fetchProductionTaskList(), fetchUpdateProductionTask(), handleClose(), handleSubmit(), loadTaskDetail()
 
-### Community 20 - "Community 20"
-Cohesion: 0.18
-Nodes (10): addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd(), loadEthnicOptions() (+2 more)
-
 ### Community 21 - "Community 21"
+Cohesion: 0.12
+Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
+
+### Community 22 - "Community 22"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 22 - "Community 22"
+### Community 23 - "Community 23"
 Cohesion: 0.2
 Nodes (2): ComponentLoader, RouteTransformer
 
-### Community 23 - "Community 23"
+### Community 24 - "Community 24"
+Cohesion: 0.17
+Nodes (11): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleDelete(), handleReset(), handleSearch() (+3 more)
+
+### Community 25 - "Community 25"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
 
-### Community 24 - "Community 24"
+### Community 26 - "Community 26"
 Cohesion: 0.14
 Nodes (8): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), useCeremony(), useTheme(), themeAnimation(), toggleTheme(), toggleTransition()
 
-### Community 26 - "Community 26"
+### Community 28 - "Community 28"
 Cohesion: 0.17
 Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
 
-### Community 27 - "Community 27"
+### Community 29 - "Community 29"
+Cohesion: 0.18
+Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
+
+### Community 30 - "Community 30"
 Cohesion: 0.2
 Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
 
-### Community 29 - "Community 29"
+### Community 32 - "Community 32"
+Cohesion: 0.28
+Nodes (1): FireworkSystem
+
+### Community 33 - "Community 33"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 30 - "Community 30"
+### Community 34 - "Community 34"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 31 - "Community 31"
+### Community 35 - "Community 35"
+Cohesion: 0.25
+Nodes (2): fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList()
+
+### Community 36 - "Community 36"
 Cohesion: 0.25
 Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
 
-### Community 32 - "Community 32"
+### Community 37 - "Community 37"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 33 - "Community 33"
+### Community 38 - "Community 38"
+Cohesion: 0.29
+Nodes (2): fetchLeaveDetail(), fetchLeaveList()
+
+### Community 39 - "Community 39"
 Cohesion: 0.47
 Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
-### Community 36 - "Community 36"
+### Community 41 - "Community 41"
+Cohesion: 0.33
+Nodes (2): fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive()
+
+### Community 42 - "Community 42"
+Cohesion: 0.33
+Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
+
+### Community 43 - "Community 43"
+Cohesion: 0.33
+Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
+
+### Community 44 - "Community 44"
+Cohesion: 0.33
+Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
+
+### Community 46 - "Community 46"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 37 - "Community 37"
+### Community 47 - "Community 47"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 38 - "Community 38"
+### Community 48 - "Community 48"
+Cohesion: 0.5
+Nodes (1): fetchStorageEntrySmsList()
+
+### Community 49 - "Community 49"
 Cohesion: 0.5
 Nodes (1): fetchSampleSmsNotificationList()
 
-### Community 40 - "Community 40"
+### Community 51 - "Community 51"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 42 - "Community 42"
+### Community 53 - "Community 53"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
 ## Knowledge Gaps
-- **Thin community `Community 22`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
+- **Thin community `Community 23`** (17 nodes): `ComponentLoader`, `.constructor()`, `.createEmptyComponent()`, `.createErrorComponent()`, `.load()`, `.loadIframe()`, `.loadLayout()`, `.load()`, `RouteTransformer`, `.extractFirstSegment()`, `.handleFirstLevelRoute()`, `.handleIframeRoute()`, `.handleNormalRoute()`, `.isFirstLevelRoute()`, `.transform()`, `ComponentLoader.ts`, `RouteTransformer.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 30`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 32`** (9 nodes): `FireworkSystem`, `.constructor()`, `.createFirework()`, `.createParticle()`, `.getAvailableParticle()`, `.initializePool()`, `.preloadAllImages()`, `.preloadImage()`, `index.vue`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 34`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 35`** (8 nodes): `fetchCompleteProductionWorkOrder()`, `fetchConfigureProductionWorkOrder()`, `fetchCreateProductionWorkOrder()`, `fetchDeleteProductionWorkOrder()`, `fetchProductionWorkOrderDetail()`, `fetchProductionWorkOrderList()`, `fetchUpdateProductionWorkOrder()`, `production-work-order.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 36`** (8 nodes): `fetchCompleteQcTask()`, `fetchCreateQcTask()`, `fetchDeleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 37`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 38`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 41`** (6 nodes): `fetchCreateSampleReceptionCheckItem()`, `fetchDeleteSampleReceptionCheckItem()`, `fetchSampleReceptionCheckItemList()`, `fetchSampleReceptionCheckItemsActive()`, `fetchUpdateSampleReceptionCheckItem()`, `sample-reception-check-item.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 27`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
+- **Thin community `Community 42`** (6 nodes): `fetchCreateQcThirdPartyItem()`, `fetchDeleteQcThirdPartyItem()`, `fetchQcThirdPartyItemList()`, `fetchQcThirdPartyItemsActive()`, `fetchUpdateQcThirdPartyItem()`, `qc-third-party-item.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 30`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+- **Thin community `Community 43`** (6 nodes): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 31`** (8 nodes): `fetchCompleteQcTask()`, `fetchCreateQcTask()`, `fetchDeleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
+- **Thin community `Community 44`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 32`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
+- **Thin community `Community 46`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 36`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 47`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 48`** (4 nodes): `fetchGenerateStorageEntrySms()`, `fetchStorageEntrySmsList()`, `fetchToggleStorageEntrySmsNotified()`, `storage-entry-sms-notification.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 38`** (4 nodes): `fetchGenerateSms()`, `fetchSampleSmsNotificationList()`, `fetchToggleSmsNotified()`, `sample-sms-notification.ts`
+- **Thin community `Community 49`** (4 nodes): `fetchGenerateSms()`, `fetchSampleSmsNotificationList()`, `fetchToggleSmsNotified()`, `sample-sms-notification.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 40`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 51`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 42`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 53`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
 
 ## Suggested Questions
 _Questions this graph is uniquely positioned to answer:_
 
-- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 38`, `Community 8`, `Community 10`, `Community 11`, `Community 14`, `Community 15`, `Community 17`, `Community 19`, `Community 26`, `Community 27`, `Community 31`?**
-  _High betweenness centrality (0.244) - this node is a cross-community bridge._
-- **Why does `push()` connect `Community 5` to `Community 1`, `Community 4`, `Community 6`, `Community 8`, `Community 9`, `Community 11`, `Community 12`, `Community 13`, `Community 17`, `Community 20`?**
-  _High betweenness centrality (0.136) - this node is a cross-community bridge._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 10`, `Community 12`, `Community 14`, `Community 16`, `Community 18`, `Community 20`, `Community 21`, `Community 24`, `Community 28`, `Community 29`, `Community 30`, `Community 35`, `Community 36`, `Community 38`, `Community 41`, `Community 42`, `Community 43`, `Community 44`, `Community 48`, `Community 49`?**
+  _High betweenness centrality (0.269) - this node is a cross-community bridge._
+- **Why does `push()` connect `Community 3` to `Community 32`, `Community 0`, `Community 5`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 17`?**
+  _High betweenness centrality (0.127) - this node is a cross-community bridge._
+- **Why does `handleDynamicRoutes()` connect `Community 2` to `Community 5`?**
+  _High betweenness centrality (0.050) - this node is a cross-community bridge._
 - **Are the 192 inferred relationships involving `get()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `buildTreePathLabelFromFlat()`) actually correct?**
   _`get()` has 192 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 31 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
-  _`push()` has 31 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 32 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
+  _`push()` has 32 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 15 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**
   _`withFullscreenLoading()` has 15 INFERRED edges - model-reasoned connections that need verification._
 - **Should `Community 0` be split into smaller, more focused modules?**
-  _Cohesion score 0.01 - nodes in this community are weakly interconnected._
-- **Should `Community 1` be split into smaller, more focused modules?**
   _Cohesion score 0.01 - nodes in this community are weakly interconnected._

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 1
web-admin/graphify-out/graph.html


+ 9 - 8
web-admin/graphify-out/manifest.json

@@ -17,8 +17,8 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/common/index.ts": 1778582068.0254738,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/directive/directive.d.ts": 1778582068.026978,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/api/api.d.ts": 1778582068.0246184,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779791592.3369818,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779791022.789668,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779800864.935052,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779800759.9701834,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/store/index.ts": 1778582068.0293276,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/router/index.ts": 1778775532.895603,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/locales/index.ts": 1778582068.004279,
@@ -44,9 +44,10 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fullscreen-loading.ts": 1779117454.818312,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/router.ts": 1778582068.0393815,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/protocol-payment-labels.ts": 1779199125.4894068,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1779792402.558828,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1779800687.4427748,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/fetch-paged-list.ts": 1778775532.895782,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/sort-order.ts": 1779790341.2952042,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/outbound-qc-pilot-view-sections.ts": 1779801033.9457984,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/tree-path.ts": 1779790341.776808,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/mask-sensitive.ts": 1778984825.4182227,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/production-labels.ts": 1779615806.1798687,
@@ -227,7 +228,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/material.ts": 1779283387.4196396,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-reception.ts": 1779692503.163169,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/auth.ts": 1778582067.8610063,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779791829.2259753,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779800625.4572983,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1779790342.0297484,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1779790347.4764113,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779790344.8150065,
@@ -310,13 +311,13 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779790359.3889246,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779790358.9345891,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/index/index.vue": 1779525781.6949234,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779792730.677042,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue": 1779792406.1338549,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779799317.640114,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue": 1779801046.9472926,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779790362.415862,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779790361.1549423,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779790361.5001903,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779790362.5893314,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779790361.7822216,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779801145.0078545,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/index.vue": 1779790362.6423476,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue": 1779701230.688804,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue": 1779790362.6831455,
@@ -332,7 +333,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleViewDialog.vue": 1779791979.0257256,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleCancelDialog.vue": 1779785733.3155978,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/infusion-schedule-management/ScheduleEditDialog.vue": 1779790355.024449,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue": 1779792715.1482005,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue": 1779792910.3544722,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/ServiceOrderTxnTable.vue": 1779790352.443069,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/ServiceOrderTxnDialog.vue": 1779792403.5058112,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel/index.vue": 1779790353.7514098,

+ 31 - 1
web-admin/src/api/production-work-order.ts

@@ -1,5 +1,12 @@
 import request from '@/utils/http'
-import type { QcWorkOrderReceptionDetail } from '@/api/qc-work-order'
+import type {
+  QcWorkOrderReceptionDetail,
+  OutboundQcDetailApplication,
+  OutboundQcDetailPatient,
+  OutboundQcDetailInfusionPlan,
+  OutboundQcDetailInfusionSchedule,
+  OutboundQcDetailProduct
+} from '@/api/qc-work-order'
 
 export interface ProductionWorkOrderItem {
   id: string
@@ -19,6 +26,29 @@ export interface ProductionWorkOrderItem {
 
 export type ProductionWorkOrderDetail = ProductionWorkOrderItem & {
   reception?: QcWorkOrderReceptionDetail
+  bizType?: string
+  bizTypeLabel?: string
+  /** 临床应用出库:简要申请单 */
+  pilotApplication?: {
+    applicationNo?: string
+    applicationName?: string
+    applicationStatus?: string
+    applicationStatusLabel?: string
+  }
+  /** 临床应用出库:简要输注计划 */
+  infusionSchedule?: {
+    cellType?: string
+    specText?: string
+    infusionTime?: string
+    infusionLocation?: string
+    infusionMethod?: string
+    scheduleStatus?: string
+  }
+  /** 中试服务出库:与出库质检工单查看弹窗一致 */
+  application?: OutboundQcDetailApplication
+  patient?: OutboundQcDetailPatient
+  infusionPlan?: OutboundQcDetailInfusionPlan
+  products?: OutboundQcDetailProduct[]
 }
 
 export function fetchProductionWorkOrderList(params?: {

+ 1 - 0
web-admin/src/api/qc-work-order.ts

@@ -74,6 +74,7 @@ export type QcWorkOrder = {
   hasThirdParty: boolean
   configuredTime?: string
   configuredBy?: string
+  configuredByName?: string
   thirdPartyFeeTotal?: number
   remark?: string
   createTime?: string

+ 1 - 0
web-admin/src/graphify-out/cache/ast/616b119c1a80f8cdacafd192567f17c1c5846fa57a3a28a90057f45209c294fe.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

+ 1 - 0
web-admin/src/graphify-out/cache/ast/63f687ead14f5ec06abc4f6f2ae1711a348da572e823991ff348959971f80857.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/af36c0616309a30527735013d4a1e21d9b9955d5fe1cfc1f72c53cfe11b06b3d.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/b21735e02675abdb668df78cc46d34886c8a0c385368d8ccabedce17cb5a1bfa.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/b856bf1fd075ba90f88669187c1597ae44335897a9257834779fe0b68e1180d9.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/b9c8ba969bfdcefada06da2d9277f2c4417f6483df2ad89a4d71ceb66dc01337.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/c25128d9313e66e30de5f1f9d17605997aee20440f66f785d97e7a5a8299e6a3.json

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_service_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue", "source_location": "L1"}, {"id": "channel_service_order_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue", "source_location": "L1311"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_pilot_service_channel_service_order_index_vue", "target": "channel_service_order_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue", "source_location": "L1311", "weight": 1.0}], "raw_calls": [{"caller_nid": "channel_service_order_index_if", "callee": "handleChannelSearch", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/pilot-service/channel-service-order/index.vue", "source_location": "L1312"}]}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/d4ceca96746d6d9130d9dcec1b4d45387075d38d605ad4dd5e8cd154f7e8acdb.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/e11551776d4da80d9182d11a804734b02bca62af70772362866c499f0b664178.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/f8bd6783f6511ccf88be9968362c3a06d13a3f561107218111633b38addf990a.json


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
web-admin/src/graphify-out/cache/ast/fc9840e32c212a7150970562c0406e8bddb8f5f92f5ca086e01c261debe7d8f6.json


+ 120 - 0
web-admin/src/utils/outbound-qc-pilot-view-sections.ts

@@ -0,0 +1,120 @@
+import type { OutboundQcDetail } from '@/api/qc-work-order'
+import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
+import { maskIdCard } from '@/utils/mask-sensitive'
+import {
+  applicationStatusLabel,
+  applicationStatusTagType,
+  infusionMethodLabel,
+  infusionStatusLabel,
+  infusionStatusTagType,
+  qcStatusLabel,
+  qcStatusTagType,
+  scheduleStatusLabel,
+  scheduleStatusTagType
+} from '@/utils/status-tag-registry'
+
+/** 出库质检 + 中试服务:申请单 / 患者 / 输注方案 / 输注计划 / 产品明细 */
+export function buildOutboundPilotContextSections(detail: OutboundQcDetail): ViewSection[] {
+  const app = detail.application
+  const patient = detail.patient
+  const plan = detail.infusionPlan
+  const schedule = detail.infusionSchedule
+  const products = detail.products || []
+  const result: ViewSection[] = []
+
+  if (app) {
+    result.push({
+      title: '中试服务申请单信息',
+      fields: [
+        { label: '申请单号', value: app.applicationNo || '—' },
+        { label: '申请单名称', value: app.applicationName || '—' },
+        {
+          label: '申请单状态',
+          value: applicationStatusLabel(app.applicationStatus || ''),
+          tagType: applicationStatusTagType(app.applicationStatus || '') as ViewField['tagType']
+        },
+        { label: '渠道名称', value: app.channelName || '—' }
+      ]
+    })
+  }
+
+  if (patient) {
+    result.push({
+      title: '患者信息',
+      fields: [
+        { label: '患者姓名', value: patient.patientName || '—' },
+        { label: '性别', value: patient.sex || '—' },
+        { label: '年龄', value: patient.age || '—' },
+        { label: '联系电话', value: patient.phoneNo || '—' },
+        {
+          label: '身份证号',
+          value: patient.idCardNo ? maskIdCard(patient.idCardNo) : '—'
+        },
+        { label: '紧急联系人', value: patient.emergencyContact || '—' },
+        { label: '紧急联系电话', value: patient.emergencyPhone || '—' },
+        { label: '临床诊断', value: patient.clinicalDiagnosis || '—', span: true }
+      ],
+      slotTitle: '诊断附件',
+      slotTitleStyle: 'field',
+      slot: 'diagnosis-attachments'
+    })
+  }
+
+  if (plan) {
+    result.push({
+      title: '输注方案信息',
+      fields: [
+        {
+          label: '总疗程数',
+          value: plan.totalCourseCount != null ? String(plan.totalCourseCount) : '—'
+        },
+        { label: '疗程间隔', value: plan.courseInterval || '—' },
+        { label: '输注开始日期', value: plan.infusionStartDate || '—' }
+      ]
+    })
+  }
+
+  if (schedule) {
+    const scheduleFields: ViewField[] = [
+      { label: '输注时间', value: schedule.infusionTime || '—' },
+      { label: '输注地点', value: schedule.infusionLocation || '—' },
+      { label: '输注方式', value: infusionMethodLabel(schedule.infusionMethod || '') },
+      {
+        label: '计划状态',
+        value: scheduleStatusLabel(schedule.scheduleStatus || ''),
+        tagType: scheduleStatusTagType(schedule.scheduleStatus || '') as ViewField['tagType']
+      },
+      {
+        label: '输注状态',
+        value: infusionStatusLabel(schedule.infusionStatus || ''),
+        tagType: infusionStatusTagType(schedule.infusionStatus || '') as ViewField['tagType']
+      },
+      {
+        label: '质检状态',
+        value: qcStatusLabel(schedule.qcStatus || ''),
+        tagType: qcStatusTagType(schedule.qcStatus || '') as ViewField['tagType']
+      }
+    ]
+    if (schedule.cancelReason) {
+      scheduleFields.push({ label: '取消原因', value: schedule.cancelReason, span: true })
+    }
+    result.push({ title: '本次输注计划信息', fields: scheduleFields })
+  }
+
+  if (products.length) {
+    result.push({
+      title: '产品明细',
+      fields: [],
+      slot: 'product-table'
+    })
+  }
+
+  return result
+}
+
+export function isOutboundPilotServiceQcTask(d?: {
+  qcBizType?: string
+  receptionSource?: string
+} | null): boolean {
+  return d?.qcBizType === 'outbound' && d?.receptionSource === 'pilot_service'
+}

+ 33 - 0
web-admin/src/utils/production-labels.ts

@@ -14,3 +14,36 @@ export function productionConclusionTagType(code?: string | null): TagType | und
   if (c === 'sb') return 'danger'
   return undefined
 }
+
+/** 生产任务来源:inbound / temporary / outbound */
+export function productionOrderSourceTag(orderSource?: string | null): {
+  label: string
+  type: TagType
+} {
+  const s = (orderSource || '').trim() || 'inbound'
+  if (s === 'temporary') return { label: '临时', type: 'warning' }
+  if (s === 'outbound') return { label: '出库', type: 'primary' }
+  return { label: '入库', type: 'success' }
+}
+
+/** 生产任务类型(临时单看 orderSource,入库/出库看 taskType) */
+export function productionTaskTypeTag(
+  taskType?: string | null,
+  orderSource?: string | null
+): { label: string; type: TagType } {
+  if ((orderSource || '').trim() === 'temporary') {
+    return { label: '临时生产单', type: 'warning' }
+  }
+  const t = (taskType || '').trim() || 'cell_storage'
+  const map: Record<string, { label: string; type: TagType }> = {
+    cell_storage: { label: '入库生产单', type: 'primary' },
+    outbound: { label: '出库生产单', type: 'warning' }
+  }
+  return map[t] || { label: t, type: 'info' }
+}
+
+/** 按工单类型派生创建任务时的 task_type */
+export function productionTaskTypeForWorkOrder(workOrderType?: string | null): string {
+  if (workOrderType === 'outbound') return 'outbound'
+  return 'cell_storage'
+}

+ 15 - 1
web-admin/src/utils/status-tag-registry.ts

@@ -109,7 +109,10 @@ export const transportTypeTagType = createTagTypeGetter(transportType)
 
 export const receptionSource: StatusRegistry = {
   cell_storage: { label: '细胞存储', tagType: 'primary' },
-  research_donation: { label: '科研捐赠', tagType: 'warning' }
+  research_donation: { label: '科研捐赠', tagType: 'warning' },
+  outbound: { label: '出库', tagType: 'primary' },
+  pilot_service: { label: '中试服务', tagType: 'primary' },
+  clinical_application: { label: '临床应用', tagType: 'warning' }
 }
 export const receptionSourceLabel = createLabelGetter(receptionSource)
 export const receptionSourceTagType = createTagTypeGetter(receptionSource)
@@ -508,6 +511,17 @@ export function infusionStatusTagType(status: string): string {
   return map[status] || 'info'
 }
 
+// ========== 中试服务 - 输注方式(字典 szfs) ==========
+export function infusionMethodLabel(method: string): string {
+  const map: Record<string, string> = {
+    jmsz: '静脉输注',
+    pxzs: '皮下注射',
+    jbzs: '局部注射',
+    qt: '其他'
+  }
+  return map[method] || method || '—'
+}
+
 // ========== 中试服务 - 质检状态 ==========
 export function qcStatusLabel(status: string): string {
   const map: Record<string, string> = { dzc: '待质检', yzj: '已质检', yqx: '已取消' }

+ 14 - 36
web-admin/src/views/production/production-task/index.vue

@@ -47,9 +47,7 @@
             </el-form-item>
           </div>
           <div class="filter-actions-row">
-            <span class="filter-hint"
-              >默认展示近 30 天内已开始的任务;清空日期可查看尚未开始的待配置任务。</span
-            >
+            <span class="filter-hint">可按任务开始时间筛选;不选日期则展示全部任务。</span>
             <div class="filter-actions-end">
               <el-button class="btn-ghost" @click="handleReset"
                 ><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -81,8 +79,8 @@
           <el-table-column prop="workOrderNo" label="所属工单号" min-width="140" align="center" />
           <el-table-column prop="orderSource" label="来源" min-width="100" align="center">
             <template #default="{ row }">
-              <el-tag :type="row.orderSource === 'temporary' ? 'warning' : 'success'" size="small">
-                {{ row.orderSource === 'temporary' ? '临时' : '入库' }}
+              <el-tag :type="productionOrderSourceTag(row.orderSource).type" size="small">
+                {{ productionOrderSourceTag(row.orderSource).label }}
               </el-tag>
             </template>
           </el-table-column>
@@ -118,9 +116,9 @@
           </el-table-column>
           <el-table-column prop="taskType" label="任务类型" min-width="120" align="center">
             <template #default="{ row }">
-              <el-tag :type="taskTypeTag(row.taskType, row.orderSource).type" size="small">{{
-                taskTypeTag(row.taskType, row.orderSource).label
-              }}</el-tag>
+              <el-tag :type="productionTaskTypeTag(row.taskType, row.orderSource).type" size="small">
+                {{ productionTaskTypeTag(row.taskType, row.orderSource).label }}
+              </el-tag>
             </template>
           </el-table-column>
           <el-table-column
@@ -325,7 +323,12 @@
   import TaskExecutionDialog from '../production-work-order/TaskExecutionDialog.vue'
   import DispatchTaskDialog from '../production-work-order/DispatchTaskDialog.vue'
   import EditTaskInfoDialog from '../production-work-order/EditTaskInfoDialog.vue'
-  import { productionConclusionLabel, productionConclusionTagType } from '@/utils/production-labels'
+  import {
+    productionConclusionLabel,
+    productionConclusionTagType,
+    productionOrderSourceTag,
+    productionTaskTypeTag
+  } from '@/utils/production-labels'
 
   /** 根据来源类型返回协议状态标签 */
   function protocolStatusLabelBySource(status: string, source: string): string {
@@ -407,36 +410,11 @@
     return map[status] || { label: status, type: 'info' }
   }
 
-  function taskTypeTag(taskType: string, orderSource?: string) {
-    if (orderSource === 'temporary') {
-      return { label: '临时生产单', type: 'warning' }
-    }
-    const map: Record<string, { label: string; type: string }> = {
-      cell_storage: { label: '入库生产单', type: 'primary' },
-      outbound: { label: '出库生产单', type: 'warning' }
-    }
-    return map[taskType] || { label: taskType, type: 'info' }
-  }
-
   // ---------------------------------------------------------------------------
   // 列表视图
   // ---------------------------------------------------------------------------
-  function formatLocalDate(d: Date): string {
-    const y = d.getFullYear()
-    const m = String(d.getMonth() + 1).padStart(2, '0')
-    const day = String(d.getDate()).padStart(2, '0')
-    return `${y}-${m}-${day}`
-  }
-
-  function getDefaultLast30DaysRange(): [string, string] {
-    const end = new Date()
-    const start = new Date()
-    start.setDate(start.getDate() - 30)
-    return [formatLocalDate(start), formatLocalDate(end)]
-  }
-
   const searchForm = reactive({ keyword: '', taskStatus: '' })
-  const startedTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
+  const startedTimeRange = ref<[string, string] | null>(null)
   const tableData = ref<Record<string, any>[]>([])
   const loading = ref(false)
   const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -474,7 +452,7 @@
   function handleReset() {
     searchForm.keyword = ''
     searchForm.taskStatus = ''
-    startedTimeRange.value = getDefaultLast30DaysRange()
+    startedTimeRange.value = null
     pagination.page = 1
     loadData()
   }

+ 2 - 1
web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue

@@ -199,6 +199,7 @@
   import { Rank, Plus, CopyDocument, Delete } from '@element-plus/icons-vue'
   import { VueDraggable as draggable } from 'vue-draggable-plus'
   import { fetchCreateProductionTask } from '@/api/production-task'
+  import { productionTaskTypeForWorkOrder } from '@/utils/production-labels'
   import { fetchProcessRouteList } from '@/api/production'
   import type { FormInstance, FormRules } from 'element-plus'
   import { useTaskStepEditor } from './useTaskStepEditor'
@@ -264,7 +265,7 @@
     try {
       const task = await fetchCreateProductionTask({
         work_order_id: props.workOrderRow.id,
-        task_type: 'cell_storage',
+        task_type: productionTaskTypeForWorkOrder(props.workOrderRow.workOrderType),
         task_name: form.taskName,
         route_id: form.routeId,
         description: form.description || undefined,

+ 143 - 59
web-admin/src/views/production/production-work-order/index.vue

@@ -145,24 +145,6 @@
           >
             <template #default="{ row }">{{ row.applicationNo || '—' }}</template>
           </el-table-column>
-          <el-table-column
-            v-if="activeTypeTab === 'outbound'"
-            prop="cellType"
-            label="细胞类型"
-            min-width="110"
-            align="center"
-          >
-            <template #default="{ row }">{{ row.cellType || '—' }}</template>
-          </el-table-column>
-          <el-table-column
-            v-if="activeTypeTab === 'outbound'"
-            prop="specText"
-            label="规格"
-            min-width="110"
-            align="center"
-          >
-            <template #default="{ row }">{{ row.specText || '—' }}</template>
-          </el-table-column>
           <el-table-column
             v-if="activeTypeTab === 'outbound'"
             prop="infusionTime"
@@ -332,10 +314,72 @@
       <SystemViewDialog
         v-model:visible="viewVisible"
         title="工单详情"
+        :width="viewDialogWidth"
+        :grid-columns="viewDialogGridColumns"
+        :compact="viewDialogCompact"
         :icon="Document"
         :meta="detail?.workOrderNo || ''"
         :sections="viewSections"
       >
+        <template #diagnosis-attachments>
+          <ProtocolAttachmentReadonly
+            :files="viewDiagnosisAttachmentFiles"
+            @preview="handleReceptionAttachmentPreview"
+            @download="handleReceptionAttachmentDownload"
+          />
+        </template>
+        <template #product-table>
+          <div v-if="detail?.products?.length" class="view-subtable-wrap">
+            <el-table
+              :data="detail?.products || []"
+              stripe
+              size="small"
+              class="list-table view-subtable"
+            >
+              <el-table-column type="index" label="序号" min-width="60" align="center" />
+              <el-table-column
+                prop="projectCode"
+                label="项目编码"
+                min-width="120"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column
+                prop="projectName"
+                label="项目名称"
+                min-width="120"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column
+                prop="cellTypeName"
+                label="细胞类型"
+                min-width="100"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column
+                prop="specText"
+                label="规格"
+                min-width="100"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column prop="quantity" label="数量" min-width="70" align="center" />
+              <el-table-column label="产品状态" min-width="100" align="center">
+                <template #default="{ row }">
+                  <el-tag
+                    :type="productStatusTagType(row.productStatus || '') as any"
+                    size="small"
+                  >
+                    {{ productStatusLabel(row.productStatus || '') }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <p v-else class="view-empty-hint">暂无产品明细</p>
+        </template>
         <template #reception-extra>
           <div class="view-reception-attachments">
             <div class="view-kv view-kv-span-2">
@@ -548,8 +592,12 @@
     protocolStatusLabel,
     protocolStatusTagType,
     donationStatusLabel,
-    donationStatusTagType
+    donationStatusTagType,
+    productStatusLabel,
+    productStatusTagType
   } from '@/utils/status-tag-registry'
+  import { buildOutboundPilotContextSections } from '@/utils/outbound-qc-pilot-view-sections'
+  import type { OutboundQcDetail } from '@/api/qc-work-order'
   import type { TagType } from '@/utils/status-tag-registry'
 
   /** 根据来源类型返回协议状态标签 */
@@ -767,6 +815,15 @@
 
   const viewVisible = ref(false)
   const detail = ref<ProductionWorkOrderDetail | null>(null)
+  const isOutboundPilotService = computed(
+    () =>
+      detail.value?.workOrderType === 'outbound' && detail.value?.bizType === 'pilot_service'
+  )
+  const viewDialogWidth = computed(() => (isOutboundPilotService.value ? '1200px' : '720px'))
+  const viewDialogGridColumns = computed<2 | 3 | 4>(() =>
+    isOutboundPilotService.value ? 4 : 2
+  )
+  const viewDialogCompact = computed(() => isOutboundPilotService.value)
   const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
 
   const viewReceptionAttachmentFiles = computed(() => {
@@ -799,6 +856,21 @@
   function handleReceptionAttachmentDownload(file: Record<string, unknown>) {
     downloadAttachmentFile(file, { fullFileUrl: receptionAttachmentFullUrl })
   }
+
+  const viewDiagnosisAttachmentFiles = computed(() => {
+    const urls = detail.value?.patient?.diagnosisAttachments || ''
+    if (!urls.trim()) return []
+    return urls
+      .split(',')
+      .map((u, i) => {
+        const url = u.trim()
+        if (!url) return null
+        const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
+        return { id: `url_${i}`, fileUrl: receptionAttachmentFullUrl(url), fileName: name }
+      })
+      .filter(Boolean) as Array<Record<string, unknown>>
+  })
+
   function formatMetric(v: unknown) {
     if (v === null || v === undefined || v === '') return '—'
     return String(v)
@@ -880,50 +952,62 @@
 
     const sections: ViewSection[] = [{ title: '基本信息', fields: baseFields }]
 
-    // outbound: 申请单 + 输注计划
+    // outbound: 中试服务 — 与出库质检工单查看弹窗一致;临床应用 — 保留简要申请/输注信息
     if (isOutbound) {
-      const pa = d.pilotApplication || {}
-      const sched = d.infusionSchedule || {}
-      const appStatusLabel = (s: string) => {
-        const m: Record<string, string> = {
-          dks: '待开始',
-          jxz: '进行中',
-          ywc: '已完成',
-          yzf: '已作废'
+      if (d.bizType === 'pilot_service') {
+        const pilotDetail: OutboundQcDetail = {
+          workOrder: {} as OutboundQcDetail['workOrder'],
+          application: d.application,
+          patient: d.patient,
+          infusionPlan: d.infusionPlan,
+          infusionSchedule: d.infusionSchedule,
+          products: d.products
         }
-        return m[s] || s || '—'
-      }
-      const appStatusTagType = (s: string) => {
-        const m: Record<string, string> = {
-          dks: 'info',
-          jxz: 'warning',
-          ywc: 'success',
-          yzf: 'danger'
+        sections.push(...buildOutboundPilotContextSections(pilotDetail))
+      } else {
+        const pa = d.pilotApplication || {}
+        const sched = d.infusionSchedule || {}
+        const appStatusLabel = (s: string) => {
+          const m: Record<string, string> = {
+            dks: '待开始',
+            jxz: '进行中',
+            ywc: '已完成',
+            yzf: '已作废'
+          }
+          return m[s] || s || '—'
         }
-        return m[s] || 'info'
-      }
-      sections.push({
-        title: '中试申请信息',
-        fields: [
-          { label: '申请单号', value: pa.applicationNo || '—' },
-          { label: '申请名称', value: pa.applicationName || '—' },
-          {
-            label: '申请状态',
-            value: appStatusLabel(pa.applicationStatus),
-            tagType: appStatusTagType(pa.applicationStatus) as any
+        const appStatusTagType = (s: string) => {
+          const m: Record<string, string> = {
+            dks: 'info',
+            jxz: 'warning',
+            ywc: 'success',
+            yzf: 'danger'
           }
-        ]
-      })
-      sections.push({
-        title: '输注计划信息',
-        fields: [
-          { label: '细胞类型', value: sched.cellType || '—' },
-          { label: '规格', value: sched.specText || '—' },
-          { label: '输注时间', value: sched.infusionTime || '—' },
-          { label: '输注地点', value: sched.infusionLocation || '—' },
-          { label: '输注方式', value: sched.infusionMethod || '—' }
-        ]
-      })
+          return m[s] || 'info'
+        }
+        sections.push({
+          title: '中试申请信息',
+          fields: [
+            { label: '申请单号', value: pa.applicationNo || '—' },
+            { label: '申请名称', value: pa.applicationName || '—' },
+            {
+              label: '申请状态',
+              value: appStatusLabel(pa.applicationStatus || ''),
+              tagType: appStatusTagType(pa.applicationStatus || '') as any
+            }
+          ]
+        })
+        sections.push({
+          title: '输注计划信息',
+          fields: [
+            { label: '细胞类型', value: sched.cellType || '—' },
+            { label: '规格', value: sched.specText || '—' },
+            { label: '输注时间', value: sched.infusionTime || '—' },
+            { label: '输注地点', value: sched.infusionLocation || '—' },
+            { label: '输注方式', value: sched.infusionMethod || '—' }
+          ]
+        })
+      }
       return sections
     }
 

+ 150 - 24
web-admin/src/views/quality-inspection/qc-task/index.vue

@@ -99,8 +99,23 @@
           <el-table-column prop="workOrderNo" label="工单号" min-width="140" align="center" />
           <el-table-column prop="orderSource" label="来源" min-width="100" align="center">
             <template #default="{ row }">
-              <el-tag :type="row.orderSource === 'temporary' ? 'warning' : 'success'" size="small">
-                {{ row.orderSource === 'temporary' ? '临时' : '入库' }}
+              <el-tag
+                :type="
+                  row.orderSource === 'temporary'
+                    ? 'warning'
+                    : row.orderSource === 'outbound'
+                      ? 'primary'
+                      : 'success'
+                "
+                size="small"
+              >
+                {{
+                  row.orderSource === 'temporary'
+                    ? '临时'
+                    : row.orderSource === 'outbound'
+                      ? '出库'
+                      : '入库'
+                }}
               </el-tag>
             </template>
           </el-table-column>
@@ -390,12 +405,73 @@
       <SystemViewDialog
         v-model:visible="viewVisible"
         title="任务详情"
-        width="960px"
-        :grid-columns="3"
+        :width="viewDialogWidth"
+        :grid-columns="viewDialogGridColumns"
+        :compact="viewDialogCompact"
+        :loading="viewLoading"
         :icon="Tickets"
         :meta="viewDetail?.taskNo || ''"
         :sections="viewSections"
       >
+        <template #diagnosis-attachments>
+          <ProtocolAttachmentReadonly
+            :files="viewDiagnosisAttachmentFiles"
+            @preview="handleViewAttachmentPreview"
+            @download="handleViewAttachmentDownload"
+          />
+        </template>
+        <template #product-table>
+          <div v-if="viewOutboundDetail?.products?.length" class="view-subtable-wrap">
+            <el-table
+              :data="viewOutboundDetail?.products || []"
+              stripe
+              size="small"
+              class="list-table view-subtable"
+            >
+              <el-table-column type="index" label="序号" min-width="60" align="center" />
+              <el-table-column
+                prop="projectCode"
+                label="项目编码"
+                min-width="120"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column
+                prop="projectName"
+                label="项目名称"
+                min-width="120"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column
+                prop="cellTypeName"
+                label="细胞类型"
+                min-width="100"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column
+                prop="specText"
+                label="规格"
+                min-width="100"
+                align="center"
+                show-overflow-tooltip
+              />
+              <el-table-column prop="quantity" label="数量" min-width="70" align="center" />
+              <el-table-column label="产品状态" min-width="100" align="center">
+                <template #default="{ row }">
+                  <el-tag
+                    :type="productStatusTagType(row.productStatus || '') as any"
+                    size="small"
+                  >
+                    {{ productStatusLabel(row.productStatus || '') }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <p v-else class="view-empty-hint">暂无产品明细</p>
+        </template>
         <template #reception-extra>
           <div class="view-reception-attachments">
             <div class="view-kv view-kv-span-2">
@@ -496,6 +572,7 @@
     type QcTask,
     type QcTaskDetail
   } from '@/api/qc-task'
+  import { fetchOutboundQcDetail, type OutboundQcDetail } from '@/api/qc-work-order'
   import {
     fetchQcTaskResults,
     fetchQcTaskMaterialUsages,
@@ -519,8 +596,14 @@
     protocolStatusLabel,
     protocolStatusTagType,
     donationStatusLabel,
-    donationStatusTagType
+    donationStatusTagType,
+    productStatusLabel,
+    productStatusTagType
   } from '@/utils/status-tag-registry'
+  import {
+    buildOutboundPilotContextSections,
+    isOutboundPilotServiceQcTask
+  } from '@/utils/outbound-qc-pilot-view-sections'
   import type { TagType } from '@/utils/status-tag-registry'
 
   /** 根据来源类型返回协议状态标签 */
@@ -762,8 +845,17 @@
   }
 
   const viewVisible = ref(false)
+  const viewLoading = ref(false)
   const viewDetail = ref<QcTaskDetail | null>(null)
+  const viewOutboundDetail = ref<OutboundQcDetail | null>(null)
   const viewMaterialUsages = ref<MaterialUsageRow[]>([])
+  const viewDialogWidth = computed(() =>
+    isOutboundPilotServiceQcTask(viewDetail.value) ? '1200px' : '960px'
+  )
+  const viewDialogGridColumns = computed<2 | 3 | 4>(() =>
+    isOutboundPilotServiceQcTask(viewDetail.value) ? 4 : 3
+  )
+  const viewDialogCompact = computed(() => isOutboundPilotServiceQcTask(viewDetail.value))
   const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
 
   function apiFilesToFinanceItems(
@@ -794,6 +886,20 @@
     downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
   }
 
+  const viewDiagnosisAttachmentFiles = computed(() => {
+    const urls = viewOutboundDetail.value?.patient?.diagnosisAttachments || ''
+    if (!urls.trim()) return []
+    return urls
+      .split(',')
+      .map((u, i) => {
+        const url = u.trim()
+        if (!url) return null
+        const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
+        return { id: `url_${i}`, fileUrl: attachmentFullUrl(url), fileName: name }
+      })
+      .filter(Boolean) as Array<Record<string, unknown>>
+  })
+
   const viewReceptionAttachmentFiles = computed(() => {
     const r = viewDetail.value?.reception
     if (!r) return []
@@ -857,6 +963,7 @@
   const viewSections = computed<ViewSection[]>(() => {
     const d = viewDetail.value
     if (!d) return []
+    const isOutboundPilot = isOutboundPilotServiceQcTask(d)
     const r = d.reception || {}
     const isTempQc = d.qcBizType === 'temporary'
     const transportFields: ViewSection['fields'] = [
@@ -890,7 +997,11 @@
           }
         ]
       : []
-    const sampleSections: ViewSection[] = isTempQc
+    const outboundPilotSections: ViewSection[] =
+      isOutboundPilot && viewOutboundDetail.value
+        ? buildOutboundPilotContextSections(viewOutboundDetail.value)
+        : []
+    const sampleSections: ViewSection[] = isTempQc || isOutboundPilot
       ? []
       : [
           {
@@ -1018,6 +1129,7 @@
         ]
       },
       ...cellProductSection,
+      ...outboundPilotSections,
       ...sampleSections,
       ...resultSection,
       ...tailSections
@@ -1069,29 +1181,43 @@
   }
 
   async function handleView(row: QcTask) {
-    const detail = await fetchQcTaskDetail(row.id)
-    try {
-      const results = await fetchQcTaskResults(row.id)
-      ;(detail as any).results = results || []
-    } catch {
-      ;(detail as any).results = []
-    }
+    viewVisible.value = true
+    viewLoading.value = true
+    viewDetail.value = null
+    viewOutboundDetail.value = null
     viewMaterialUsages.value = []
-    if (row.taskType !== 'dsfjc') {
+    try {
+      const detail = await fetchQcTaskDetail(row.id)
       try {
-        const [savedUsages, opts] = await Promise.all([
-          fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[]),
-          fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
-        ])
-        const rows = materialRowsFromApi(savedUsages || [])
-        enrichMaterialUsageRows(rows, opts || [])
-        viewMaterialUsages.value = rows
+        const results = await fetchQcTaskResults(row.id)
+        ;(detail as any).results = results || []
       } catch {
-        viewMaterialUsages.value = []
+        ;(detail as any).results = []
       }
+      if (row.taskType !== 'dsfjc') {
+        try {
+          const [savedUsages, opts] = await Promise.all([
+            fetchQcTaskMaterialUsages(row.id).catch(() => [] as QcTaskMaterialUsageItem[]),
+            fetchQcMaterialOptions().catch(() => [] as QcMaterialOption[])
+          ])
+          const rows = materialRowsFromApi(savedUsages || [])
+          enrichMaterialUsageRows(rows, opts || [])
+          viewMaterialUsages.value = rows
+        } catch {
+          viewMaterialUsages.value = []
+        }
+      }
+      if (isOutboundPilotServiceQcTask(detail) && row.workOrderId) {
+        try {
+          viewOutboundDetail.value = await fetchOutboundQcDetail(row.workOrderId)
+        } catch {
+          viewOutboundDetail.value = null
+        }
+      }
+      viewDetail.value = detail
+    } finally {
+      viewLoading.value = false
     }
-    viewDetail.value = detail
-    viewVisible.value = true
   }
 
   async function openResult(row: QcTask) {

+ 26 - 130
web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue

@@ -19,6 +19,23 @@
         @download="handleDownload"
       />
     </template>
+    <template #product-table>
+      <el-table :data="detail?.products || []" stripe size="small" class="outbound-product-table">
+        <el-table-column type="index" label="序号" min-width="60" align="center" />
+        <el-table-column prop="projectCode" label="项目编码" min-width="120" align="center" show-overflow-tooltip />
+        <el-table-column prop="projectName" label="项目名称" min-width="120" align="center" show-overflow-tooltip />
+        <el-table-column prop="cellTypeName" label="细胞类型" min-width="100" align="center" show-overflow-tooltip />
+        <el-table-column prop="specText" label="规格" min-width="100" align="center" show-overflow-tooltip />
+        <el-table-column prop="quantity" label="数量" min-width="70" align="center" />
+        <el-table-column label="产品状态" min-width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="productStatusTagType(row.productStatus || '') as any" size="small">
+              {{ productStatusLabel(row.productStatus || '') }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </template>
     <template v-for="task in tasksWithAttachments" #[taskAttachmentSlotName(task.id)]>
       <div :key="task.id" class="outbound-qc-task-attachments">
         <ProtocolAttachmentReadonly
@@ -45,25 +62,12 @@
     taskStatusLabel,
     qcTaskConclusionLabel
   } from '@/utils/qc-labels'
-  import {
-    scheduleStatusLabel,
-    scheduleStatusTagType,
-    infusionStatusLabel,
-    infusionStatusTagType,
-    qcStatusLabel,
-    qcStatusTagType,
-    applicationStatusLabel,
-    applicationStatusTagType,
-    bizTypeLabel,
-    bizTypeTagType,
-    productStatusLabel,
-    productStatusTagType
-  } from '@/utils/status-tag-registry'
+  import { bizTypeLabel, bizTypeTagType, productStatusLabel, productStatusTagType } from '@/utils/status-tag-registry'
+  import { buildOutboundPilotContextSections } from '@/utils/outbound-qc-pilot-view-sections'
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
   import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
   import { downloadAttachmentFile } from '@/utils/download-attachment'
-  import { formatMoneyDisplay } from '@/utils/money-format'
 
   defineOptions({ name: 'OutboundQcViewDialog' })
 
@@ -282,12 +286,6 @@
     if (!detail.value) return []
     const d = detail.value
     const wo: Partial<QcWorkOrderDetail> = d.workOrder || {}
-    const app = d.application
-    const patient = d.patient
-    const plan = d.infusionPlan
-    const schedule = d.infusionSchedule
-    const products = d.products || []
-
     const result: ViewSection[] = []
 
     // 1. 工单基本信息
@@ -308,121 +306,15 @@
         },
         { label: '检测方式', value: formatDetectMethods(wo) },
         { label: '配置时间', value: wo.configuredTime || '—' },
-        { label: '配置人', value: wo.configuredBy || '—' },
+        { label: '配置人', value: wo.configuredByName || wo.configuredBy || '—' },
         { label: '备注', value: wo.remark || '—', span: true },
         { label: '创建时间', value: wo.createTime || '—' }
       ]
     })
 
-    // 2. 中试服务申请单信息
-    if (app) {
-      result.push({
-        title: '中试服务申请单信息',
-        fields: [
-          { label: '申请单号', value: app.applicationNo || '—' },
-          { label: '申请单名称', value: app.applicationName || '—' },
-          {
-            label: '申请单状态',
-            value: applicationStatusLabel(app.applicationStatus || ''),
-            tagType: applicationStatusTagType(app.applicationStatus || '') as ViewField['tagType']
-          },
-          { label: '渠道名称', value: app.channelName || '—' }
-        ]
-      })
-    }
-
-    // 3. 患者信息
-    if (patient) {
-      const patientFields: ViewField[] = [
-        { label: '患者姓名', value: patient.patientName || '—' },
-        { label: '性别', value: patient.sex || '—' },
-        { label: '年龄', value: patient.age || '—' },
-        { label: '联系电话', value: patient.phoneNo || '—' },
-        { label: '身份证号', value: patient.idCardNo || '—' },
-        { label: '紧急联系人', value: patient.emergencyContact || '—' },
-        { label: '紧急联系电话', value: patient.emergencyPhone || '—' },
-        { label: '临床诊断', value: patient.clinicalDiagnosis || '—', span: true }
-      ]
-      result.push({
-        title: '患者信息',
-        fields: patientFields,
-        slotTitle: '诊断附件',
-        slotTitleStyle: 'field',
-        slot: 'diagnosis-attachments'
-      })
-    }
-
-    // 4. 输注方案信息
-    if (plan) {
-      result.push({
-        title: '输注方案信息',
-        fields: [
-          { label: '细胞类型', value: plan.cellType || '—' },
-          { label: '规格', value: plan.specText || '—' },
-          { label: '总疗程数', value: plan.totalCourseCount != null ? String(plan.totalCourseCount) : '—' },
-          { label: '疗程间隔', value: plan.courseInterval || '—' },
-          { label: '输注途径', value: plan.infusionRoute || '—' },
-          { label: '输注开始日期', value: plan.infusionStartDate || '—' }
-        ]
-      })
-    }
-
-    // 5. 本次输注计划信息
-    if (schedule) {
-      const scheduleFields: ViewField[] = [
-        { label: '输注时间', value: schedule.infusionTime || '—' },
-        { label: '输注地点', value: schedule.infusionLocation || '—' },
-        { label: '输注方式', value: schedule.infusionMethod || '—' },
-        {
-          label: '计划状态',
-          value: scheduleStatusLabel(schedule.scheduleStatus || ''),
-          tagType: scheduleStatusTagType(schedule.scheduleStatus || '') as ViewField['tagType']
-        },
-        {
-          label: '输注状态',
-          value: infusionStatusLabel(schedule.infusionStatus || ''),
-          tagType: infusionStatusTagType(schedule.infusionStatus || '') as ViewField['tagType']
-        },
-        {
-          label: '质检状态',
-          value: qcStatusLabel(schedule.qcStatus || ''),
-          tagType: qcStatusTagType(schedule.qcStatus || '') as ViewField['tagType']
-        }
-      ]
-      if (schedule.cancelReason) {
-        scheduleFields.push({ label: '取消原因', value: schedule.cancelReason, span: true })
-      }
-      result.push({ title: '本次输注计划信息', fields: scheduleFields })
-    }
-
-    // 6. 产品明细
-    if (products.length) {
-      const productItems: string[] = products.map(
-        (p) =>
-          `${p.projectCode || '—'} / ${p.projectName || '—'} / ${p.cellTypeName || '—'} / ${p.specText || '—'} / 协议价:¥${p.agreementPrice != null ? formatMoneyDisplay(p.agreementPrice) : '—'} / 实际价:¥${p.actualPrice != null ? formatMoneyDisplay(p.actualPrice) : '—'} / 数量:${p.quantity ?? '—'} / ${productStatusLabel(p.productStatus || '')}`
-      )
-      result.push({
-        title: '产品明细',
-        fields: [{ label: '产品列表', value: '', span: true }],
-        slot: 'product-table'
-      })
-      // Override: we'll use items instead of a separate slot — use span field with items list
-      result[result.length - 1] = {
-        title: '产品明细',
-        fields: [
-          {
-            label: '产品列表',
-            span: true,
-            items: products.map(
-              (p) =>
-                `${p.projectCode || '—'} ${p.projectName || '—'} | ${p.cellTypeName || '—'} | ${p.specText || '—'} | 协议价 ¥${p.agreementPrice != null ? formatMoneyDisplay(p.agreementPrice) : '—'} | 实际价 ¥${p.actualPrice != null ? formatMoneyDisplay(p.actualPrice) : '—'} | 数量 ${p.quantity ?? '—'}`
-            )
-          }
-        ]
-      }
-    }
+    result.push(...buildOutboundPilotContextSections(d))
 
-    // 7. 检测结果 — lab tasks
+    // 检测结果 — lab tasks
     const labSections = buildTaskTypeSections(tasksWithResults.value, 'sysz', '实验室项目')
     result.push(...labSections)
 
@@ -438,4 +330,8 @@
   .outbound-qc-task-attachments {
     margin-top: 0;
   }
+
+  .outbound-product-table {
+    margin-top: 4px;
+  }
 </style>

+ 3 - 3
web-admin/src/views/quality-inspection/qc-work-order/index.vue

@@ -295,7 +295,7 @@
             <template #default="{ row }">{{ formatTempEmptyField(row, row.sampleCode) }}</template>
           </el-table-column>
           <el-table-column
-            v-if="showReceptionCols"
+            v-if="showReceptionCols || activeTypeTab === 'outbound'"
             prop="receptionSourceLabel"
             label="来源类型"
             min-width="100"
@@ -722,8 +722,8 @@
 
   type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'
   const activeTypeTab = ref<WorkOrderTypeTab>('inbound')
-  const showReceptionCols = computed(() => activeTypeTab.value !== 'temporary')
-  const showTempProductCols = computed(() => activeTypeTab.value !== 'inbound')
+  const showReceptionCols = computed(() => activeTypeTab.value !== 'temporary' && activeTypeTab.value !== 'outbound')
+  const showTempProductCols = computed(() => activeTypeTab.value !== 'inbound' && activeTypeTab.value !== 'outbound')
   const showOutboundCols = computed(() => activeTypeTab.value === 'outbound')
   const addMode = ref<'inbound' | 'temporary'>('inbound')
   const editingId = ref('')

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio