zlwu 5 місяців тому
батько
коміт
13e4c9530a
85 змінених файлів з 2620 додано та 2443 видалено
  1. 1 1
      backend/app.pid
  2. 43 2
      backend/app/api/production.py
  3. 1 1
      backend/app/api/qc.py
  4. 37 1
      backend/app/crud/collection_protocol.py
  5. 32 4
      backend/app/crud/production_task.py
  6. 25 0
      backend/app/crud/production_work_order.py
  7. 28 6
      backend/app/crud/qc_work_order.py
  8. 32 89
      backend/app/crud/storage_release_review.py
  9. 0 0
      backend/app/graphify-out/cache/ast/005de0d14a9ef82069bec101d710bff3f955e15cd7aab036cd51df8ad2944e1d.json
  10. 0 0
      backend/app/graphify-out/cache/ast/05f3aeaa80b4c03b6034f47917cd689b02cf82e0728a997dfe429e1c9fca9ad5.json
  11. 0 0
      backend/app/graphify-out/cache/ast/10ea216158c96a359898b496c83f5191b268f2854fffe357b5101093258baf82.json
  12. 0 0
      backend/app/graphify-out/cache/ast/246a31cdb285f8641a207f1331e54866ce0afb6e8d0020d4eb2983fd93dee6c4.json
  13. 0 0
      backend/app/graphify-out/cache/ast/2a529271ed29ae4c3d2be63908aa0ec855e84cfb2838e042de588f63f59afc60.json
  14. 0 0
      backend/app/graphify-out/cache/ast/42ca1427b7bbdeef3f67acd96456f9de6b4ea5913b14a55fb327fbb642e6c29b.json
  15. 0 0
      backend/app/graphify-out/cache/ast/4bb30bf53cab626c3e5367beccb45ac67862278c923a771de48c2a8c4a35cd70.json
  16. 0 0
      backend/app/graphify-out/cache/ast/4dfa58fcd63f41529e7d144e47b73b852f419267614d5b49b950d3adb10bb7e4.json
  17. 0 0
      backend/app/graphify-out/cache/ast/5cc8537eba2c9257d75a611f7b85b25a212321a81f66a605afe578333e9e4586.json
  18. 0 0
      backend/app/graphify-out/cache/ast/6d4fe9c20bce076ef778081776a1ee4ee1457b4dcc9af27eae69de2b4260543a.json
  19. 0 0
      backend/app/graphify-out/cache/ast/79594257395fd4ce7c3cd6048a91adb0a3fe74aed51c6774d304e7df6a78460d.json
  20. 0 0
      backend/app/graphify-out/cache/ast/9448a727443a33042b0d40898fe11deec12ddf1c54d81a40cc1665c9d98466c9.json
  21. 0 0
      backend/app/graphify-out/cache/ast/96bb6653e904a1fd925c045479b72de65fa27ae2ddaa120188a130485c52dc16.json
  22. 0 0
      backend/app/graphify-out/cache/ast/9fb3fd2ef65c3ca5caaedd5c56122ae6565632d86adef2ac664f54119225bb35.json
  23. 0 0
      backend/app/graphify-out/cache/ast/a99a7f43ce51a1d6a144d1619764f1d907e0784382bdbc26b286fff385287e43.json
  24. 0 0
      backend/app/graphify-out/cache/ast/b6d669f2644b46a4c7c130a34d0b5e1cf04443e4899c07d9afcad242b6a2da10.json
  25. 0 0
      backend/app/graphify-out/cache/ast/b790c50a2d64d858776205590ade48e68fc19731a608be797996f230cbf8a18e.json
  26. 0 0
      backend/app/graphify-out/cache/ast/b80a0135d06c5e5429cdb42bca89e54b926a8af65b47a0b96d43f6c7cb465c47.json
  27. 0 0
      backend/app/graphify-out/cache/ast/bea602b2c24187c018a1098456318cf8bd4e84c744c8060bfbab9b041820be02.json
  28. 0 0
      backend/app/graphify-out/cache/ast/d72ffd371e3b9302e0d8ff016442974476497f759d60e96bb019c80dbfc42c9b.json
  29. 0 0
      backend/app/graphify-out/cache/ast/f16bf3d7e9da3d45a279ff33062798fbca2d9c9a99e94162e9d6f0475c0c8a6b.json
  30. 1 0
      backend/app/models/models.py
  31. 2 0
      backend/app/schemas/production_task.py
  32. 6 0
      backend/app/schemas/production_work_order.py
  33. 1 1
      backend/app/schemas/qc_work_order.py
  34. 620 681
      backend/graphify-out/.graphify_analysis.json
  35. 182 168
      backend/graphify-out/.graphify_extract.json
  36. 101 126
      backend/graphify-out/GRAPH_REPORT.md
  37. 1 1
      backend/graphify-out/graph.html
  38. 112 112
      backend/graphify-out/graph.json
  39. 12 12
      backend/graphify-out/manifest.json
  40. 5 0
      docs/sql/production_task_conclusion.sql
  41. 181 0
      docs/superpowers/specs/2026-05-24-work-order-task-edit-design.md
  42. 393 421
      web-admin/graphify-out/.graphify_analysis.json
  43. 170 478
      web-admin/graphify-out/.graphify_extract.json
  44. 118 102
      web-admin/graphify-out/GRAPH_REPORT.md
  45. 1 1
      web-admin/graphify-out/graph.html
  46. 19 21
      web-admin/graphify-out/manifest.json
  47. 7 0
      web-admin/src/api/production-work-order.ts
  48. 2 0
      web-admin/src/api/storage-release-review.ts
  49. 9 0
      web-admin/src/components/business/production-task-view-dialog/index.vue
  50. 19 0
      web-admin/src/components/business/sample-production-info-cards/index.vue
  51. 80 0
      web-admin/src/components/business/work-order-remark-dialog/index.vue
  52. 1 0
      web-admin/src/graphify-out/cache/ast/0326a0d78ed8ab4da1c51622bea0696fb3194c7b05a64b4446dbc97867c3c56d.json
  53. 0 0
      web-admin/src/graphify-out/cache/ast/0db7112586f59f6245d3c11f3dfac7ad297dc018b05e7ad01e9332e2d1cb7666.json
  54. 1 0
      web-admin/src/graphify-out/cache/ast/0f7cee6fd3eb8d8f32e5a1b0b2e4900e4cf6a256a4252ae89905337553a47f9b.json
  55. 1 0
      web-admin/src/graphify-out/cache/ast/123dbbc6291d6c9e20bd5e5bfa4c228ae04436784efe8f833d01ba5e2972f50c.json
  56. 1 0
      web-admin/src/graphify-out/cache/ast/2263c525f31e49c7db5d6ece16cb526812060a430ba57bc8bc9408da78c137f7.json
  57. 0 0
      web-admin/src/graphify-out/cache/ast/2c35ca436eef4f43d6f97a335ad5407832a73ce051a18b33753d260f776af84e.json
  58. 0 0
      web-admin/src/graphify-out/cache/ast/4803c3b9e37f39eace08dd92d5250b8159f96b195bfcc9d51fcf1cd6b94cf125.json
  59. 0 0
      web-admin/src/graphify-out/cache/ast/53c01e0481a977179f4381c1f13ce246401e437771839a4cf5cd329cd364897c.json
  60. 0 0
      web-admin/src/graphify-out/cache/ast/5ec9d8bb11459ac70a7cca0699b632eefb8392caac75d7aeb3460b2ade926698.json
  61. 0 0
      web-admin/src/graphify-out/cache/ast/762d0aec7fd283bf432fbe7c87e90959db27584d71be1d9a29a7145b43ace923.json
  62. 1 0
      web-admin/src/graphify-out/cache/ast/7d402544f33336663b61c02144a40ec069cfe6e75676d0a95c3c1360df8776b7.json
  63. 0 0
      web-admin/src/graphify-out/cache/ast/80d80fc79f423c97e112f108a69d96e16414244b034aea9ac85887090ee3a2df.json
  64. 0 0
      web-admin/src/graphify-out/cache/ast/84aa4c53ab66bebdeafa9b33bfebebd283a4144f0bb924b9952b3b088d67ebfc.json
  65. 0 0
      web-admin/src/graphify-out/cache/ast/887c435abe68ec3886b10ca4681797688444370cd7ed7eee6c8fae34cc641067.json
  66. 1 0
      web-admin/src/graphify-out/cache/ast/9fdf20553fbc7111cfa1985981c70d42fbc13b500c4e6e2978237acc89f24e48.json
  67. 0 0
      web-admin/src/graphify-out/cache/ast/ac679be3eed03ff65a7970562694beca577ab212511d41ee81d0a38845fb4d62.json
  68. 1 0
      web-admin/src/graphify-out/cache/ast/bd84ceddf7f9617da4c8421c4024cb9323e87caa6ed98785c44e645462ccb6f8.json
  69. 1 0
      web-admin/src/graphify-out/cache/ast/d6bccdba5b476f81594b2a18b6ed181f75a161c923652a10bca4f0d715c8589d.json
  70. 1 0
      web-admin/src/graphify-out/cache/ast/d72c373def89cf5795ae22fac3c9a21e4daf1d286066d543d83dd7542d5c2cab.json
  71. 0 0
      web-admin/src/graphify-out/cache/ast/dd9a1e9f6114799f7605f1c596b6c2a057fc7a3f21705cbe49eb1ab68d8af8a5.json
  72. 1 0
      web-admin/src/graphify-out/cache/ast/e69a7616fd641e3433aa75c5f31204672acd4e7c69a55ea87b00a60ccdac617f.json
  73. 0 0
      web-admin/src/graphify-out/cache/ast/f857186d0fba6a796dccfc1f46e57c3e5ebd83cca08ae745cd505b8fa6287e3c.json
  74. 0 0
      web-admin/src/graphify-out/cache/ast/ff354d0d24badc8f54eb717453419d854d061a9c6370950bb103ca73c4ba89eb.json
  75. 16 0
      web-admin/src/utils/production-labels.ts
  76. 1 1
      web-admin/src/views/cell-storage/sample-reception-info/index.vue
  77. 14 2
      web-admin/src/views/cell-storage/storage-cert/index.vue
  78. 1 1
      web-admin/src/views/cell-storage/storage-entry-info/index.vue
  79. 36 2
      web-admin/src/views/production/production-task/index.vue
  80. 99 0
      web-admin/src/views/production/production-work-order/CompleteProductionTaskDialog.vue
  81. 46 2
      web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue
  82. 56 1
      web-admin/src/views/production/production-work-order/index.vue
  83. 32 12
      web-admin/src/views/production/production-work-order/useTaskStepEditor.ts
  84. 44 2
      web-admin/src/views/quality-inspection/qc-work-order/index.vue
  85. 24 192
      web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue

+ 1 - 1
backend/app.pid

@@ -1 +1 @@
-95768
+50128

+ 43 - 2
backend/app/api/production.py

@@ -34,7 +34,13 @@ from app.models.models import SysUser
 from app.crud import production_work_order as production_work_order_svc
 from app.crud import production_task as production_task_svc
 from app.crud import cell_product as cell_product_svc
-from app.schemas.production_work_order import ProductionWorkOrderListQuery, ProductionWorkOrderComplete, ProductionWorkOrderCreate, ProductionWorkOrderConfigureBody
+from app.schemas.production_work_order import (
+    ProductionWorkOrderListQuery,
+    ProductionWorkOrderComplete,
+    ProductionWorkOrderCreate,
+    ProductionWorkOrderConfigureBody,
+    ProductionWorkOrderUpdate,
+)
 from app.schemas.production_task import (
     ProductionTaskCreate,
     ProductionTaskUpdate,
@@ -1624,6 +1630,35 @@ async def get_production_work_order(
     return success_response(production_work_order_svc._wo_to_response_dict(db, wo))
 
 
+@router.put("/production-work-orders/{wo_id}")
+async def update_production_work_order_api(
+    wo_id: str,
+    body: ProductionWorkOrderUpdate,
+    user: dict = Depends(require_admin_or_super_admin),
+    db: Session = Depends(get_db),
+):
+    try:
+        tenant_no = _get_tenant_no(db, user)
+        wo = production_work_order_svc.update_production_work_order(
+            db,
+            wo_id,
+            tenant_no,
+            user.get("user_id"),
+            remark=body.remark,
+        )
+        db.commit()
+        return success_response(
+            production_work_order_svc._wo_to_response_dict(db, wo),
+            message="保存成功",
+        )
+    except ValueError as e:
+        db.rollback()
+        return error_response(400, str(e))
+    except Exception as e:
+        db.rollback()
+        return error_response(400, str(e))
+
+
 @router.put("/production-work-orders/{wo_id}/complete")
 async def complete_production_work_order(
     wo_id: str,
@@ -1787,7 +1822,13 @@ async def update_production_task_status(
     try:
         tenant_no = _get_tenant_no(db, user)
         task = production_task_svc.update_task_status(
-            db, task_id, tenant_no, user.get("user_id"), body.task_status,
+            db,
+            task_id,
+            tenant_no,
+            user.get("user_id"),
+            body.task_status,
+            conclusion=body.conclusion,
+            remark=body.remark,
         )
         return success_response(production_task_svc._task_to_dict(task), message="操作成功")
     except ValueError as e:

+ 1 - 1
backend/app/api/qc.py

@@ -645,7 +645,7 @@ async def update_qc_work_order(
 ):
     try:
         tenant_no = _get_tenant_no(db, user)
-        wo = wo_svc.update_temp_qc_work_order(
+        wo = wo_svc.update_qc_work_order(
             db,
             work_order_id,
             tenant_no=tenant_no,

+ 37 - 1
backend/app/crud/collection_protocol.py

@@ -31,6 +31,7 @@ from app.models.models import (
     BizProtocolStorageCert,
     BizQcWorkOrder,
     BizSampleReception,
+    BizStorageReleaseReview,
     BizCollectionRegion,
     BizCollectionHospital,
     BizCellSampleType,
@@ -578,6 +579,37 @@ def _pick_storage_in_date(samples: List[Dict[str, Any]]) -> str:
     return ""
 
 
+def _load_protocol_storage_in_date_map(
+    db: Session,
+    protocol_ids: List[str],
+    tenant_no: str,
+) -> Dict[str, str]:
+    """从入库放行审核记录补全入库日期(历史数据可能未写入样本 storage_in_date)。"""
+    if not protocol_ids:
+        return {}
+    rows = (
+        db.query(BizStorageReleaseReview)
+        .filter(
+            BizStorageReleaseReview.protocol_id.in_(protocol_ids),
+            BizStorageReleaseReview.tenant_no == tenant_no,
+            BizStorageReleaseReview.del_flag == 0,
+            BizStorageReleaseReview.storage_status == "yrk",
+            BizStorageReleaseReview.storage_time.isnot(None),
+        )
+        .all()
+    )
+    out: Dict[str, str] = {}
+    for rev in rows:
+        pid = (rev.protocol_id or "").strip()
+        if not pid or not rev.storage_time:
+            continue
+        d = rev.storage_time.strftime("%Y%m%d")
+        prev = out.get(pid)
+        if not prev or d < prev:
+            out[pid] = d
+    return out
+
+
 def _load_sample_type_name_map(db: Session, samples: List[BizProtocolSample]) -> Dict[str, str]:
     type_ids = list({s.sample_type_id for s in samples if s.sample_type_id})
     if not type_ids:
@@ -634,6 +666,7 @@ def list_storage_certs(
     sc_map: Dict[str, BizProtocolStorageCert] = {}
     sample_map: Dict[str, List[Dict[str, Any]]] = {}
     type_map: Dict[str, str] = {}
+    storage_in_date_map: Dict[str, str] = {}
     if protocol_ids:
         for pc in (
             db.query(BizProtocolPartyCustomer)
@@ -667,6 +700,8 @@ def list_storage_certs(
         for s in s_rows:
             sample_map.setdefault(s.collection_protocol_id, []).append(_sample_to_dict(s))
 
+        storage_in_date_map = _load_protocol_storage_in_date_map(db, protocol_ids, tenant_no)
+
     items: List[Dict[str, Any]] = []
     ms_filter = (make_status or "").strip()
     for r in rows_all:
@@ -693,7 +728,8 @@ def list_storage_certs(
                 "newbornWeightGram": pc.newborn_weight_gram if pc else None,
                 "newbornInfo": _format_newborn_info(pc),
                 "sampleInfo": _format_storage_cert_sample_info(samples, type_map),
-                "storageInDate": _pick_storage_in_date(samples),
+                "storageInDate": _pick_storage_in_date(samples)
+                or storage_in_date_map.get(r.id, ""),
                 "samples": samples,
                 "makeStatus": cert_dict.get("makeStatus", "0"),
                 "certImageUrls": cert_dict.get("certImageUrls", ""),

+ 32 - 4
backend/app/crud/production_task.py

@@ -48,6 +48,17 @@ _PROTOCOL_STATUS_LABELS = {
 _DONATION_STATUS_LABELS = {
     "dsx": "待生效", "sxz": "生效中", "ygq": "已过期", "yzf": "已作废", "zckc": "转细胞存储"
 }
+_PRODUCTION_CONCLUSION_LABELS = {
+    "cg": "成功",
+    "sb": "失败",
+}
+
+
+def _production_conclusion_label(v: Optional[str]) -> str:
+    code = (v or "").strip()
+    if not code:
+        return ""
+    return _PRODUCTION_CONCLUSION_LABELS.get(code, code)
 
 
 def _fmt_dt(t: Optional[datetime]) -> str:
@@ -82,6 +93,8 @@ def _task_to_dict(t: BizProductionTask, *, work_order_no: str = "") -> Dict[str,
         "taskStatus": t.task_status,
         "startedTime": _fmt_dt(t.started_time),
         "completedTime": _fmt_dt(t.completed_time),
+        "conclusion": (getattr(t, "conclusion", None) or "").strip(),
+        "conclusionLabel": _production_conclusion_label(getattr(t, "conclusion", None)),
         "remark": t.remark or "",
         "attachments": t.attachments or "",
         "tenantNo": t.tenant_no,
@@ -746,8 +759,14 @@ def update_production_task(
 
 
 def update_task_status(
-    db: Session, task_id: str, tenant_no: str,
-    user_id: Optional[str], status: str,
+    db: Session,
+    task_id: str,
+    tenant_no: str,
+    user_id: Optional[str],
+    status: str,
+    *,
+    conclusion: Optional[str] = None,
+    remark: Optional[str] = None,
 ) -> BizProductionTask:
     task = db.query(BizProductionTask).filter(
         and_(
@@ -758,12 +777,21 @@ def update_task_status(
     ).first()
     if not task:
         raise ValueError("任务不存在")
+    if task.task_status == "ywc" and status != "ywc":
+        raise ValueError("任务已完成,不可变更状态")
     now = datetime.now()
     task.task_status = status
     if status == "jxz" and not task.started_time:
         task.started_time = now
-    if status == "ywc" and not task.completed_time:
-        task.completed_time = now
+    if status == "ywc":
+        concl = (conclusion or "").strip()
+        if concl not in ("cg", "sb"):
+            raise ValueError("须选择生产结论(成功或失败)")
+        task.conclusion = concl
+        if remark is not None:
+            task.remark = (remark or "").strip() or None
+        if task.task_status != "ywc" and not task.completed_time:
+            task.completed_time = now
     task.update_by = user_id
     task.update_time = now
     db.add(task)

+ 25 - 0
backend/app/crud/production_work_order.py

@@ -379,6 +379,27 @@ def get_production_work_order(
     )
 
 
+def update_production_work_order(
+    db: Session,
+    row_id: str,
+    tenant_no: str,
+    user_id: Optional[str],
+    *,
+    remark: Optional[str] = None,
+) -> BizProductionWorkOrder:
+    """更新生产工单备注(入库/临时均可,任意状态)"""
+    wo = get_production_work_order(db, row_id, tenant_no)
+    if not wo:
+        raise ValueError("工单不存在")
+    if remark is not None:
+        wo.remark = (remark or "").strip() or None
+    wo.update_by = user_id
+    wo.update_time = datetime.now()
+    db.add(wo)
+    db.flush()
+    return wo
+
+
 def complete_production_work_order(
     db: Session,
     row_id: str,
@@ -436,6 +457,10 @@ def _production_task_info(db: Session, task: BizProductionTask) -> Dict[str, Any
         "taskName": task.task_name or "",
         "taskStatus": task.task_status or "",
         "taskStatusLabel": _TASK_STATUS_LABELS.get(task.task_status, task.task_status or ""),
+        "conclusion": (getattr(task, "conclusion", None) or "").strip(),
+        "conclusionLabel": production_task_svc._production_conclusion_label(
+            getattr(task, "conclusion", None)
+        ),
         "startedTime": _fmt_dt(task.started_time),
         "completedTime": _fmt_dt(task.completed_time),
         "inputs": [

+ 28 - 6
backend/app/crud/qc_work_order.py

@@ -1011,7 +1011,7 @@ def create_temp_qc_work_order(
     return wo
 
 
-def update_temp_qc_work_order(
+def update_qc_work_order(
     db: Session,
     work_order_id: str,
     *,
@@ -1020,14 +1020,16 @@ def update_temp_qc_work_order(
     cell_product_id: Optional[str] = None,
     remark: Optional[str] = None,
 ) -> BizQcWorkOrder:
-    """编辑临时质检工单(备注、质检对象)"""
+    """编辑质检工单:入库仅备注;临时可改备注与质检对象"""
     wo = get_work_order(db, work_order_id, tenant_no=tenant_no)
     if not wo:
         raise ValueError("工单不存在")
-    if (getattr(wo, "work_order_type", None) or "inbound") != "temporary":
-        raise ValueError("仅临时质检工单可编辑")
-    if cell_product_id is not None:
-        wo.cell_product_id = _validate_cell_product_id(db, cell_product_id, tenant_no)
+    wo_type = (getattr(wo, "work_order_type", None) or "inbound").strip()
+    if wo_type == "temporary":
+        if cell_product_id is not None:
+            wo.cell_product_id = _validate_cell_product_id(db, cell_product_id, tenant_no)
+    elif cell_product_id is not None:
+        pass
     if remark is not None:
         wo.remark = (remark or "").strip() or None
     wo.update_by = user_id
@@ -1037,6 +1039,26 @@ def update_temp_qc_work_order(
     return wo
 
 
+def update_temp_qc_work_order(
+    db: Session,
+    work_order_id: str,
+    *,
+    tenant_no: str,
+    user_id: Optional[str] = None,
+    cell_product_id: Optional[str] = None,
+    remark: Optional[str] = None,
+) -> BizQcWorkOrder:
+    """编辑临时质检工单(备注、质检对象)"""
+    return update_qc_work_order(
+        db,
+        work_order_id,
+        tenant_no=tenant_no,
+        user_id=user_id,
+        cell_product_id=cell_product_id,
+        remark=remark,
+    )
+
+
 def list_temp_qc_work_orders(
     db: Session,
     *,

+ 32 - 89
backend/app/crud/storage_release_review.py

@@ -10,12 +10,10 @@ from sqlalchemy.orm import Session
 from app.core.utils import generate_uuid
 from app.models.models import (
     BizCollectionProtocol,
-    BizProductionTask,
     BizProductionWorkOrder,
     BizProtocolPartyCustomer,
     BizProtocolSample,
     BizProtocolSampleTransport,
-    BizQcTask,
     BizQcWorkOrder,
     BizSampleReception,
     BizStorageReleaseReview,
@@ -48,10 +46,6 @@ _DONATION_STATUS_LABELS = {
     "yzf": "已作废",
     "zckc": "转细胞存储",
 }
-_QC_WORK_ORDER_STATUS_LABELS = {"dpz": "待配置", "ypz": "已配置", "ywc": "已完成"}
-_PRODUCTION_WORK_ORDER_STATUS_LABELS = {"dpz": "待配置", "jxz": "进行中", "ywc": "已完成"}
-
-
 def _fmt_dt(t: Optional[datetime]) -> str:
     return t.strftime("%Y-%m-%d %H:%M:%S") if t else ""
 
@@ -327,11 +321,8 @@ def get_review_detail(db: Session, review_id: str, tenant_no: str) -> Optional[D
     else:
         result["receptionInfo"] = _build_reception_info(db, rev)
 
-    qc_info = _build_qc_info(db, rev, tenant_no)
-    result["qcInfo"] = qc_info
-
-    production_info = _build_production_info(db, rev, tenant_no)
-    result["productionInfo"] = production_info
+    result["qcInfo"] = _build_qc_info(db, rev, tenant_no)
+    result["productionInfo"] = _build_production_info(db, rev, tenant_no)
 
     return result
 
@@ -540,90 +531,40 @@ def _build_reception_info(db: Session, rev: BizStorageReleaseReview) -> Dict[str
     }
 
 
-def _build_qc_info(db: Session, rev: BizStorageReleaseReview, tenant_no: str) -> Dict[str, Any]:
-    wo = (
-        db.query(BizQcWorkOrder)
-        .filter(
-            BizQcWorkOrder.sample_code == (rev.sample_code or ""),
-            BizQcWorkOrder.tenant_no == tenant_no,
-            BizQcWorkOrder.del_flag == 0,
-        )
-        .first()
+def _build_qc_info(
+    db: Session, rev: BizStorageReleaseReview, tenant_no: str
+) -> Optional[Dict[str, Any]]:
+    from app.crud import qc_work_order as qc_work_order_svc
+
+    code = (rev.sample_code or "").strip()
+    q = db.query(BizQcWorkOrder).filter(
+        BizQcWorkOrder.tenant_no == tenant_no,
+        BizQcWorkOrder.del_flag == 0,
     )
+    wo = q.filter(BizQcWorkOrder.sample_code == code).first() if code else None
+    if not wo and rev.reception_id:
+        wo = q.filter(BizQcWorkOrder.reception_id == rev.reception_id).first()
     if not wo:
-        return {}
-    tasks = (
-        db.query(BizQcTask)
-        .filter(
-            BizQcTask.work_order_id == wo.id,
-            BizQcTask.tenant_no == tenant_no,
-            BizQcTask.del_flag == 0,
-        )
-        .all()
-    )
-    task_list = []
-    _task_type_labels = {"sysz": "实验室自检", "dsfjc": "第三方检测"}
-    _task_status_labels = {"dzx": "待执行", "jxz": "进行中", "ywc": "已完成"}
-    _conclusion_labels = {"tg": "通过", "btg": "不通过"}
-    for t in tasks:
-        task_list.append({
-            "id": t.id,
-            "taskType": t.task_type or "",
-            "taskTypeLabel": _task_type_labels.get(t.task_type or "", t.task_type or ""),
-            "taskStatus": t.task_status or "",
-            "taskStatusLabel": _task_status_labels.get(t.task_status or "", t.task_status or ""),
-            "conclusion": getattr(t, "conclusion", "") or "",
-            "conclusionLabel": _conclusion_labels.get(getattr(t, "conclusion", "") or "", getattr(t, "conclusion", "") or ""),
-        })
-    wo_status = wo.work_order_status or ""
-    return {
-        "workOrderNo": wo.work_order_no or "",
-        "workOrderStatus": wo_status,
-        "workOrderStatusLabel": _QC_WORK_ORDER_STATUS_LABELS.get(wo_status, wo_status),
-        "tasks": task_list,
-    }
+        return None
+    return qc_work_order_svc._qc_info_item(db, wo, tenant_no)
 
 
-def _build_production_info(db: Session, rev: BizStorageReleaseReview, tenant_no: str) -> Dict[str, Any]:
-    pwo = (
-        db.query(BizProductionWorkOrder)
-        .filter(
-            BizProductionWorkOrder.reception_id == (rev.reception_id or ""),
-            BizProductionWorkOrder.tenant_no == tenant_no,
-            BizProductionWorkOrder.del_flag == 0,
-        )
-        .first()
+def _build_production_info(
+    db: Session, rev: BizStorageReleaseReview, tenant_no: str
+) -> Optional[Dict[str, Any]]:
+    from app.crud import production_work_order as production_work_order_svc
+
+    code = (rev.sample_code or "").strip()
+    q = db.query(BizProductionWorkOrder).filter(
+        BizProductionWorkOrder.tenant_no == tenant_no,
+        BizProductionWorkOrder.del_flag == 0,
     )
+    pwo = q.filter(BizProductionWorkOrder.sample_code == code).first() if code else None
+    if not pwo and rev.reception_id:
+        pwo = q.filter(BizProductionWorkOrder.reception_id == rev.reception_id).first()
     if not pwo:
-        return {}
-    tasks = (
-        db.query(BizProductionTask)
-        .filter(
-            BizProductionTask.work_order_id == pwo.id,
-            BizProductionTask.tenant_no == tenant_no,
-            BizProductionTask.del_flag == 0,
-        )
-        .all()
-    )
-    task_list = []
-    _task_status_labels = {"dpz": "待配置", "dzx": "待执行", "jxz": "执行中", "ywc": "已完成"}
-    for t in tasks:
-        task_list.append({
-            "id": t.id,
-            "taskNo": t.task_no or "",
-            "taskName": t.task_name or "",
-            "taskStatus": t.task_status or "",
-            "taskStatusLabel": _task_status_labels.get(t.task_status or "", t.task_status or ""),
-            "startedTime": _fmt_dt(t.started_time),
-            "completedTime": _fmt_dt(t.completed_time),
-        })
-    pwo_status = pwo.work_order_status or ""
-    return {
-        "workOrderNo": pwo.work_order_no or "",
-        "workOrderStatus": pwo_status,
-        "workOrderStatusLabel": _PRODUCTION_WORK_ORDER_STATUS_LABELS.get(pwo_status, pwo_status),
-        "tasks": task_list,
-    }
+        return None
+    return production_work_order_svc._production_info_item(db, pwo)
 
 
 def submit_review(
@@ -720,6 +661,7 @@ def store_sample(
     rev.update_by = user_id
     rev.update_time = datetime.now()
     db.add(rev)
+    storage_in_yyyymmdd = rev.storage_time.strftime("%Y%m%d")
 
     if (rev.reception_source or "cell_storage") == "cell_storage":
         sample_code = (rev.sample_code or "").strip()
@@ -735,6 +677,7 @@ def store_sample(
             )
             if ps:
                 ps.sample_status = "yrk"
+                ps.storage_in_date = storage_in_yyyymmdd
                 ps.update_time = datetime.now()
                 db.add(ps)
 

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/005de0d14a9ef82069bec101d710bff3f955e15cd7aab036cd51df8ad2944e1d.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/05f3aeaa80b4c03b6034f47917cd689b02cf82e0728a997dfe429e1c9fca9ad5.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/10ea216158c96a359898b496c83f5191b268f2854fffe357b5101093258baf82.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/246a31cdb285f8641a207f1331e54866ce0afb6e8d0020d4eb2983fd93dee6c4.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/2a529271ed29ae4c3d2be63908aa0ec855e84cfb2838e042de588f63f59afc60.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/42ca1427b7bbdeef3f67acd96456f9de6b4ea5913b14a55fb327fbb642e6c29b.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/4bb30bf53cab626c3e5367beccb45ac67862278c923a771de48c2a8c4a35cd70.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/4dfa58fcd63f41529e7d144e47b73b852f419267614d5b49b950d3adb10bb7e4.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/5cc8537eba2c9257d75a611f7b85b25a212321a81f66a605afe578333e9e4586.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/6d4fe9c20bce076ef778081776a1ee4ee1457b4dcc9af27eae69de2b4260543a.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/79594257395fd4ce7c3cd6048a91adb0a3fe74aed51c6774d304e7df6a78460d.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/9448a727443a33042b0d40898fe11deec12ddf1c54d81a40cc1665c9d98466c9.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/96bb6653e904a1fd925c045479b72de65fa27ae2ddaa120188a130485c52dc16.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/9fb3fd2ef65c3ca5caaedd5c56122ae6565632d86adef2ac664f54119225bb35.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/a99a7f43ce51a1d6a144d1619764f1d907e0784382bdbc26b286fff385287e43.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/b6d669f2644b46a4c7c130a34d0b5e1cf04443e4899c07d9afcad242b6a2da10.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/b790c50a2d64d858776205590ade48e68fc19731a608be797996f230cbf8a18e.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/b80a0135d06c5e5429cdb42bca89e54b926a8af65b47a0b96d43f6c7cb465c47.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/bea602b2c24187c018a1098456318cf8bd4e84c744c8060bfbab9b041820be02.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/d72ffd371e3b9302e0d8ff016442974476497f759d60e96bb019c80dbfc42c9b.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
backend/app/graphify-out/cache/ast/f16bf3d7e9da3d45a279ff33062798fbca2d9c9a99e94162e9d6f0475c0c8a6b.json


+ 1 - 0
backend/app/models/models.py

@@ -2315,6 +2315,7 @@ class BizProductionTask(Base):
     task_status = Column(String(16), nullable=False, default="dpz", comment="任务状态 dpz待配置 dpz待执行 jxz执行中 ywc已完成")
     started_time = Column(DateTime, nullable=True, comment="开始时间")
     completed_time = Column(DateTime, nullable=True, comment="完成时间")
+    conclusion = Column(String(16), nullable=True, comment="生产结论 cg=成功 sb=失败")
     remark = Column(String(512), nullable=True, comment="备注")
     attachments = Column(String(4000), nullable=True, comment="附件(逗号分隔URL)")
     tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")

+ 2 - 0
backend/app/schemas/production_task.py

@@ -31,6 +31,8 @@ class ProductionTaskUpdate(BaseModel):
 class ProductionTaskStatusUpdate(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
     task_status: str = Field(..., alias="taskStatus")
+    conclusion: Optional[str] = Field(None, description="生产结论 cg=成功 sb=失败(完成时必填)")
+    remark: Optional[str] = Field(None, description="备注(完成时可选)")
 
 
 class TaskStepAdd(BaseModel):

+ 6 - 0
backend/app/schemas/production_work_order.py

@@ -22,6 +22,12 @@ class ProductionWorkOrderComplete(BaseModel):
     remark: Optional[str] = None
 
 
+class ProductionWorkOrderUpdate(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+
+    remark: Optional[str] = Field(None, description="备注")
+
+
 class ProductionWorkOrderCreate(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
 

+ 1 - 1
backend/app/schemas/qc_work_order.py

@@ -16,7 +16,7 @@ class QcWorkOrderCreateBody(BaseModel):
 
 
 class QcWorkOrderUpdateBody(BaseModel):
-    """编辑临时质检工单"""
+    """编辑质检工单(入库仅备注;临时可含质检对象)"""
     model_config = ConfigDict(populate_by_name=True)
 
     cell_product_id: Optional[str] = Field(None, alias="cellProductId", description="质检对象-细胞产品ID")

Різницю між файлами не показано, бо вона завелика
+ 620 - 681
backend/graphify-out/.graphify_analysis.json


Різницю між файлами не показано, бо вона завелика
+ 182 - 168
backend/graphify-out/.graphify_extract.json


+ 101 - 126
backend/graphify-out/GRAPH_REPORT.md

@@ -1,12 +1,12 @@
 # Graph Report - app  (2026-05-24)
 
 ## Corpus Check
-- 148 files · ~136,649 words
+- 148 files · ~139,944 words
 - Verdict: corpus is large enough that graph structure adds value.
 
 ## Summary
-- 2619 nodes · 10690 edges · 58 communities detected
-- Extraction: 40% EXTRACTED · 60% INFERRED · 0% AMBIGUOUS · INFERRED: 6361 edges (avg confidence: 0.63)
+- 2664 nodes · 11010 edges · 53 communities detected
+- Extraction: 40% EXTRACTED · 60% INFERRED · 0% AMBIGUOUS · INFERRED: 6597 edges (avg confidence: 0.63)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -57,29 +57,24 @@
 - [[_COMMUNITY_Community 44|Community 44]]
 - [[_COMMUNITY_Community 45|Community 45]]
 - [[_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]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `success_response()` - 413 edges
-2. `error_response()` - 331 edges
+1. `success_response()` - 419 edges
+2. `error_response()` - 337 edges
 3. `SysUser` - 234 edges
-4. `BizQcMaterialUsage` - 208 edges
-5. `BizMaterialStockLog` - 204 edges
-6. `BizQcTaskResult` - 204 edges
+4. `BizQcTaskResult` - 208 edges
+5. `BizQcMaterialUsage` - 208 edges
+6. `BizMaterialStockLog` - 204 edges
 7. `BizQcMaterial` - 204 edges
 8. `BizMaterial` - 201 edges
 9. `BizQcItemParam` - 199 edges
-10. `generate_uuid()` - 151 edges
+10. `generate_uuid()` - 154 edges
 
 ## Surprising Connections (you probably didn't know these)
 - `统一记录 API 请求/响应出入参的中间件` --uses--> `SysApiRequestLog`  [INFERRED]
@@ -97,63 +92,63 @@
 
 ### Community 0 - "Community 0"
 Cohesion: 0.02
-Nodes (462): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+454 more)
+Nodes (478): attachment_download_url(), create_dept(), create_dict_data(), create_dict_type(), create_express_company(), create_item_unit(), create_menu(), create_post() (+470 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.01
-Nodes (391): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+383 more)
+Nodes (417): AttachmentDownloadUrlBody, DeptCreateBody, DeptUpdateBody, DictDataCreateBody, DictDataUpdateBody, DictTypeCreateBody, DictTypeUpdateBody, ExpressCompanyCreateBody (+409 more)
 
 ### Community 2 - "Community 2"
-Cohesion: 0.02
-Nodes (240): _persist_protocol_sub_resources(), get_designer_data(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes(), generate_uuid(), _add_months_yyyymmdd() (+232 more)
+Cohesion: 0.05
+Nodes (300): AuditCreateBody, BatchDetailIdsBody, BatchRefundFinanceSubmitBody, FinanceConfirmBody, FullmoonTodoInsuranceBody, ProtocolCreateBody, ProtocolUpdateBody, 细胞存储 — 采集协议单 / 审核 / 财务确认 / 分期预警 API (+292 more)
 
 ### Community 3 - "Community 3"
-Cohesion: 0.05
-Nodes (244): InsuranceCompanyCreateBody, InsuranceCompanyInvoiceCreateBody, InsuranceCompanyInvoiceUpdateBody, InsuranceCompanyUpdateBody, InsuranceProjectPriceCreateBody, InsuranceProjectPriceUpdateBody, 保险公司及险种价格管理 API(保险公司 + 开票信息 + 险种价格), ProcessDefCreateBody (+236 more)
+Cohesion: 0.02
+Nodes (193): generate_uuid(), batch_stock_in(), batch_stock_out(), consumable_code_exists(), create_consumable(), _dec(), get_consumable(), _is_enabled_status() (+185 more)
 
 ### Community 4 - "Community 4"
-Cohesion: 0.02
-Nodes (105): list_roles_for_select(), _avatar_url_field(), _build_user_info_payload(), _jwt_user_payload(), logout(), 公共接口(Web 登录、刷新 Token、登出;用户模型对齐 docs/sql/zkxw.sql 基线), 登出:吊销 Redis 会话(若已配置 REDIS_HOST)。, 与 web-admin `userInfo` 字段约定一致。 (+97 more)
+Cohesion: 0.03
+Nodes (141): list_collection_regions(), _ensure_customer_for_mother(), _persist_protocol_sub_resources(), list_ethnics_for_select(), apply_data_scope(), _get_child_dept_ids(), get_user_dept_id(), _resolve_user_role_scopes() (+133 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.08
-Nodes (136): CellSampleTypeCreateBody, CellSampleTypeUpdateBody, CollectionRegionCreateBody, CollectionRegionUpdateBody, CustomerCreateBody, CustomerUpdateBody, HospitalCreateBody, HospitalUpdateBody (+128 more)
+Cohesion: 0.03
+Nodes (142): _format_attachment_file_size(), urls_to_attachment_list(), _accept_conclusion_label(), _attach_reception_detail(), complete_production_work_order(), configure_temp_production_work_order(), create_production_work_order_from_reception(), create_production_work_order_manual() (+134 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.05
-Nodes (99): _format_attachment_file_size(), urls_to_attachment_list(), _accept_conclusion_label(), _attach_reception_detail(), complete_production_work_order(), create_production_work_order_manual(), _enrich_wo_items(), _fmt_dt() (+91 more)
+Cohesion: 0.03
+Nodes (90): _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)。, 获取当前用户菜单权限标识(sys_menu.perms,经角色继承)。 (+82 more)
 
 ### Community 7 - "Community 7"
-Cohesion: 0.05
-Nodes (74): get_next_cell_product_code(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), create_cell_products_batch(), delete_cell_product(), _enrich_product_dicts(), _first_chain_link() (+66 more)
-
-### Community 8 - "Community 8"
 Cohesion: 0.04
-Nodes (31): BusinessException, InvalidStatusTransitionException, log_delivery_operation(), log_operation(), log_order_operation(), log_settlement_operation(), log_user_operation(), OrderLockedException (+23 more)
+Nodes (66): get_designer_data(), ProcessDefCreateBody, ProcessDefUpdateBody, PublishVersionBody, 审批意见:前端传 commentText,兼容 comment。, StartInstanceBody, TaskActionBody, BaseStorage (+58 more)
 
-### Community 9 - "Community 9"
+### Community 8 - "Community 8"
 Cohesion: 0.07
 Nodes (67): AccessLogCreateBody, AccessLogUpdateBody, CleanRoomCreateBody, CleanRoomUpdateBody, ExhibitionAppointmentCreateBody, ExhibitionAppointmentUpdateBody, OccupancyCreateBody, 运营支撑 API(洁净室、进出记录、仓储设施、储位、展厅预约) (+59 more)
 
+### Community 9 - "Community 9"
+Cohesion: 0.35
+Nodes (61): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+53 more)
+
 ### Community 10 - "Community 10"
-Cohesion: 0.34
-Nodes (59): BatchStockInBody, BatchStockInItem, BatchStockOutBody, BatchStockOutItem, ConsumableCreateBody, ConsumablePackCreateBody, ConsumablePackUpdateBody, ConsumableUpdateBody (+51 more)
+Cohesion: 0.11
+Nodes (44): 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() (+36 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.14
-Nodes (33): list_research_donation_audits(), _audit_to_dict(), batch_delete_donations(), _collection_to_dict(), _create_collection(), create_donation(), _create_party(), _create_samples() (+25 more)
+Cohesion: 0.08
+Nodes (30): BaseHTTPMiddleware, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json(), derive_key(), encrypt_json(), gen_nonce_hex() (+22 more)
 
 ### Community 12 - "Community 12"
-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.1
+Nodes (35): get_next_cell_product_code(), _build_ancestor_chain(), _build_descendant_chain(), _build_facility_path_label(), build_split_product_codes(), create_cell_product(), create_cell_products_batch(), create_cell_products_by_quantity() (+27 more)
 
 ### Community 13 - "Community 13"
-Cohesion: 0.1
-Nodes (26): BaseHTTPMiddleware, parse_admin_request_json(), 运营 Web(/web/admin)JSON 请求体解析。  与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `, 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。      Returns:, ApiCryptoError, _b64url_decode(), _b64url_encode(), decrypt_json() (+18 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.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)
+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 15 - "Community 15"
 Cohesion: 0.21
@@ -168,36 +163,36 @@ Cohesion: 0.18
 Nodes (18): company_code_exists(), create_company(), get_company(), list_companies(), list_company_options(), _row_to_dict(), soft_delete_company(), update_company() (+10 more)
 
 ### Community 18 - "Community 18"
-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.2
+Nodes (14): deduct_for_task(), _display_remark(), list_usages(), list_usages_by_task(), _parse_receiver_purpose_from_remark(), 查询质检任务关联的物料使用记录(task_complete)。, 撤销任务此前 task_complete 扣减,便于重新填报物料使用。, 兼容旧数据:领用人/用途曾拼接在 remark 中。 (+6 more)
 
 ### Community 19 - "Community 19"
-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)
-
-### Community 20 - "Community 20"
 Cohesion: 0.25
 Nodes (13): code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), _norm_status(), options_list() (+5 more)
 
-### Community 21 - "Community 21"
+### Community 20 - "Community 20"
 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 22 - "Community 22"
+### Community 21 - "Community 21"
 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 23 - "Community 23"
+### Community 22 - "Community 22"
 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 23 - "Community 23"
+Cohesion: 0.23
+Nodes (12): Config, ProductBase, ProductCategoryBase, ProductCategoryCreate, ProductCategoryResponse, ProductCreate, ProductResponse, ProductUnitBase (+4 more)
+
 ### Community 24 - "Community 24"
-Cohesion: 0.31
-Nodes (12): _code_exists(), create_material(), _dec(), _fmt_dt(), get_material(), list_materials(), options_list(), _row_to_dict() (+4 more)
+Cohesion: 0.24
+Nodes (11): deduct_for_step(), _display_remark(), list_stock_logs(), _parse_receiver_purpose_from_remark(), 试剂变动记录 CRUD(biz_reagent_stock_log), 撤销步骤此前 step_save 扣减,便于重新填报。, 扣减试剂库存并生成变动记录。调用方控制事务,本函数不 commit。, _resolve_receiver_purpose() (+3 more)
 
 ### Community 25 - "Community 25"
-Cohesion: 0.28
-Nodes (10): _base_query(), _ensure_notification(), _fmt_dt(), generate_sms_content(), list_notifications(), resolve_notification(), _row_to_dict(), _storage_status_label() (+2 more)
+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 26 - "Community 26"
 Cohesion: 0.27
@@ -212,161 +207,141 @@ 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 29 - "Community 29"
-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 30 - "Community 30"
 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 31 - "Community 31"
+### Community 30 - "Community 30"
 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 32 - "Community 32"
+### Community 31 - "Community 31"
 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()
 
-### Community 33 - "Community 33"
+### Community 32 - "Community 32"
 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 34 - "Community 34"
+### Community 33 - "Community 33"
 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 35 - "Community 35"
-Cohesion: 0.31
-Nodes (8): create_customer(), get_customer(), id_card_taken(), list_customers(), 客户 CRUD(biz_customer), _row_to_dict(), soft_delete_customer(), update_customer()
-
-### Community 36 - "Community 36"
+### Community 34 - "Community 34"
 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 37 - "Community 37"
+### Community 35 - "Community 35"
 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 38 - "Community 38"
+### Community 36 - "Community 36"
 Cohesion: 0.36
 Nodes (7): create_post(), get_post(), list_posts(), post_code_exists(), _row_to_dict(), soft_delete_post(), update_post()
 
-### Community 39 - "Community 39"
+### Community 37 - "Community 37"
 Cohesion: 0.36
 Nodes (7): create_tenant(), get_tenant(), list_tenants(), _row_to_dict(), soft_delete_tenant(), tenant_no_exists(), update_tenant()
 
-### Community 40 - "Community 40"
-Cohesion: 0.39
-Nodes (8): 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()
-
-### Community 41 - "Community 41"
+### Community 38 - "Community 38"
 Cohesion: 0.36
 Nodes (7): create_site(), get_site(), list_sites(), _row_to_dict(), site_code_exists(), soft_delete_site(), update_site()
 
-### Community 42 - "Community 42"
+### Community 39 - "Community 39"
 Cohesion: 0.29
 Nodes (2): BaseModel, SoftDeleteModel
 
-### Community 43 - "Community 43"
-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 44 - "Community 44"
+### Community 40 - "Community 40"
 Cohesion: 0.38
 Nodes (6): derive_legacy_status_from_three(), derive_three_status_from_legacy(), 阶段A兼容:从旧 status 推导三字段。     约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;     若业务侧已有更精确, 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。, ThreeStatus, _to_decimal()
 
-### Community 45 - "Community 45"
-Cohesion: 0.6
-Nodes (5): _dec(), _fmt_dt(), list_stock_logs(), _row_to_dict(), _user_nick_name_map()
-
-### Community 46 - "Community 46"
-Cohesion: 0.4
-Nodes (4): list_ethnics_for_select(), list_ethnics(), 民族码表 CRUD(全局 sys_ethnic), _row_to_dict()
+### Community 41 - "Community 41"
+Cohesion: 0.33
+Nodes (5): 菜单 code 权限守卫(V0.3:通过 sys_user_role → sys_role_menu → sys_menu.perms), 运营端角色守卫:admin / super_admin 可访问。, 接口菜单权限守卫。super_admin 直接放行。, require_admin_or_super_admin(), require_menu_code()
 
-### Community 47 - "Community 47"
+### Community 42 - "Community 42"
 Cohesion: 0.5
 Nodes (3): BaseSettings, Config, Settings
 
-### Community 48 - "Community 48"
+### Community 43 - "Community 43"
 Cohesion: 0.5
 Nodes (2): _auto_fill_create_dept_id(), ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID
 
-### Community 49 - "Community 49"
+### Community 44 - "Community 44"
 Cohesion: 0.67
 Nodes (2): get_menu_list(), 运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)
 
-### Community 50 - "Community 50"
+### Community 45 - "Community 45"
 Cohesion: 1.0
 Nodes (1): 请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传
 
-### Community 51 - "Community 51"
+### Community 46 - "Community 46"
 Cohesion: 1.0
 Nodes (1): API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)
 
-### Community 54 - "Community 54"
+### Community 49 - "Community 49"
 Cohesion: 1.0
 Nodes (1): 发送订单状态通知                  Args:             openid: 用户openid             ord
 
-### Community 55 - "Community 55"
+### Community 50 - "Community 50"
 Cohesion: 1.0
 Nodes (1): 发送支付成功通知                  Args:             openid: 用户openid             ord
 
-### Community 56 - "Community 56"
+### Community 51 - "Community 51"
 Cohesion: 1.0
 Nodes (1): 发送配送通知                  Args:             openid: 配送员openid             task
 
-### Community 57 - "Community 57"
+### Community 52 - "Community 52"
 Cohesion: 1.0
 Nodes (1): 发送配送时效预警                  Args:             openid: 配送员openid             ta
 
-### Community 58 - "Community 58"
+### Community 53 - "Community 53"
 Cohesion: 1.0
 Nodes (1): 发送退款结果通知                  Args:             openid: 用户openid             ord
 
-### Community 59 - "Community 59"
+### Community 54 - "Community 54"
 Cohesion: 1.0
 Nodes (1): 发送催单通知给配送员                  Args:             deliver_openid: 配送员openid
 
 ## Knowledge Gaps
-- **89 isolated node(s):** `刷新会话 TTL(refreshToken 续期;已登录用户每次 API 校验通过后滑动续期)`, `写入 refreshToken(用于 one-time 使用/吊销)`, `根据 token 获取当前用户(包含 Redis 会话校验;会话滑动续期)`, `登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。`, `Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。` (+84 more)
+- **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 42`** (8 nodes): `BaseModel`, `.to_dict()`, `get_active()`, `base.py`, `SoftDeleteModel`, `.restore()`, `.soft_delete()`, `__tablename__()`
+- **Thin community `Community 39`** (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 48`** (4 nodes): `_auto_fill_create_dept_id()`, `get_db()`, `database.py`, `ORM before_insert 钩子:当 create_dept_id 为空时,自动从请求上下文填充当前用户部门 ID`
+- **Thin community `Community 43`** (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 49`** (3 nodes): `get_menu_list()`, `menu.py`, `运营端菜单接口(与 web-admin 前端静态路由 sqxp.ts 对齐,供后端菜单模式联调)`
+- **Thin community `Community 44`** (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 50`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
+- **Thin community `Community 45`** (2 nodes): `context.py`, `请求级别的用户上下文变量  通过 contextvars 在请求处理链路中传递当前用户信息, 供 SQLAlchemy 事件监听器等底层机制使用,避免逐层手动传`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 51`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
+- **Thin community `Community 46`** (2 nodes): `__init__.py`, `API 路由包(骨架:公共鉴权 + 静态菜单 + 运营端占位)`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 54`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 49`** (1 nodes): `发送订单状态通知                  Args:             openid: 用户openid             ord`
   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 50`** (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:             openid: 配送员openid             task`
+- **Thin community `Community 51`** (1 nodes): `发送配送通知                  Args:             openid: 配送员openid             task`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 57`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
+- **Thin community `Community 52`** (1 nodes): `发送配送时效预警                  Args:             openid: 配送员openid             ta`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 58`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
+- **Thin community `Community 53`** (1 nodes): `发送退款结果通知                  Args:             openid: 用户openid             ord`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 59`** (1 nodes): `发送催单通知给配送员                  Args:             deliver_openid: 配送员openid`
+- **Thin community `Community 54`** (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 `generate_uuid()` connect `Community 2` to `Community 0`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 9`, `Community 11`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 24`, `Community 25`, `Community 26`, `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 40`, `Community 41`?**
-  _High betweenness centrality (0.113) - this node is a cross-community bridge._
-- **Why does `SysUser` connect `Community 1` to `Community 0`, `Community 3`, `Community 4`, `Community 5`, `Community 9`, `Community 10`, `Community 16`, `Community 21`, `Community 22`?**
-  _High betweenness centrality (0.081) - this node is a cross-community bridge._
-- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 2`, `Community 4`, `Community 7`, `Community 11`, `Community 46`, `Community 16`, `Community 49`?**
-  _High betweenness centrality (0.068) - this node is a cross-community bridge._
-- **Are the 412 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
-  _`success_response()` has 412 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 392 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**
-  _`str` has 392 INFERRED edges - model-reasoned connections that need verification._
-- **Are the 330 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
-  _`error_response()` has 330 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `generate_uuid()` connect `Community 3` to `Community 0`, `Community 1`, `Community 2`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 11`, `Community 12`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 24`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 34`, `Community 35`, `Community 36`, `Community 37`, `Community 38`?**
+  _High betweenness centrality (0.094) - this node is a cross-community bridge._
+- **Why does `SysUser` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 16`, `Community 18`, `Community 20`, `Community 21`, `Community 24`?**
+  _High betweenness centrality (0.093) - this node is a cross-community bridge._
+- **Why does `success_response()` connect `Community 0` to `Community 1`, `Community 4`, `Community 6`, `Community 7`, `Community 44`, `Community 12`, `Community 16`?**
+  _High betweenness centrality (0.065) - this node is a cross-community bridge._
+- **Are the 418 inferred relationships involving `success_response()` (e.g. with `list_storage_release_reviews()` and `get_storage_release_review_detail()`) actually correct?**
+  _`success_response()` has 418 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 398 inferred relationships involving `str` (e.g. with `extract_login_credentials()` and `._make_request()`) actually correct?**
+  _`str` has 398 INFERRED edges - model-reasoned connections that need verification._
+- **Are the 336 inferred relationships involving `error_response()` (e.g. with `_json_error_response()` and `list_storage_release_reviews()`) actually correct?**
+  _`error_response()` has 336 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 232 inferred relationships involving `SysUser` (e.g. with `数据权限过滤(V0.3:基于角色 data_scope 的查询级行级过滤)  data_scope 值:   1 = 全部数据   2 = 本部门数据   3` and `从 SysUser 读取用户的 dept_id。`) actually correct?**
   _`SysUser` has 232 INFERRED edges - model-reasoned connections that need verification._

Різницю між файлами не показано, бо вона завелика
+ 1 - 1
backend/graphify-out/graph.html


Різницю між файлами не показано, бо вона завелика
+ 112 - 112
backend/graphify-out/graph.json


+ 12 - 12
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": 1779528692.07516,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/models/models.py": 1779614947.2542932,
   "/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,
@@ -37,13 +37,13 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/delivery.py": 1778578258.0949655,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/price_list.py": 1778578258.096609,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/user.py": 1778578258.099944,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_work_order.py": 1779598101.8960276,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_work_order.py": 1779614963.3978813,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/storage_occupancy.py": 1779418054.7652438,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/quotation.py": 1778578258.0991468,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter_value.py": 1778578258.0980055,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_review_media.py": 1778578258.0986822,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_parameter.py": 1778578258.0977957,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py": 1779586787.9019544,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/production_task.py": 1779614965.0243688,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_detail_media.py": 1778578258.0973988,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/product_banner.py": 1778578258.097102,
   "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/region.py": 1778578258.099429,
@@ -57,21 +57,21 @@
   "/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": 1779597848.673695,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/schemas/qc_work_order.py": 1779614955.9010859,
   "/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": 1779542343.2331185,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1779597967.1911633,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/storage_release_review.py": 1779613697.2166514,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_task.py": 1779612097.0528247,
   "/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,
   "/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": 1779598076.1864269,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_work_order.py": 1779614962.3649387,
   "/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,
@@ -80,7 +80,7 @@
   "/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": 1779588040.1820061,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/production_task.py": 1779615002.3167124,
   "/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": 1779542463.606357,
@@ -92,7 +92,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/express_company.py": 1779265659.3453207,
   "/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": 1779598140.610689,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/cell_product.py": 1779600789.9863036,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/menu.py": 1779265729.0381103,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/collection_region.py": 1779277867.6408322,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/sample_reception_check_item.py": 1779007909.2933013,
@@ -103,7 +103,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/ethnic.py": 1779265849.547566,
   "/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": 1779597853.4248848,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/crud/qc_work_order.py": 1779614954.1003788,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/process.py": 1779309113.0447981,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/pilot_service.py": 1779310971.8304002,
   "/Users/wuzilong/work/00code/zkxw/backend/app/crud/customer.py": 1778733031.447253,
@@ -126,7 +126,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/storage_release_review.py": 1779526635.1455016,
   "/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": 1779597978.6108778,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/qc.py": 1779614954.8762662,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/collection_protocol.py": 1779265813.1605325,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/basic_info.py": 1779280842.8447173,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/__init__.py": 1779528183.6934836,
@@ -141,7 +141,7 @@
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_sms_notification.py": 1779526395.021059,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/sample_reception.py": 1779526633.9689634,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/insurance_company.py": 1779281097.279193,
-  "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779598093.744562,
+  "/Users/wuzilong/work/00code/zkxw/backend/app/api/production.py": 1779614987.9014578,
   "/Users/wuzilong/work/00code/zkxw/backend/app/api/leave.py": 1779266168.8693042,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_callback.py": 1778727966.7277644,
   "/Users/wuzilong/work/00code/zkxw/backend/app/services/workflow_engine.py": 1778634664.6293883,

+ 5 - 0
docs/sql/production_task_conclusion.sql

@@ -0,0 +1,5 @@
+-- 生产任务:生产结论(cg=成功 sb=失败)
+ALTER TABLE biz_production_task
+  ADD COLUMN conclusion VARCHAR(16) NULL
+  COMMENT '生产结论 cg=成功 sb=失败'
+  AFTER completed_time;

+ 181 - 0
docs/superpowers/specs/2026-05-24-work-order-task-edit-design.md

@@ -0,0 +1,181 @@
+# 工单/任务编辑与生产结论 — 设计文档
+
+> 日期: 2026-05-24  
+> 状态: 已实现(2026-05-24)  
+> 范围: 质检工单、生产工单、生产任务(web-admin + backend)
+
+## 1. 背景
+
+在工单融合(`work_order_type`: inbound / temporary)之后,列表侧缺少对**备注**与**生产任务结论**的维护入口。本变更补齐编辑能力,并复用既有 API/组件,控制改动面。
+
+## 2. 已确认的产品决策
+
+| 问题 | 决策 |
+|------|------|
+| 入库工单编辑备注:已完成状态是否可改? | **可以**(任意工单状态均可改备注) |
+| 临时生产工单「编辑信息」范围? | **仅备注** |
+| 生产任务结论「失败」是否触发业务副作用? | **不触发**;v1 仅结果存储 |
+| 结论字段编码? | **拼音缩写**存库;界面展示**中文** |
+| 生产任务列表:已完成任务是否可编辑基础信息? | **不需要**(已完成任务不展示「编辑」) |
+
+## 3. 功能清单
+
+### 3.1 质检工单 — 入库 Tab
+
+- 列表操作列增加 **「编辑」**(`EditPen`)。
+- 弹窗字段:**仅备注**(`textarea`,max 512)。
+- 任意 `work_order_status`(含 `ywc`)可保存。
+- 与临时 Tab 的「编辑」(细胞产品 + 备注)区分:入库仅走备注弹窗。
+
+### 3.2 生产工单 — 入库 Tab
+
+- 列表增加 **「编辑」** → 仅备注弹窗(同上)。
+- 任意工单状态可改备注。
+
+### 3.3 生产工单 — 临时 Tab
+
+- 列表增加 **「编辑」** → 仅备注弹窗(与入库共用交互)。
+- 不包含改任务名称/工艺路线(任务级能力保留在「派发任务」「编辑任务」)。
+
+### 3.4 生产任务 — 列表
+
+- 列表增加 **「编辑」** → 复用 `EditTaskInfoDialog`(`taskName` / `description` / `remark`)。
+- **显示条件**:`task_status` 为 `dpz` 或 `jxz`;`ywc` **不展示**编辑按钮。
+- 后端可对 `ywc` 拒绝更新(双保险,可选)。
+
+### 3.5 生产任务 — 完成任务结论
+
+- 入口:任务执行弹窗 `TaskExecutionDialog` →「完成任务」。
+- 流程:现有 I/O 校验通过 → 弹出**结论弹窗**(非简单 `confirm`)→ 提交完成。
+- 结论(必填):
+  - 存库:`cg` = 成功,`sb` = 失败
+  - 展示:成功 / 失败;Tag:`success` / `danger`
+- 可选备注(`textarea`)。
+- v1:**不**改变细胞产品入库、工单状态同步、通知等既有逻辑;仅写入 `conclusion`(及可选 `remark`)并置 `task_status=ywc`。
+
+## 4. 数据模型
+
+### 4.1 已有字段(无需 DDL)
+
+- `biz_qc_work_order.remark`
+- `biz_production_work_order.remark`
+- `biz_production_task.remark` / `task_name` / `description`
+
+### 4.2 新增字段
+
+```sql
+ALTER TABLE biz_production_task
+  ADD COLUMN conclusion VARCHAR(16) NULL
+  COMMENT '生产结论 cg=成功 sb=失败'
+  AFTER completed_time;
+```
+
+- 迁移脚本:`docs/sql/production_task_conclusion.sql`
+- 全量基线:同步 `docs/sql/zkxw.sql`
+
+### 4.3 结论枚举
+
+| 编码 | 中文标签 | Tag type |
+|------|----------|----------|
+| `cg` | 成功 | `success` |
+| `sb` | 失败 | `danger` |
+
+## 5. API 设计
+
+### 5.1 质检工单 `PUT /web/admin/qc-work-orders/{id}`
+
+**现状**:仅 `temporary` 可更新(`update_temp_qc_work_order`)。
+
+**调整**:
+
+- `inbound`:仅接受 `remark`(忽略 `cellProductId`)。
+- `temporary`:保持 `cellProductId` + `remark`。
+- 不校验工单是否已完成(备注始终可改)。
+
+### 5.2 生产工单 `PUT /web/admin/production-work-orders/{id}`(新增)
+
+- Body:`{ remark?: string }`
+- 入库/临时均可更新备注;任意 `work_order_status`。
+- CRUD:`update_production_work_order(db, wo_id, tenant_no, user_id, remark=...)`
+
+### 5.3 生产任务 `PUT /web/admin/production-tasks/{id}/status`
+
+**扩展 Body**(完成时):
+
+```json
+{
+  "taskStatus": "ywc",
+  "conclusion": "cg",
+  "remark": "可选"
+}
+```
+
+- `taskStatus=ywc` 时 **`conclusion` 必填**,且必须为 `cg` | `sb`。
+- `remark` 可选;若传入则更新任务 `remark`(与完成备注合并策略:写入 `remark` 字段)。
+- `jxz` / `dpz` 状态变更逻辑不变。
+
+### 5.4 列表/详情响应
+
+- `biz_production_task` 序列化增加:`conclusion`、`conclusionLabel`(后端映射中文)。
+
+## 6. 前端设计
+
+### 6.1 共用组件(建议)
+
+`components/business/work-order-remark-dialog/index.vue`(可选抽取)
+
+- Props:`visible`, `title`, `initialRemark`, `loading`
+- Emit:`submit(remark)`, `update:visible`
+- 规范:`dialog-form-layout`、`label-position="top"`、底栏 Close/Check
+
+若不抽取,各页内联单字段弹窗亦可(最小 diff)。
+
+### 6.2 页面改动
+
+| 页面 | 文件 | 改动 |
+|------|------|------|
+| 质检工单 | `views/quality-inspection/qc-work-order/index.vue` | 入库 Tab「编辑」+ 备注弹窗;`handleEditRemark` |
+| 生产工单 | `views/production/production-work-order/index.vue` | 入库/临时 Tab「编辑」+ 备注弹窗 |
+| 生产任务 | `views/production/production-task/index.vue` | `EditTaskInfoDialog`;`v-if` 非 `ywc` |
+| 任务执行 | `useTaskStepEditor.ts` + 新 `CompleteProductionTaskDialog.vue` | 完成前结论弹窗 |
+| API | `qc-work-order.ts`, `production-work-order.ts`, `production-task.ts` | 新增/扩展类型 |
+
+### 6.3 生产任务列表(可选增强)
+
+- 增加列「生产结论」:`ywc` 行展示 `el-tag`;未完成显示 `—`。
+
+### 6.4 UI 规范
+
+- 操作列 `fixed="right"`,宽度预留(质检约 +60px,生产工单约 +60px,生产任务约 +80px)。
+- 结论 Tag 遵循 OpenSpec ui-conventions(状态/结论字段用 `el-tag`)。
+
+## 7. 权限
+
+- 质检:沿用 `biz:qcWorkOrder:list`(现有 PUT)。
+- 生产工单/任务:沿用 `require_admin_or_super_admin`。
+
+## 8. 不在本期范围
+
+- 结论为 `sb` 时禁止产出、回滚库存、消息通知。
+- 已完成生产任务的基础信息编辑。
+- 质检任务完成结论(已有 `tg`/`btg` 流程,不改动)。
+
+## 9. 实施顺序
+
+1. DDL + Model `BizProductionTask.conclusion`
+2. 后端:QC 工单 PUT 扩展、生产工单 PUT 新增、完成任务 conclusion 校验
+3. 前端 API 类型
+4. 质检/生产工单备注编辑 UI
+5. 生产任务列表编辑(非 ywc)
+6. 完成任务结论弹窗 + 列表结论列(可选)
+7. `scripts/check_field_consistency.py`(若扩展 schema)
+8. `./scripts/update-graph.sh backend` + `frontend`
+
+## 10. 验收要点
+
+- [ ] 入库质检工单:任意状态可改备注并列表刷新
+- [ ] 临时质检工单:原编辑(产品+备注)不受影响
+- [ ] 入库/临时生产工单:任意状态可改备注
+- [ ] 生产任务:`dpz`/`jxz` 可编辑基础信息;`ywc` 无编辑按钮
+- [ ] 完成任务:必须选成功/失败;完成后详情/列表可见中文结论
+- [ ] 失败结论不改变细胞产品/工单等下游行为

Різницю між файлами не показано, бо вона завелика
+ 393 - 421
web-admin/graphify-out/.graphify_analysis.json


Різницю між файлами не показано, бо вона завелика
+ 170 - 478
web-admin/graphify-out/.graphify_extract.json


+ 118 - 102
web-admin/graphify-out/GRAPH_REPORT.md

@@ -1,11 +1,11 @@
 # Graph Report - src  (2026-05-24)
 
 ## Corpus Check
-- Large corpus: 609 files · ~334,365 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
+- Large corpus: 607 files · ~332,821 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
 
 ## Summary
-- 1581 nodes · 1982 edges · 44 communities detected
-- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 431 edges (avg confidence: 0.8)
+- 1546 nodes · 1923 edges · 46 communities detected
+- Extraction: 78% EXTRACTED · 22% INFERRED · 0% AMBIGUOUS · INFERRED: 415 edges (avg confidence: 0.8)
 - Token cost: 0 input · 0 output
 
 ## Community Hubs (Navigation)
@@ -32,30 +32,32 @@
 - [[_COMMUNITY_Community 20|Community 20]]
 - [[_COMMUNITY_Community 21|Community 21]]
 - [[_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 39|Community 39]]
+- [[_COMMUNITY_Community 40|Community 40]]
 - [[_COMMUNITY_Community 41|Community 41]]
-- [[_COMMUNITY_Community 42|Community 42]]
+- [[_COMMUNITY_Community 43|Community 43]]
 - [[_COMMUNITY_Community 44|Community 44]]
 - [[_COMMUNITY_Community 45|Community 45]]
 - [[_COMMUNITY_Community 46|Community 46]]
 - [[_COMMUNITY_Community 47|Community 47]]
-- [[_COMMUNITY_Community 49|Community 49]]
+- [[_COMMUNITY_Community 48|Community 48]]
+- [[_COMMUNITY_Community 50|Community 50]]
 
 ## God Nodes (most connected - your core abstractions)
-1. `get()` - 170 edges
+1. `get()` - 166 edges
 2. `WebSocketClient` - 26 edges
 3. `push()` - 20 edges
 4. `withFullscreenLoading()` - 17 edges
@@ -73,232 +75,246 @@
   views/basic-info/customer/index.vue → utils/fullscreen-loading.ts
 - `handleUnauthorizedResponse()` --calls--> `fetchRefreshToken()`  [INFERRED]
   utils/http/index.ts → api/auth.ts
-- `toLogin()` --calls--> `logOut()`  [INFERRED]
-  components/core/layouts/art-screen-lock/index.vue → utils/http/index.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 (87): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+79 more)
+Nodes (84): fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchCellSampleTypeList(), fetchChannelAgreementPrices() (+76 more)
 
 ### Community 1 - "Community 1"
 Cohesion: 0.03
-Nodes (61): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+53 more)
+Nodes (56): fetchBatchGenerateProtocolNos(), fetchBatchGenerateSampleCodes(), fetchBatchIssueProtocolNos(), fetchBatchIssueSampleCodes(), fetchCollectionProtocolNoList(), fetchCreateCollectionProtocolNo(), fetchCreateSampleCode(), fetchDeleteCollectionProtocolNo() (+48 more)
 
 ### Community 2 - "Community 2"
 Cohesion: 0.03
-Nodes (39): fetchGetMenuList(), fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), clickTab(), handleTouchEnd(), setTransition() (+31 more)
+Nodes (44): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+36 more)
 
 ### Community 3 - "Community 3"
 Cohesion: 0.04
-Nodes (40): fetchDeptTree(), fetchPostBoundUserIds(), FireworkSystem, reload(), formatUserLabel(), loadDeptData(), onPersonSelectChange(), subscribeTokenRefresh() (+32 more)
+Nodes (31): fetchGetUserInfo(), clickTab(), handleTouchEnd(), setTransition(), IframeRouteManager, RouteRegistry, useCommon(), setupAfterEachGuard() (+23 more)
 
 ### Community 4 - "Community 4"
 Cohesion: 0.03
-Nodes (21): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllReagentStockLogs() (+13 more)
+Nodes (22): fetchAllConsumablePacks(), fetchAllConsumables(), fetchAllConsumableStockLogs(), fetchAllOutputTypes(), fetchAllProcessCategories(), fetchAllProcesses(), fetchAllReagents(), fetchAllReagentStockLogs() (+14 more)
 
 ### Community 5 - "Community 5"
-Cohesion: 0.04
-Nodes (33): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+25 more)
+Cohesion: 0.05
+Nodes (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
 
 ### Community 6 - "Community 6"
-Cohesion: 0.04
-Nodes (23): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+15 more)
+Cohesion: 0.1
+Nodes (2): WebSocketClient, TableCache
 
 ### Community 7 - "Community 7"
-Cohesion: 0.05
-Nodes (40): fetchAccessLogList(), fetchCleanRoomList(), fetchCreateAccessLog(), fetchCreateCleanRoom(), fetchCreateSite(), fetchDeleteAccessLog(), fetchDeleteCleanRoom(), fetchDeleteSite() (+32 more)
+Cohesion: 0.07
+Nodes (21): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+13 more)
 
 ### Community 8 - "Community 8"
-Cohesion: 0.09
-Nodes (5): handleReset(), handleSearch(), WebSocketClient, TableCache, emit()
+Cohesion: 0.06
+Nodes (18): fetchAttachmentDownloadUrl(), fetchOrderAdjusts(), fetchOrderDetail(), fetchOrderItems(), fetchOrderList(), fetchPriceVersionDetail(), fetchSystemConfig(), handleReceptionAttachmentDownload() (+10 more)
 
 ### Community 9 - "Community 9"
-Cohesion: 0.06
-Nodes (12): collectIds(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), ComponentLoader, RoutePermissionValidator, RouteTransformer, useCeremony() (+4 more)
+Cohesion: 0.09
+Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
 
 ### Community 10 - "Community 10"
-Cohesion: 0.07
-Nodes (21): auditorDisplay(), dash(), expressName(), formatYm(), formatYmd(), sampleTypeName(), siteName(), unitName() (+13 more)
+Cohesion: 0.09
+Nodes (29): base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes(), sha256Hex() (+21 more)
 
 ### Community 11 - "Community 11"
-Cohesion: 0.09
-Nodes (30): base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes(), sha256Hex() (+22 more)
+Cohesion: 0.07
+Nodes (17): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+9 more)
 
 ### Community 12 - "Community 12"
-Cohesion: 0.09
-Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData()
+Cohesion: 0.08
+Nodes (10): collectIds(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), themeAnimation() (+2 more)
 
 ### Community 13 - "Community 13"
 Cohesion: 0.09
 Nodes (16): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+8 more)
 
 ### Community 14 - "Community 14"
-Cohesion: 0.1
-Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
+Cohesion: 0.11
+Nodes (8): disableDevTools(), handleLock(), handleUnlock(), isMobile(), toLogin(), logOut(), systemUpgrade(), VersionManager
 
 ### Community 15 - "Community 15"
-Cohesion: 0.12
-Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
+Cohesion: 0.1
+Nodes (8): fetchGetMenuList(), fetchGetMenuCodes(), fetchLogin(), fetchRefreshToken(), MenuProcessor, useAppMode(), useAuth(), buildWebEncryptedLoginBody()
 
 ### Community 16 - "Community 16"
-Cohesion: 0.14
-Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
+Cohesion: 0.1
+Nodes (13): fetchEthnicList(), addSample(), applyDonationBundleToForm(), confirmBatchTransportApply(), createEmptyTransport(), ensureDonationProtocolNoInOptions(), ensureEthnicOptionInList(), handleOpenForAdd() (+5 more)
 
 ### Community 17 - "Community 17"
-Cohesion: 0.15
-Nodes (15): fetchProcessRouteList(), fetchConfigureTempProductionWorkOrder(), fetchCreateTempProductionWorkOrder(), fetchDeleteTempProductionWorkOrder(), fetchTempProductionWorkOrderDetail(), fetchTempProductionWorkOrderList(), handleConfigure(), handleDelete() (+7 more)
+Cohesion: 0.1
+Nodes (18): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchHospitalList(), fetchUpdateCollectionRegion(), fetchUpdateHospital() (+10 more)
 
 ### Community 18 - "Community 18"
-Cohesion: 0.19
-Nodes (3): fetchGetLatestUpgradeLog(), systemUpgrade(), VersionManager
+Cohesion: 0.12
+Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more)
 
 ### Community 19 - "Community 19"
-Cohesion: 0.12
-Nodes (12): fetchApproveTask(), fetchBusinessAllTasks(), fetchCompletedTasks(), fetchDesignerData(), fetchInstanceDetail(), fetchMyInstances(), fetchPendingTasks(), fetchProcessDefList() (+4 more)
+Cohesion: 0.1
+Nodes (6): fetchProductionTaskDetail(), fetchProductionTaskList(), fetchUpdateProductionTask(), handleClose(), handleSubmit(), loadTaskDetail()
 
 ### Community 20 - "Community 20"
+Cohesion: 0.14
+Nodes (17): fetchCreateUser(), fetchDeleteUser(), fetchResetPassword(), fetchUpdateUser(), fetchUserDetail(), formatUserDeptDisplay(), handleAdd(), handleDelete() (+9 more)
+
+### Community 21 - "Community 21"
+Cohesion: 0.2
+Nodes (2): ComponentLoader, RouteTransformer
+
+### Community 22 - "Community 22"
 Cohesion: 0.16
 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more)
 
-### Community 21 - "Community 21"
+### Community 23 - "Community 23"
+Cohesion: 0.11
+Nodes (5): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList(), fetchSampleReceptionCheckItemList(), fetchSampleReceptionCheckItemsActive()
+
+### Community 24 - "Community 24"
 Cohesion: 0.27
 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more)
 
-### Community 22 - "Community 22"
+### Community 25 - "Community 25"
 Cohesion: 0.17
 Nodes (12): fetchCreateCustomer(), fetchCustomerList(), fetchDeleteCustomer(), fetchUpdateCustomer(), handleAdd(), handleBatchDelete(), handleDelete(), handleReset() (+4 more)
 
-### Community 24 - "Community 24"
+### Community 27 - "Community 27"
 Cohesion: 0.17
 Nodes (3): fetchResearchDonationAuditList(), fetchResearchDonationDetail(), fetchResearchDonationList()
 
-### Community 25 - "Community 25"
-Cohesion: 0.18
-Nodes (3): fetchMaterialList(), fetchMaterialOptions(), fetchMaterialStockLogList()
-
-### Community 26 - "Community 26"
+### Community 28 - "Community 28"
 Cohesion: 0.2
 Nodes (2): fetchQcMaterialList(), fetchQcMaterialOptions()
 
-### Community 28 - "Community 28"
-Cohesion: 0.32
-Nodes (3): annotateForkEdges(), chainApiToElements(), nextAncestorLink()
-
-### Community 29 - "Community 29"
+### Community 30 - "Community 30"
 Cohesion: 0.39
 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle()
 
-### Community 30 - "Community 30"
+### Community 31 - "Community 31"
 Cohesion: 0.36
 Nodes (1): TableHeightCalculator
 
-### Community 31 - "Community 31"
+### Community 32 - "Community 32"
+Cohesion: 0.25
+Nodes (2): fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList()
+
+### Community 33 - "Community 33"
 Cohesion: 0.25
 Nodes (2): fetchQcTaskDetail(), fetchQcTaskList()
 
-### Community 32 - "Community 32"
+### Community 34 - "Community 34"
 Cohesion: 0.33
 Nodes (2): logApiCryptoBootInfo(), maskSecret()
 
-### Community 33 - "Community 33"
+### Community 35 - "Community 35"
 Cohesion: 0.29
 Nodes (2): fetchLeaveDetail(), fetchLeaveList()
 
-### Community 34 - "Community 34"
-Cohesion: 0.47
-Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
-
 ### Community 36 - "Community 36"
-Cohesion: 0.33
-Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
+Cohesion: 0.29
+Nodes (2): fetchQcWorkOrderDetail(), fetchQcWorkOrderList()
 
 ### Community 37 - "Community 37"
-Cohesion: 0.33
-Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
+Cohesion: 0.47
+Nodes (3): isPaymentDueWithinWarnWindow(), parsePaymentDueYmd(), startOfLocalDay()
 
-### Community 38 - "Community 38"
+### Community 39 - "Community 39"
 Cohesion: 0.33
-Nodes (2): fetchTempQcWorkOrderDetail(), fetchTempQcWorkOrderList()
+Nodes (2): fetchQcThirdPartyItemList(), fetchQcThirdPartyItemsActive()
 
-### Community 39 - "Community 39"
+### Community 40 - "Community 40"
 Cohesion: 0.33
-Nodes (2): fetchQcWorkOrderDetail(), fetchQcWorkOrderList()
+Nodes (2): fetchQcThirdPartyCompanyList(), fetchQcThirdPartyCompanyOptions()
 
 ### Community 41 - "Community 41"
+Cohesion: 0.33
+Nodes (2): fetchQcLabTestItemList(), fetchQcLabTestItemsActive()
+
+### Community 43 - "Community 43"
 Cohesion: 0.53
 Nodes (4): isEtaTodayOrEarlier(), parseEtaLocalDate(), sampleReceptionRowClassName(), startOfLocalDay()
 
-### Community 42 - "Community 42"
+### Community 44 - "Community 44"
 Cohesion: 0.5
 Nodes (2): focusInput(), handleKeydown()
 
-### Community 44 - "Community 44"
+### Community 45 - "Community 45"
 Cohesion: 0.67
 Nodes (2): if(), logSidebarMenu()
 
-### Community 45 - "Community 45"
+### Community 46 - "Community 46"
 Cohesion: 0.5
 Nodes (1): fetchStorageEntrySmsList()
 
-### Community 46 - "Community 46"
+### Community 47 - "Community 47"
 Cohesion: 0.5
 Nodes (1): fetchSampleSmsNotificationList()
 
-### Community 47 - "Community 47"
+### Community 48 - "Community 48"
 Cohesion: 1.0
 Nodes (2): getSettingDefaults(), resetToDefaults()
 
-### Community 49 - "Community 49"
+### Community 50 - "Community 50"
 Cohesion: 1.0
 Nodes (2): defaultAvatarPathByRole(), resolveUserAvatarUrl()
 
 ## Knowledge Gaps
-- **Thin community `Community 26`** (10 nodes): `fetchBatchStockInQcMaterial()`, `fetchBatchStockOutQcMaterial()`, `fetchCreateQcMaterial()`, `fetchDeleteQcMaterial()`, `fetchQcMaterialList()`, `fetchQcMaterialOptions()`, `fetchStockInQcMaterial()`, `fetchStockOutQcMaterial()`, `fetchUpdateQcMaterial()`, `qc-material.ts`
+- **Thin community `Community 6`** (42 nodes): `WebSocketClient`, `.calculateReconnectDelay()`, `.clearAllTimers()`, `.clearConnectionTimers()`, `.clearTimer()`, `.close()`, `.closeCurrentSocketForReconnect()`, `.connect()`, `.connectionStatusText()`, `.constructor()`, `.destroyInstance()`, `.flushMessageQueue()`, `.getInstance()`, `.getReadyStateText()`, `.handleClose()`, `.handleConnectionTimeout()`, `.handleError()`, `.handleMessage()`, `.handleOpen()`, `.init()`, `.isWebSocketConnected()`, `.reconnect()`, `.resetHeartbeat()`, `.send()`, `.startHeartbeat()`, `.startPing()`, `TableCache`, `.cleanupExpired()`, `.clear()`, `.clearByTags()`, `.clearCurrentSearch()`, `.clearPagination()`, `.constructor()`, `.evictLRU()`, `.generateKey()`, `.generateTags()`, `.get()`, `.getStats()`, `.log()`, `.set()`, `index.ts`, `tableCache.ts`
+  Too small to be a meaningful cluster - may be noise or needs more connections extracted.
+- **Thin community `Community 21`** (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 28`** (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 31`** (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 30`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts`
+- **Thin community `Community 32`** (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 31`** (8 nodes): `fetchCompleteQcTask()`, `fetchCreateQcTask()`, `fetchDeleteQcTask()`, `fetchQcTaskDetail()`, `fetchQcTaskList()`, `fetchStartQcTask()`, `fetchUpdateQcTaskAttachments()`, `qc-task.ts`
+- **Thin community `Community 33`** (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 32`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts`
+- **Thin community `Community 34`** (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 33`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts`
+- **Thin community `Community 35`** (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 36`** (6 nodes): `fetchCreateQcThirdPartyCompany()`, `fetchDeleteQcThirdPartyCompany()`, `fetchQcThirdPartyCompanyList()`, `fetchQcThirdPartyCompanyOptions()`, `fetchUpdateQcThirdPartyCompany()`, `qc-third-party-company.ts`
+- **Thin community `Community 36`** (7 nodes): `fetchConfigureQcWorkOrder()`, `fetchCreateQcWorkOrder()`, `fetchDeleteQcWorkOrder()`, `fetchQcWorkOrderDetail()`, `fetchQcWorkOrderList()`, `fetchUpdateQcWorkOrder()`, `qc-work-order.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 37`** (6 nodes): `fetchCreateQcLabTestItem()`, `fetchDeleteQcLabTestItem()`, `fetchQcLabTestItemList()`, `fetchQcLabTestItemsActive()`, `fetchUpdateQcLabTestItem()`, `qc-lab-test-item.ts`
+- **Thin community `Community 39`** (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 38`** (6 nodes): `fetchConfigureTempQcWorkOrder()`, `fetchCreateTempQcWorkOrder()`, `fetchDeleteTempQcWorkOrder()`, `fetchTempQcWorkOrderDetail()`, `fetchTempQcWorkOrderList()`, `temp-qc-work-order.ts`
+- **Thin community `Community 40`** (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 39`** (6 nodes): `fetchConfigureQcWorkOrder()`, `fetchCreateQcWorkOrder()`, `fetchDeleteQcWorkOrder()`, `fetchQcWorkOrderDetail()`, `fetchQcWorkOrderList()`, `qc-work-order.ts`
+- **Thin community `Community 41`** (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 42`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue`
+- **Thin community `Community 44`** (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 44`** (4 nodes): `if()`, `logSidebarMenu()`, `index.vue`, `SidebarSubmenu.vue`
+- **Thin community `Community 45`** (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 45`** (4 nodes): `fetchGenerateStorageEntrySms()`, `fetchStorageEntrySmsList()`, `fetchToggleStorageEntrySmsNotified()`, `storage-entry-sms-notification.ts`
+- **Thin community `Community 46`** (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 46`** (4 nodes): `fetchGenerateSms()`, `fetchSampleSmsNotificationList()`, `fetchToggleSmsNotified()`, `sample-sms-notification.ts`
+- **Thin community `Community 47`** (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 47`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
+- **Thin community `Community 48`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts`
   Too small to be a meaningful cluster - may be noise or needs more connections extracted.
-- **Thin community `Community 49`** (3 nodes): `defaultAvatarPathByRole()`, `resolveUserAvatarUrl()`, `user-avatar.ts`
+- **Thin community `Community 50`** (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 13`, `Community 14`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 22`, `Community 24`, `Community 25`, `Community 26`, `Community 28`, `Community 31`, `Community 33`, `Community 36`, `Community 37`, `Community 38`, `Community 39`, `Community 45`, `Community 46`?**
-  _High betweenness centrality (0.311) - this node is a cross-community bridge._
-- **Why does `push()` connect `Community 3` to `Community 0`, `Community 6`, `Community 8`, `Community 9`, `Community 14`, `Community 18`?**
-  _High betweenness centrality (0.100) - this node is a cross-community bridge._
-- **Why does `fetchGetLatestUpgradeLog()` connect `Community 18` to `Community 0`?**
-  _High betweenness centrality (0.044) - this node is a cross-community bridge._
-- **Are the 169 inferred relationships involving `get()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `buildTreePathLabelFromFlat()`) actually correct?**
-  _`get()` has 169 INFERRED edges - model-reasoned connections that need verification._
+- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 8`, `Community 11`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 23`, `Community 25`, `Community 27`, `Community 28`, `Community 32`, `Community 33`, `Community 35`, `Community 36`, `Community 39`, `Community 40`, `Community 41`, `Community 46`, `Community 47`?**
+  _High betweenness centrality (0.299) - this node is a cross-community bridge._
+- **Why does `push()` connect `Community 2` to `Community 0`, `Community 6`, `Community 12`, `Community 14`, `Community 16`?**
+  _High betweenness centrality (0.099) - this node is a cross-community bridge._
+- **Why does `fetchGetLatestUpgradeLog()` connect `Community 0` to `Community 14`?**
+  _High betweenness centrality (0.043) - this node is a cross-community bridge._
+- **Are the 165 inferred relationships involving `get()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `buildTreePathLabelFromFlat()`) actually correct?**
+  _`get()` has 165 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 19 inferred relationships involving `push()` (e.g. with `collectLeafFacilityNodesInTreeOrder()` and `fetchAllPagedRecords()`) actually correct?**
   _`push()` has 19 INFERRED edges - model-reasoned connections that need verification._
 - **Are the 16 inferred relationships involving `withFullscreenLoading()` (e.g. with `.close()` and `handleSubmit()`) actually correct?**

Різницю між файлами не показано, бо вона завелика
+ 1 - 1
web-admin/graphify-out/graph.html


+ 19 - 21
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": 1779604315.8915417,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779604032.215516,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts": 1779615010.407172,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/auto-imports.d.ts": 1779613863.0353637,
   "/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,7 +44,7 @@
   "/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": 1779591615.5295093,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts": 1779611349.8407457,
   "/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": 1778775532.8963482,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/tree-path.ts": 1778800847.103174,
@@ -171,16 +171,17 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/media/art-cutter-img/index.vue": 1779525781.6051006,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/index.vue": 1779525781.605779,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-finance-confirm-fields/finance-attachments.ts": 1778987672.644368,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1779586303.1664853,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/qc-task-view-dialog/index.vue": 1779612144.3309839,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/dialog-form-body/index.vue": 1779525781.6069736,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-qc-info-cards/index.vue": 1779525781.6078143,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-local-dialog/index.vue": 1779525781.608513,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/product-view-dialog/index.vue": 1779525781.6090777,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-production-info-cards/index.vue": 1779591224.5145729,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-storage-release-info/index.vue": 1779542542.2434525,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/work-order-remark-dialog/index.vue": 1779615010.0847185,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/protocol-payment-detail-readonly/index.vue": 1779525781.6098895,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/system-view-dialog/index.vue": 1779586311.488459,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/production-task-view-dialog/index.vue": 1779591221.904441,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/production-task-view-dialog/index.vue": 1779615141.6682072,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/order-operate-dialog-bridge/index.vue": 1779525781.611354,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/hooks/index.ts": 1778582068.0037498,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/hooks/core/useFastEnter.ts": 1778582068.0014625,
@@ -201,7 +202,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-third-party-item.ts": 1779017836.101254,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/storage-release-review.ts": 1779521577.6135197,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/workflow.ts": 1778729873.096252,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-work-order.ts": 1779600111.2513275,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-work-order.ts": 1779615080.6717544,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task-result.ts": 1779332042.258692,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-third-party-company.ts": 1779021425.3773527,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts": 1779244706.1724706,
@@ -214,18 +215,16 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts": 1779594447.3049195,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/sample-sms-notification.ts": 1779526400.8134577,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/research-donation.ts": 1779160004.1431847,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task.ts": 1779602723.2575788,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-task.ts": 1779612098.519313,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-item-param.ts": 1779022632.5658252,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/production-task.ts": 1779586811.7854652,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/temp-production-work-order.ts": 1779602838.322659,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/temp-qc-work-order.ts": 1779602786.8834233,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-material-usage.ts": 1779284726.00147,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-material.ts": 1779283280.0705447,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/system.ts": 1778582067.862994,
   "/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": 1779255909.8553667,
   "/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": 1779600090.8001392,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/qc-work-order.ts": 1779611630.4600656,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/auth/login/index.vue": 1779525781.6116724,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/refund-confirm/index.vue": 1779525781.6125906,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue": 1779543540.7143836,
@@ -279,7 +278,6 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dept/index.vue": 1779543540.7411766,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/dict/index.vue": 1779543540.7390752,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/system/leave/index.vue": 1779543540.7368186,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/temp-production-work-order/index.vue": 1779602838.3227367,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/output-type/index.vue": 1779543540.722031,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue": 1779600859.6291723,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/SourceChainGraph.vue": 1779601463.4693549,
@@ -292,32 +290,32 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/source-chain/chainToFlow.ts": 1779601450.3906622,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue": 1779543540.7289982,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/sop/index.vue": 1779543540.726837,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779602728.4930344,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue": 1779615118.6620471,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/reagent/index.vue": 1779589143.1296964,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779602286.5549738,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779590059.4362776,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue": 1779615100.306273,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/useTaskStepEditor.ts": 1779615065.4906602,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/StepDetailPanel.vue": 1779525781.678735,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/CompleteProductionTaskDialog.vue": 1779615017.576425,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue": 1779525781.679505,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue": 1779601455.886211,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue": 1779615057.3333266,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/EditTaskInfoDialog.vue": 1779525781.6816807,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputList.vue": 1779525781.6822913,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779525781.6835842,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue": 1779608520.251511,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue": 1779525781.6850414,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable-pack/index.vue": 1779543540.719702,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/consumable/index.vue": 1779588932.4529731,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process/index.vue": 1779543540.717012,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-category/index.vue": 1779543540.7154598,
   "/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": 1779602482.5582962,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-work-order/index.vue": 1779615108.02306,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception-check-item/index.vue": 1779543540.7019148,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/lab-test-item/index.vue": 1779543540.7013087,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue": 1779543540.7000535,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/sample-reception/index.vue": 1779543540.698455,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/temp-qc-work-order/index.vue": 1779604458.3034396,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779602725.6401753,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-task/index.vue": 1779612120.724959,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/index.vue": 1779543540.695075,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseReviewDialog.vue": 1779525781.7374997,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue": 1779525781.743236,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue": 1779613667.0297782,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue": 1779543540.693297,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/third-party-company/index.vue": 1779543540.692346,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/exception/500/index.vue": 1779546148.0553653,
@@ -354,7 +352,7 @@
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/RoutePermissionValidator.ts": 1778582068.0127926,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/index.ts": 1778582068.0138035,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/core/IframeRouteManager.ts": 1778582068.0121944,
-  "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1779603531.1964977,
+  "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts": 1779607268.816103,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/index.ts": 1778598510.6043763,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/exception.ts": 1779546186.993006,
   "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/routes/staticRoutes.ts": 1779546226.1088154,

+ 7 - 0
web-admin/src/api/production-work-order.ts

@@ -74,6 +74,13 @@ export function fetchConfigureProductionWorkOrder(
   })
 }
 
+export function fetchUpdateProductionWorkOrder(id: string, data: { remark?: string }) {
+  return request.put<ProductionWorkOrderItem>({
+    url: `/web/admin/production-work-orders/${id}`,
+    data
+  })
+}
+
 export function fetchDeleteProductionWorkOrder(id: string) {
   return request.del({ url: `/web/admin/production-work-orders/${id}` })
 }

+ 2 - 0
web-admin/src/api/storage-release-review.ts

@@ -94,6 +94,8 @@ export type ProductionInfoTask = {
   taskName?: string
   taskStatus?: string
   taskStatusLabel?: string
+  conclusion?: string
+  conclusionLabel?: string
   startedTime?: string
   completedTime?: string
 }

+ 9 - 0
web-admin/src/components/business/production-task-view-dialog/index.vue

@@ -103,6 +103,10 @@
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
   import { fetchProductionTaskDetail } from '@/api/production-task'
+  import {
+    productionConclusionLabel,
+    productionConclusionTagType
+  } from '@/utils/production-labels'
 
   const props = defineProps<{
     visible: boolean
@@ -231,6 +235,11 @@
           { label: '协议号', value: d.protocolNo || '—' },
           { label: '样本编码', value: d.sampleCode || '—' },
           { label: '开始时间', value: d.startedTime || '—' },
+          {
+            label: '生产结论',
+            value: productionConclusionLabel(d.conclusion),
+            tagType: productionConclusionTagType(d.conclusion)
+          },
           { label: '完成时间', value: d.completedTime || '—' },
           { label: '备注', value: d.remark || '—', span: true }
         ]

+ 19 - 0
web-admin/src/components/business/sample-production-info-cards/index.vue

@@ -59,6 +59,19 @@
               <span class="qc-kv__label">完成时间</span>
               <span class="qc-kv__value">{{ task.completedTime || '—' }}</span>
             </div>
+            <div class="qc-kv">
+              <span class="qc-kv__label">生产结论</span>
+              <span class="qc-kv__value">
+                <el-tag
+                  v-if="productionConclusionTagType(task.conclusion)"
+                  :type="productionConclusionTagType(task.conclusion)"
+                  size="small"
+                >
+                  {{ task.conclusionLabel || productionConclusionLabel(task.conclusion) }}
+                </el-tag>
+                <template v-else>—</template>
+              </span>
+            </div>
           </div>
           <div class="production-task-card__foot">
             <button type="button" class="production-task-card__link" @click.stop="openTaskView(task)">
@@ -82,6 +95,10 @@
 <script setup lang="ts">
   import { ref } from 'vue'
   import ProductionTaskViewDialog from '@/components/business/production-task-view-dialog/index.vue'
+  import {
+    productionConclusionLabel,
+    productionConclusionTagType
+  } from '@/utils/production-labels'
 
   export type ProductionTaskInput = {
     inputType?: string
@@ -101,6 +118,8 @@
     taskName?: string
     taskStatus?: string
     taskStatusLabel?: string
+    conclusion?: string
+    conclusionLabel?: string
     startedTime?: string
     completedTime?: string
     inputs?: ProductionTaskInput[]

+ 80 - 0
web-admin/src/components/business/work-order-remark-dialog/index.vue

@@ -0,0 +1,80 @@
+<!-- 工单备注编辑(入库/临时生产、入库质检) -->
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    :title="title"
+    width="560px"
+    class="dialog-form-layout"
+    align-center
+    destroy-on-close
+    :close-on-click-modal="false"
+    @close="handleClose"
+  >
+    <el-form label-position="top" class="biz-dialog-form">
+      <el-form-item label="备注">
+        <el-input
+          v-model="remark"
+          type="textarea"
+          :rows="4"
+          placeholder="选填"
+          maxlength="512"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button class="btn-ghost" @click="handleClose"><el-icon><Close /></el-icon>取消</el-button>
+      <el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
+        <el-icon><Check /></el-icon>保存
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed, watch } from 'vue'
+  import { Close, Check } from '@element-plus/icons-vue'
+
+  const props = withDefaults(
+    defineProps<{
+      visible: boolean
+      title?: string
+      initialRemark?: string
+      submitting?: boolean
+    }>(),
+    {
+      title: '编辑备注',
+      initialRemark: '',
+      submitting: false
+    }
+  )
+
+  const emit = defineEmits<{
+    (e: 'update:visible', val: boolean): void
+    (e: 'submit', remark: string): void
+  }>()
+
+  const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+  })
+
+  const remark = ref('')
+
+  function handleClose() {
+    dialogVisible.value = false
+  }
+
+  function handleSubmit() {
+    emit('submit', remark.value.trim())
+  }
+
+  watch(
+    () => props.visible,
+    (val) => {
+      if (val) {
+        remark.value = props.initialRemark || ''
+      }
+    }
+  )
+</script>

+ 1 - 0
web-admin/src/graphify-out/cache/ast/0326a0d78ed8ab4da1c51622bea0696fb3194c7b05a64b4446dbc97867c3c56d.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": []}

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/0db7112586f59f6245d3c11f3dfac7ad297dc018b05e7ad01e9332e2d1cb7666.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_router_modules_sqxp_ts", "label": "sqxp.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_router_modules_sqxp_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_types_router", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_status_tag_registry_ts", "label": "status-tag-registry.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L1"}, {"id": "utils_status_tag_registry_createlabelgetter", "label": "createLabelGetter()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L29"}, {"id": "utils_status_tag_registry_createtagtypegetter", "label": "createTagTypeGetter()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L33"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_status_tag_registry_ts", "target": "utils_status_tag_registry_createlabelgetter", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L29", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_status_tag_registry_ts", "target": "utils_status_tag_registry_createtagtypegetter", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/utils/status-tag-registry.ts", "source_location": "L33", "weight": 1.0}], "raw_calls": []}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_taskexecutiondialog_vue", "label": "TaskExecutionDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/2c35ca436eef4f43d6f97a335ad5407832a73ce051a18b33753d260f776af84e.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/4803c3b9e37f39eace08dd92d5250b8159f96b195bfcc9d51fcf1cd6b94cf125.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/53c01e0481a977179f4381c1f13ce246401e437771839a4cf5cd329cd364897c.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/5ec9d8bb11459ac70a7cca0699b632eefb8392caac75d7aeb3460b2ade926698.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/762d0aec7fd283bf432fbe7c87e90959db27584d71be1d9a29a7145b43ace923.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/80d80fc79f423c97e112f108a69d96e16414244b034aea9ac85887090ee3a2df.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/84aa4c53ab66bebdeafa9b33bfebebd283a4144f0bb924b9952b3b088d67ebfc.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/887c435abe68ec3886b10ca4681797688444370cd7ed7eee6c8fae34cc641067.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_task_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/ac679be3eed03ff65a7970562694beca577ab212511d41ee81d0a38845fb4d62.json


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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_dispatchtaskdialog_vue", "label": "DispatchTaskDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L1"}, {"id": "production_work_order_dispatchtaskdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L289"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_dispatchtaskdialog_vue", "target": "production_work_order_dispatchtaskdialog_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L289", "weight": 1.0}], "raw_calls": [{"caller_nid": "production_work_order_dispatchtaskdialog_if", "callee": "initEditor", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue", "source_location": "L293"}]}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_task_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-task/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

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

@@ -0,0 +1 @@
+{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_production_work_order_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/production-work-order/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/dd9a1e9f6114799f7605f1c596b6c2a057fc7a3f21705cbe49eb1ab68d8af8a5.json


+ 1 - 0
web-admin/src/graphify-out/cache/ast/e69a7616fd641e3433aa75c5f31204672acd4e7c69a55ea87b00a60ccdac617f.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": []}

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/f857186d0fba6a796dccfc1f46e57c3e5ebd83cca08ae745cd505b8fa6287e3c.json


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
web-admin/src/graphify-out/cache/ast/ff354d0d24badc8f54eb717453419d854d061a9c6370950bb103ca73c4ba89eb.json


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

@@ -0,0 +1,16 @@
+import type { TagType } from '@/utils/status-tag-registry'
+
+/** 生产结论 cg=成功 sb=失败 */
+export function productionConclusionLabel(code?: string | null): string {
+  const c = (code || '').trim()
+  if (c === 'cg') return '成功'
+  if (c === 'sb') return '失败'
+  return c || '—'
+}
+
+export function productionConclusionTagType(code?: string | null): TagType | undefined {
+  const c = (code || '').trim()
+  if (c === 'cg') return 'success'
+  if (c === 'sb') return 'danger'
+  return undefined
+}

+ 1 - 1
web-admin/src/views/cell-storage/sample-reception-info/index.vue

@@ -97,7 +97,7 @@
               </el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
+          <el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
             <template #default="{ row }">
               <el-button class="btn-op-item primary" @click="handleView(row)">
                 <el-icon><View /></el-icon>查看

+ 14 - 2
web-admin/src/views/cell-storage/storage-cert/index.vue

@@ -80,7 +80,7 @@
           <el-table-column prop="newbornInfo" label="新生儿信息" min-width="200" align="center" header-align="center" show-overflow-tooltip />
           <el-table-column prop="sampleInfo" label="样本信息" min-width="200" align="center" header-align="center" show-overflow-tooltip />
           <el-table-column prop="storageInDate" label="入库日期" min-width="120" align="center" header-align="center">
-            <template #default="{ row }">{{ row.storageInDate || '—' }}</template>
+            <template #default="{ row }">{{ formatStorageInDate(row.storageInDate) }}</template>
           </el-table-column>
           <el-table-column prop="makeStatus" label="制作状态" min-width="100" align="center" header-align="center">
             <template #default="{ row }">
@@ -89,7 +89,9 @@
               </el-tag>
             </template>
           </el-table-column>
-          <el-table-column prop="makeDate" label="制作日期" min-width="120" align="center" header-align="center" />
+          <el-table-column prop="makeDate" label="制作日期" min-width="120" align="center" header-align="center">
+            <template #default="{ row }">{{ formatStorageInDate(row.makeDate) }}</template>
+          </el-table-column>
           <el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
             <template #default="{ row }">
               <el-button class="btn-op-item primary" @click="handleUpload(row)">
@@ -371,6 +373,16 @@
     return isStorageCertMakePendingRow(row) ? 'row-storage-cert-pending' : ''
   }
 
+  function formatStorageInDate(val: unknown) {
+    const s = String(val || '').trim()
+    if (!s) return '—'
+    const digits = s.replace(/\D/g, '')
+    if (digits.length >= 8) {
+      return `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`
+    }
+    return s
+  }
+
   // ---------------------------------------------------------------------------
   // 数据加载
   // ---------------------------------------------------------------------------

+ 1 - 1
web-admin/src/views/cell-storage/storage-entry-info/index.vue

@@ -103,7 +103,7 @@
               </el-tag>
             </template>
           </el-table-column>
-          <el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
+          <el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
             <template #default="{ row }">
               <el-button class="btn-op-item primary" @click="handleView(row)">
                 <el-icon><View /></el-icon>查看

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

@@ -91,15 +91,30 @@
               <el-tag :type="taskStatusTag(row.taskStatus).type" size="small">{{ taskStatusTag(row.taskStatus).label }}</el-tag>
             </template>
           </el-table-column>
+          <el-table-column prop="conclusion" label="生产结论" min-width="100" align="center">
+            <template #default="{ row }">
+              <el-tag
+                v-if="productionConclusionTagType(row.conclusion)"
+                :type="productionConclusionTagType(row.conclusion)"
+                size="small"
+              >
+                {{ row.conclusionLabel || productionConclusionLabel(row.conclusion) }}
+              </el-tag>
+              <span v-else>—</span>
+            </template>
+          </el-table-column>
           <el-table-column prop="startedTime" label="开始时间" min-width="170" align="center">
             <template #default="{ row }">{{ row.startedTime || '—' }}</template>
           </el-table-column>
           <el-table-column prop="completedTime" label="完成时间" min-width="170" align="center">
             <template #default="{ row }">{{ row.completedTime || '—' }}</template>
           </el-table-column>
-          <el-table-column label="操作" min-width="320" fixed="right" align="center">
+          <el-table-column label="操作" min-width="400" fixed="right" align="center">
             <template #default="{ row }">
               <el-button class="btn-op-item primary" @click="handleEnterExecution(row)"><el-icon><VideoPlay /></el-icon>进入执行</el-button>
+              <el-button class="btn-op-item" @click="handleEditTask(row)">
+                <el-icon><EditPen /></el-icon>编辑
+              </el-button>
               <el-button class="btn-op-item" @click="handleViewDetail(row)"><el-icon><View /></el-icon>查看详情</el-button>
               <el-button v-if="row.taskStatus === 'dpz'" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
             </template>
@@ -190,13 +205,19 @@
         :work-order-row="addDispatchRow"
         @success="loadData"
       />
+
+      <EditTaskInfoDialog
+        v-model:visible="editTaskInfoVisible"
+        :task-id="editTaskInfoId"
+        @success="loadData"
+      />
     </section>
   </div>
 </template>
 
 <script setup lang="ts">
   import { ElMessage, ElMessageBox } from 'element-plus'
-  import { RefreshLeft, Search, VideoPlay, View, Plus, Close, Check } from '@element-plus/icons-vue'
+  import { RefreshLeft, Search, VideoPlay, View, Plus, Close, Check, EditPen } from '@element-plus/icons-vue'
   import { fetchProductionTaskList, fetchDeleteProductionTask } from '@/api/production-task'
   import { fetchProductionWorkOrderList } from '@/api/production-work-order'
   import {
@@ -208,6 +229,11 @@
   import type { TagType } from '@/utils/status-tag-registry'
   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'
 
   /** 根据来源类型返回协议状态标签 */
   function protocolStatusLabelBySource(status: string, source: string): string {
@@ -361,6 +387,14 @@
     executionDialogVisible.value = true
   }
 
+  const editTaskInfoVisible = ref(false)
+  const editTaskInfoId = ref('')
+
+  function handleEditTask(row: Record<string, any>) {
+    editTaskInfoId.value = row.id
+    editTaskInfoVisible.value = true
+  }
+
   // ---------------------------------------------------------------------------
   // 初始化(支持从工单页面跳转带 keyword 筛选)
   // ---------------------------------------------------------------------------

+ 99 - 0
web-admin/src/views/production/production-work-order/CompleteProductionTaskDialog.vue

@@ -0,0 +1,99 @@
+<!-- 完成任务 — 生产结论 -->
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    :title="isCompletedEdit ? '修改生产结论' : '完成任务'"
+    width="520px"
+    class="dialog-form-layout"
+    align-center
+    destroy-on-close
+    :close-on-click-modal="false"
+    :append-to-body="true"
+    @close="handleClose"
+  >
+    <el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="biz-dialog-form">
+      <el-form-item label="生产结论" prop="conclusion">
+        <el-radio-group v-model="form.conclusion">
+          <el-radio value="cg">成功</el-radio>
+          <el-radio value="sb">失败</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注">
+        <el-input
+          v-model="form.remark"
+          type="textarea"
+          :rows="3"
+          placeholder="选填"
+          maxlength="512"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button class="btn-ghost" @click="handleClose"><el-icon><Close /></el-icon>取消</el-button>
+      <el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
+        <el-icon><Check /></el-icon>{{ isCompletedEdit ? '保存' : '确认完成' }}
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed, watch } from 'vue'
+  import { Close, Check } from '@element-plus/icons-vue'
+  import type { FormInstance, FormRules } from 'element-plus'
+
+  const props = withDefaults(
+    defineProps<{
+      visible: boolean
+      submitting?: boolean
+      isCompletedEdit?: boolean
+      initialConclusion?: string
+      initialRemark?: string
+    }>(),
+    {
+      isCompletedEdit: false,
+      initialConclusion: '',
+      initialRemark: ''
+    }
+  )
+
+  const emit = defineEmits<{
+    (e: 'update:visible', val: boolean): void
+    (e: 'submit', payload: { conclusion: string; remark?: string }): void
+  }>()
+
+  const dialogVisible = computed({
+    get: () => props.visible,
+    set: (val) => emit('update:visible', val)
+  })
+
+  const formRef = ref<FormInstance>()
+  const form = reactive({ conclusion: '', remark: '' })
+  const rules: FormRules = {
+    conclusion: [{ required: true, message: '请选择生产结论', trigger: 'change' }]
+  }
+
+  function handleClose() {
+    dialogVisible.value = false
+  }
+
+  async function handleSubmit() {
+    if (!formRef.value) return
+    await formRef.value.validate()
+    emit('submit', {
+      conclusion: form.conclusion,
+      remark: form.remark.trim() || undefined
+    })
+  }
+
+  watch(
+    () => props.visible,
+    (val) => {
+      if (val) {
+        form.conclusion = props.initialConclusion || ''
+        form.remark = props.initialRemark || ''
+        formRef.value?.clearValidate()
+      }
+    }
+  )
+</script>

+ 46 - 2
web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue

@@ -1,4 +1,5 @@
 <template>
+  <div class="task-execution-dialog-root">
   <el-dialog
     v-model="dialogVisible"
     :title="readonly ? `任务详情 — ${taskName}` : `任务执行 — ${taskName}`"
@@ -25,6 +26,17 @@
         <el-tag :type="editor.taskStatusTag(editor.taskDetail.value.taskStatus).type">
           {{ editor.taskStatusTag(editor.taskDetail.value.taskStatus).label }}
         </el-tag>
+        <span class="execution-header-item execution-header-conclusion">
+          生产结论:
+          <el-tag
+            v-if="productionConclusionTagType(executionTaskConclusion)"
+            :type="productionConclusionTagType(executionTaskConclusion)"
+            size="small"
+          >
+            {{ productionConclusionLabel(executionTaskConclusion) }}
+          </el-tag>
+          <template v-else>—</template>
+        </span>
         <div v-if="!readonly" class="execution-header-actions">
           <el-button
             v-if="editor.taskDetail.value.taskStatus === 'dpz'"
@@ -37,7 +49,7 @@
             <el-icon><VideoPlay /></el-icon>开始执行
           </el-button>
           <el-button
-            v-if="editor.taskDetail.value.taskStatus === 'jxz'"
+            v-if="editor.taskDetail.value.taskStatus === 'jxz' || editor.taskDetail.value.taskStatus === 'ywc'"
             class="btn-main"
             type="success"
             size="small"
@@ -303,6 +315,16 @@
       </div>
     </div>
   </el-dialog>
+
+  <CompleteProductionTaskDialog
+    v-model:visible="completeConclusionVisible"
+    :submitting="taskActionLoading"
+    :is-completed-edit="completeConclusionIsEdit"
+    :initial-conclusion="completeConclusionInitial.conclusion"
+    :initial-remark="completeConclusionInitial.remark"
+    @submit="submitCompleteWithConclusion"
+  />
+  </div>
 </template>
 
 <script setup lang="ts">
@@ -312,6 +334,11 @@
   import StepDetailPanel from './StepDetailPanel.vue'
   import TaskInputEnhancedTable from './TaskInputEnhancedTable.vue'
   import TaskOutputList from './TaskOutputList.vue'
+  import CompleteProductionTaskDialog from './CompleteProductionTaskDialog.vue'
+  import {
+    productionConclusionLabel,
+    productionConclusionTagType
+  } from '@/utils/production-labels'
 
   const props = defineProps<{
     visible: boolean
@@ -333,6 +360,19 @@
   })
 
   const editor = useTaskStepEditor()
+  /** 顶层解构 Ref,避免 v-model 收到 Ref 对象被当成 truthy 而误开弹窗 */
+  const {
+    completeConclusionVisible,
+    completeConclusionIsEdit,
+    completeConclusionInitial,
+    taskActionLoading,
+    submitCompleteWithConclusion,
+    taskDetail
+  } = editor
+
+  const executionTaskConclusion = computed(
+    () => (taskDetail.value as { conclusion?: string } | null)?.conclusion || ''
+  )
 
   function handleClose() {
     dialogVisible.value = false
@@ -348,7 +388,11 @@
   watch(
     () => props.visible,
     async (val) => {
-      if (val && props.taskId) {
+      if (!val) {
+        completeConclusionVisible.value = false
+        return
+      }
+      if (props.taskId) {
         await editor.initEditor(props.taskId, { mode: 'execution' })
       }
     }

+ 56 - 1
web-admin/src/views/production/production-work-order/index.vue

@@ -132,7 +132,7 @@
           <el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
           <el-table-column
             label="操作"
-            min-width="540"
+            min-width="600"
             fixed="right"
             align="center"
             header-align="center"
@@ -141,6 +141,9 @@
               <el-button class="btn-op-item" @click="handleView(row)"
                 ><el-icon><View /></el-icon>查看详情</el-button
               >
+              <el-button class="btn-op-item" @click="handleEditRemark(row)">
+                <el-icon><EditPen /></el-icon>编辑
+              </el-button>
               <el-button class="btn-op-item" @click="handleEditTask(row)"
                 ><el-icon><EditPen /></el-icon>编辑任务</el-button
               >
@@ -249,6 +252,18 @@
                 </el-tag>
               </template>
             </el-table-column>
+            <el-table-column prop="conclusion" label="生产结论" min-width="100" align="center">
+              <template #default="{ row }">
+                <el-tag
+                  v-if="productionConclusionTagType(row.conclusion)"
+                  :type="productionConclusionTagType(row.conclusion)"
+                  size="small"
+                >
+                  {{ row.conclusionLabel || productionConclusionLabel(row.conclusion) }}
+                </el-tag>
+                <span v-else>—</span>
+              </template>
+            </el-table-column>
             <el-table-column prop="remark" label="备注" min-width="140">
               <template #default="{ row }">{{ row.remark || '—' }}</template>
             </el-table-column>
@@ -320,6 +335,14 @@
           </el-button>
         </template>
       </el-dialog>
+
+      <WorkOrderRemarkDialog
+        v-model:visible="remarkEditVisible"
+        :title="remarkEditTitle"
+        :initial-remark="remarkEditInitialRemark"
+        :submitting="remarkEditLoading"
+        @submit="submitRemarkEdit"
+      />
     </section>
   </div>
 </template>
@@ -344,6 +367,7 @@
     fetchProductionWorkOrderDetail,
     fetchCompleteProductionWorkOrder,
     fetchCreateProductionWorkOrder,
+    fetchUpdateProductionWorkOrder,
     fetchDeleteProductionWorkOrder,
     type ProductionWorkOrderDetail
   } from '@/api/production-work-order'
@@ -381,6 +405,11 @@
   import { downloadAttachmentFile } from '@/utils/download-attachment'
   import DispatchTaskDialog from './DispatchTaskDialog.vue'
   import EditTaskInfoDialog from './EditTaskInfoDialog.vue'
+  import WorkOrderRemarkDialog from '@/components/business/work-order-remark-dialog/index.vue'
+  import {
+    productionConclusionLabel,
+    productionConclusionTagType
+  } from '@/utils/production-labels'
   import {
     fetchProductionTaskList,
     fetchDeleteProductionTask
@@ -440,6 +469,32 @@
     }
   }
 
+  const remarkEditVisible = ref(false)
+  const remarkEditLoading = ref(false)
+  const remarkEditRow = ref<{ id: string; workOrderNo?: string; remark?: string } | null>(null)
+  const remarkEditTitle = computed(
+    () => `编辑备注${remarkEditRow.value?.workOrderNo ? ` — ${remarkEditRow.value.workOrderNo}` : ''}`
+  )
+  const remarkEditInitialRemark = computed(() => remarkEditRow.value?.remark || '')
+
+  function handleEditRemark(row: { id: string; workOrderNo?: string; remark?: string }) {
+    remarkEditRow.value = row
+    remarkEditVisible.value = true
+  }
+
+  async function submitRemarkEdit(remark: string) {
+    if (!remarkEditRow.value?.id) return
+    remarkEditLoading.value = true
+    try {
+      await fetchUpdateProductionWorkOrder(remarkEditRow.value.id, { remark })
+      ElMessage.success('保存成功')
+      remarkEditVisible.value = false
+      loadData()
+    } finally {
+      remarkEditLoading.value = false
+    }
+  }
+
   const woStatusTag = (status: string) => {
     const map: Record<string, { label: string; type: string }> = {
       dpz: { label: '待配置', type: 'info' },

+ 32 - 12
web-admin/src/views/production/production-work-order/useTaskStepEditor.ts

@@ -938,22 +938,38 @@ export function useTaskStepEditor() {
     return null
   }
 
+  const completeConclusionVisible = ref(false)
+  const completeConclusionIsEdit = ref(false)
+  const completeConclusionInitial = reactive({ conclusion: '', remark: '' })
+
   async function handleCompleteTask() {
     if (!taskDetail.value?.id || taskActionLoading.value) return
-    const ioErr = validateTaskIoRequired()
-    if (ioErr) {
-      await ElMessageBox.alert(ioErr, '无法完成任务', { type: 'warning' })
-      return
-    }
-    try {
-      await ElMessageBox.confirm('确定完成该任务吗?完成后不可恢复。', '提示', { type: 'warning' })
-    } catch {
-      return
+    const isCompleted = taskDetail.value.taskStatus === 'ywc'
+    completeConclusionIsEdit.value = isCompleted
+    completeConclusionInitial.conclusion = (taskDetail.value as { conclusion?: string }).conclusion || ''
+    completeConclusionInitial.remark = taskDetail.value.remark || ''
+    if (!isCompleted) {
+      const ioErr = validateTaskIoRequired()
+      if (ioErr) {
+        await ElMessageBox.alert(ioErr, '无法完成任务', { type: 'warning' })
+        return
+      }
     }
+    completeConclusionVisible.value = true
+  }
+
+  async function submitCompleteWithConclusion(payload: { conclusion: string; remark?: string }) {
+    if (!taskDetail.value?.id || taskActionLoading.value) return
+    const isCompleted = taskDetail.value.taskStatus === 'ywc'
     taskActionLoading.value = true
     try {
-      await fetchUpdateProductionTaskStatus(taskDetail.value.id, { taskStatus: 'ywc' })
-      ElMessage.success('任务已完成')
+      await fetchUpdateProductionTaskStatus(taskDetail.value.id, {
+        taskStatus: 'ywc',
+        conclusion: payload.conclusion,
+        remark: payload.remark
+      })
+      ElMessage.success(isCompleted ? '结论与备注已更新' : '任务已完成')
+      completeConclusionVisible.value = false
       await loadTaskDetail(taskDetail.value.id)
     } catch {
       /* global error handling */
@@ -1118,6 +1134,11 @@ export function useTaskStepEditor() {
     handleCompleteStep,
     handleStartTask,
     handleCompleteTask,
+    completeConclusionVisible,
+    completeConclusionIsEdit,
+    completeConclusionInitial,
+    submitCompleteWithConclusion,
+    taskActionLoading,
     addTaskInputRow,
     removeTaskInputRow,
     saveTaskInputs,
@@ -1149,7 +1170,6 @@ export function useTaskStepEditor() {
     saveDataCollections,
     manualSaveLoading,
     stepActionLoading,
-    taskActionLoading,
     handleManualSave,
     initEditor
   }

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

@@ -136,16 +136,23 @@
             </template>
           </el-table-column>
           <el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
-          <el-table-column label="操作" :min-width="activeTypeTab === 'temporary' ? 380 : 320" fixed="right" align="center">
+          <el-table-column label="操作" min-width="380" fixed="right" align="center">
             <template #default="{ row }">
               <el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
               <el-button
-                v-if="activeTypeTab === 'temporary'"
+                v-if="activeTypeTab === 'temporary' || (activeTypeTab === 'all' && row.workOrderType === 'temporary')"
                 class="btn-op-item"
                 @click="handleEdit(row)"
               >
                 <el-icon><EditPen /></el-icon>编辑
               </el-button>
+              <el-button
+                v-if="activeTypeTab === 'inbound' || (activeTypeTab === 'all' && row.workOrderType !== 'temporary')"
+                class="btn-op-item"
+                @click="handleEditRemark(row)"
+              >
+                <el-icon><EditPen /></el-icon>编辑
+              </el-button>
               <el-button
                 v-if="row.workOrderStatus === 'dpz' || row.workOrderStatus === 'ypz'"
                 class="btn-op-item primary"
@@ -316,6 +323,14 @@
           </el-button>
         </template>
       </el-dialog>
+
+      <WorkOrderRemarkDialog
+        v-model:visible="remarkEditVisible"
+        :title="remarkEditTitle"
+        :initial-remark="remarkEditInitialRemark"
+        :submitting="remarkEditLoading"
+        @submit="submitRemarkEdit"
+      />
     </section>
   </div>
 </template>
@@ -376,6 +391,7 @@
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
   import { downloadAttachmentFile } from '@/utils/download-attachment'
+  import WorkOrderRemarkDialog from '@/components/business/work-order-remark-dialog/index.vue'
 
   defineOptions({ name: 'QualityInspectionQcWorkOrder' })
 
@@ -500,6 +516,32 @@
     addVisible.value = true
   }
 
+  const remarkEditVisible = ref(false)
+  const remarkEditLoading = ref(false)
+  const remarkEditRow = ref<QcWorkOrder | null>(null)
+  const remarkEditTitle = computed(
+    () => `编辑备注${remarkEditRow.value?.workOrderNo ? ` — ${remarkEditRow.value.workOrderNo}` : ''}`
+  )
+  const remarkEditInitialRemark = computed(() => remarkEditRow.value?.remark || '')
+
+  function handleEditRemark(row: QcWorkOrder) {
+    remarkEditRow.value = row
+    remarkEditVisible.value = true
+  }
+
+  async function submitRemarkEdit(remark: string) {
+    if (!remarkEditRow.value?.id) return
+    remarkEditLoading.value = true
+    try {
+      await fetchUpdateQcWorkOrder(remarkEditRow.value.id, { remark })
+      ElMessage.success('保存成功')
+      remarkEditVisible.value = false
+      loadData()
+    } finally {
+      remarkEditLoading.value = false
+    }
+  }
+
   function handleTypeTabChange() {
     pagination.page = 1
     loadData()

+ 24 - 192
web-admin/src/views/quality-inspection/storage-release-review/StorageReleaseViewDialog.vue

@@ -29,72 +29,12 @@
       </div>
     </template>
 
-    <template #qc-tasks>
-      <div v-if="detail?.qcInfo?.tasks?.length" class="view-section-slot">
-        <div class="qc-task-cards">
-          <div v-for="task in detail.qcInfo.tasks" :key="task.id" class="qc-task-card">
-            <div class="qc-task-card__header">
-              <span class="qc-task-card__type">
-                <el-tag type="info" size="small">{{
-                  task.taskTypeLabel || task.taskType || '—'
-                }}</el-tag>
-              </span>
-              <span class="qc-task-card__conclusion">
-                <el-tag
-                  v-if="task.conclusion"
-                  :type="task.conclusion === 'tg' ? 'success' : 'danger'"
-                  size="small"
-                  >{{ task.conclusionLabel || task.conclusion }}</el-tag
-                >
-                <span v-else class="qc-kv__muted">—</span>
-              </span>
-            </div>
-            <div class="qc-task-card__body">
-              <div class="qc-kv">
-                <span class="qc-kv__label">任务状态</span>
-                <span class="qc-kv__value">
-                  <el-tag :type="taskStatusTagType(task.taskStatus)" size="small">{{
-                    task.taskStatusLabel || taskStatusLabel(task.taskStatus || '') || '—'
-                  }}</el-tag>
-                </span>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div v-else class="view-section-slot"><p class="view-empty-hint">暂无质检任务</p></div>
+    <template #qc-info>
+      <SampleQcInfoCards :qc-info="detail?.qcInfo" :sample-reception="viewReceptionBrief" />
     </template>
 
-    <template #production-tasks>
-      <div v-if="detail?.productionInfo?.tasks?.length" class="view-section-slot">
-        <div class="qc-task-cards">
-          <div v-for="task in detail.productionInfo.tasks" :key="task.id" class="qc-task-card">
-            <div class="qc-task-card__header">
-              <span class="qc-task-card__name">{{ task.taskName || task.taskNo || '—' }}</span>
-              <span class="qc-task-card__conclusion">
-                <el-tag :type="taskStatusTagType(task.taskStatus)" size="small">{{
-                  task.taskStatusLabel || productionTaskStatusLabel(task.taskStatus || '') || '—'
-                }}</el-tag>
-              </span>
-            </div>
-            <div class="qc-task-card__body">
-              <div class="qc-kv">
-                <span class="qc-kv__label">任务号</span>
-                <span class="qc-kv__value">{{ task.taskNo || '—' }}</span>
-              </div>
-              <div class="qc-kv">
-                <span class="qc-kv__label">开始时间</span>
-                <span class="qc-kv__value">{{ task.startedTime || '—' }}</span>
-              </div>
-              <div class="qc-kv">
-                <span class="qc-kv__label">完成时间</span>
-                <span class="qc-kv__value">{{ task.completedTime || '—' }}</span>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div v-else class="view-section-slot"><p class="view-empty-hint">暂无生产任务</p></div>
+    <template #production-info>
+      <SampleProductionInfoCards :production-info="detail?.productionInfo" />
     </template>
 
     <template #review-attachments>
@@ -114,6 +54,8 @@
   import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
   import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
   import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
+  import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
+  import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
   import type { QcWorkOrderReceptionDetail } from '@/api/qc-work-order'
   import {
     fetchStorageReleaseReviewDetail,
@@ -131,14 +73,7 @@
     donationStatusLabel,
     donationStatusTagType
   } from '@/utils/status-tag-registry'
-  import {
-    workOrderStatusLabel,
-    workOrderStatusTagType,
-    acceptConclusionLabel,
-    transportTypeLabel,
-    taskStatusLabel,
-    taskStatusTagType
-  } from '@/utils/qc-labels'
+  import { acceptConclusionLabel, transportTypeLabel } from '@/utils/qc-labels'
   import { downloadAttachmentFile } from '@/utils/download-attachment'
 
   const props = defineProps<{
@@ -160,35 +95,6 @@
 
   const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
 
-  const PRODUCTION_WO_STATUS_LABELS: Record<string, string> = {
-    dpz: '待配置',
-    jxz: '进行中',
-    ywc: '已完成'
-  }
-
-  const PRODUCTION_TASK_STATUS_LABELS: Record<string, string> = {
-    dpz: '待配置',
-    dzx: '待执行',
-    jxz: '执行中',
-    ywc: '已完成'
-  }
-
-  function productionWorkOrderStatusLabel(status?: string) {
-    const s = status || ''
-    return PRODUCTION_WO_STATUS_LABELS[s] || s || '—'
-  }
-
-  function productionWorkOrderStatusTagType(status?: string) {
-    if (status === 'ywc') return 'success' as const
-    if (status === 'jxz') return 'warning' as const
-    return 'info' as const
-  }
-
-  function productionTaskStatusLabel(status?: string) {
-    const s = status || ''
-    return PRODUCTION_TASK_STATUS_LABELS[s] || s || '—'
-  }
-
   watch(
     () => [props.visible, props.reviewId] as const,
     async ([vis, id]) => {
@@ -250,6 +156,12 @@
     } as QcWorkOrderReceptionDetail
   })
 
+  const viewReceptionBrief = computed(() => {
+    const r = viewReception.value
+    if (!r?.acceptConclusion) return null
+    return { acceptConclusion: r.acceptConclusion }
+  })
+
   const viewReceptionAttachmentFiles = computed(() => {
     const r = viewReception.value
     if (!r) return []
@@ -289,8 +201,6 @@
     const rev: Partial<StorageReleaseReview> = d.review || {}
     const proto: Partial<ProtocolInfo> = d.protocolInfo || {}
     const r = viewReception.value
-    const qc = d.qcInfo || {}
-    const production = d.productionInfo || {}
     const source = rev.receptionSource || 'cell_storage'
     const protocolStatus = protocolStatusDisplay(proto, source)
 
@@ -327,9 +237,6 @@
         ? [{ title: '运输信息', fields: transportFields }]
         : []
 
-    const qcWoStatus = qc.workOrderStatus || ''
-    const prodWoStatus = production.workOrderStatus || ''
-
     return [
       {
         title: '基础信息',
@@ -378,35 +285,13 @@
       },
       {
         title: '质检信息',
-        slot: 'qc-tasks',
-        fields: [
-          { label: '工单号', value: qc.workOrderNo || '—' },
-          {
-            label: '工单状态',
-            value:
-              qc.workOrderStatusLabel ||
-              workOrderStatusLabel(qcWoStatus) ||
-              qcWoStatus ||
-              '—',
-            tagType: workOrderStatusTagType(qcWoStatus)
-          }
-        ]
+        slot: 'qc-info',
+        fields: []
       },
       {
         title: '生产信息',
-        slot: 'production-tasks',
-        fields: [
-          { label: '工单号', value: production.workOrderNo || '—' },
-          {
-            label: '工单状态',
-            value:
-              production.workOrderStatusLabel ||
-              productionWorkOrderStatusLabel(prodWoStatus) ||
-              prodWoStatus ||
-              '—',
-            tagType: productionWorkOrderStatusTagType(prodWoStatus)
-          }
-        ]
+        slot: 'production-info',
+        fields: []
       },
       {
         title: '放行审核',
@@ -451,68 +336,15 @@
 </script>
 
 <style scoped lang="scss">
-  .view-empty-hint {
-    margin: 0;
-    font-size: 13px;
-    color: #94a3b8;
-  }
-
-  .qc-task-cards {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
-    gap: 10px;
-  }
-
-  .qc-task-card {
-    border: 1px solid #e2e8f0;
-    border-radius: 8px;
-    background: #fff;
-    overflow: hidden;
-  }
-
-  .qc-task-card__header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    gap: 8px;
-    padding: 10px 12px;
-    border-bottom: 1px solid #f1f5f9;
-  }
-
-  .qc-task-card__name {
-    font-size: 13px;
-    font-weight: 600;
-    color: #1e293b;
-  }
-
-  .qc-task-card__body {
-    padding: 10px 12px;
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-  }
-
-  .qc-kv {
-    display: flex;
-    align-items: flex-start;
-    gap: 8px;
-    font-size: 12px;
-  }
-
-  .qc-kv__label {
-    color: #94a3b8;
-    flex-shrink: 0;
-    min-width: 56px;
-  }
-
-  .qc-kv__value {
-    color: #1e293b;
-    font-weight: 500;
+  .view-section-slot :deep(.sample-card__section-label) {
+    display: none;
   }
 
-  .qc-kv__muted {
-    color: #94a3b8;
-    font-weight: 400;
+  .view-section-slot :deep(.sample-card__section--qc),
+  .view-section-slot :deep(.sample-card__section--production) {
+    margin-top: 0;
+    padding-top: 0;
+    border-top: none;
   }
 
   .view-reception-attachments {

Деякі файли не було показано, через те що забагато файлів було змінено