From db31990ebe244a0c59f023852f89357d506e1b88 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sat, 10 Oct 2026 21:20:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=86=E8=83=9E=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=BC=B4=E8=B4=B9=E5=82=AC=E7=BC=B4=E3=80=81=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E6=A0=B7=E6=9C=AC=E4=BF=A1=E6=81=AF=E4=B8=8E=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E8=83=BD=E5=8A=9B=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 汇总近期业务改动:催缴菜单与后端、样本信息快捷登记与进度、运费关联、客户档案扩展、协议优惠/期次与押金划转,并同步 SQL 迁移与管理端页面收敛。 Co-authored-by: Cursor --- backend/app/api/basic_info.py | 27 + backend/app/api/collection_protocol.py | 88 + backend/app/api/menu.py | 17 +- backend/app/api/protocol_sample_collection.py | 121 +- .../research_donation_sample_collection.py | 8 + backend/app/crud/collection_protocol.py | 30 +- backend/app/crud/collection_region.py | 40 + backend/app/crud/customer.py | 24 + .../app/crud/protocol_sample_collection.py | 162 +- backend/app/crud/research_donation.py | 74 +- .../research_donation_sample_collection.py | 46 +- backend/app/crud/sample_freight.py | 196 +- .../app/crud/sample_info_quick_register.py | 260 ++ backend/app/models/models.py | 8 +- backend/app/models/research_donation.py | 3 +- backend/app/services/oa_assignee.py | 3 +- backend/app/services/party_customer_sync.py | 135 +- backend/app/services/payment_chase.py | 50 + .../app/services/protocol_pay_mode_switch.py | 4 +- .../services/protocol_sample_list_summary.py | 13 +- .../app/services/qc_work_order_fee_summary.py | 70 +- .../app/services/sample_info_list_enrich.py | 145 ++ backend/app/services/sample_info_progress.py | 98 + .../services/sample_info_quick_register.py | 45 + .../services/sample_payment_fgd_convert.py | 103 + .../services/sample_payment_period_summary.py | 90 + .../sample_payment_schedule_validate.py | 101 + backend/start-test.sh | 3 +- .../test_collection_regions_by_staff_user.py | 25 + backend/tests/test_oa_assignee.py | 19 + backend/tests/test_party_customer_sync.py | 76 +- backend/tests/test_payment_chase.py | 103 + .../test_protocol_list_sample_reception_qc.py | 32 + backend/tests/test_sample_freight.py | 73 +- backend/tests/test_sample_info_handle.py | 34 + backend/tests/test_sample_info_progress.py | 107 + .../tests/test_sample_info_quick_register.py | 42 + .../tests/test_sample_payment_fgd_convert.py | 56 + .../test_sample_payment_period_summary.py | 94 + .../test_sample_payment_schedule_validate.py | 72 + ...2026-10-08-freight-link-existing-demo.html | 746 ++++++ ...26-10-08-payment-info-excel-list-demo.html | 964 +++++++ .../index.html | 514 ++++ docs/sql/biz_per_sample_payment_presign.sql | 41 +- ...26-09-18-rename-collection-region-menu.sql | 18 - .../2026-09-20-oa-instance-comment.sql | 20 - .../2026-09-21-oa-instance-view-grant.sql | 97 - .../2026-09-22-purge-oa-instance-data.sql | 68 - .../2026-10-05-presign-order-menu.sql | 73 + .../2026-10-05-unhide-payment-info-menu.sql | 38 +- ...ide-research-donation-sample-info-menu.sql | 29 + .../2026-10-08-customer-profile-expand.sql | 10 + .../2026-10-08-protocol-region-manager.sql | 10 + ...e-cell-storage-menu-to-storage-service.sql | 21 + ...026-10-08-sample-freight-no-fix-format.sql | 30 + .../2026-10-08-sample-freight-no.sql | 35 + .../2026-10-09-payment-chase-menu.sql | 74 + .../2026-10-10-protocol-deposit-transfer.sql | 8 + .../plans/2026-10-05-unified-sample-info.md | 736 ++++++ ...2026-10-08-customer-profile-expand-sync.md | 40 +- ...2026-10-08-freight-link-existing-enrich.md | 270 ++ ...otocol-market-specialist-region-manager.md | 425 +++ ...10-09-payment-info-excel-list-and-chase.md | 868 ++++++ ...ayment-period-columns-and-schedule-edit.md | 286 ++ ...-09-protocol-preferential-price-payment.md | 61 + ...-10-10-payment-detail-responsive-dialog.md | 135 + ...-oa-auto-pass-initiator-approver-design.md | 4 +- .../2026-10-05-unified-sample-info-design.md | 249 ++ ...-08-freight-link-existing-enrich-design.md | 155 ++ ...market-specialist-region-manager-design.md | 128 + ...ayment-info-excel-list-and-chase-design.md | 244 ++ ...period-columns-and-schedule-edit-design.md | 115 + ...tocol-preferential-price-payment-design.md | 123 + .../graphify-out/.graphify_analysis.json | 2319 ++++++++--------- web-admin/graphify-out/.graphify_extract.json | 218 +- web-admin/graphify-out/GRAPH_REPORT.md | 258 +- web-admin/graphify-out/graph.html | 8 +- web-admin/graphify-out/manifest.json | 6 +- .../test-payment-detail-dialog-layout.ts | 21 + .../scripts/test-protocol-payment-presets.ts | 18 - web-admin/src/api/admin.ts | 5 + .../src/api/protocol-sample-collection.ts | 7 +- web-admin/src/api/qc-work-order.ts | 2 + .../research-donation-sample-collection.ts | 5 +- web-admin/src/api/sample-freight.ts | 6 + web-admin/src/api/sample-info-handle.ts | 37 + web-admin/src/assets/styles/core/app.scss | 65 +- .../index.vue | 174 +- .../outbound-material-form-sections/index.vue | 7 +- .../business/protocol-handle-panel/index.vue | 283 ++ .../index.vue | 3 + .../sample-freight-pick-table/index.vue | 196 ++ .../sample-payment-detail-dialog/index.vue | 129 +- .../sample-production-info-cards/index.vue | 10 +- .../sample-qc-info-grouped-panel/index.vue | 14 +- .../index.vue | 14 +- .../sample-storage-release-info/index.vue | 10 +- .../sample-transport-form-section/index.vue | 123 +- .../business/system-view-dialog/index.vue | 23 +- .../oa-approval/OaAppIconPicker.vue | 5 +- .../src/components/oa-approval/OaFcUpload.vue | 1 + .../oa-approval/OaRelatedApproval.vue | 3 +- .../oa-approval/OaStartFormDialog.vue | 3 +- web-admin/src/router/modules/sqxp.ts | 17 +- web-admin/src/types/api.d.ts | 4 + web-admin/src/utils/datetime-display.ts | 7 +- .../utils/donation-standard-import-parse.ts | 350 --- .../src/utils/protocol-audit-flatten.test.ts | 43 +- web-admin/src/utils/protocol-audit-flatten.ts | 56 +- .../src/utils/protocol-payment-flow-steps.ts | 5 +- .../src/utils/protocol-payment-presets.ts | 48 +- .../src/utils/protocol-sample-payment.test.ts | 333 +++ .../src/utils/protocol-sample-payment.ts | 365 ++- .../utils/protocol-standard-import-parse.ts | 443 ---- web-admin/src/utils/sample-freight-flatten.ts | 98 + web-admin/src/utils/status-tag-registry.ts | 58 +- .../cell-sample-type/SampleCodeTab.vue | 1 + .../basic-info/cell-sample-type/index.vue | 1 + .../CollectionProtocolNoTab.vue | 1 + .../basic-info/collection-region/index.vue | 8 +- .../src/views/basic-info/customer/index.vue | 310 ++- .../basic-info/insurance-company/index.vue | 4 - .../cell-storage/fullmoon-todo/index.vue | 6 +- .../cell-storage/my-material-usage/index.vue | 3 - .../cell-storage/payment-chase/index.vue | 550 ++++ .../payment-confirm/InvoiceRequestTab.vue | 6 +- .../payment-confirm/RefundConfirmTab.vue | 8 +- .../views/cell-storage/payment-info/index.vue | 83 +- .../cell-storage/presign-order/index.vue | 8 +- .../protocol-audit/DonationProtocolTab.vue | 1164 +++------ .../protocol-audit/FullmoonTodoTab.vue | 2205 ---------------- .../protocol-audit/PaymentInfoTab.vue | 435 ++-- .../protocol-audit/ProtocolAuditTab.vue | 37 +- .../SampleCollectionInfoTab.vue | 301 --- .../protocol-audit/SampleQcInfoTab.vue | 355 --- .../protocol-audit/SampleReceptionInfoTab.vue | 345 --- .../protocol-audit/StorageCertTab.vue | 866 ------ .../protocol-audit/StorageEntryInfoTab.vue | 336 --- .../protocol-audit/UnifiedProtocolPage.vue | 2 +- .../protocol/ProtocolAttachmentReadonly.vue | 37 +- ...ock.vue => ProtocolRefundRecordDialog.vue} | 173 +- .../src/views/cell-storage/protocol/index.vue | 41 +- .../sample-freight/FreightConfigDialog.vue | 209 +- .../cell-storage/sample-freight/index.vue | 157 +- .../SampleInfoQuickRegisterDialog.vue | 12 +- .../sample-info/UnifiedSampleInfoPage.vue | 17 + .../sample-reception-info/index.vue | 4 +- .../views/cell-storage/storage-cert/index.vue | 6 +- .../cell-storage/storage-entry-info/index.vue | 4 +- .../src/views/dashboard/console/index.vue | 3 - web-admin/src/views/oa-approval/cc/index.vue | 4 - .../src/views/oa-approval/done/index.vue | 4 - .../src/views/oa-approval/drafts/index.vue | 4 - .../oa-approval/form-designer-spike/index.vue | 3 - .../src/views/oa-approval/history/index.vue | 4 - .../src/views/oa-approval/mentions/index.vue | 4 - .../src/views/oa-approval/mine/index.vue | 4 - .../src/views/oa-approval/pending/index.vue | 4 - .../oa-approval/receive-accounts/index.vue | 4 - .../src/views/oa-approval/start/index.vue | 87 +- .../src/views/operations/clean-room/index.vue | 9 +- web-admin/src/views/operations/demo/index.vue | 4 - .../exhibition-appointment/index.vue | 5 +- .../operations/storage-facility/index.vue | 4 +- .../cell-product-type/CellProductPriceTab.vue | 1 + .../cell-product-type/CellProductSpecTab.vue | 1 + .../ProjectPriceSystemTab.vue | 4 +- .../pilot-service/cell-product-type/index.vue | 4 - .../channel-service-order-finance/index.vue | 2 +- .../src/views/pilot-service/channel/index.vue | 11 +- .../checkup-institution/index.vue | 4 - .../checkup-management/index.vue | 3 - .../RefundConfirmTab.vue | 6 +- .../clinical-payment-confirm/index.vue | 5 +- .../infusion-schedule-management/index.vue | 3 - .../outbound-release-review/index.vue | 4 - .../pilot-service/pilot-application/index.vue | 48 +- .../views/production/cell-product/index.vue | 5 +- .../production/cleanroom-garment/index.vue | 4 - .../consumable/ConsumablePackTab.vue | 5 +- .../src/views/production/consumable/index.vue | 10 +- .../src/views/production/equipment/index.vue | 5 +- .../production/pre-transit-sample/index.vue | 4 - .../process-category/OutputTypeTab.vue | 1 + .../production/process-category/index.vue | 4 - .../production/process/ProcessRouteTab.vue | 27 +- .../src/views/production/process/index.vue | 20 +- ...ductionMaterialConfiguredCatalogDialog.vue | 4 +- .../production/production-task-set/index.vue | 4 - .../EditTaskInfoDialog.vue | 5 +- .../production-work-order/StepDetailPanel.vue | 4 +- .../TaskInputEnhancedTable.vue | 5 +- .../tabs/ProductionWorkOrderTabPanel.vue | 2 +- .../src/views/production/reagent/index.vue | 10 +- web-admin/src/views/production/sop/index.vue | 5 +- .../views/production/waste-output/index.vue | 4 - .../env-detect-item/index.vue | 5 +- .../env-detect-record/index.vue | 5 +- .../inspection-item-template/index.vue | 4 - .../lab-test-item/ThirdPartyCompanyTab.vue | 1 + .../lab-test-item/index.vue | 4 - .../my-qc-material-usage/index.vue | 3 - .../pre-transit-sample/index.vue | 4 - .../qc-material-amount-auth/index.vue | 4 - .../qc-work-order/QcWorkOrderTaskSubList.vue | 1 + .../tabs/QcWorkOrderTabPanel.vue | 2 +- .../sample-reception-check-item/index.vue | 11 +- .../StorageReleaseReviewDialog.vue | 11 +- .../storage-release-review/index.vue | 4 - .../list/DonationFormDialog.vue | 1191 ++++----- .../list/DonationViewDialog.vue | 113 +- .../views/research-donation/list/index.vue | 6 +- .../DonationSampleCollectionInfoTab.vue | 302 --- .../research-donation/sample-info/index.vue | 8 +- web-admin/src/views/system/category/index.vue | 15 +- web-admin/src/views/system/dept/index.vue | 13 +- web-admin/src/views/system/dict/index.vue | 12 +- web-admin/src/views/system/express/index.vue | 5 +- web-admin/src/views/system/leave/index.vue | 14 +- web-admin/src/views/system/menu/index.vue | 13 +- web-admin/src/views/system/order/index.vue | 26 +- web-admin/src/views/system/post/index.vue | 6 +- web-admin/src/views/system/product/index.vue | 14 +- web-admin/src/views/system/role/index.vue | 11 +- web-admin/src/views/system/site/index.vue | 5 +- web-admin/src/views/system/tenant/index.vue | 5 +- web-admin/src/views/system/unit/index.vue | 5 +- web-admin/src/views/system/user/detail.vue | 32 +- web-admin/src/views/system/user/index.vue | 4 - .../system/workflow/designer/FlowDesigner.vue | 21 +- .../system/workflow/my-applications/index.vue | 4 - .../views/system/workflow/process/index.vue | 4 - .../system/workflow/task-completed/index.vue | 4 - .../system/workflow/task-pending/index.vue | 4 - 提到我的 (1).png | Bin 7195 -> 0 bytes 235 files changed, 14898 insertions(+), 10196 deletions(-) create mode 100644 backend/app/crud/sample_info_quick_register.py create mode 100644 backend/app/services/payment_chase.py create mode 100644 backend/app/services/sample_info_list_enrich.py create mode 100644 backend/app/services/sample_info_progress.py create mode 100644 backend/app/services/sample_info_quick_register.py create mode 100644 backend/app/services/sample_payment_fgd_convert.py create mode 100644 backend/app/services/sample_payment_period_summary.py create mode 100644 backend/app/services/sample_payment_schedule_validate.py create mode 100644 backend/tests/test_collection_regions_by_staff_user.py create mode 100644 backend/tests/test_payment_chase.py create mode 100644 backend/tests/test_sample_info_handle.py create mode 100644 backend/tests/test_sample_info_progress.py create mode 100644 backend/tests/test_sample_info_quick_register.py create mode 100644 backend/tests/test_sample_payment_fgd_convert.py create mode 100644 backend/tests/test_sample_payment_period_summary.py create mode 100644 backend/tests/test_sample_payment_schedule_validate.py create mode 100644 docs/demos/2026-10-08-freight-link-existing-demo.html create mode 100644 docs/demos/2026-10-08-payment-info-excel-list-demo.html create mode 100644 docs/protocol-audit-tab-height-demos/index.html delete mode 100644 docs/sql/migrations/2026-09-18-rename-collection-region-menu.sql delete mode 100644 docs/sql/migrations/2026-09-20-oa-instance-comment.sql delete mode 100644 docs/sql/migrations/2026-09-21-oa-instance-view-grant.sql delete mode 100644 docs/sql/migrations/2026-09-22-purge-oa-instance-data.sql create mode 100644 docs/sql/migrations/2026-10-05-presign-order-menu.sql create mode 100644 docs/sql/migrations/2026-10-06-hide-research-donation-sample-info-menu.sql create mode 100644 docs/sql/migrations/2026-10-08-customer-profile-expand.sql create mode 100644 docs/sql/migrations/2026-10-08-protocol-region-manager.sql create mode 100644 docs/sql/migrations/2026-10-08-rename-cell-storage-menu-to-storage-service.sql create mode 100644 docs/sql/migrations/2026-10-08-sample-freight-no-fix-format.sql create mode 100644 docs/sql/migrations/2026-10-08-sample-freight-no.sql create mode 100644 docs/sql/migrations/2026-10-09-payment-chase-menu.sql create mode 100644 docs/sql/migrations/2026-10-10-protocol-deposit-transfer.sql create mode 100644 docs/superpowers/plans/2026-10-05-unified-sample-info.md create mode 100644 docs/superpowers/plans/2026-10-08-freight-link-existing-enrich.md create mode 100644 docs/superpowers/plans/2026-10-08-protocol-market-specialist-region-manager.md create mode 100644 docs/superpowers/plans/2026-10-09-payment-info-excel-list-and-chase.md create mode 100644 docs/superpowers/plans/2026-10-09-payment-period-columns-and-schedule-edit.md create mode 100644 docs/superpowers/plans/2026-10-09-protocol-preferential-price-payment.md create mode 100644 docs/superpowers/plans/2026-10-10-payment-detail-responsive-dialog.md create mode 100644 docs/superpowers/specs/2026-10-05-unified-sample-info-design.md create mode 100644 docs/superpowers/specs/2026-10-08-freight-link-existing-enrich-design.md create mode 100644 docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md create mode 100644 docs/superpowers/specs/2026-10-09-payment-info-excel-list-and-chase-design.md create mode 100644 docs/superpowers/specs/2026-10-09-payment-period-columns-and-schedule-edit-design.md create mode 100644 docs/superpowers/specs/2026-10-09-protocol-preferential-price-payment-design.md create mode 100644 web-admin/scripts/test-payment-detail-dialog-layout.ts create mode 100644 web-admin/src/api/sample-info-handle.ts create mode 100644 web-admin/src/components/business/protocol-handle-panel/index.vue create mode 100644 web-admin/src/components/business/sample-freight-pick-table/index.vue delete mode 100644 web-admin/src/utils/donation-standard-import-parse.ts create mode 100644 web-admin/src/utils/protocol-sample-payment.test.ts delete mode 100644 web-admin/src/utils/protocol-standard-import-parse.ts create mode 100644 web-admin/src/utils/sample-freight-flatten.ts create mode 100644 web-admin/src/views/cell-storage/payment-chase/index.vue delete mode 100644 web-admin/src/views/cell-storage/protocol-audit/FullmoonTodoTab.vue delete mode 100644 web-admin/src/views/cell-storage/protocol-audit/SampleCollectionInfoTab.vue delete mode 100644 web-admin/src/views/cell-storage/protocol-audit/SampleQcInfoTab.vue delete mode 100644 web-admin/src/views/cell-storage/protocol-audit/SampleReceptionInfoTab.vue delete mode 100644 web-admin/src/views/cell-storage/protocol-audit/StorageCertTab.vue delete mode 100644 web-admin/src/views/cell-storage/protocol-audit/StorageEntryInfoTab.vue rename web-admin/src/views/cell-storage/protocol/{ProtocolRefundRecordBlock.vue => ProtocolRefundRecordDialog.vue} (68%) delete mode 100644 web-admin/src/views/research-donation/sample-info/DonationSampleCollectionInfoTab.vue delete mode 100644 提到我的 (1).png diff --git a/backend/app/api/basic_info.py b/backend/app/api/basic_info.py index e969d646..ad96d83b 100644 --- a/backend/app/api/basic_info.py +++ b/backend/app/api/basic_info.py @@ -51,6 +51,10 @@ class CustomerCreateBody(BaseModel): sex: Optional[str] = Field("2", alias="sex") id_card_no: Optional[str] = Field(None, alias="idCardNo") age: Optional[int] = None + birth_ym: Optional[str] = Field(None, alias="birthYm") + ethnic: Optional[str] = None + email: Optional[str] = None + address: Optional[str] = None phone_no: Optional[str] = Field(None, alias="phoneNo") customer_tags: Optional[str] = Field(None, alias="customerTags") order_num: Optional[int] = Field(0, alias="orderNum") @@ -64,6 +68,10 @@ class CustomerUpdateBody(BaseModel): sex: Optional[str] = None id_card_no: Optional[str] = Field(None, alias="idCardNo") age: Optional[int] = None + birth_ym: Optional[str] = Field(None, alias="birthYm") + ethnic: Optional[str] = None + email: Optional[str] = None + address: Optional[str] = None phone_no: Optional[str] = Field(None, alias="phoneNo") customer_tags: Optional[str] = Field(None, alias="customerTags") order_num: Optional[int] = Field(None, alias="orderNum") @@ -147,6 +155,10 @@ async def create_customer( sex=body.sex or "2", id_card_no=body.id_card_no, age=body.age, + birth_ym=body.birth_ym, + ethnic=body.ethnic, + email=body.email, + address=body.address, phone_no=body.phone_no, customer_tags=body.customer_tags, tenant_no=tenant_no, @@ -176,6 +188,10 @@ async def update_customer( sex=payload.get("sex"), id_card_no=payload.get("id_card_no"), age=payload.get("age"), + birth_ym=payload.get("birth_ym"), + ethnic=payload.get("ethnic"), + email=payload.get("email"), + address=payload.get("address"), phone_no=payload.get("phone_no"), customer_tags=payload.get("customer_tags"), order_num=payload.get("order_num"), @@ -447,6 +463,17 @@ async def delete_collection_region( return error_response(400, str(e)) +@router.get("/collection-regions/by-staff-user/{user_id}") +async def api_list_regions_by_staff_user( + user_id: str, + user: dict = Depends(require_menu_code("biz:collectionRegion:list")), + db: Session = Depends(get_db), +): + tenant_no = _get_tenant_no(db, user) + items = region_svc.list_regions_by_staff_user(db, user_id=user_id, tenant_no=tenant_no) + return success_response({"list": items}) + + # --------------------------------------------------------------------------- # 采集地区 — 下属医院 # --------------------------------------------------------------------------- diff --git a/backend/app/api/collection_protocol.py b/backend/app/api/collection_protocol.py index 427d97bd..8b57026b 100644 --- a/backend/app/api/collection_protocol.py +++ b/backend/app/api/collection_protocol.py @@ -1090,6 +1090,94 @@ async def update_fullmoon_todo_insurance( return error_response(400, str(e)) +class StorageCertHandleBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + handle_time: Optional[str] = Field(None, alias="handleTime") + handler_user_id: Optional[str] = Field(None, alias="handlerUserId") + cert_image_urls: Optional[str] = Field(None, alias="certImageUrls") + remark: Optional[str] = None + + +class FullmoonHandleBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + handle_time: Optional[str] = Field(None, alias="handleTime") + handler_user_id: Optional[str] = Field(None, alias="handlerUserId") + insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments") + handle_remark: Optional[str] = Field(None, alias="handleRemark") + + +def _attach_handler_name(db: Session, payload: Dict[str, Any]) -> Dict[str, Any]: + uid = (payload.get("handlerUserId") or "").strip() + name = "" + if uid: + u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first() + name = (u.nick_name or "") if u else "" + payload["handlerUserName"] = name + return payload + + +@router.put("/storage-certs/{protocol_id}") +async def update_storage_cert_handle( + protocol_id: str, + body: StorageCertHandleBody, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _get_tenant_no(db, user) + user_id = _get_current_user_id(user) + if not _protocol_accessible(db, protocol_id, tenant_no, user): + return error_response(404, "协议单不存在") + try: + ht = parse_datetime(body.handle_time) if body.handle_time else None + sc = cp_svc.upsert_storage_cert_handle( + db, + protocol_id, + tenant_no, + user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + cert_image_urls=body.cert_image_urls, + remark=body.remark, + ) + return success_response( + _attach_handler_name(db, cp_svc._storage_cert_to_dict(sc)), message="更新成功" + ) + except ValueError as e: + return error_response(400, str(e)) + + +@router.put("/fullmoon-todos/{protocol_id}/handle") +async def update_fullmoon_todo_handle( + protocol_id: str, + body: FullmoonHandleBody, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _get_tenant_no(db, user) + user_id = _get_current_user_id(user) + if not _protocol_accessible(db, protocol_id, tenant_no, user): + return error_response(404, "协议单不存在") + try: + ht = parse_datetime(body.handle_time) if body.handle_time else None + ft = cp_svc.upsert_fullmoon_todo_handle( + db, + protocol_id, + tenant_no, + user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + insurance_attachments=body.insurance_attachments, + handle_remark=body.handle_remark, + ) + return success_response( + _attach_handler_name(db, cp_svc._fullmoon_todo_to_dict(ft)), message="更新成功" + ) + except ValueError as e: + return error_response(400, str(e)) + + class InvoiceRequestCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) diff --git a/backend/app/api/menu.py b/backend/app/api/menu.py index 4ecacc14..35f0186d 100644 --- a/backend/app/api/menu.py +++ b/backend/app/api/menu.py @@ -7,7 +7,7 @@ from app.core.response import success_response router = APIRouter(prefix="/web/menu", tags=["菜单管理"]) # 与 web-admin/src/router/modules/sqxp.ts 一致:5 个一级菜单分组 -# 市场二级顺序:细胞存储、科研捐赠、市场基础信息、中试服务、基础实验 +# 市场二级顺序:存储服务、科研捐赠、市场基础信息、中试服务、基础实验 STATIC_MENU_TREE = [ # ──────────────────────────────────────────── # 1. 市场 (menu:market) @@ -26,14 +26,14 @@ STATIC_MENU_TREE = [ "roles": ["admin"], }, "children": [ - # 细胞存储 + # 存储服务(含细胞存储 / 科研捐赠统一业务入口) { "id": "CEL00000000000000000000000000001", "name": "CellStorage", "path": "cell-storage", "component": "", "meta": { - "title": "细胞存储", + "title": "存储服务", "icon": "ri:test-tube-line", "keepAlive": False, "roles": ["admin"], @@ -71,6 +71,17 @@ STATIC_MENU_TREE = [ "menuCode": "cellStorage:paymentInfo:list", }, }, + { + "id": "50000000000000000000000000000022", + "name": "CellStoragePaymentChase", + "path": "payment-chase", + "component": "/cell-storage/payment-chase/index", + "meta": { + "title": "催缴信息", + "keepAlive": True, + "menuCode": "cellStorage:paymentChase:list", + }, + }, { "id": "50000000000000000000000000000013", "name": "CellStorageSampleInfo", diff --git a/backend/app/api/protocol_sample_collection.py b/backend/app/api/protocol_sample_collection.py index a41bb623..bd132147 100644 --- a/backend/app/api/protocol_sample_collection.py +++ b/backend/app/api/protocol_sample_collection.py @@ -1,13 +1,16 @@ """采集协议样本采集信息 API""" -from typing import Optional +from typing import List, Optional from fastapi import APIRouter, Depends, Query +from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.orm import Session from app.core.database import get_db from app.core.menu_guard import require_any_menu_code from app.core.response import error_response, success_response +from app.core.utils import parse_datetime from app.crud import protocol_sample_collection as svc +from app.crud import sample_info_quick_register as quick_svc from app.crud.collection_protocol import total_pages from app.models.models import SysUser @@ -17,6 +20,11 @@ _SAMPLE_INFO_MENU = require_any_menu_code( "cell-storage:sample-info:list", ) +_QUICK_REGISTER_MENU = require_any_menu_code( + "cell-storage:sample-info:list", + "research-donation:sample-info:list", +) + def _get_tenant_no(db: Session, user: dict) -> str: user_id = user.get("user_id") @@ -44,6 +52,12 @@ async def list_protocol_sample_collections( pageSize: int = Query(10, ge=1, le=100), keyword: Optional[str] = None, sampleStatus: Optional[str] = Query(None, alias="sampleStatus"), + collectionFlag: Optional[str] = Query(None, alias="collectionFlag"), + receptionFlag: Optional[str] = Query(None, alias="receptionFlag"), + qcFlag: Optional[str] = Query(None, alias="qcFlag"), + storageFlag: Optional[str] = Query(None, alias="storageFlag"), + storageCertHandled: Optional[str] = Query(None, alias="storageCertHandled"), + insuranceHandled: Optional[str] = Query(None, alias="insuranceHandled"), db: Session = Depends(get_db), user: dict = Depends(_SAMPLE_INFO_MENU), ): @@ -56,6 +70,12 @@ async def list_protocol_sample_collections( tenant_no=tenant_no, keyword=keyword, sample_status=sampleStatus, + collection_flag=collectionFlag, + reception_flag=receptionFlag, + qc_flag=qcFlag, + storage_flag=storageFlag, + storage_cert_handled=storageCertHandled, + insurance_handled=insuranceHandled, user_info=user, ) return success_response(_page_payload(items, total, page, pageSize)) @@ -63,3 +83,102 @@ async def list_protocol_sample_collections( return error_response(403, str(e)) except Exception as e: return error_response(400, str(e)) + + +def _get_current_user_id(user: dict) -> str: + user_id = user.get("user_id") + if not user_id: + raise PermissionError("用户身份信息缺失") + return str(user_id) + + +class QuickRegisterItem(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + sample_code: str = Field(..., alias="sampleCode") + protocol_id: str = Field(..., alias="protocolId") + + +class QuickRegisterBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + kind: str + reception_source: str = Field(..., alias="receptionSource") + items: List[QuickRegisterItem] + handle_time: Optional[str] = Field(None, alias="handleTime") + handler_user_id: Optional[str] = Field(None, alias="handlerUserId") + attachments: Optional[str] = None + remark: Optional[str] = None + + +@router.post("/sample-info/quick-register") +async def quick_register_sample_info( + body: QuickRegisterBody, + db: Session = Depends(get_db), + user: dict = Depends(_QUICK_REGISTER_MENU), +): + try: + tenant_no = _get_tenant_no(db, user) + user_id = _get_current_user_id(user) + ht = parse_datetime(body.handle_time) if body.handle_time else None + if ht is None: + return error_response(400, "请填写办理时间") + items = [ + {"sampleCode": it.sample_code, "protocolId": it.protocol_id} for it in (body.items or []) + ] + protocol_ids = [it["protocolId"] for it in items] + kind = (body.kind or "").strip() + source = (body.reception_source or "").strip() + attachments = body.attachments or "" + remark = body.remark or "" + if kind == "storage": + result = quick_svc.batch_quick_register_storage( + db, + items=items, + reception_source=source, + tenant_no=tenant_no, + user=user, + user_id=user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + attachments=attachments, + remark=remark, + ) + elif kind == "storage_cert": + if source != "cell_storage": + return error_response(400, "储存证仅支持细胞存储样本") + result = quick_svc.batch_handle_storage_certs( + db, + protocol_ids=protocol_ids, + tenant_no=tenant_no, + user=user, + user_id=user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + attachments=attachments, + remark=remark, + ) + elif kind == "insurance": + if source != "cell_storage": + return error_response(400, "保险仅支持细胞存储样本") + result = quick_svc.batch_handle_insurance( + db, + protocol_ids=protocol_ids, + tenant_no=tenant_no, + user=user, + user_id=user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + attachments=attachments, + remark=remark, + ) + else: + return error_response(400, "登记类型无效") + return success_response(result, message="登记完成") + except PermissionError as e: + return error_response(403, str(e)) + except ValueError as e: + return error_response(400, str(e)) + except Exception as e: + return error_response(400, str(e)) + diff --git a/backend/app/api/research_donation_sample_collection.py b/backend/app/api/research_donation_sample_collection.py index 345bce18..3c2b9a16 100644 --- a/backend/app/api/research_donation_sample_collection.py +++ b/backend/app/api/research_donation_sample_collection.py @@ -44,6 +44,10 @@ async def list_donation_sample_collections( pageSize: int = Query(10, ge=1, le=100), keyword: Optional[str] = None, sampleStatus: Optional[str] = Query(None, alias="sampleStatus"), + collectionFlag: Optional[str] = Query(None, alias="collectionFlag"), + receptionFlag: Optional[str] = Query(None, alias="receptionFlag"), + qcFlag: Optional[str] = Query(None, alias="qcFlag"), + storageFlag: Optional[str] = Query(None, alias="storageFlag"), db: Session = Depends(get_db), user: dict = Depends(_SAMPLE_INFO_MENU), ): @@ -56,6 +60,10 @@ async def list_donation_sample_collections( tenant_no=tenant_no, keyword=keyword, sample_status=sampleStatus, + collection_flag=collectionFlag, + reception_flag=receptionFlag, + qc_flag=qcFlag, + storage_flag=storageFlag, user_info=user, ) return success_response(_page_payload(items, total, page, pageSize)) diff --git a/backend/app/crud/collection_protocol.py b/backend/app/crud/collection_protocol.py index 6fbf0ed7..5c3211b5 100644 --- a/backend/app/crud/collection_protocol.py +++ b/backend/app/crud/collection_protocol.py @@ -550,6 +550,7 @@ def _collection_info_to_dict(c: BizProtocolCollectionInfo) -> Dict[str, Any]: "hospitalId": c.hospital_id or "", "medicalStaff": c.medical_staff or "", "salesUserId": c.sales_user_id or "", + "regionManagerUserId": c.region_manager_user_id or "", "sampleCollectTime": _fmt_dt(c.sample_collect_time), "maternalBloodCollectTime": _fmt_dt(c.maternal_blood_collect_time), "tenantNo": c.tenant_no, @@ -1047,10 +1048,13 @@ def list_collection_protocols( for h in db.query(BizCollectionHospital.id, BizCollectionHospital.hospital_name).filter(BizCollectionHospital.id.in_(hospital_ids)).all(): hospital_name_map[h.id] = h.hospital_name or "" - # 批量解析销售人员名称 + # 批量解析市场专员 / 地区经理名称 sales_user_ids = list({ci_map.get(pid, {}).get("salesUserId", "") for pid in protocol_ids} - {""}) + region_manager_user_ids = list( + {ci_map.get(pid, {}).get("regionManagerUserId", "") for pid in protocol_ids} - {""} + ) collector_user_ids = list({_protocol_to_dict(r).get("collectorUserId", "") for r in rows} - {""}) - all_user_ids = list(set(sales_user_ids + collector_user_ids)) + all_user_ids = list(set(sales_user_ids + region_manager_user_ids + collector_user_ids)) user_name_map: Dict[str, str] = {} if all_user_ids: for u in db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(all_user_ids)).all(): @@ -1166,11 +1170,13 @@ def list_collection_protocols( d["regionName"] = region_name_map.get(ci.get("regionId", ""), "") d["hospitalName"] = hospital_name_map.get(ci.get("hospitalId", ""), "") d["salesUserName"] = user_name_map.get(ci.get("salesUserId", ""), "") + d["regionManagerUserName"] = user_name_map.get(ci.get("regionManagerUserId", ""), "") d["sampleCollectTime"] = ci.get("sampleCollectTime", "") else: d["regionName"] = "" d["hospitalName"] = "" d["salesUserName"] = "" + d["regionManagerUserName"] = "" d["sampleCollectTime"] = "" protocol_samples = sample_map.get(r.id, []) type_names: List[str] = [] @@ -1280,12 +1286,17 @@ def get_protocol_detail_bundle( user_ids.append(str(protocol_dict["collectorUserId"])) if ci_dict and ci_dict.get("salesUserId"): user_ids.append(str(ci_dict["salesUserId"])) + if ci_dict and ci_dict.get("regionManagerUserId"): + user_ids.append(str(ci_dict["regionManagerUserId"])) user_name_map = _user_nick_name_map(db, user_ids) protocol_dict["collectorUserName"] = user_name_map.get( str(protocol_dict.get("collectorUserId") or ""), "" ) if ci_dict: ci_dict["salesUserName"] = user_name_map.get(str(ci_dict.get("salesUserId") or ""), "") + ci_dict["regionManagerUserName"] = user_name_map.get( + str(ci_dict.get("regionManagerUserId") or ""), "" + ) region_id = str(ci_dict.get("regionId") or "") if region_id: r = ( @@ -1437,9 +1448,14 @@ def get_protocol_detail_bundle( out["refundRecords"] = rr_svc.list_refund_records( db, collection_protocol_id=protocol_id, tenant_no=tenant_no ) + source_presign_id = getattr(p, "source_presign_order_id", None) + # 意向金与预签共用 biz_presign_transfer,仅通过 source_presign_order_id 关联 + out["depositTransfers"] = sp_pay_svc.list_deposit_transfers_for_protocol( + db, source_presign_order_id=source_presign_id, tenant_no=tenant_no + ) out["presignDepositHint"] = sp_pay_svc.get_presign_deposit_hint( db, - source_presign_order_id=getattr(p, "source_presign_order_id", None), + source_presign_order_id=source_presign_id, tenant_no=tenant_no, ) return out @@ -2647,7 +2663,13 @@ def upsert_collection_info( ) -> BizProtocolCollectionInfo: fields = _normalize_fields(fields) obj = get_collection_info(db, collection_protocol_id, tenant_no) - _STR_FIELDS = ["region_id", "hospital_id", "medical_staff", "sales_user_id"] + _STR_FIELDS = [ + "region_id", + "hospital_id", + "medical_staff", + "sales_user_id", + "region_manager_user_id", + ] _DT_FIELDS = ["sample_collect_time", "maternal_blood_collect_time"] if obj: for k in _STR_FIELDS: diff --git a/backend/app/crud/collection_region.py b/backend/app/crud/collection_region.py index f4875829..54d82f1f 100644 --- a/backend/app/crud/collection_region.py +++ b/backend/app/crud/collection_region.py @@ -456,6 +456,46 @@ def soft_delete_hospital(db: Session, hospital_id: str, tenant_no: Optional[str] db.commit() +def map_staff_region_rows_to_list(rows: List[Tuple[str, str]]) -> List[Dict[str, Any]]: + out: List[Dict[str, Any]] = [] + for rid, name in rows: + rid = (rid or "").strip() + if not rid: + continue + out.append({"id": rid, "regionName": (name or "").strip()}) + return out + + +def list_regions_by_staff_user( + db: Session, user_id: str, tenant_no: Optional[str] = None +) -> List[Dict[str, Any]]: + uid = (user_id or "").strip() + if not uid: + return [] + q = ( + db.query(BizCollectionRegionStaff.region_id, BizCollectionRegion.region_name) + .join( + BizCollectionRegion, + BizCollectionRegion.id == BizCollectionRegionStaff.region_id, + ) + .filter( + BizCollectionRegionStaff.user_id == uid, + BizCollectionRegionStaff.del_flag == 0, + BizCollectionRegion.del_flag == 0, + ) + ) + if tenant_no: + q = q.filter( + BizCollectionRegionStaff.tenant_no == tenant_no, + BizCollectionRegion.tenant_no == tenant_no, + ) + q = q.order_by( + BizCollectionRegionStaff.order_num.asc(), + BizCollectionRegionStaff.create_time.desc(), + ) + return map_staff_region_rows_to_list([(r[0], r[1]) for r in q.all()]) + + def list_staff( db: Session, *, diff --git a/backend/app/crud/customer.py b/backend/app/crud/customer.py index 26e1f980..fe4106fa 100644 --- a/backend/app/crud/customer.py +++ b/backend/app/crud/customer.py @@ -19,6 +19,10 @@ def _row_to_dict(c: BizCustomer) -> Dict[str, Any]: "sex": (c.sex or "2").strip() if c.sex else "2", "idCardNo": c.id_card_no or "", "age": c.age, + "birthYm": c.birth_ym or "", + "ethnic": c.ethnic or "", + "email": c.email or "", + "address": c.address or "", "phoneNo": c.phone_no or "", "customerTags": c.customer_tags or "", "tenantNo": c.tenant_no, @@ -110,6 +114,10 @@ def create_customer( sex: str = "2", id_card_no: Optional[str] = None, age: Optional[int] = None, + birth_ym: Optional[str] = None, + ethnic: Optional[str] = None, + email: Optional[str] = None, + address: Optional[str] = None, phone_no: Optional[str] = None, customer_tags: Optional[str] = None, tenant_no: str = "zkxw", @@ -129,6 +137,10 @@ def create_customer( sex=(sex or "2")[:1] if sex else "2", id_card_no=ic, age=age, + birth_ym=(birth_ym or "").strip()[:6] or None, + ethnic=(ethnic or "").strip() or None, + email=(email or "").strip() or None, + address=(address or "").strip() or None, phone_no=(phone_no or "").strip() or None, customer_tags=(customer_tags or "").strip() or None, tenant_no=tenant_no, @@ -155,6 +167,10 @@ def update_customer( sex: Optional[str] = None, id_card_no: Optional[str] = None, age: Optional[int] = None, + birth_ym: Optional[str] = None, + ethnic: Optional[str] = None, + email: Optional[str] = None, + address: Optional[str] = None, phone_no: Optional[str] = None, customer_tags: Optional[str] = None, order_num: Optional[int] = None, @@ -179,6 +195,14 @@ def update_customer( obj.id_card_no = ic if age is not None: obj.age = age + if birth_ym is not None: + obj.birth_ym = birth_ym.strip()[:6] or None + if ethnic is not None: + obj.ethnic = ethnic.strip() or None + if email is not None: + obj.email = email.strip() or None + if address is not None: + obj.address = address.strip() or None if phone_no is not None: obj.phone_no = phone_no.strip() or None if customer_tags is not None: diff --git a/backend/app/crud/protocol_sample_collection.py b/backend/app/crud/protocol_sample_collection.py index 58fd59a4..eeeb571c 100644 --- a/backend/app/crud/protocol_sample_collection.py +++ b/backend/app/crud/protocol_sample_collection.py @@ -126,9 +126,113 @@ def _apply_sample_status_filter(q, sample_status: Optional[str]): BizProtocolSample.sample_status == "", ) ) + if st == "collected": + return q.filter(BizProtocolSample.sample_status.in_(["ycj", "jcz", "yrk"])) + if st == "voided": + return q.filter(BizProtocolSample.sample_status == "yzf") return q.filter(BizProtocolSample.sample_status == st) +def _apply_reception_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str): + from sqlalchemy import exists + + from app.models.models import BizSampleReception + + f = (flag or "").strip() + if not f: + return q + base = and_( + BizSampleReception.tenant_no == tenant_no, + BizSampleReception.del_flag == 0, + BizSampleReception.reception_source == source, + BizSampleReception.sample_code == sample_code_col, + ) + if f == "js": + return q.filter(exists().where(and_(base, BizSampleReception.accept_conclusion == "js"))) + if f == "bj": + return q.filter(exists().where(and_(base, BizSampleReception.accept_conclusion == "bj"))) + if f == "none": + return q.filter( + ~exists().where(and_(base, BizSampleReception.accept_conclusion.in_(["js", "bj"]))) + ) + return q + + +def _apply_qc_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str): + from sqlalchemy import exists + + from app.models.models import BizQcWorkOrder + + f = (flag or "").strip() + if not f: + return q + base = and_( + BizQcWorkOrder.tenant_no == tenant_no, + BizQcWorkOrder.del_flag == 0, + BizQcWorkOrder.reception_source == source, + BizQcWorkOrder.trigger_source == "reception", + BizQcWorkOrder.sample_code == sample_code_col, + ) + if f == "complete": + return q.filter(exists().where(and_(base, BizQcWorkOrder.conclusion.in_(["tg", "btg"])))) + if f == "voided": + return q.filter(exists().where(and_(base, BizQcWorkOrder.work_order_status == "yzf"))) + if f == "incomplete": + return q.filter( + ~exists().where( + and_( + base, + or_( + BizQcWorkOrder.conclusion.in_(["tg", "btg"]), + BizQcWorkOrder.work_order_status == "yzf", + ), + ) + ) + ) + return q + + +def _apply_storage_flag_filter(q, flag: Optional[str], *, sample_code_col, tenant_no: str, source: str): + from sqlalchemy import exists + + from app.models.models import BizStorageReleaseReview + + f = (flag or "").strip() + if not f: + return q + base = and_( + BizStorageReleaseReview.tenant_no == tenant_no, + BizStorageReleaseReview.del_flag == 0, + BizStorageReleaseReview.reception_source == source, + BizStorageReleaseReview.sample_code == sample_code_col, + ) + if f == "yrk": + return q.filter(exists().where(and_(base, BizStorageReleaseReview.storage_status == "yrk"))) + if f == "rejected": + return q.filter( + exists().where( + and_( + base, + BizStorageReleaseReview.storage_status != "yrk", + BizStorageReleaseReview.review_conclusion == "btg", + ) + ) + ) + if f == "none": + return q.filter( + ~exists().where( + and_( + base, + or_( + BizStorageReleaseReview.storage_status == "yrk", + BizStorageReleaseReview.review_conclusion == "btg", + ), + ) + ) + ) + return q + + def list_protocol_sample_collections( db: Session, *, @@ -137,13 +241,64 @@ def list_protocol_sample_collections( tenant_no: str, keyword: Optional[str] = None, sample_status: Optional[str] = None, + collection_flag: Optional[str] = None, + reception_flag: Optional[str] = None, + qc_flag: Optional[str] = None, + storage_flag: Optional[str] = None, + storage_cert_handled: Optional[str] = None, + insurance_handled: Optional[str] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: if not tenant_no: return [], 0 q = _base_query(db, tenant_no) - q = _apply_sample_status_filter(q, sample_status) + q = _apply_sample_status_filter(q, collection_flag or sample_status) + q = _apply_reception_flag_filter( + q, reception_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage" + ) + q = _apply_qc_flag_filter( + q, qc_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage" + ) + q = _apply_storage_flag_filter( + q, storage_flag, sample_code_col=BizProtocolSample.sample_code, tenant_no=tenant_no, source="cell_storage" + ) + from app.models.models import BizProtocolFullmoonTodo, BizProtocolStorageCert + + sch = (storage_cert_handled or "").strip() + if sch in ("1", "0", "true", "false"): + want = sch in ("1", "true") + handled_pred = or_( + BizProtocolStorageCert.handle_time.isnot(None), + BizProtocolStorageCert.make_status == "1", + and_(BizProtocolStorageCert.make_date.isnot(None), BizProtocolStorageCert.make_date != ""), + ) + q = q.outerjoin( + BizProtocolStorageCert, + and_( + BizProtocolStorageCert.collection_protocol_id == BizCollectionProtocol.id, + BizProtocolStorageCert.tenant_no == tenant_no, + BizProtocolStorageCert.del_flag == 0, + ), + ) + q = q.filter(handled_pred) if want else q.filter(~handled_pred) + ih = (insurance_handled or "").strip() + if ih in ("1", "0", "true", "false"): + want = ih in ("1", "true") + handled_pred = or_( + BizProtocolFullmoonTodo.handle_time.isnot(None), + BizProtocolFullmoonTodo.todo_status == "1", + and_(BizProtocolFullmoonTodo.handle_date.isnot(None), BizProtocolFullmoonTodo.handle_date != ""), + ) + q = q.outerjoin( + BizProtocolFullmoonTodo, + and_( + BizProtocolFullmoonTodo.collection_protocol_id == BizCollectionProtocol.id, + BizProtocolFullmoonTodo.tenant_no == tenant_no, + BizProtocolFullmoonTodo.del_flag == 0, + ), + ) + q = q.filter(handled_pred) if want else q.filter(~handled_pred) if keyword and keyword.strip(): kw = f"%{keyword.strip()}%" @@ -255,4 +410,9 @@ def list_protocol_sample_collections( for item in items: sc = (item.get("sampleCode") or "").strip() item["labStorageCode"] = code_map.get(sc, "") if sc else "" + from app.services.sample_info_list_enrich import enrich_sample_info_items + + enrich_sample_info_items( + db, tenant_no, items, reception_source="cell_storage", include_handle_flags=True + ) return items, total diff --git a/backend/app/crud/research_donation.py b/backend/app/crud/research_donation.py index ccf09506..d722c3e4 100644 --- a/backend/app/crud/research_donation.py +++ b/backend/app/crud/research_donation.py @@ -17,6 +17,7 @@ from app.crud.collection_protocol import ( urls_to_attachment_list, validate_sample_codes_not_used, _format_sample_collect_summary, + _load_express_name_map, _load_sample_type_name_map, _load_site_name_map, ) @@ -360,6 +361,7 @@ def _collection_to_dict(c: BizResearchDonationCollection) -> Dict[str, Any]: "hospitalId": c.hospital_id or "", "medicalStaff": c.medical_staff or "", "salesUserId": c.sales_user_id or "", + "regionManagerUserId": c.region_manager_user_id or "", "sampleCollectTime": _fmt_dt(c.sample_collect_time), "maternalBloodCollectTime": _fmt_dt(c.maternal_blood_collect_time), "tenantNo": c.tenant_no, @@ -638,12 +640,21 @@ def list_donations( ): hospital_name_map[hosp.id] = hosp.hospital_name or "" - # 批量解析采集专员 / 销售人员名称 + # 批量解析采集专员 / 市场专员 / 地区经理名称 sales_user_ids = list( {collection_map.get(did, {}).get("salesUserId", "") for did in donation_ids} - {""} ) + region_manager_user_ids = list( + { + collection_map.get(did, {}).get("regionManagerUserId", "") + for did in donation_ids + } + - {""} + ) collector_ids = list({r.collector_user_id for r in rows if r.collector_user_id}) - user_name_map = _user_nick_name_map(db, list(set(collector_ids + sales_user_ids))) + user_name_map = _user_nick_name_map( + db, list(set(collector_ids + sales_user_ids + region_manager_user_ids)) + ) # 批量加载捐赠样本(列表展示样本类型、样本编码、采集情况) sample_map: Dict[str, List[BizResearchDonationSample]] = {} @@ -693,6 +704,30 @@ def list_donations( db, tenant_no, all_sample_codes, reception_source="research_donation" ) + transport_by_sample_id: Dict[str, BizResearchDonationSampleTransport] = {} + if donation_ids: + for t in ( + db.query(BizResearchDonationSampleTransport) + .filter( + BizResearchDonationSampleTransport.research_donation_id.in_(donation_ids), + BizResearchDonationSampleTransport.tenant_no == tenant_no, + BizResearchDonationSampleTransport.del_flag == 0, + ) + .all() + ): + transport_by_sample_id[t.sample_id] = t + site_name_map = _load_site_name_map( + db, [t.dest_site_id for t in transport_by_sample_id.values() if t.dest_site_id] + ) + express_name_map = _load_express_name_map( + db, + [ + t.express_company_id + for t in transport_by_sample_id.values() + if t.express_company_id + ], + ) + result = [] for r in rows: d = _donation_to_dict(r, db, tenant_no) @@ -725,11 +760,15 @@ def list_donations( d["regionName"] = region_name_map.get(ci.get("regionId", ""), "") d["hospitalName"] = hospital_name_map.get(ci.get("hospitalId", ""), "") d["salesUserName"] = user_name_map.get(ci.get("salesUserId", ""), "") + d["regionManagerUserName"] = user_name_map.get( + ci.get("regionManagerUserId", ""), "" + ) d["sampleCollectTime"] = ci.get("sampleCollectTime", "") else: d["regionName"] = "" d["hospitalName"] = "" d["salesUserName"] = "" + d["regionManagerUserName"] = "" d["sampleCollectTime"] = "" donation_samples = sample_map.get(r.id, []) type_names: List[str] = [] @@ -742,12 +781,29 @@ def list_donations( code = (s.sample_code or "").strip() if code: codes.append(code) + transport_obj = transport_by_sample_id.get(s.id) + transport_dict: Optional[Dict[str, Any]] = None + if transport_obj and (transport_obj.transport_type or "").strip(): + dest_site_id = transport_obj.dest_site_id or "" + express_company_id = transport_obj.express_company_id or "" + transport_dict = { + "transportType": transport_obj.transport_type or "", + "destSiteId": dest_site_id, + "destSiteName": site_name_map.get(dest_site_id, ""), + "expressCompanyId": express_company_id, + "expressCompanyName": express_name_map.get(express_company_id, ""), + "waybillNo": transport_obj.waybill_no or "", + "carrierName": transport_obj.carrier_name or "", + "carrierPhone": transport_obj.carrier_phone or "", + "etaTime": _fmt_dt(transport_obj.eta_time), + } sample_items.append( { "sampleTypeName": type_name, "sampleCode": code, "sampleStatus": (s.sample_status or "").strip(), "labStorageCode": lab_code_map.get(code, "") if code else "", + "transport": transport_dict, **sample_special_to_dict(s), **build_sample_item_reception_qc_fields( reception_summary_map.get(code) if code else None, @@ -814,6 +870,8 @@ def get_donation_detail( coll_dict = _collection_to_dict(coll) if coll else None if coll_dict and coll_dict.get("salesUserId"): user_ids.append(coll_dict["salesUserId"]) + if coll_dict and coll_dict.get("regionManagerUserId"): + user_ids.append(coll_dict["regionManagerUserId"]) if coll_dict: region_id = str(coll_dict.get("regionId") or "") if region_id: @@ -905,6 +963,10 @@ def get_donation_detail( coll_dict["salesUserName"] = user_name_map.get( coll_dict["salesUserId"], "" ) + if coll_dict: + coll_dict["regionManagerUserName"] = user_name_map.get( + coll_dict.get("regionManagerUserId") or "", "" + ) return out @@ -1970,7 +2032,13 @@ def _create_party( return obj -_COLL_STR_FIELDS = ["region_id", "hospital_id", "medical_staff", "sales_user_id"] +_COLL_STR_FIELDS = [ + "region_id", + "hospital_id", + "medical_staff", + "sales_user_id", + "region_manager_user_id", +] _COLL_DT_FIELDS = ["sample_collect_time", "maternal_blood_collect_time"] diff --git a/backend/app/crud/research_donation_sample_collection.py b/backend/app/crud/research_donation_sample_collection.py index 3936fc6c..615e35f5 100644 --- a/backend/app/crud/research_donation_sample_collection.py +++ b/backend/app/crud/research_donation_sample_collection.py @@ -128,6 +128,10 @@ def _apply_sample_status_filter(q, sample_status: Optional[str]): BizResearchDonationSample.sample_status == "", ) ) + if st == "collected": + return q.filter(BizResearchDonationSample.sample_status.in_(["ycj", "jcz", "yrk"])) + if st == "voided": + return q.filter(BizResearchDonationSample.sample_status == "yzf") return q.filter(BizResearchDonationSample.sample_status == st) @@ -139,13 +143,44 @@ def list_donation_sample_collections( tenant_no: str, keyword: Optional[str] = None, sample_status: Optional[str] = None, + collection_flag: Optional[str] = None, + reception_flag: Optional[str] = None, + qc_flag: Optional[str] = None, + storage_flag: Optional[str] = None, user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: if not tenant_no: return [], 0 + from app.crud.protocol_sample_collection import ( + _apply_qc_flag_filter, + _apply_reception_flag_filter, + _apply_storage_flag_filter, + ) + q = _base_query(db, tenant_no) - q = _apply_sample_status_filter(q, sample_status) + q = _apply_sample_status_filter(q, collection_flag or sample_status) + q = _apply_reception_flag_filter( + q, + reception_flag, + sample_code_col=BizResearchDonationSample.sample_code, + tenant_no=tenant_no, + source="research_donation", + ) + q = _apply_qc_flag_filter( + q, + qc_flag, + sample_code_col=BizResearchDonationSample.sample_code, + tenant_no=tenant_no, + source="research_donation", + ) + q = _apply_storage_flag_filter( + q, + storage_flag, + sample_code_col=BizResearchDonationSample.sample_code, + tenant_no=tenant_no, + source="research_donation", + ) if keyword and keyword.strip(): kw = f"%{keyword.strip()}%" @@ -257,4 +292,13 @@ def list_donation_sample_collections( for item in items: sc = (item.get("sampleCode") or "").strip() item["labStorageCode"] = code_map.get(sc, "") if sc else "" + from app.services.sample_info_list_enrich import enrich_sample_info_items + + enrich_sample_info_items( + db, + tenant_no, + items, + reception_source="research_donation", + include_handle_flags=False, + ) return items, total diff --git a/backend/app/crud/sample_freight.py b/backend/app/crud/sample_freight.py index a8283a86..5cf0f775 100644 --- a/backend/app/crud/sample_freight.py +++ b/backend/app/crud/sample_freight.py @@ -13,12 +13,18 @@ from app.core.utils import generate_uuid from app.models.models import ( BizCellSampleType, BizCollectionProtocol, + BizProtocolPartyCustomer, BizProtocolSample, BizSampleFreight, BizSampleFreightItem, SysDictData, + SysUser, +) +from app.models.research_donation import ( + BizResearchDonation, + BizResearchDonationParty, + BizResearchDonationSample, ) -from app.models.research_donation import BizResearchDonation, BizResearchDonationSample from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value SAMPLE_SOURCE_CELL = "cell_storage" @@ -103,15 +109,52 @@ def _linked_count(db: Session, freight_id: str, tenant_no: str) -> int: ) +def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]: + ids = list({x for x in user_ids if x}) + if not ids: + return {} + rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all() + return {u.id: (u.nick_name or "").strip() for u in rows} + + +def _generate_freight_no( + db: Session, tenant_no: str, when: Optional[datetime] = None +) -> str: + """生成批次号:YF + YYYYMMDD + 001~999(按租户、按日)。""" + day = (when or _now()).strftime("%Y%m%d") + prefix = f"YF{day}" + existing = ( + db.query(BizSampleFreight.freight_no) + .filter( + BizSampleFreight.tenant_no == tenant_no, + BizSampleFreight.freight_no.like(f"{prefix}%"), + ) + .order_by(BizSampleFreight.freight_no.desc()) + .first() + ) + seq = 1 + if existing and existing[0]: + try: + seq = int(str(existing[0])[-3:]) + 1 + except ValueError: + seq = 1 + if seq > 999: + raise ValueError("当日运费批次号已达上限(999)") + return f"{prefix}{seq:03d}" + + def _row_to_dict( obj: BizSampleFreight, *, linked_sample_count: int = 0, transport_mode_label: str = "", + create_by_name: str = "", ) -> Dict[str, Any]: amount = obj.freight_amount + create_by = obj.create_by or "" return { "id": obj.id, + "freightNo": getattr(obj, "freight_no", None) or "", "transportMode": obj.transport_mode or "", "transportModeLabel": transport_mode_label or (obj.transport_mode or ""), "freightAmount": float(amount) if amount is not None else 0.0, @@ -119,6 +162,8 @@ def _row_to_dict( "tenantNo": obj.tenant_no, "orderNum": obj.order_num if obj.order_num is not None else 0, "remark": obj.remark or "", + "createBy": create_by, + "createByName": create_by_name or create_by, "createTime": _fmt_dt(obj.create_time), "updateTime": _fmt_dt(obj.update_time), } @@ -140,10 +185,14 @@ def _item_to_dict_enriched( *, protocol_no: str = "", sample_type_name: str = "", + mother_name: str = "", + newborn_name: str = "", ) -> Dict[str, Any]: data = _item_to_dict(item) data["protocolNo"] = protocol_no or "" data["sampleTypeName"] = sample_type_name or "" + data["motherName"] = mother_name or "" + data["newbornName"] = newborn_name or "" return data @@ -169,12 +218,11 @@ def _enrich_items_maps( db: Session, items: List[BizSampleFreightItem], tenant_no: str, -) -> Tuple[Dict[str, str], Dict[str, str]]: - """批量解析 protocolNo / sampleTypeName,key = sample_source|sample_id。""" - protocol_no_map: Dict[str, str] = {} - type_name_map: Dict[str, str] = {} +) -> Dict[str, Dict[str, str]]: + """批量解析协议号/样本类型/母亲子女姓名,key = sample_source|sample_id。""" + out: Dict[str, Dict[str, str]] = {} if not items: - return protocol_no_map, type_name_map + return out cell_ids = [ i.sample_id @@ -193,6 +241,8 @@ def _enrich_items_maps( BizProtocolSample.id, BizCollectionProtocol.protocol_no, BizCellSampleType.type_name, + BizProtocolPartyCustomer.mother_name, + BizProtocolPartyCustomer.newborn_name, ) .outerjoin( BizCollectionProtocol, @@ -208,6 +258,14 @@ def _enrich_items_maps( BizCellSampleType.del_flag == 0, ), ) + .outerjoin( + BizProtocolPartyCustomer, + and_( + BizProtocolPartyCustomer.collection_protocol_id + == BizProtocolSample.collection_protocol_id, + BizProtocolPartyCustomer.del_flag == 0, + ), + ) .filter( BizProtocolSample.id.in_(cell_ids), BizProtocolSample.tenant_no == tenant_no, @@ -215,10 +273,13 @@ def _enrich_items_maps( ) .all() ) - for sid, pno, tname in rows: - key = f"{SAMPLE_SOURCE_CELL}|{sid}" - protocol_no_map[key] = pno or "" - type_name_map[key] = tname or "" + for sid, pno, tname, mname, nname in rows: + out[f"{SAMPLE_SOURCE_CELL}|{sid}"] = { + "protocolNo": pno or "", + "sampleTypeName": tname or "", + "motherName": mname or "", + "newbornName": nname or "", + } if donation_ids: rows = ( @@ -226,6 +287,8 @@ def _enrich_items_maps( BizResearchDonationSample.id, BizResearchDonation.donation_no, BizCellSampleType.type_name, + BizResearchDonationParty.mother_name, + BizResearchDonationParty.newborn_name, ) .outerjoin( BizResearchDonation, @@ -242,6 +305,14 @@ def _enrich_items_maps( BizCellSampleType.del_flag == 0, ), ) + .outerjoin( + BizResearchDonationParty, + and_( + BizResearchDonationParty.research_donation_id + == BizResearchDonationSample.research_donation_id, + BizResearchDonationParty.del_flag == 0, + ), + ) .filter( BizResearchDonationSample.id.in_(donation_ids), BizResearchDonationSample.tenant_no == tenant_no, @@ -249,12 +320,15 @@ def _enrich_items_maps( ) .all() ) - for sid, dno, tname in rows: - key = f"{SAMPLE_SOURCE_DONATION}|{sid}" - protocol_no_map[key] = dno or "" - type_name_map[key] = tname or "" + for sid, dno, tname, mname, nname in rows: + out[f"{SAMPLE_SOURCE_DONATION}|{sid}"] = { + "protocolNo": dno or "", + "sampleTypeName": tname or "", + "motherName": mname or "", + "newbornName": nname or "", + } - return protocol_no_map, type_name_map + return out def get_freight(db: Session, freight_id: str, tenant_no: str) -> Optional[BizSampleFreight]: @@ -332,7 +406,10 @@ def list_freights( ) .all() ] - conds = [BizSampleFreight.transport_mode.like(like)] + conds = [ + BizSampleFreight.transport_mode.like(like), + BizSampleFreight.freight_no.like(like), + ] if label_values: conds.append(BizSampleFreight.transport_mode.in_(label_values)) @@ -346,34 +423,6 @@ def list_freights( ) conds.append(sample_code_exists) - cell_proto_exists = exists().where( - and_( - BizSampleFreightItem.freight_id == BizSampleFreight.id, - BizSampleFreightItem.tenant_no == tenant_no, - BizSampleFreightItem.del_flag == 0, - BizSampleFreightItem.sample_source == SAMPLE_SOURCE_CELL, - BizSampleFreightItem.protocol_id == BizCollectionProtocol.id, - BizCollectionProtocol.tenant_no == tenant_no, - BizCollectionProtocol.del_flag == 0, - BizCollectionProtocol.protocol_no.like(like), - ) - ) - conds.append(cell_proto_exists) - - donation_proto_exists = exists().where( - and_( - BizSampleFreightItem.freight_id == BizSampleFreight.id, - BizSampleFreightItem.tenant_no == tenant_no, - BizSampleFreightItem.del_flag == 0, - BizSampleFreightItem.sample_source == SAMPLE_SOURCE_DONATION, - BizSampleFreightItem.protocol_id == BizResearchDonation.id, - BizResearchDonation.tenant_no == tenant_no, - BizResearchDonation.del_flag == 0, - BizResearchDonation.donation_no.like(like), - ) - ) - conds.append(donation_proto_exists) - q = q.filter(or_(*conds)) total = q.count() @@ -384,6 +433,7 @@ def list_freights( .all() ) labels = _label_map(db, tenant_no, [r.transport_mode for r in rows]) + name_map = _user_nick_name_map(db, [r.create_by for r in rows if r.create_by]) items_by_freight: Dict[str, List[BizSampleFreightItem]] = {} if include_items and rows: @@ -403,9 +453,9 @@ def list_freights( ) for it in all_items: items_by_freight.setdefault(it.freight_id, []).append(it) - protocol_no_map, type_name_map = _enrich_items_maps(db, all_items, tenant_no) + enrich_map = _enrich_items_maps(db, all_items, tenant_no) else: - protocol_no_map, type_name_map = {}, {} + enrich_map = {} out: List[Dict[str, Any]] = [] for r in rows: @@ -418,16 +468,20 @@ def list_freights( r, linked_sample_count=linked, transport_mode_label=labels.get(r.transport_mode or "", r.transport_mode or ""), + create_by_name=name_map.get(r.create_by or "", ""), ) if include_items: enriched = [] for it in items_by_freight.get(r.id, []): key = f"{it.sample_source}|{it.sample_id}" + info = enrich_map.get(key, {}) enriched.append( _item_to_dict_enriched( it, - protocol_no=protocol_no_map.get(key, ""), - sample_type_name=type_name_map.get(key, ""), + protocol_no=info.get("protocolNo", ""), + sample_type_name=info.get("sampleTypeName", ""), + mother_name=info.get("motherName", ""), + newborn_name=info.get("newbornName", ""), ) ) data["items"] = enriched @@ -450,20 +504,25 @@ def get_freight_detail(db: Session, freight_id: str, tenant_no: str) -> Dict[str .all() ) labels = _label_map(db, tenant_no, [obj.transport_mode]) + name_map = _user_nick_name_map(db, [obj.create_by] if obj.create_by else []) data = _row_to_dict( obj, linked_sample_count=len(items), transport_mode_label=labels.get(obj.transport_mode or "", obj.transport_mode or ""), + create_by_name=name_map.get(obj.create_by or "", ""), ) - protocol_no_map, type_name_map = _enrich_items_maps(db, items, tenant_no) + enrich_map = _enrich_items_maps(db, items, tenant_no) enriched = [] for i in items: key = f"{i.sample_source}|{i.sample_id}" + info = enrich_map.get(key, {}) enriched.append( _item_to_dict_enriched( i, - protocol_no=protocol_no_map.get(key, ""), - sample_type_name=type_name_map.get(key, ""), + protocol_no=info.get("protocolNo", ""), + sample_type_name=info.get("sampleTypeName", ""), + mother_name=info.get("motherName", ""), + newborn_name=info.get("newbornName", ""), ) ) data["items"] = enriched @@ -499,6 +558,8 @@ def list_sample_options( BizProtocolSample.create_time, BizCollectionProtocol.protocol_no, BizCellSampleType.type_name, + BizProtocolPartyCustomer.mother_name, + BizProtocolPartyCustomer.newborn_name, ) .outerjoin( BizCollectionProtocol, @@ -515,6 +576,14 @@ def list_sample_options( BizCellSampleType.del_flag == 0, ), ) + .outerjoin( + BizProtocolPartyCustomer, + and_( + BizProtocolPartyCustomer.collection_protocol_id + == BizProtocolSample.collection_protocol_id, + BizProtocolPartyCustomer.del_flag == 0, + ), + ) .filter( BizProtocolSample.tenant_no == tenant_no, BizProtocolSample.del_flag == 0, @@ -524,10 +593,11 @@ def list_sample_options( cq = cq.filter( or_( BizProtocolSample.sample_code.like(like), - BizCollectionProtocol.protocol_no.like(like), + BizProtocolPartyCustomer.mother_name.like(like), + BizProtocolPartyCustomer.newborn_name.like(like), ) ) - for sid, scode, pid, ctime, pno, tname in cq.all(): + for sid, scode, pid, ctime, pno, tname, mname, nname in cq.all(): rows.append( { "sampleSource": SAMPLE_SOURCE_CELL, @@ -536,6 +606,8 @@ def list_sample_options( "protocolId": pid or "", "protocolNo": pno or "", "sampleTypeName": tname or "", + "motherName": mname or "", + "newbornName": nname or "", "createTime": ctime, "freightId": "", } @@ -550,6 +622,8 @@ def list_sample_options( BizResearchDonationSample.create_time, BizResearchDonation.donation_no, BizCellSampleType.type_name, + BizResearchDonationParty.mother_name, + BizResearchDonationParty.newborn_name, ) .outerjoin( BizResearchDonation, @@ -566,6 +640,14 @@ def list_sample_options( BizCellSampleType.del_flag == 0, ), ) + .outerjoin( + BizResearchDonationParty, + and_( + BizResearchDonationParty.research_donation_id + == BizResearchDonationSample.research_donation_id, + BizResearchDonationParty.del_flag == 0, + ), + ) .filter( BizResearchDonationSample.tenant_no == tenant_no, BizResearchDonationSample.del_flag == 0, @@ -575,10 +657,11 @@ def list_sample_options( dq = dq.filter( or_( BizResearchDonationSample.sample_code.like(like), - BizResearchDonation.donation_no.like(like), + BizResearchDonationParty.mother_name.like(like), + BizResearchDonationParty.newborn_name.like(like), ) ) - for sid, scode, pid, ctime, dno, tname in dq.all(): + for sid, scode, pid, ctime, dno, tname, mname, nname in dq.all(): rows.append( { "sampleSource": SAMPLE_SOURCE_DONATION, @@ -587,6 +670,8 @@ def list_sample_options( "protocolId": pid or "", "protocolNo": dno or "", "sampleTypeName": tname or "", + "motherName": mname or "", + "newbornName": nname or "", "createTime": ctime, "freightId": "", } @@ -693,6 +778,7 @@ def create_freight( now = _now() obj = BizSampleFreight( id=generate_uuid(), + freight_no=_generate_freight_no(db, tenant_no, when=now), transport_mode=mode, freight_amount=amount, tenant_no=tenant_no, diff --git a/backend/app/crud/sample_info_quick_register.py b/backend/app/crud/sample_info_quick_register.py new file mode 100644 index 00000000..394aaf33 --- /dev/null +++ b/backend/app/crud/sample_info_quick_register.py @@ -0,0 +1,260 @@ +"""样本信息列表快捷登记写入。""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session + +from app.core.data_scope import apply_data_scope +from app.core.utils import generate_uuid +from app.crud import collection_protocol as cp_svc +from app.crud.storage_release_review import _get_user_name +from app.models.models import ( + BizCollectionProtocol, + BizProtocolSample, + BizStorageReleaseReview, +) +from app.models.research_donation import BizResearchDonation, BizResearchDonationSample +from app.services.sample_info_quick_register import ( + apply_quick_storage_payload, + unique_nonempty_ids, +) + + +def _protocol_accessible( + db: Session, protocol_id: str, tenant_no: str, user: dict +) -> bool: + q = db.query(BizCollectionProtocol).filter( + BizCollectionProtocol.id == protocol_id, + BizCollectionProtocol.del_flag == 0, + BizCollectionProtocol.tenant_no == tenant_no, + ) + q = apply_data_scope(q, BizCollectionProtocol, user, db, tenant_no) + return q.first() is not None + + +def _donation_accessible( + db: Session, donation_id: str, tenant_no: str, user: dict +) -> bool: + q = db.query(BizResearchDonation).filter( + BizResearchDonation.id == donation_id, + BizResearchDonation.del_flag == 0, + BizResearchDonation.tenant_no == tenant_no, + ) + q = apply_data_scope(q, BizResearchDonation, user, db, tenant_no) + return q.first() is not None + + +def _pick_latest_review( + rows: List[BizStorageReleaseReview], +) -> Optional[BizStorageReleaseReview]: + best = None + best_t = None + for rev in rows: + cur_t = rev.storage_time or rev.update_time or rev.create_time + if best is None: + best, best_t = rev, cur_t + continue + if cur_t is None: + continue + if best_t is None or cur_t > best_t: + best, best_t = rev, cur_t + return best + + +def batch_handle_storage_certs( + db: Session, + *, + protocol_ids: List[str], + tenant_no: str, + user: dict, + user_id: str, + handle_time: Optional[datetime], + handler_user_id: Optional[str], + attachments: str, + remark: str, +) -> Dict[str, int]: + ids = unique_nonempty_ids(protocol_ids) + if not ids: + raise ValueError("请选择样本") + success = 0 + fail = 0 + for pid in ids: + if not _protocol_accessible(db, pid, tenant_no, user): + fail += 1 + continue + try: + cp_svc.upsert_storage_cert_handle( + db, + pid, + tenant_no, + user_id, + handle_time=handle_time, + handler_user_id=handler_user_id, + cert_image_urls=attachments, + remark=remark, + ) + success += 1 + except ValueError: + fail += 1 + return {"success": success, "fail": fail} + + +def batch_handle_insurance( + db: Session, + *, + protocol_ids: List[str], + tenant_no: str, + user: dict, + user_id: str, + handle_time: Optional[datetime], + handler_user_id: Optional[str], + attachments: str, + remark: str, +) -> Dict[str, int]: + ids = unique_nonempty_ids(protocol_ids) + if not ids: + raise ValueError("请选择样本") + success = 0 + fail = 0 + for pid in ids: + if not _protocol_accessible(db, pid, tenant_no, user): + fail += 1 + continue + try: + cp_svc.upsert_fullmoon_todo_handle( + db, + pid, + tenant_no, + user_id, + handle_time=handle_time, + handler_user_id=handler_user_id, + insurance_attachments=attachments, + handle_remark=remark, + ) + success += 1 + except ValueError: + fail += 1 + return {"success": success, "fail": fail} + + +def batch_quick_register_storage( + db: Session, + *, + items: List[Dict[str, str]], + reception_source: str, + tenant_no: str, + user: dict, + user_id: str, + handle_time: Optional[datetime], + handler_user_id: Optional[str], + attachments: str, + remark: str, +) -> Dict[str, int]: + source = (reception_source or "").strip() + if source not in ("cell_storage", "research_donation"): + raise ValueError("来源无效") + fields = apply_quick_storage_payload( + handle_time=handle_time, + handler_user_id=handler_user_id, + attachments=attachments, + remark=remark, + ) + seen = set() + pairs: List[tuple] = [] + for it in items or []: + code = (it.get("sampleCode") or "").strip() + pid = (it.get("protocolId") or "").strip() + if not code or code in seen: + continue + seen.add(code) + pairs.append((code, pid)) + if not pairs: + raise ValueError("请选择样本") + + handler_name = _get_user_name(db, fields["storage_by"] or user_id) + success = 0 + fail = 0 + now = datetime.now() + for sample_code, protocol_id in pairs: + if source == "cell_storage": + if not protocol_id or not _protocol_accessible(db, protocol_id, tenant_no, user): + fail += 1 + continue + else: + if not protocol_id or not _donation_accessible(db, protocol_id, tenant_no, user): + fail += 1 + continue + rows = ( + db.query(BizStorageReleaseReview) + .filter( + BizStorageReleaseReview.tenant_no == tenant_no, + BizStorageReleaseReview.del_flag == 0, + BizStorageReleaseReview.reception_source == source, + BizStorageReleaseReview.sample_code == sample_code, + ) + .all() + ) + rev = _pick_latest_review(rows) + if rev is None: + rev = BizStorageReleaseReview( + id=generate_uuid(), + sample_code=sample_code, + reception_source=source, + protocol_id=protocol_id, + storage_status="yrk", + tenant_no=tenant_no, + create_by=user_id, + create_dept_id="", + update_by=user_id, + create_time=now, + update_time=now, + del_flag=0, + ) + db.add(rev) + rev.storage_status = fields["storage_status"] + rev.storage_time = fields["storage_time"] + rev.storage_by = fields["storage_by"] + rev.storage_by_name = handler_name + rev.remark = fields["remark"] or None + if fields["attachments"]: + rev.review_attachments = fields["attachments"] + rev.update_by = user_id + rev.update_time = now + db.add(rev) + + ymd = fields["storage_in_date"] + if source == "cell_storage": + ps = ( + db.query(BizProtocolSample) + .filter( + BizProtocolSample.sample_code == sample_code, + BizProtocolSample.tenant_no == tenant_no, + BizProtocolSample.del_flag == 0, + ) + .first() + ) + if ps: + ps.sample_status = "yrk" + ps.storage_in_date = ymd + ps.update_time = now + db.add(ps) + else: + ds = ( + db.query(BizResearchDonationSample) + .filter( + BizResearchDonationSample.sample_code == sample_code, + BizResearchDonationSample.tenant_no == tenant_no, + BizResearchDonationSample.del_flag == 0, + ) + .first() + ) + if ds: + ds.sample_status = "yrk" + ds.storage_in_date = ymd + ds.update_time = now + db.add(ds) + success += 1 + db.commit() + return {"success": success, "fail": fail} diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 83bf266d..912ada1d 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -334,6 +334,10 @@ class BizCustomer(Base): sex = Column(CHAR(1), nullable=True, default="2", comment="性别(0男1女2未知)") id_card_no = Column(String(32), nullable=True, comment="身份证号") age = Column(Integer, nullable=True, comment="年龄") + birth_ym = Column(CHAR(6), nullable=True, comment="出生年月YYYYMM") + ethnic = Column(String(32), nullable=True, comment="民族") + email = Column(String(128), nullable=True, comment="邮箱") + address = Column(String(512), nullable=True, comment="家庭住址") phone_no = Column(String(32), nullable=True, comment="联系电话") customer_tags = Column(String(512), nullable=True, comment="客户标签,逗号分隔") tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") @@ -1496,7 +1500,8 @@ class BizProtocolCollectionInfo(Base): region_id = Column(String(64), nullable=True, comment="区域ID") hospital_id = Column(String(64), nullable=True, comment="医院ID") medical_staff = Column(String(200), nullable=True, comment="医护人员") - sales_user_id = Column(String(64), nullable=True, comment="销售人员用户ID") + sales_user_id = Column(String(64), nullable=True, comment="市场专员用户ID") + region_manager_user_id = Column(String(64), nullable=True, comment="地区经理用户ID") sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间") maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间") tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") @@ -1577,6 +1582,7 @@ class BizSampleFreight(Base): __tablename__ = "biz_sample_freight" id = Column(String(64), primary_key=True, comment="主键ID") + freight_no = Column(String(32), nullable=False, default="", comment="运费批次号 YF+YYYYMMDD+序号") transport_mode = Column(String(100), nullable=False, default="", comment="运输方式字典值") freight_amount = Column(Numeric(18, 2), nullable=False, default=0, comment="运费(元)") tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") diff --git a/backend/app/models/research_donation.py b/backend/app/models/research_donation.py index 0f988ee8..9c7ebce5 100644 --- a/backend/app/models/research_donation.py +++ b/backend/app/models/research_donation.py @@ -116,7 +116,8 @@ class BizResearchDonationCollection(Base): region_id = Column(String(64), nullable=True, comment="区域ID") hospital_id = Column(String(64), nullable=True, comment="医院ID") medical_staff = Column(String(200), nullable=True, comment="医护人员") - sales_user_id = Column(String(64), nullable=True, comment="销售人员用户ID") + sales_user_id = Column(String(64), nullable=True, comment="市场专员用户ID") + region_manager_user_id = Column(String(64), nullable=True, comment="地区经理用户ID") sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间") maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间") tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号") diff --git a/backend/app/services/oa_assignee.py b/backend/app/services/oa_assignee.py index 85f377d6..1e9959d9 100644 --- a/backend/app/services/oa_assignee.py +++ b/backend/app/services/oa_assignee.py @@ -159,8 +159,7 @@ def resolve_direct_supervisor( db, getattr(initiator, "supervisor_user_id", None), tenant_no ) if supervisor_uid: - if supervisor_uid == initiator_user_id: - raise AssigneeResolveError("直属主管不能是发起人本人,请修正用户主管配置") + # 允许主管配置为本人(如部门经理),由高级设置 autoPassWhenApproverIsInitiator 决定是否自动通过 return [supervisor_uid], {"resolve_source": "direct_supervisor"} if fallback_to_dept_leader: users, meta = resolve_dept_leader( diff --git a/backend/app/services/party_customer_sync.py b/backend/app/services/party_customer_sync.py index 963b5b57..ee2832cd 100644 --- a/backend/app/services/party_customer_sync.py +++ b/backend/app/services/party_customer_sync.py @@ -23,6 +23,10 @@ _ROLE_SPECS = ( "name_keys": ("mother_name", "motherName"), "phone_keys": ("mother_phone", "motherPhone"), "id_card_keys": ("mother_id_card", "motherIdCard"), + "birth_ym_keys": ("mother_birth_ym", "motherBirthYm"), + "ethnic_keys": ("mother_ethnic", "motherEthnic"), + "email_keys": ("mother_email", "motherEmail"), + "address_keys": ("mother_address", "motherAddress"), "sex_keys": (), "default_sex": "1", }, @@ -33,6 +37,10 @@ _ROLE_SPECS = ( "name_keys": ("father_name", "fatherName"), "phone_keys": ("father_phone", "fatherPhone"), "id_card_keys": ("father_id_card", "fatherIdCard"), + "birth_ym_keys": (), + "ethnic_keys": (), + "email_keys": (), + "address_keys": (), "sex_keys": (), "default_sex": "0", }, @@ -43,11 +51,27 @@ _ROLE_SPECS = ( "name_keys": ("newborn_name", "newbornName"), "phone_keys": ("newborn_phone", "newbornPhone"), "id_card_keys": ("newborn_id_card", "newbornIdCard"), + "birth_ym_keys": (), + "ethnic_keys": (), + "email_keys": (), + "address_keys": (), "sex_keys": ("newborn_sex", "newbornSex"), "default_sex": "2", }, ) +_PROFILE_ATTRS = ( + "customer_name", + "phone_no", + "id_card_no", + "sex", + "age", + "birth_ym", + "ethnic", + "email", + "address", +) + def _is_empty(val: Any) -> bool: if val is None: @@ -92,6 +116,22 @@ def _age_from_id_card(id_card: str) -> Optional[int]: return age +def apply_role_profile_overwrite(target: Any, profile: Any) -> None: + """协议非空字段覆盖客户主档;空串/None 跳过,不碰 tags/remark。""" + for attr in _PROFILE_ATTRS: + val = getattr(profile, attr, None) + if _is_empty(val): + continue + if attr == "sex": + setattr(target, attr, str(val).strip()[:1]) + elif attr == "birth_ym": + setattr(target, attr, str(val).strip()[:6]) + elif attr == "age": + setattr(target, attr, val) + else: + setattr(target, attr, str(val).strip()) + + def merge_customer_fields(target: Any, source: Any) -> None: """目标为空才补;性别在空或未知(2)时补;标签逗号并集;姓名保留目标。""" if _is_empty(getattr(target, "phone_no", None)): @@ -111,6 +151,11 @@ def merge_customer_fields(target: Any, source: Any) -> None: src_remark = getattr(source, "remark", None) if not _is_empty(src_remark): target.remark = src_remark + for attr in ("birth_ym", "ethnic", "email", "address"): + if _is_empty(getattr(target, attr, None)): + src_val = getattr(source, attr, None) + if not _is_empty(src_val): + setattr(target, attr, src_val) target.customer_tags = _union_tags( getattr(target, "customer_tags", None), getattr(source, "customer_tags", None), @@ -152,51 +197,67 @@ def repoint_customer_refs(db: Session, from_id: str, to_id: str, tenant_no: str) ).update({col_name: to_id}, synchronize_session=False) +def _build_role_profile(party: Dict[str, Any], spec: Dict[str, Any]) -> SimpleNamespace: + name = _pick(party, spec["name_keys"]) + phone = _pick(party, spec["phone_keys"]) + id_card = _pick(party, spec["id_card_keys"]) + sex = _pick(party, spec["sex_keys"]) or spec["default_sex"] + birth_ym = _pick(party, spec.get("birth_ym_keys") or ()) + ethnic = _pick(party, spec.get("ethnic_keys") or ()) + email = _pick(party, spec.get("email_keys") or ()) + address = _pick(party, spec.get("address_keys") or ()) + return SimpleNamespace( + customer_name=name or None, + phone_no=phone or None, + id_card_no=id_card or None, + sex=sex or None, + age=_age_from_id_card(id_card), + birth_ym=birth_ym or None, + ethnic=ethnic or None, + email=email or None, + address=address or None, + ) + + def _create_role_customer( db: Session, *, - name: str, - sex: str, - id_card: str, - phone: str, + profile: SimpleNamespace, tenant_no: str, user_id: str, ) -> str: obj = customer_crud.create_customer( db, - customer_name=name, - sex=sex, - id_card_no=id_card or None, - age=_age_from_id_card(id_card), - phone_no=phone or None, + customer_name=profile.customer_name, + sex=profile.sex or "2", + id_card_no=profile.id_card_no or None, + age=profile.age, + birth_ym=profile.birth_ym, + ethnic=profile.ethnic, + email=profile.email, + address=profile.address, + phone_no=profile.phone_no or None, tenant_no=tenant_no, user_id=user_id, ) return obj.id -def _apply_direct_identity(obj: Any, *, name: str, phone: str, id_card: str) -> None: - obj.customer_name = name - if phone: - obj.phone_no = phone - if id_card: - obj.id_card_no = id_card - age = _age_from_id_card(id_card) - if age is not None and _is_empty(getattr(obj, "age", None)): - obj.age = age - - def _merge_and_repoint( db: Session, *, from_id: str, to_customer: Any, tenant_no: str, + profile: Optional[SimpleNamespace] = None, ) -> str: source = get_customer(db, from_id, tenant_no=tenant_no) if source is not None: merge_customer_fields(to_customer, source) db.add(to_customer) + if profile is not None: + apply_role_profile_overwrite(to_customer, profile) + db.add(to_customer) repoint_customer_refs(db, from_id, to_customer.id, tenant_no) customer_crud.soft_delete_customer(db, from_id, tenant_no=tenant_no) return to_customer.id @@ -209,45 +270,37 @@ def _sync_one_role( tenant_no: str, user_id: str, ) -> str: - name = _pick(party, spec["name_keys"]) - if not name: + profile = _build_role_profile(party, spec) + if _is_empty(profile.customer_name): return "" - phone = _pick(party, spec["phone_keys"]) - id_card = _pick(party, spec["id_card_keys"]) + id_card = profile.id_card_no or "" linked_id = _pick(party, spec["id_keys"]) - sex = _pick(party, spec["sex_keys"]) or spec["default_sex"] found = find_customer_by_id_card(db, id_card, tenant_no) if id_card else None if found is not None: if linked_id and linked_id != found.id: - return _merge_and_repoint(db, from_id=linked_id, to_customer=found, tenant_no=tenant_no) - merge_customer_fields( - found, - SimpleNamespace( - phone_no=phone or None, - age=_age_from_id_card(id_card), - sex=sex, - remark=None, - customer_tags=None, - ), - ) + return _merge_and_repoint( + db, + from_id=linked_id, + to_customer=found, + tenant_no=tenant_no, + profile=profile, + ) + apply_role_profile_overwrite(found, profile) db.add(found) return found.id if linked_id: linked = get_customer(db, linked_id, tenant_no=tenant_no) if linked is not None: - _apply_direct_identity(linked, name=name, phone=phone, id_card=id_card) + apply_role_profile_overwrite(linked, profile) db.add(linked) return linked.id return _create_role_customer( db, - name=name, - sex=sex, - id_card=id_card, - phone=phone, + profile=profile, tenant_no=tenant_no, user_id=user_id, ) diff --git a/backend/app/services/payment_chase.py b/backend/app/services/payment_chase.py new file mode 100644 index 00000000..e3999518 --- /dev/null +++ b/backend/app/services/payment_chase.py @@ -0,0 +1,50 @@ +"""催缴纳入规则(纯函数)""" + +from __future__ import annotations + +from datetime import date, timedelta +from decimal import Decimal +from typing import Any, Dict, Iterable, Mapping, Optional, Tuple + +from app.services.sample_payment_period_summary import summarize_period + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def chase_period_money( + *, + due_amount: Any, + transfers: Iterable[Mapping[str, Any]], +) -> Tuple[Decimal, Decimal]: + """ + 与缴费信息期次口径一致: + 已缴 = dfj + jfqrz + yjf;欠缴 = max(应缴 − 已缴, 0)。 + 返回 (paidAmount, arrearsAmount)。 + """ + summary: Dict[str, Any] = summarize_period( + due_amount=due_amount, transfers=transfers + ) + return summary["displayPaidAmount"], summary["arrearsAmount"] + + +def is_chase_candidate(*, due_date: date, today: date, arrears: Any) -> bool: + """欠缴>0,且应缴日已逾期或落在 [today, today+30]。""" + if _dec(arrears) <= 0: + return False + if due_date < today: + return True + return today <= due_date <= today + timedelta(days=30) + + +def parse_due_date_ymd(raw: Any) -> Optional[date]: + s = str(raw or "").strip().replace("-", "")[:8] + if len(s) != 8 or not s.isdigit(): + return None + try: + return date(int(s[:4]), int(s[4:6]), int(s[6:8])) + except ValueError: + return None diff --git a/backend/app/services/protocol_pay_mode_switch.py b/backend/app/services/protocol_pay_mode_switch.py index 275d25d9..1e543760 100644 --- a/backend/app/services/protocol_pay_mode_switch.py +++ b/backend/app/services/protocol_pay_mode_switch.py @@ -58,10 +58,10 @@ def validate_payment_details_for_save( yjf_rows = [r for r in rows if (r.get("pay_status") or r.get("payStatus")) == "yjf"] - if pay_mode in ("fq", "djfq"): + if pay_mode in ("fq", "djfq", "fgd"): count = int(installment_count or 0) if count < 1: - raise ValueError("分期模式须填写分期期数") + raise ValueError("分期/分改趸模式须填写期数") if installment_advance_days is not None and int(installment_advance_days) < 0: raise ValueError("应缴费提前天数不能为负数") if installment_ratios is not None: diff --git a/backend/app/services/protocol_sample_list_summary.py b/backend/app/services/protocol_sample_list_summary.py index fec136a6..97a98992 100644 --- a/backend/app/services/protocol_sample_list_summary.py +++ b/backend/app/services/protocol_sample_list_summary.py @@ -155,12 +155,23 @@ def get_list_reception_summary_map_by_sample_codes( out: Dict[str, Dict[str, str]] = {} for rec in rows: code = (rec.sample_code or "").strip() - if not code or code in out: + if not code: + continue + prev = out.get(code) + prev_t = prev.get("_receptionTime") if prev else None + rec_t = rec.reception_time + if prev and rec_t and prev_t and rec_t <= prev_t: + continue + if prev and rec_t is None and prev_t is not None: continue out[code] = { "acceptConclusion": (rec.accept_conclusion or "").strip(), "remark": (rec.remark or "").strip(), + "receptionTime": rec_t.strftime("%Y-%m-%d %H:%M:%S") if rec_t else "", + "_receptionTime": rec_t, } + for v in out.values(): + v.pop("_receptionTime", None) return out diff --git a/backend/app/services/qc_work_order_fee_summary.py b/backend/app/services/qc_work_order_fee_summary.py index 9ce59264..fa4ff589 100644 --- a/backend/app/services/qc_work_order_fee_summary.py +++ b/backend/app/services/qc_work_order_fee_summary.py @@ -12,6 +12,8 @@ from app.models.models import ( BizPilotInfusionSchedule, BizProtocolPayment, BizProtocolPaymentDetail, + BizProtocolPaymentSchedule, + BizProtocolPaymentTransfer, ) _PAY_MODE_LABELS = {"dj": "趸交", "fq": "分期", "djfq": "趸交+分期"} @@ -169,6 +171,45 @@ def build_protocol_payment_summary( } +def _first_period_last_transfer_times( + db: Session, + protocol_ids: List[str], + tenant_no: str, +) -> Dict[str, str]: + """协议 → 第 1 期(schedule_no=1)多笔转账中 transfer_time 最晚的一笔。""" + ids = [x.strip() for x in protocol_ids if (x or "").strip()] + if not ids: + return {} + rows = ( + db.query( + BizProtocolPaymentTransfer.collection_protocol_id, + BizProtocolPaymentTransfer.transfer_time, + ) + .join( + BizProtocolPaymentSchedule, + BizProtocolPaymentSchedule.id == BizProtocolPaymentTransfer.schedule_id, + ) + .filter( + BizProtocolPaymentTransfer.collection_protocol_id.in_(ids), + BizProtocolPaymentTransfer.tenant_no == tenant_no, + BizProtocolPaymentTransfer.del_flag == 0, + BizProtocolPaymentSchedule.tenant_no == tenant_no, + BizProtocolPaymentSchedule.del_flag == 0, + BizProtocolPaymentSchedule.schedule_no == 1, + BizProtocolPaymentTransfer.transfer_time.isnot(None), + ) + .all() + ) + latest: Dict[str, datetime] = {} + for pid, t in rows: + if not pid or t is None: + continue + prev = latest.get(pid) + if prev is None or t > prev: + latest[pid] = t + return {pid: _fmt_dt(t) for pid, t in latest.items() if _fmt_dt(t)} + + def build_protocol_payment_summaries( db: Session, protocol_ids: List[str], @@ -200,14 +241,35 @@ def build_protocol_payment_summaries( .all() ): details_by_payment.setdefault(d.payment_id, []).append(d) + first_pay_map = _first_period_last_transfer_times(db, ids, tenant_no) out: Dict[str, Dict[str, Any]] = {} for pid in ids: pay = pay_by_proto.get(pid) - if not pay: + summary = ( + build_protocol_payment_summary(pay, details_by_payment.get(pay.id, [])) + if pay + else None + ) + first_pay = first_pay_map.get(pid) or "" + if not summary and not first_pay: continue - summary = build_protocol_payment_summary(pay, details_by_payment.get(pay.id, [])) - if summary: - out[pid] = summary + if not summary: + summary = { + "payMode": "", + "payModeLabel": "", + "contractAmount": None, + "receivedAmount": None, + "pendingAmount": None, + "statusHint": "", + "installmentCount": None, + "paymentDetails": [], + "overallPayStatus": "", + "overallPayStatusLabel": "—", + "overallPayStatusTagType": "info", + } + if first_pay: + summary["firstPayTransferTime"] = first_pay + out[pid] = summary return out diff --git a/backend/app/services/sample_info_list_enrich.py b/backend/app/services/sample_info_list_enrich.py new file mode 100644 index 00000000..c406631e --- /dev/null +++ b/backend/app/services/sample_info_list_enrich.py @@ -0,0 +1,145 @@ +"""样本信息列表:批量补进度标识(接收/质检/入库/证保险)。""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from sqlalchemy.orm import Session + +from app.models.models import ( + BizProtocolFullmoonTodo, + BizProtocolStorageCert, + BizStorageReleaseReview, +) +from app.services.protocol_sample_list_summary import ( + get_list_qc_conclusion_map_by_sample_codes, + get_list_reception_summary_map_by_sample_codes, +) +from app.services.sample_info_progress import ( + collection_progress, + insurance_handled, + qc_progress, + reception_progress, + storage_cert_handled, + storage_progress, +) + + +def _fmt_dt(t: Optional[datetime]) -> str: + return t.strftime("%Y-%m-%d %H:%M:%S") if t else "" + + +def get_storage_review_map_by_sample_codes( + db: Session, + tenant_no: str, + sample_codes: List[str], + *, + reception_source: str, +) -> Dict[str, Dict[str, str]]: + codes = list({(c or "").strip() for c in sample_codes if (c or "").strip()}) + if not tenant_no or not codes or not (reception_source or "").strip(): + return {} + rows = ( + db.query(BizStorageReleaseReview) + .filter( + BizStorageReleaseReview.tenant_no == tenant_no, + BizStorageReleaseReview.del_flag == 0, + BizStorageReleaseReview.reception_source == reception_source, + BizStorageReleaseReview.sample_code.in_(codes), + ) + .all() + ) + out: Dict[str, Dict[str, Any]] = {} + for rev in rows: + code = (rev.sample_code or "").strip() + if not code: + continue + prev = out.get(code) + prev_t = prev.get("_t") if prev else None + cur_t = rev.storage_time or rev.update_time or rev.create_time + if prev and cur_t and prev_t and cur_t <= prev_t: + continue + if prev and cur_t is None and prev_t is not None: + continue + out[code] = { + "storageStatus": (rev.storage_status or "").strip(), + "reviewConclusion": (rev.review_conclusion or "").strip(), + "storageTime": _fmt_dt(rev.storage_time), + "_t": cur_t, + } + for v in out.values(): + v.pop("_t", None) + return out + + +def enrich_sample_info_items( + db: Session, + tenant_no: str, + items: List[Dict[str, Any]], + *, + reception_source: str, + include_handle_flags: bool, +) -> None: + if not items or not tenant_no: + return + codes = [str(i.get("sampleCode") or "").strip() for i in items] + codes = [c for c in codes if c] + rec_map = get_list_reception_summary_map_by_sample_codes( + db, tenant_no, codes, reception_source=reception_source + ) + qc_map = get_list_qc_conclusion_map_by_sample_codes( + db, tenant_no, codes, reception_source=reception_source + ) + st_map = get_storage_review_map_by_sample_codes( + db, tenant_no, codes, reception_source=reception_source + ) + cert_map: Dict[str, Any] = {} + ins_map: Dict[str, Any] = {} + if include_handle_flags: + pids = list({str(i.get("collectionProtocolId") or "").strip() for i in items if i.get("collectionProtocolId")}) + if pids: + for sc in ( + db.query(BizProtocolStorageCert) + .filter( + BizProtocolStorageCert.collection_protocol_id.in_(pids), + BizProtocolStorageCert.tenant_no == tenant_no, + BizProtocolStorageCert.del_flag == 0, + ) + .all() + ): + cert_map[sc.collection_protocol_id] = sc + for ft in ( + db.query(BizProtocolFullmoonTodo) + .filter( + BizProtocolFullmoonTodo.collection_protocol_id.in_(pids), + BizProtocolFullmoonTodo.tenant_no == tenant_no, + BizProtocolFullmoonTodo.del_flag == 0, + ) + .all() + ): + ins_map[ft.collection_protocol_id] = ft + + for item in items: + code = str(item.get("sampleCode") or "").strip() + cflag, clabel = collection_progress(str(item.get("sampleStatus") or "")) + item["collectionFlag"] = cflag + item["collectionFlagLabel"] = clabel + rec = rec_map.get(code) + rflag, rlabel = reception_progress(rec) + item["receptionFlag"] = rflag + item["receptionFlagLabel"] = rlabel + item["receptionTime"] = (rec or {}).get("receptionTime") or "" + qc = qc_map.get(code) + qflag, qlabel = qc_progress(qc) + item["qcFlag"] = qflag + item["qcFlagLabel"] = qlabel + item["qcCompleteTime"] = "" + st = st_map.get(code) + sflag, slabel = storage_progress(st) + item["storageFlag"] = sflag + item["storageFlagLabel"] = slabel + item["storageTime"] = (st or {}).get("storageTime") or "" + if include_handle_flags: + pid = str(item.get("collectionProtocolId") or "") + item["storageCertHandled"] = storage_cert_handled(cert_map.get(pid)) + item["insuranceHandled"] = insurance_handled(ins_map.get(pid)) diff --git a/backend/app/services/sample_info_progress.py b/backend/app/services/sample_info_progress.py new file mode 100644 index 00000000..673332f3 --- /dev/null +++ b/backend/app/services/sample_info_progress.py @@ -0,0 +1,98 @@ +"""样本信息管理:进度展示码、证/保险已办理判定、办理字段同步。""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, Optional, Tuple + + +def collection_progress(sample_status: str) -> Tuple[str, str]: + st = (sample_status or "").strip() + if not st: + return "uncollected", "未采集" + if st == "yzf": + return "voided", "已作废" + if st in ("ycj", "jcz", "yrk"): + return "collected", "已采集" + return "uncollected", "未采集" + + +def reception_progress(reception: Optional[Dict[str, Any]]) -> Tuple[str, str]: + if not reception: + return "none", "未接收" + c = (reception.get("acceptConclusion") or "").strip() + if c == "js": + return "js", "已接收" + if c == "bj": + return "bj", "拒收" + return "none", "未接收" + + +def qc_progress(qc: Optional[Dict[str, Any]]) -> Tuple[str, str]: + if not qc: + return "incomplete", "未完成" + if (qc.get("qcWorkOrderStatus") or "").strip() == "yzf": + return "voided", "已作废" + conc = (qc.get("qcConclusion") or "").strip() + if conc in ("tg", "btg"): + return "complete", "已完成" + return "incomplete", "未完成" + + +def storage_progress(review: Optional[Dict[str, Any]]) -> Tuple[str, str]: + if not review: + return "none", "未入库" + if (review.get("storageStatus") or "").strip() == "yrk": + return "yrk", "已入库" + if (review.get("reviewConclusion") or "").strip() == "btg": + return "rejected", "拒绝入库" + return "none", "未入库" + + +def storage_cert_handled(row: Any) -> bool: + if row is None: + return False + if getattr(row, "handle_time", None): + return True + if (getattr(row, "make_status", None) or "").strip() == "1": + return True + return bool((getattr(row, "make_date", None) or "").strip()) + + +def insurance_handled(row: Any) -> bool: + if row is None: + return False + if getattr(row, "handle_time", None): + return True + if (getattr(row, "todo_status", None) or "").strip() == "1": + return True + return bool((getattr(row, "handle_date", None) or "").strip()) + + +def sync_handle_status_fields(handle_time: Optional[datetime]) -> Tuple[str, str]: + if handle_time is None: + return "0", "" + return "1", handle_time.strftime("%Y%m%d") + + +def apply_handle_payload( + *, + handle_time: Optional[datetime], + handler_user_id: Optional[str], + attachments: str = "", + remark: str = "", +) -> Dict[str, Any]: + """规范化办理写入字段。有办理时间则办理人员必填。""" + uid = (handler_user_id or "").strip() or None + if handle_time is not None and not uid: + raise ValueError("填写办理时间时必须指定办理人员") + status, legacy_date = sync_handle_status_fields(handle_time) + if handle_time is None: + uid = None + return { + "handle_time": handle_time, + "handler_user_id": uid, + "attachments": (attachments or "").strip(), + "remark": (remark or "").strip(), + "make_or_todo_status": status, + "legacy_date": legacy_date, + } diff --git a/backend/app/services/sample_info_quick_register.py b/backend/app/services/sample_info_quick_register.py new file mode 100644 index 00000000..6ef8e0e7 --- /dev/null +++ b/backend/app/services/sample_info_quick_register.py @@ -0,0 +1,45 @@ +"""样本信息列表快捷登记:去重与入库字段规范化。""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, Iterable, List, Optional + +from app.services.sample_info_progress import apply_handle_payload + + +def unique_nonempty_ids(ids: Iterable[Optional[str]]) -> List[str]: + seen = set() + out: List[str] = [] + for raw in ids: + s = (raw or "").strip() + if not s or s in seen: + continue + seen.add(s) + out.append(s) + return out + + +def apply_quick_storage_payload( + *, + handle_time: Optional[datetime], + handler_user_id: Optional[str], + attachments: str = "", + remark: str = "", +) -> Dict[str, Any]: + """快捷登记入库必须填写办理时间;不走生产/质检双门禁。""" + if handle_time is None: + raise ValueError("请填写办理时间") + payload = apply_handle_payload( + handle_time=handle_time, + handler_user_id=handler_user_id, + attachments=attachments, + remark=remark, + ) + return { + "storage_status": "yrk", + "storage_time": payload["handle_time"], + "storage_by": payload["handler_user_id"], + "storage_in_date": payload["legacy_date"], + "attachments": payload["attachments"], + "remark": payload["remark"], + } diff --git a/backend/app/services/sample_payment_fgd_convert.py b/backend/app/services/sample_payment_fgd_convert.py new file mode 100644 index 00000000..854acff2 --- /dev/null +++ b/backend/app/services/sample_payment_fgd_convert.py @@ -0,0 +1,103 @@ +"""fq → fgd:计算并期变更指令(纯函数,不写库)""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Dict, Iterable, List, Mapping + +from app.services.protocol_transfer_status import schedule_paid_and_arrears + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def _status(row: Mapping[str, Any], *keys: str) -> str: + for k in keys: + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +def plan_fq_to_fgd_merge( + *, + schedules: Iterable[Mapping[str, Any]], + transfers: Iterable[Mapping[str, Any]], +) -> Dict[str, Any]: + sch_list = [ + s + for s in schedules + if _status(s, "scheduleStatus", "schedule_status") != "void" + and not s.get("_deleted") + ] + sch_list.sort(key=lambda s: int(s.get("scheduleNo") or s.get("schedule_no") or 0)) + tr_list = list(transfers or []) + + by_sch: Dict[str, List[Mapping[str, Any]]] = {} + for t in tr_list: + sid = str(t.get("scheduleId") or t.get("schedule_id") or "") + by_sch.setdefault(sid, []).append(t) + + def _arrears(s: Mapping[str, Any]) -> Decimal: + sid = str(s.get("id") or "") + confirmed = [ + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in by_sch.get(sid, []) + if _status(t, "payStatus", "pay_status") == "yjf" + ] + _paid, arr = schedule_paid_and_arrears( + due=s.get("dueAmount", s.get("due_amount")), + confirmed_transfers=confirmed, + ) + arr_d = _dec(arr) + return arr_d if arr_d > 0 else Decimal("0") + + target = None + for s in sch_list: + if _arrears(s) > 0: + target = s + break + if target is None: + for s in sch_list: + if _status(s, "scheduleStatus", "schedule_status") != "cleared": + target = s + break + if target is None: + raise ValueError("没有可合并的未清期次") + + target_no = int(target.get("scheduleNo") or target.get("schedule_no") or 0) + later = [ + s + for s in sch_list + if int(s.get("scheduleNo") or s.get("schedule_no") or 0) > target_no + ] + for s in later: + sid = str(s.get("id") or "") + if any( + _status(t, "payStatus", "pay_status") == "yjf" for t in by_sch.get(sid, []) + ): + raise ValueError("后续期已有已确认转账,无法合并为分改趸") + + remain = sum((_arrears(s) for s in sch_list), Decimal("0")) + tid = str(target.get("id") or "") + target_confirmed = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in by_sch.get(tid, []) + if _status(t, "payStatus", "pay_status") == "yjf" + ), + Decimal("0"), + ) + new_due = target_confirmed + remain + soft_ids = [str(s.get("id") or "") for s in later if str(s.get("id") or "")] + soft_set = set(soft_ids) + kept = [s for s in sch_list if str(s.get("id") or "") not in soft_set] + + return { + "targetScheduleId": tid, + "newDueAmount": new_due, + "softDeleteScheduleIds": soft_ids, + "newInstallmentCount": len(kept), + } diff --git a/backend/app/services/sample_payment_period_summary.py b/backend/app/services/sample_payment_period_summary.py new file mode 100644 index 00000000..fe8b72dc --- /dev/null +++ b/backend/app/services/sample_payment_period_summary.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Dict, Iterable, List, Mapping + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def _status(row: Mapping[str, Any], *keys: str) -> str: + for k in keys: + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +_DISPLAY_PAID_STATUSES = ("dfj", "jfqrz", "yjf") + + +def summarize_period( + *, + due_amount: Any, + transfers: Iterable[Mapping[str, Any]], +) -> Dict[str, Any]: + """ + displayPaidAmount: dfj + jfqrz + yjf 合计(列表「已缴费金额」) + arrearsAmount: max(due - displayPaidAmount, 0) + financeAudit: jfqrz > (confirmed>=due 且无确认中 → yjf) > (0 dfj + payDate: 首笔 transferTime 的日期部分(调用方应按 create_time ASC 传入) + payTypeSummary: 0 笔空;1 笔用该笔 type;多笔「多笔」 + """ + trs: List[Mapping[str, Any]] = list(transfers or []) + display_paid = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in trs + if _status(t, "payStatus", "pay_status") in _DISPLAY_PAID_STATUSES + ), + Decimal("0"), + ) + due = _dec(due_amount) + arrears = due - display_paid + if arrears < 0: + arrears = Decimal("0") + + has_jfqrz = any(_status(t, "payStatus", "pay_status") == "jfqrz" for t in trs) + confirmed = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in trs + if _status(t, "payStatus", "pay_status") == "yjf" + ), + Decimal("0"), + ) + if has_jfqrz: + audit = "jfqrz" + elif due > 0 and confirmed >= due: + audit = "yjf" + elif confirmed > 0: + audit = "partial" + else: + audit = "dfj" + + pay_date = "" + if trs: + raw = str(trs[0].get("transferTime") or trs[0].get("transfer_time") or "") + pay_date = raw[:10] if len(raw) >= 10 else raw + + types = [ + _status(t, "transferType", "transfer_type") + for t in trs + if _status(t, "transferType", "transfer_type") + ] + if len(types) == 0: + pay_type = "" + elif len(set(types)) == 1 and len(types) == 1: + pay_type = types[0] + else: + pay_type = "多笔" + + return { + "displayPaidAmount": display_paid, + "arrearsAmount": arrears, + "payDate": pay_date, + "payTypeSummary": pay_type, + "financeAudit": audit, + } diff --git a/backend/app/services/sample_payment_schedule_validate.py b/backend/app/services/sample_payment_schedule_validate.py new file mode 100644 index 00000000..e9aff129 --- /dev/null +++ b/backend/app/services/sample_payment_schedule_validate.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Iterable, Mapping, Optional, Tuple + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def _status(row: Mapping[str, Any], *keys: str) -> str: + for k in keys: + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +def _due_date(row: Mapping[str, Any]) -> str: + for k in ("dueDate", "due_date"): + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +def schedule_required_fields_ok( + *, + schedules: Iterable[Mapping[str, Any]], + sample_code: str = "", + require_fields: bool = True, +) -> Tuple[bool, Optional[str]]: + """ + 未作废期:应缴金额与应缴时间必填(分期场景由调用方 require_fields=True)。 + """ + if not require_fields: + return True, None + code = (sample_code or "").strip() + prefix = f"样本 {code} " if code else "" + idx = 0 + for s in schedules or []: + if not isinstance(s, Mapping): + continue + st = _status(s, "scheduleStatus", "schedule_status") or "open" + if st == "void": + continue + idx += 1 + amt_raw = s.get("dueAmount", s.get("due_amount")) + if amt_raw is None or amt_raw == "": + return False, f"{prefix}第 {idx} 期应缴金额必须填写" + amt = _dec(amt_raw) + if amt <= 0: + return False, f"{prefix}第 {idx} 期应缴金额必须填写" + if not _due_date(s): + return False, f"{prefix}第 {idx} 期应缴时间必须填写" + return True, None + + +def schedule_dues_match_contract( + *, + contract_amount: Any, + schedules: Iterable[Mapping[str, Any]], + sample_code: str = "", + tol: Decimal = Decimal("0.015"), + require_schedule_fields: bool = False, +) -> Tuple[bool, Optional[str]]: + """ + 未作废期 dueAmount 合计须等于合同金额。 + require_schedule_fields=True 时先校验每期金额与应缴日必填。 + 返回 (True, None) 或 (False, 错误文案)。 + """ + sched_list = [s for s in (schedules or []) if isinstance(s, Mapping)] + if require_schedule_fields: + ok_req, req_msg = schedule_required_fields_ok( + schedules=sched_list, sample_code=sample_code, require_fields=True + ) + if not ok_req: + return False, req_msg + + if contract_amount is None or contract_amount == "": + return True, None + contract = _dec(contract_amount) + total = Decimal("0") + active_count = 0 + for s in sched_list: + st = _status(s, "scheduleStatus", "schedule_status") or "open" + if st == "void": + continue + active_count += 1 + total += _dec(s.get("dueAmount", s.get("due_amount"))) + # 尚未生成应缴批次时不拦(与前端一致) + if active_count == 0: + return True, None + if abs(contract - total) <= tol: + return True, None + code = (sample_code or "").strip() + prefix = f"样本 {code} " if code else "" + msg = ( + f"{prefix}合同金额 {contract:.2f} 与各期应缴合计 {total:.2f} 不一致,请调整后再保存" + ) + return False, msg diff --git a/backend/start-test.sh b/backend/start-test.sh index a0be6b54..84ec51e0 100755 --- a/backend/start-test.sh +++ b/backend/start-test.sh @@ -272,7 +272,8 @@ stop_server() { if ! is_running; then log_warn "测试服务未运行" [ -f "$PID_FILE" ] && rm -f "$PID_FILE" - exit 0 + # 用 return 而非 exit,避免 restart 在「已停止」时整脚本退出、无法再 start + return 0 fi local pid diff --git a/backend/tests/test_collection_regions_by_staff_user.py b/backend/tests/test_collection_regions_by_staff_user.py new file mode 100644 index 00000000..45a67fa5 --- /dev/null +++ b/backend/tests/test_collection_regions_by_staff_user.py @@ -0,0 +1,25 @@ +import unittest + +from app.crud.collection_region import map_staff_region_rows_to_list + + +class RegionsByStaffUserTests(unittest.TestCase): + def test_map_preserves_order_and_fields(self): + rows = [ + ("r2", "华东"), + ("r1", "华南"), + ] + out = map_staff_region_rows_to_list(rows) + self.assertEqual( + out, + [{"id": "r2", "regionName": "华东"}, {"id": "r1", "regionName": "华南"}], + ) + + def test_map_skips_empty_id(self): + rows = [("", "无"), ("r1", "华南")] + out = map_staff_region_rows_to_list(rows) + self.assertEqual(out, [{"id": "r1", "regionName": "华南"}]) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/test_oa_assignee.py b/backend/tests/test_oa_assignee.py index d367d2b8..40d6d61a 100644 --- a/backend/tests/test_oa_assignee.py +++ b/backend/tests/test_oa_assignee.py @@ -64,6 +64,25 @@ class TestOaAssignee(unittest.TestCase): self.assertEqual(users, ["leaderA"]) self.assertEqual(meta.get("resolve_source"), "supervisor_fallback_dept_leader") + def test_direct_supervisor_allows_self(self): + """部门经理等场景:直属主管可配置为本人,解析应成功。""" + db = MagicMock() + initiator = MagicMock(supervisor_user_id="manager_self", dept_id="deptA") + db.query.return_value.filter.return_value.first.return_value = initiator + with patch( + "app.services.oa_assignee._active_user_id", + side_effect=lambda _db, uid, _tn: uid if uid else None, + ): + users, meta = resolve_assignees( + db, + tenant_no="zkxw", + assignee_config={"assigneeType": "direct_supervisor"}, + initiator_user_id="manager_self", + initiator_dept_id="deptA", + ) + self.assertEqual(users, ["manager_self"]) + self.assertEqual(meta.get("resolve_source"), "direct_supervisor") + def test_direct_supervisor_no_fallback_raises(self): db = MagicMock() initiator = MagicMock(supervisor_user_id=None, dept_id="deptA") diff --git a/backend/tests/test_party_customer_sync.py b/backend/tests/test_party_customer_sync.py index 42d846bc..83893387 100644 --- a/backend/tests/test_party_customer_sync.py +++ b/backend/tests/test_party_customer_sync.py @@ -3,6 +3,7 @@ import unittest from unittest.mock import MagicMock, patch from app.services.party_customer_sync import ( + apply_role_profile_overwrite, merge_customer_fields, sync_party_customers, ROLE_MOTHER, @@ -172,6 +173,79 @@ class TestSyncPartyCustomers(unittest.TestCase): deleted_id = sd_args[1] if len(sd_args) > 1 else sd_kwargs.get("customer_id") self.assertEqual(deleted_id, "A") self.assertEqual(customer_b.phone_no, "111") - self.assertEqual(customer_b.customer_name, "已有") + self.assertEqual(customer_b.customer_name, "妈") tags = set((customer_b.customer_tags or "").split(",")) self.assertEqual(tags, {"vip", "tmp"}) + + +class TestApplyRoleProfileOverwrite(unittest.TestCase): + def test_nonempty_overwrites_empty_skips(self): + target = MagicMock( + customer_name="旧名", + phone_no="111", + id_card_no="A", + sex="1", + age=20, + birth_ym="199001", + ethnic="汉族", + email="old@x.com", + address="旧址", + ) + apply_role_profile_overwrite( + target, + MagicMock( + customer_name="新名", + phone_no="222", + id_card_no="", + sex="1", + age=None, + birth_ym="199102", + ethnic="", + email="new@x.com", + address=None, + ), + ) + self.assertEqual(target.customer_name, "新名") + self.assertEqual(target.phone_no, "222") + self.assertEqual(target.id_card_no, "A") + self.assertEqual(target.birth_ym, "199102") + self.assertEqual(target.ethnic, "汉族") + self.assertEqual(target.email, "new@x.com") + self.assertEqual(target.address, "旧址") + + def test_mother_sync_overwrites_extended_fields_on_linked(self): + db = MagicMock() + linked = MagicMock( + id="cust-a", + customer_name="旧名", + phone_no="111", + id_card_no="110101199001011234", + sex="1", + age=30, + birth_ym="199001", + ethnic="回族", + email="old@x.com", + address="旧址", + ) + party = { + "mother_name": "妈", + "mother_phone": "13900000000", + "mother_id_card": "110101199001011234", + "mother_birth_ym": "199002", + "mother_ethnic": "汉族", + "mother_email": "new@x.com", + "mother_address": "新址", + "customer_id": "cust-a", + } + with patch("app.services.party_customer_sync.find_customer_by_id_card", return_value=None): + with patch("app.services.party_customer_sync.get_customer", return_value=linked): + with patch("app.services.party_customer_sync.customer_crud.create_customer") as create: + out = sync_party_customers(db, party, tenant_no="zkxw", user_id="u1") + create.assert_not_called() + self.assertEqual(out.get("customer_id"), "cust-a") + self.assertEqual(linked.customer_name, "妈") + self.assertEqual(linked.phone_no, "13900000000") + self.assertEqual(linked.birth_ym, "199002") + self.assertEqual(linked.ethnic, "汉族") + self.assertEqual(linked.email, "new@x.com") + self.assertEqual(linked.address, "新址") diff --git a/backend/tests/test_payment_chase.py b/backend/tests/test_payment_chase.py new file mode 100644 index 00000000..1780f8a1 --- /dev/null +++ b/backend/tests/test_payment_chase.py @@ -0,0 +1,103 @@ +"""催缴纳入规则与金额口径""" + +from datetime import date +from decimal import Decimal +import unittest + +from app.services.payment_chase import ( + chase_period_money, + is_chase_candidate, + parse_due_date_ymd, +) + + +class TestChase(unittest.TestCase): + def test_chase_paid_includes_dfj_jfqrz_yjf(self): + """与缴费信息期次一致:已缴 = dfj + jfqrz + yjf""" + paid, arrears = chase_period_money( + due_amount=Decimal("1000"), + transfers=[ + { + "transferAmount": Decimal("300"), + "payStatus": "dfj", + "transferTime": "2026-04-01 10:00:00", + }, + { + "transferAmount": Decimal("200"), + "payStatus": "jfqrz", + "transferTime": "2026-04-02 10:00:00", + }, + { + "transferAmount": Decimal("100"), + "payStatus": "yjf", + "transferTime": "2026-04-03 10:00:00", + }, + ], + ) + self.assertEqual(paid, Decimal("600")) + self.assertEqual(arrears, Decimal("400")) + + def test_chase_full_display_paid_not_candidate(self): + paid, arrears = chase_period_money( + due_amount=Decimal("500"), + transfers=[ + { + "transferAmount": Decimal("500"), + "payStatus": "dfj", + "transferTime": "2026-04-01 10:00:00", + } + ], + ) + self.assertEqual(paid, Decimal("500")) + self.assertEqual(arrears, Decimal("0")) + self.assertFalse( + is_chase_candidate( + due_date=date(2026, 9, 1), + today=date(2026, 10, 9), + arrears=arrears, + ) + ) + + def test_overdue(self): + self.assertTrue( + is_chase_candidate( + due_date=date(2026, 9, 1), + today=date(2026, 10, 9), + arrears=Decimal("100"), + ) + ) + + def test_within_30(self): + self.assertTrue( + is_chase_candidate( + due_date=date(2026, 11, 1), + today=date(2026, 10, 9), + arrears=Decimal("100"), + ) + ) + + def test_beyond_30(self): + self.assertFalse( + is_chase_candidate( + due_date=date(2026, 11, 10), + today=date(2026, 10, 9), + arrears=Decimal("100"), + ) + ) + + def test_zero_arrears(self): + self.assertFalse( + is_chase_candidate( + due_date=date(2026, 9, 1), + today=date(2026, 10, 9), + arrears=Decimal("0"), + ) + ) + + def test_parse_due_date(self): + self.assertEqual(parse_due_date_ymd("20261009"), date(2026, 10, 9)) + self.assertEqual(parse_due_date_ymd("2026-10-09"), date(2026, 10, 9)) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/test_protocol_list_sample_reception_qc.py b/backend/tests/test_protocol_list_sample_reception_qc.py index ca934f29..a027dd4b 100644 --- a/backend/tests/test_protocol_list_sample_reception_qc.py +++ b/backend/tests/test_protocol_list_sample_reception_qc.py @@ -51,6 +51,37 @@ class TestSampleItemReceptionQcFields(unittest.TestCase): class TestListReceptionSummaryMap(unittest.TestCase): + def test_picks_later_reception_time(self): + from datetime import datetime + + from app.services.protocol_sample_list_summary import ( + get_list_reception_summary_map_by_sample_codes, + ) + + early = MagicMock() + early.sample_code = "SC-001" + early.accept_conclusion = "bj" + early.remark = "old" + early.reception_time = datetime(2026, 1, 1, 8, 0, 0) + + late = MagicMock() + late.sample_code = "SC-001" + late.accept_conclusion = "js" + late.remark = "new" + late.reception_time = datetime(2026, 2, 1, 8, 0, 0) + + db = MagicMock() + q = MagicMock() + db.query.return_value = q + q.filter.return_value = q + q.all.return_value = [early, late] + + out = get_list_reception_summary_map_by_sample_codes( + db, "t1", ["SC-001"], reception_source="cell_storage" + ) + self.assertEqual(out["SC-001"]["acceptConclusion"], "js") + self.assertEqual(out["SC-001"]["remark"], "new") + def test_keyed_by_sample_code(self): from app.services.protocol_sample_list_summary import ( get_list_reception_summary_map_by_sample_codes, @@ -60,6 +91,7 @@ class TestListReceptionSummaryMap(unittest.TestCase): rec.sample_code = "SC-001" rec.accept_conclusion = "js" rec.remark = "ok" + rec.reception_time = None db = MagicMock() q = MagicMock() diff --git a/backend/tests/test_sample_freight.py b/backend/tests/test_sample_freight.py index b3b250ac..c71b4ace 100644 --- a/backend/tests/test_sample_freight.py +++ b/backend/tests/test_sample_freight.py @@ -77,32 +77,71 @@ class TestFreightListHelpers(unittest.TestCase): I(), protocol_no="CP001", sample_type_name="脐带血", + mother_name="张三", + newborn_name="张小宝", ) self.assertEqual(d["protocolNo"], "CP001") self.assertEqual(d["sampleTypeName"], "脐带血") self.assertEqual(d["sampleSource"], "cell_storage") + self.assertEqual(d["motherName"], "张三") + self.assertEqual(d["newbornName"], "张小宝") + + def test_row_dict_includes_create_by(self): + from app.crud.sample_freight import _row_to_dict + + class F: + id = "f1" + freight_no = "YF20261008001" + transport_mode = "sfkd" + freight_amount = Decimal("10.00") + tenant_no = "zkxw" + order_num = 0 + remark = None + create_by = "u1" + create_time = None + update_time = None + + d = _row_to_dict(F(), linked_sample_count=2, create_by_name="张三") + self.assertEqual(d["createBy"], "u1") + self.assertEqual(d["createByName"], "张三") + self.assertEqual(d["linkedSampleCount"], 2) + self.assertEqual(d["freightNo"], "YF20261008001") -class TestFreightListHelpers(unittest.TestCase): - def test_item_dict_has_enrichment_keys(self): - from app.crud.sample_freight import _item_to_dict_enriched +class TestGenerateFreightNo(unittest.TestCase): + def test_first_of_day(self): + from datetime import datetime - class I: - id = "i1" - freight_id = "f1" - sample_source = "cell_storage" - sample_id = "s1" - sample_code = "CODE1" - protocol_id = "p1" + from app.crud.sample_freight import _generate_freight_no - d = _item_to_dict_enriched( - I(), - protocol_no="CP001", - sample_type_name="脐带血", + db = MagicMock() + db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None + no = _generate_freight_no(db, "zkxw", when=datetime(2026, 10, 8, 12, 0, 0)) + self.assertEqual(no, "YF20261008001") + + def test_increments_from_existing(self): + from datetime import datetime + + from app.crud.sample_freight import _generate_freight_no + + db = MagicMock() + db.query.return_value.filter.return_value.order_by.return_value.first.return_value = ( + "YF20261008003", ) - self.assertEqual(d["protocolNo"], "CP001") - self.assertEqual(d["sampleTypeName"], "脐带血") - self.assertEqual(d["sampleSource"], "cell_storage") + no = _generate_freight_no(db, "zkxw", when=datetime(2026, 10, 8, 12, 0, 0)) + self.assertEqual(no, "YF20261008004") + + def test_rejects_over_999(self): + from datetime import datetime + + from app.crud.sample_freight import _generate_freight_no + + db = MagicMock() + db.query.return_value.filter.return_value.order_by.return_value.first.return_value = ( + "YF20261008999", + ) + with self.assertRaises(ValueError): + _generate_freight_no(db, "zkxw", when=datetime(2026, 10, 8, 12, 0, 0)) if __name__ == "__main__": diff --git a/backend/tests/test_sample_info_handle.py b/backend/tests/test_sample_info_handle.py new file mode 100644 index 00000000..fbb4cfe9 --- /dev/null +++ b/backend/tests/test_sample_info_handle.py @@ -0,0 +1,34 @@ +"""办理 payload 规范化与必填校验。""" +import unittest +from datetime import datetime + +from app.services.sample_info_progress import apply_handle_payload + + +class TestApplyHandlePayload(unittest.TestCase): + def test_apply_sets_status(self): + out = apply_handle_payload( + handle_time=datetime(2026, 10, 5, 9, 0, 0), + handler_user_id="u1", + attachments="http://a.png", + remark="ok", + ) + self.assertEqual(out["handle_time"].year, 2026) + self.assertEqual(out["handler_user_id"], "u1") + self.assertEqual(out["make_or_todo_status"], "1") + self.assertEqual(out["legacy_date"], "20261005") + self.assertEqual(out["attachments"], "http://a.png") + self.assertEqual(out["remark"], "ok") + + def test_apply_clear(self): + out = apply_handle_payload( + handle_time=None, handler_user_id="u1", attachments="", remark="" + ) + self.assertIsNone(out["handle_time"]) + self.assertIsNone(out["handler_user_id"]) + self.assertEqual(out["make_or_todo_status"], "0") + self.assertEqual(out["legacy_date"], "") + + def test_handle_time_requires_handler(self): + with self.assertRaises(ValueError): + apply_handle_payload(handle_time=datetime(2026, 10, 5, 9, 0, 0), handler_user_id="") diff --git a/backend/tests/test_sample_info_progress.py b/backend/tests/test_sample_info_progress.py new file mode 100644 index 00000000..878c2610 --- /dev/null +++ b/backend/tests/test_sample_info_progress.py @@ -0,0 +1,107 @@ +"""样本信息管理:进度展示码与证/保险已办理判定。""" +import unittest +from datetime import datetime +from types import SimpleNamespace + +from app.services.sample_info_progress import ( + collection_progress, + insurance_handled, + qc_progress, + reception_progress, + storage_cert_handled, + storage_progress, + sync_handle_status_fields, +) + + +class TestCollectionProgress(unittest.TestCase): + def test_empty(self): + self.assertEqual(collection_progress(""), ("uncollected", "未采集")) + + def test_collected_includes_mid_states(self): + for st in ("ycj", "jcz", "yrk"): + self.assertEqual(collection_progress(st)[0], "collected") + self.assertEqual(collection_progress("ycj")[1], "已采集") + + def test_void(self): + self.assertEqual(collection_progress("yzf"), ("voided", "已作废")) + + +class TestReceptionProgress(unittest.TestCase): + def test_missing_or_pending(self): + self.assertEqual(reception_progress(None)[0], "none") + self.assertEqual(reception_progress({"acceptConclusion": "dqr"})[0], "none") + self.assertEqual(reception_progress({"acceptConclusion": ""})[0], "none") + + def test_accept_reject(self): + self.assertEqual(reception_progress({"acceptConclusion": "js"}), ("js", "已接收")) + self.assertEqual(reception_progress({"acceptConclusion": "bj"}), ("bj", "拒收")) + + +class TestQcProgress(unittest.TestCase): + def test_none(self): + self.assertEqual(qc_progress(None)[0], "incomplete") + + def test_complete_pass_or_fail(self): + self.assertEqual(qc_progress({"qcConclusion": "tg", "qcWorkOrderStatus": "ywc"})[0], "complete") + self.assertEqual(qc_progress({"qcConclusion": "btg", "qcWorkOrderStatus": "ywc"})[0], "complete") + + def test_void(self): + self.assertEqual(qc_progress({"qcWorkOrderStatus": "yzf"})[0], "voided") + + def test_in_progress(self): + self.assertEqual(qc_progress({"qcWorkOrderStatus": "ypz", "qcConclusion": ""})[0], "incomplete") + + +class TestStorageProgress(unittest.TestCase): + def test_none_or_pending(self): + self.assertEqual(storage_progress(None)[0], "none") + self.assertEqual(storage_progress({"storageStatus": "drk"})[0], "none") + + def test_in_and_reject(self): + self.assertEqual(storage_progress({"storageStatus": "yrk"})[0], "yrk") + self.assertEqual( + storage_progress({"storageStatus": "drk", "reviewConclusion": "btg"})[0], + "rejected", + ) + + +class TestHandleFlags(unittest.TestCase): + def test_cert_handle_time_wins(self): + row = SimpleNamespace(handle_time=datetime(2026, 10, 1, 8, 0), make_status="0", make_date="") + self.assertTrue(storage_cert_handled(row)) + + def test_cert_legacy_make_status(self): + row = SimpleNamespace(handle_time=None, make_status="1", make_date="") + self.assertTrue(storage_cert_handled(row)) + + def test_cert_legacy_make_date(self): + row = SimpleNamespace(handle_time=None, make_status="0", make_date="20260101") + self.assertTrue(storage_cert_handled(row)) + + def test_cert_empty(self): + self.assertFalse(storage_cert_handled(None)) + row = SimpleNamespace(handle_time=None, make_status="0", make_date="") + self.assertFalse(storage_cert_handled(row)) + + def test_insurance_legacy_todo_or_date(self): + self.assertTrue( + insurance_handled(SimpleNamespace(handle_time=None, todo_status="1", handle_date="")) + ) + self.assertTrue( + insurance_handled(SimpleNamespace(handle_time=None, todo_status="0", handle_date="20260131")) + ) + self.assertFalse( + insurance_handled(SimpleNamespace(handle_time=None, todo_status="0", handle_date="")) + ) + + +class TestSyncHandleStatus(unittest.TestCase): + def test_set_and_clear(self): + ht = datetime(2026, 10, 5, 15, 0, 0) + st, ymd = sync_handle_status_fields(ht) + self.assertEqual(st, "1") + self.assertEqual(ymd, "20261005") + st2, ymd2 = sync_handle_status_fields(None) + self.assertEqual(st2, "0") + self.assertEqual(ymd2, "") diff --git a/backend/tests/test_sample_info_quick_register.py b/backend/tests/test_sample_info_quick_register.py new file mode 100644 index 00000000..c0c090f3 --- /dev/null +++ b/backend/tests/test_sample_info_quick_register.py @@ -0,0 +1,42 @@ +"""样本信息列表快捷登记:去重与入库字段规范化。""" +import unittest +from datetime import datetime + +from app.services.sample_info_quick_register import ( + apply_quick_storage_payload, + unique_nonempty_ids, +) + + +class TestUniqueNonemptyIds(unittest.TestCase): + def test_keeps_order_and_drops_blank_dupes(self): + self.assertEqual( + unique_nonempty_ids([" a ", "b", "a", "", None, "b"]), + ["a", "b"], + ) + + def test_empty(self): + self.assertEqual(unique_nonempty_ids([]), []) + + +class TestApplyQuickStoragePayload(unittest.TestCase): + def test_requires_handle_time(self): + with self.assertRaises(ValueError): + apply_quick_storage_payload( + handle_time=None, handler_user_id="u1", attachments="", remark="" + ) + + def test_maps_to_stored_fields(self): + ht = datetime(2026, 10, 6, 10, 30, 0) + out = apply_quick_storage_payload( + handle_time=ht, + handler_user_id="u1", + attachments="http://f.pdf", + remark="ok", + ) + self.assertEqual(out["storage_status"], "yrk") + self.assertEqual(out["storage_time"], ht) + self.assertEqual(out["storage_by"], "u1") + self.assertEqual(out["storage_in_date"], "20261006") + self.assertEqual(out["attachments"], "http://f.pdf") + self.assertEqual(out["remark"], "ok") diff --git a/backend/tests/test_sample_payment_fgd_convert.py b/backend/tests/test_sample_payment_fgd_convert.py new file mode 100644 index 00000000..959e09ba --- /dev/null +++ b/backend/tests/test_sample_payment_fgd_convert.py @@ -0,0 +1,56 @@ +"""fq → fgd:软删后续期,剩余欠缴并入第一期未清期""" + +from decimal import Decimal +import unittest + +from app.services.sample_payment_fgd_convert import plan_fq_to_fgd_merge + + +class TestFgdConvert(unittest.TestCase): + def test_merge_into_period_3(self): + schedules = [ + {"id": "s1", "scheduleNo": 1, "dueAmount": Decimal("5000"), "scheduleStatus": "cleared"}, + {"id": "s2", "scheduleNo": 2, "dueAmount": Decimal("5000"), "scheduleStatus": "cleared"}, + {"id": "s3", "scheduleNo": 3, "dueAmount": Decimal("5000"), "scheduleStatus": "open"}, + {"id": "s4", "scheduleNo": 4, "dueAmount": Decimal("5000"), "scheduleStatus": "open"}, + {"id": "s5", "scheduleNo": 5, "dueAmount": Decimal("5000"), "scheduleStatus": "open"}, + {"id": "s6", "scheduleNo": 6, "dueAmount": Decimal("4800"), "scheduleStatus": "open"}, + ] + transfers = [ + {"scheduleId": "s1", "payStatus": "yjf", "transferAmount": Decimal("5000")}, + {"scheduleId": "s2", "payStatus": "yjf", "transferAmount": Decimal("5000")}, + ] + plan = plan_fq_to_fgd_merge(schedules=schedules, transfers=transfers) + self.assertEqual(plan["targetScheduleId"], "s3") + self.assertEqual(plan["newDueAmount"], Decimal("19800")) + self.assertEqual(set(plan["softDeleteScheduleIds"]), {"s4", "s5", "s6"}) + self.assertEqual(plan["newInstallmentCount"], 3) + + def test_allow_when_jfqrz(self): + """缴费确认中不再阻断分改趸并期规划。""" + schedules = [ + {"id": "s1", "scheduleNo": 1, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + {"id": "s2", "scheduleNo": 2, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + ] + transfers = [ + {"scheduleId": "s1", "payStatus": "jfqrz", "transferAmount": Decimal("500")}, + ] + plan = plan_fq_to_fgd_merge(schedules=schedules, transfers=transfers) + self.assertEqual(plan["targetScheduleId"], "s1") + self.assertEqual(set(plan["softDeleteScheduleIds"]), {"s2"}) + + def test_reject_when_later_period_has_yjf(self): + schedules = [ + {"id": "s1", "scheduleNo": 1, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + {"id": "s2", "scheduleNo": 2, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + ] + transfers = [ + {"scheduleId": "s2", "payStatus": "yjf", "transferAmount": Decimal("1000")}, + ] + with self.assertRaises(ValueError) as ctx: + plan_fq_to_fgd_merge(schedules=schedules, transfers=transfers) + self.assertIn("后续期", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/test_sample_payment_period_summary.py b/backend/tests/test_sample_payment_period_summary.py new file mode 100644 index 00000000..3cc5144d --- /dev/null +++ b/backend/tests/test_sample_payment_period_summary.py @@ -0,0 +1,94 @@ +"""单期展示摘要:已缴合计、首笔日期、欠缴、方式、财务审核码""" + +from decimal import Decimal +import unittest + +from app.services.sample_payment_period_summary import summarize_period + + +class TestPeriodSummary(unittest.TestCase): + def test_empty_transfers(self): + r = summarize_period( + due_amount=Decimal("1000"), + transfers=[], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("0")) + self.assertEqual(r["arrearsAmount"], Decimal("1000")) + self.assertEqual(r["payDate"], "") + self.assertEqual(r["payTypeSummary"], "") + self.assertEqual(r["financeAudit"], "dfj") + + def test_dfj_counts_as_display_paid(self): + r = summarize_period( + due_amount=Decimal("1000"), + transfers=[ + { + "transferAmount": Decimal("400"), + "transferType": "wxzz", + "transferTime": "2026-04-19 10:00:00", + "payStatus": "dfj", + } + ], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("400")) + self.assertEqual(r["arrearsAmount"], Decimal("600")) + self.assertEqual(r["payDate"], "2026-04-19") + + def test_multi_transfer_summary(self): + r = summarize_period( + due_amount=Decimal("5000"), + transfers=[ + { + "transferAmount": Decimal("3000"), + "transferType": "wxzz", + "transferTime": "2026-04-19 10:11:00", + "payStatus": "yjf", + }, + { + "transferAmount": Decimal("2000"), + "transferType": "yhkzz", + "transferTime": "2026-04-20 09:40:00", + "payStatus": "yjf", + }, + ], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("5000")) + self.assertEqual(r["payDate"], "2026-04-19") + self.assertEqual(r["arrearsAmount"], Decimal("0")) + self.assertEqual(r["payTypeSummary"], "多笔") + self.assertEqual(r["financeAudit"], "yjf") + + def test_pay_date_is_first_transfer_not_latest(self): + r = summarize_period( + due_amount=Decimal("5000"), + transfers=[ + { + "transferAmount": Decimal("3000"), + "transferType": "wxzz", + "transferTime": "2026-04-19 10:11:00", + "payStatus": "yjf", + }, + { + "transferAmount": Decimal("2000"), + "transferType": "yhkzz", + "transferTime": "2026-04-20 09:40:00", + "payStatus": "yjf", + }, + ], + ) + self.assertEqual(r["payDate"], "2026-04-19") + + def test_confirming_wins(self): + r = summarize_period( + due_amount=Decimal("4960"), + transfers=[ + { + "transferAmount": Decimal("2000"), + "transferType": "pos", + "transferTime": "2026-06-18 15:20:00", + "payStatus": "jfqrz", + } + ], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("2000")) + self.assertEqual(r["financeAudit"], "jfqrz") diff --git a/backend/tests/test_sample_payment_schedule_validate.py b/backend/tests/test_sample_payment_schedule_validate.py new file mode 100644 index 00000000..905356b4 --- /dev/null +++ b/backend/tests/test_sample_payment_schedule_validate.py @@ -0,0 +1,72 @@ +"""样本各期应缴合计 vs 合同金额校验""" + +from decimal import Decimal +import unittest + +from app.services.sample_payment_schedule_validate import ( + schedule_dues_match_contract, + schedule_required_fields_ok, +) + + +class TestScheduleValidate(unittest.TestCase): + def test_match(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("1000"), + schedules=[{"dueAmount": 600}, {"dueAmount": 400}], + ) + self.assertTrue(ok) + self.assertIsNone(msg) + + def test_mismatch(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("1000"), + schedules=[{"dueAmount": 600}, {"dueAmount": 300}], + sample_code="S001", + ) + self.assertFalse(ok) + self.assertIn("900", msg or "") + self.assertIn("1000", msg or "") + self.assertIn("S001", msg or "") + + def test_skip_void(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("500"), + schedules=[ + {"dueAmount": 500, "scheduleStatus": "open"}, + {"dueAmount": 200, "scheduleStatus": "void"}, + ], + ) + self.assertTrue(ok) + self.assertIsNone(msg) + + def test_empty_schedules_skip(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("1000"), + schedules=[], + ) + self.assertTrue(ok) + self.assertIsNone(msg) + + def test_require_amount_and_due_date(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("1000"), + schedules=[ + {"dueAmount": None, "dueDate": "20261001"}, + {"dueAmount": 500, "dueDate": ""}, + ], + sample_code="S1", + require_schedule_fields=True, + ) + self.assertFalse(ok) + self.assertIn("应缴金额", msg or "") + + def test_require_fields_ok(self): + ok, msg = schedule_required_fields_ok( + schedules=[ + {"dueAmount": 500, "dueDate": "20261001"}, + {"dueAmount": 500, "due_date": "20261101"}, + ] + ) + self.assertTrue(ok) + self.assertIsNone(msg) diff --git a/docs/demos/2026-10-08-freight-link-existing-demo.html b/docs/demos/2026-10-08-freight-link-existing-demo.html new file mode 100644 index 00000000..721936d8 --- /dev/null +++ b/docs/demos/2026-10-08-freight-link-existing-demo.html @@ -0,0 +1,746 @@ + + + + + + 配置运费 · 关联已有 — 列表展示方案 Demo + + + + + + +
+
+

配置运费 → 关联已有:列表展示方案

+

+ 采集协议单(细胞存储 / 科研捐赠)与「运费信息」页共用「配置运费」弹窗。 + 当前「关联已有」仅展示运输方式、金额、关联数、创建时间,难以判断该挂哪一笔批次。 + 下方对比三种展示方式(示例数据为假数据)。 +

+
+ 入口:协议单 · 样本及运输信息 + 入口:运费信息 · 配置运费 + API 已有 includeItems(协议单号 / 样本编码 / 来源) +
+
+ + +
+
+
+

方案 A · 现状(过简)

+

仅批次摘要四列,多笔「顺丰快递 / 同金额」时无法区分。

+
+ 现状 +
+
+
配置运费 ✕
+
+

将运费应用到已选中的 2 个样本(可跨协议共用一笔)。

+
+ 新建运费 + 关联已有 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运输方式运费(元)关联样本数创建时间
顺丰快递¥280.0032026-10-07 14:22
顺丰快递¥280.0022026-10-06 09:10
专线车¥1,200.0052026-10-05 16:48
+
+
+ + +
+
+
+

优点

  • 实现简单、弹窗窄
+

问题

  • 同方式同金额无法辨认
  • 看不到已挂了哪些协议/样本
+
+
+ + +
+
+
+

方案 B · 批次行内摘要(推荐)

+

+ 列表请求带 includeItems=true,增加「已关联样本」摘要列:来源 Tag + 协议单号/样本编码 chips(超出折叠为 +N)。 + 弹窗略加宽,仍保持一行一批次、点选即关联。 +

+
+ 推荐 +
+
+
配置运费 ✕
+
+

将运费应用到已选中的 2 个样本(可跨协议共用一笔)。点击一行选择要关联的批次。

+
+ 新建运费 + 关联已有 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运输方式运费(元)关联数已关联样本创建时间
顺丰快递¥280.003 +
+ 细胞存储 + XY-2026-1001 + S-A001 + S-A002 + +1 +
+
协议 XY-2026-1001 · XY-2026-1008
+
2026-10-07 14:22
顺丰快递¥280.002 +
+ 科研捐赠 + JZ-2026-0033 + D-B011 + D-B012 +
+
2026-10-06 09:10
专线车¥1,200.005 +
+ 细胞存储 + 科研捐赠 + XY-2026-0888 + JZ-2026-0012 + +3 样本 +
+
2026-10-05 16:48
+
+
+ + +
+
+
+
+

优点

+
    +
  • 一眼区分同金额批次
  • +
  • 复用现有 list + includeItems,无需新接口
  • +
  • 协议单弹窗与运费页弹窗可共用同一表格结构
  • +
+
+
+

取舍

+
    +
  • 多样本时 chips 需截断
  • +
  • 弹窗建议加宽至约 900~960px
  • +
+
+
+
+ + +
+
+
+

方案 C · 行展开明细

+

+ 主表仍较精简;点击「明细」展开已关联样本表(来源 / 协议单号 / 样本编码 / 样本类型),对齐「运费信息」台账字段。 +

+
+ 备选 +
+
+
配置运费 ✕
+
+

将运费应用到已选中的 2 个样本。可展开查看批次下样本后再选择。

+
+ 新建运费 + 关联已有 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运输方式运费(元)关联数创建时间
▾ 明细顺丰快递¥280.0032026-10-07 14:22
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
来源协议单号样本编码样本类型
细胞存储XY-2026-1001S-A001脐带血
细胞存储XY-2026-1001S-A002脐带血
细胞存储XY-2026-1008S-C019胎盘
+
+
▸ 明细顺丰快递¥280.0022026-10-06 09:10
▸ 明细专线车¥1,200.0052026-10-05 16:48
+
+
+ + +
+
+
+
+

优点

+
    +
  • 明细完整,与运费台账一致
  • +
  • 默认视图不拥挤
  • +
+
+
+

取舍

+
    +
  • 多一次展开操作,扫一眼成本更高
  • +
  • 交互与表格高度略复杂
  • +
+
+
+
+ + +
+
+
+

请选择倾向方案

+

点选后告诉我你的选择(或回复 A / B / C)。也可回复「B + 可展开看全量」等组合。

+
+
+
+ + + +
+ +
+
+ + + diff --git a/docs/demos/2026-10-08-payment-info-excel-list-demo.html b/docs/demos/2026-10-08-payment-info-excel-list-demo.html new file mode 100644 index 00000000..cac5d63c --- /dev/null +++ b/docs/demos/2026-10-08-payment-info-excel-list-demo.html @@ -0,0 +1,964 @@ + + + + + + 缴费信息 · 单列表(Excel 表头)Demo + + + + + + +
+
+

缴费信息 · 单列表 Demo(对齐 Excel 表头)

+

+ 去掉「已缴费 / 确认中 / 成功 / 按样本」四个 Tab,改为一行一样本:左侧基础信息 + 右侧第 1~6 期缴费块。 + 趸交 / 分改趸仅第 1 期有值;分期填满 1~6 期(无数据期显示 —)。 + 点击各期「实缴金额」打开弹窗,查看该期单笔/多笔转账与财务审核。 +

+
+ 一行 = 一个样本缴费方案 + 期块固定 6 列组(对齐 Excel) + 实缴金额可点击 → 转账明细弹窗 +
+
+ 第1期 + 第2期 + 第3期 + 第4期 + 第5期 + 第6期 +
+
+ +
+
+

缴费信息

+
横向滚动查看各期 · 姓名列左侧冻结
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
基础信息第一期第二期第三期第四期第五期第六期汇总
序号姓名手机号合同编号合同签署日期存储年限存储项目合同总额缴费方式优惠金额应收总额应缴金额实缴金额缴费日期支付方式财务审核应缴金额实缴金额缴费日期支付方式财务审核应缴金额实缴金额缴费日期支付方式财务审核应缴金额实缴金额缴费日期支付方式财务审核应缴金额实缴金额缴费日期支付方式财务审核应缴金额实缴金额缴费日期支付方式财务审核累计实缴待缴金额备注
+
+
共 3 条 · 演示数据
+
+ +
+

与现有系统字段映射(Demo 说明)

+
    +
  • 一行粒度:样本缴费方案(与现「按样本」一致),不是按转账笔、也不是按协议整单。
  • +
  • 趸交 / 分改趸:仅第 1 期有 schedule;第 2~6 期显示 —。
  • +
  • 分期:按实际期数填 1~N;本 Demo 固定展示 6 列组,未用到的期显示 —(产品若确认最多 6 期则与 Excel 一致)。
  • +
  • 期块「实缴金额」:该期全部转账金额合计(含确认中);点击后弹窗列出每笔转账与 pay_status / 财务审核时间线。
  • +
  • 期块「缴费日期 / 支付方式 / 财务审核」:多笔时 Demo 取「最近一笔成功/确认中」摘要;完整明细以弹窗为准。
  • +
  • 原流程三 Tab:待缴费/确认中/成功可由筛选「到款状态」或行内 Tag 替代,不再分 Tab。
  • +
+
+
+ + + + + + diff --git a/docs/protocol-audit-tab-height-demos/index.html b/docs/protocol-audit-tab-height-demos/index.html new file mode 100644 index 00000000..6525553d --- /dev/null +++ b/docs/protocol-audit-tab-height-demos/index.html @@ -0,0 +1,514 @@ + + + + + + 采集协议单 · 胶囊 Tab 高度对比 Demo + + + + + + +
+ +
+
+

一屏并排(便于比高度)

+ 对齐底边,看垂直占用 +
+
+
+ A · 现状 --lg +
+
+ + +
+
+
+ — +
+
+ B · 默认(推荐) +
+
+ + +
+
+
+ — +
+
+ C · 中间档 --md +
+
+ + +
+
+
+ — +
+
+
+ + +
+
+

A · 现状 panel-pill-tabs--lg

+ 当前线上 +
+

采集协议单

+
+
+ + +
+
+
+ Tab 区域高度(含上下 margin)≈ — +
+
下方列表区占位(筛选 / 表格…)
+
+ 尺寸:margin 16/8 · track padding 5 · btn padding 10×28 · 字号 14 · + min-width 140 +
+
+ + +
+
+

B · 默认 panel-pill-tabs(去掉 --lg)

+ 方案 1 推荐 +
+

采集协议单

+
+
+ + +
+
+
+ Tab 区域高度(含上下 margin)≈ — +
+
下方列表区占位(筛选 / 表格…)
+
+ 尺寸:margin 12/4 · track padding 4 · btn padding 8×20 · 字号 13 · + min-width 108 · 仅改本页 class,全站 --lg 不变 +
+
+ + +
+
+

C · 中间档 panel-pill-tabs--md(提案)

+ 方案 3 +
+

采集协议单

+
+
+ + +
+
+
+ Tab 区域高度(含上下 margin)≈ — +
+
下方列表区占位(筛选 / 表格…)
+
+ 尺寸:margin 12/6 · track padding 4 · btn padding 8×24 · 字号 14 · + min-width 124 · 需新增全局修饰类 +
+
+
+ + + + diff --git a/docs/sql/biz_per_sample_payment_presign.sql b/docs/sql/biz_per_sample_payment_presign.sql index 3434f64d..f1ff8968 100644 --- a/docs/sql/biz_per_sample_payment_presign.sql +++ b/docs/sql/biz_per_sample_payment_presign.sql @@ -3,7 +3,8 @@ -- 说明: -- 1) biz_collection_protocol 增量字段用 information_schema 判断后 ADD,可重复执行 -- 2) payment_model_version 默认 2;首次升级将存量行置为 1(已有 version=1 则跳过 UPDATE) --- 3) 新表 CREATE TABLE IF NOT EXISTS +-- 3) 新表 CREATE TABLE IF NOT EXISTS,校对规则与存量库一致:utf8mb4_unicode_ci +-- 4) 第 9 节 CONVERT 用于已建表纠偏(可重复执行) -- --------------------------------------------------------------------------- -- 1) 采集协议单增量字段 @@ -75,7 +76,7 @@ CREATE TABLE IF NOT EXISTS `biz_protocol_sample_payment` ( PRIMARY KEY (`id`), KEY `idx_psp_tenant_protocol` (`tenant_no`,`collection_protocol_id`,`del_flag`), KEY `idx_psp_tenant_sample` (`tenant_no`,`sample_code`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采集协议单-按样本缴费方案'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-按样本缴费方案'; -- --------------------------------------------------------------------------- -- 3) 应缴批次 @@ -100,7 +101,7 @@ CREATE TABLE IF NOT EXISTS `biz_protocol_payment_schedule` ( `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标记', PRIMARY KEY (`id`), KEY `idx_pps_tenant_protocol` (`tenant_no`,`collection_protocol_id`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采集协议单-应缴批次'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-应缴批次'; -- --------------------------------------------------------------------------- -- 4) 转账笔(无退费字段) @@ -131,7 +132,7 @@ CREATE TABLE IF NOT EXISTS `biz_protocol_payment_transfer` ( KEY `idx_ppt_tenant_protocol` (`tenant_no`,`collection_protocol_id`,`del_flag`), KEY `idx_ppt_tenant_sample` (`tenant_no`,`sample_code`,`del_flag`), KEY `idx_ppt_tenant_pay_status` (`tenant_no`,`pay_status`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采集协议单-缴费转账笔'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-缴费转账笔'; -- --------------------------------------------------------------------------- -- 5) 协议退费登记 @@ -155,7 +156,7 @@ CREATE TABLE IF NOT EXISTS `biz_protocol_refund_record` ( `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标记', PRIMARY KEY (`id`), KEY `idx_prr_tenant_protocol` (`tenant_no`,`collection_protocol_id`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采集协议单-退费登记'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='采集协议单-退费登记'; -- --------------------------------------------------------------------------- -- 6) 预签约订单 @@ -171,8 +172,9 @@ CREATE TABLE IF NOT EXISTS `biz_presign_order` ( `sample_collect_time` datetime DEFAULT NULL COMMENT '采集时间', `pay_mode` varchar(8) DEFAULT NULL COMMENT '整单缴费方式 dj/fq', `contract_amount` decimal(18,2) DEFAULT NULL COMMENT '整单合同金额', + `collector_user_id` varchar(64) DEFAULT NULL COMMENT '采集专员用户ID', `sales_user_id` varchar(64) DEFAULT NULL COMMENT '市场专员用户ID', - `region_manager_user_id` varchar(64) DEFAULT NULL COMMENT '地区经理用户ID', + `region_manager_user_id` varchar(64) DEFAULT NULL COMMENT '采集经理/地区经理用户ID', `order_status` varchar(16) DEFAULT NULL COMMENT '订单状态 draft/active/converted/void', `converted_protocol_id` varchar(64) DEFAULT NULL COMMENT '转成的正式协议ID', `tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', @@ -186,7 +188,17 @@ CREATE TABLE IF NOT EXISTS `biz_presign_order` ( `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标记', PRIMARY KEY (`id`), KEY `idx_po_tenant_protocol_no` (`tenant_no`,`protocol_no`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预签约订单'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='预签约订单'; + +-- 已建表环境增量:采集专员 +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'biz_presign_order' AND COLUMN_NAME = 'collector_user_id' +); +SET @sql := IF(@col_exists = 0, + 'ALTER TABLE `biz_presign_order` ADD COLUMN `collector_user_id` VARCHAR(64) DEFAULT NULL COMMENT ''采集专员用户ID'' AFTER `contract_amount`', + 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- --------------------------------------------------------------------------- -- 7) 预签约样本类型 @@ -206,7 +218,7 @@ CREATE TABLE IF NOT EXISTS `biz_presign_order_sample_type` ( `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标记', PRIMARY KEY (`id`), KEY `idx_post_presign` (`tenant_no`,`presign_order_id`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预签约订单-样本类型'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='预签约订单-样本类型'; -- --------------------------------------------------------------------------- -- 8) 预签约意向金转账笔 @@ -233,4 +245,15 @@ CREATE TABLE IF NOT EXISTS `biz_presign_transfer` ( PRIMARY KEY (`id`), KEY `idx_pt_tenant_pay_status` (`tenant_no`,`pay_status`,`del_flag`), KEY `idx_pt_presign` (`tenant_no`,`presign_order_id`,`del_flag`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预签约订单-意向金转账笔'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='预签约订单-意向金转账笔'; + +-- --------------------------------------------------------------------------- +-- 9) 已建表校对规则对齐存量库 utf8mb4_unicode_ci(避免 JOIN 报 1267 mix of collations) +-- --------------------------------------------------------------------------- +ALTER TABLE `biz_protocol_sample_payment` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `biz_protocol_payment_schedule` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `biz_protocol_payment_transfer` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `biz_protocol_refund_record` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `biz_presign_order` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `biz_presign_order_sample_type` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE `biz_presign_transfer` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/docs/sql/migrations/2026-09-18-rename-collection-region-menu.sql b/docs/sql/migrations/2026-09-18-rename-collection-region-menu.sql deleted file mode 100644 index ac935ca7..00000000 --- a/docs/sql/migrations/2026-09-18-rename-collection-region-menu.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 菜单更名:细胞采集基础信息维护 → 采集地区及医院 --- 对应前端路由/静态菜单:sqxp.ts、backend/app/api/menu.py - -UPDATE `sys_menu` -SET - `menu_name` = '采集地区及医院', - `update_time` = NOW() -WHERE `id` = '123' - AND `del_flag` = 0; - --- 兼容按权限标识匹配(若环境环境 id 不同) -UPDATE `sys_menu` -SET - `menu_name` = '采集地区及医院', - `update_time` = NOW() -WHERE `del_flag` = 0 - AND `perms` = 'biz:collectionRegion:list' - AND `menu_name` <> '采集地区及医院'; diff --git a/docs/sql/migrations/2026-09-20-oa-instance-comment.sql b/docs/sql/migrations/2026-09-20-oa-instance-comment.sql deleted file mode 100644 index 0d63a12f..00000000 --- a/docs/sql/migrations/2026-09-20-oa-instance-comment.sql +++ /dev/null @@ -1,20 +0,0 @@ --- OA 审批实例评论 -CREATE TABLE IF NOT EXISTS `oa_instance_comment` ( - `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID', - `instance_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '所属实例ID', - `content` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '评论正文', - `attachment_refs_json` text COLLATE utf8mb4_unicode_ci COMMENT '引用表单附件快照 JSON', - `status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'normal' COMMENT 'normal/revoked', - `revoked_time` datetime DEFAULT NULL COMMENT '撤回时间', - `tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', - `order_num` int(11) DEFAULT NULL COMMENT '排序号', - `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', - `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id(评论作者)', - `create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id', - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标记', - PRIMARY KEY (`id`), - KEY `idx_oa_comment_tenant_inst_time` (`tenant_no`,`instance_id`,`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OA审批实例评论'; diff --git a/docs/sql/migrations/2026-09-21-oa-instance-view-grant.sql b/docs/sql/migrations/2026-09-21-oa-instance-view-grant.sql deleted file mode 100644 index 3d126f72..00000000 --- a/docs/sql/migrations/2026-09-21-oa-instance-view-grant.sql +++ /dev/null @@ -1,97 +0,0 @@ --- 评论 @ 用户:查看授权(不写入抄送任务) -ALTER TABLE `oa_instance_comment` - ADD COLUMN `mention_user_ids_json` text COLLATE utf8mb4_unicode_ci NULL COMMENT '评论@用户ID列表 JSON' AFTER `revoked_time`; - -CREATE TABLE IF NOT EXISTS `oa_instance_view_grant` ( - `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID', - `instance_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '实例ID', - `user_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '被授权查看人', - `source` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'comment_mention' COMMENT 'comment_mention', - `comment_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源评论ID,撤回评论不删除授权', - `granted_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '授权人', - `tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号', - `order_num` int(11) DEFAULT NULL COMMENT '排序号', - `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', - `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id', - `create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id', - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `del_flag` smallint(6) NOT NULL DEFAULT '0' COMMENT '删除标记', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_oa_view_grant_user` (`tenant_no`,`instance_id`,`user_id`), - KEY `idx_oa_view_grant_user_time` (`tenant_no`,`user_id`,`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OA审批实例查看授权'; - --- 移动端「提到我的」:挂在与「抄送我的」相同的父菜单下,并复制其角色授权 -INSERT INTO `sys_menu` ( - `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, - `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, - `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` -) -SELECT - MD5(CONCAT('oa-mention-', s.`tenant_no`, '-', s.`id`)), - '提到我的', - s.`parent_id`, - s.`menu_type`, - '/pages/oa/mentions/index', - s.`component`, - 'oa:mention', - s.`is_frame`, - s.`visible`, - s.`status`, - s.`tenant_no`, - IFNULL(s.`order_num`, 0) + 1, - '评论@后可查看的审批单', - s.`create_by`, - s.`create_dept_id`, - NOW(), - s.`update_by`, - NOW(), - 0 -FROM `sys_menu` s -WHERE s.`del_flag` = 0 - AND s.`menu_name` = '抄送我的' - AND s.`path` LIKE '%oa/cc%' - AND NOT EXISTS ( - SELECT 1 FROM `sys_menu` x - WHERE x.`del_flag` = 0 - AND x.`tenant_no` = s.`tenant_no` - AND x.`path` LIKE '%oa/mentions%' - ); - -INSERT INTO `sys_role_menu` ( - `id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`, - `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` -) -SELECT - MD5(CONCAT('oa-mention-role-', rm.`role_id`, '-', m.`id`)), - rm.`role_id`, - m.`id`, - m.`tenant_no`, - NULL, - NULL, - rm.`create_by`, - IFNULL(rm.`create_dept_id`, ''), - NOW(), - rm.`update_by`, - NOW(), - 0 -FROM `sys_menu` m -JOIN `sys_menu` cc - ON cc.`del_flag` = 0 - AND cc.`tenant_no` = m.`tenant_no` - AND cc.`parent_id` = m.`parent_id` - AND cc.`menu_name` = '抄送我的' - AND cc.`path` LIKE '%oa/cc%' -JOIN `sys_role_menu` rm - ON rm.`del_flag` = 0 - AND rm.`menu_id` = cc.`id` -WHERE m.`del_flag` = 0 - AND m.`path` LIKE '%oa/mentions%' - AND NOT EXISTS ( - SELECT 1 FROM `sys_role_menu` x - WHERE x.`del_flag` = 0 - AND x.`role_id` = rm.`role_id` - AND x.`menu_id` = m.`id` - ); diff --git a/docs/sql/migrations/2026-09-22-purge-oa-instance-data.sql b/docs/sql/migrations/2026-09-22-purge-oa-instance-data.sql deleted file mode 100644 index 6ef2fc1e..00000000 --- a/docs/sql/migrations/2026-09-22-purge-oa-instance-data.sql +++ /dev/null @@ -1,68 +0,0 @@ --- ============================================================================= --- 清空 OA 审批「运行时实例」数据(上线前清理测试数据用) --- --- 删除范围(物理删除): --- oa_instance_view_grant 评论@产生的查看授权 --- oa_instance_comment 实例评论 --- oa_relation 关联审批 --- oa_instance_event 实例操作事件 --- oa_task 待办/已办/抄送任务 --- oa_instance 审批实例 --- oa_instance_draft 发起审批草稿(非模板设计草稿) --- --- 保留(不删除): --- oa_app_group / oa_app / oa_app_version 流程定义与已发布版本 --- oa_receive_account 收款账户主数据 --- --- 使用前请先备份;默认清理全部租户。若只清某一租户,取消下方 @tenant_no 注释并赋值。 --- ============================================================================= - --- SET @tenant_no = 'zkxw'; - -START TRANSACTION; - --- 1) 评论查看授权 -DELETE FROM `oa_instance_view_grant` --- WHERE `tenant_no` = @tenant_no -; - --- 2) 实例评论 -DELETE FROM `oa_instance_comment` --- WHERE `tenant_no` = @tenant_no -; - --- 3) 关联审批 -DELETE FROM `oa_relation` --- WHERE `tenant_no` = @tenant_no -; - --- 4) 实例事件 -DELETE FROM `oa_instance_event` --- WHERE `tenant_no` = @tenant_no -; - --- 5) 审批任务 -DELETE FROM `oa_task` --- WHERE `tenant_no` = @tenant_no -; - --- 6) 审批实例 -DELETE FROM `oa_instance` --- WHERE `tenant_no` = @tenant_no -; - --- 7) 发起草稿 -DELETE FROM `oa_instance_draft` --- WHERE `tenant_no` = @tenant_no -; - -COMMIT; - --- 执行后核对(期望均为 0) -SELECT 'oa_instance_view_grant' AS tbl, COUNT(*) AS cnt FROM `oa_instance_view_grant` -UNION ALL SELECT 'oa_instance_comment', COUNT(*) FROM `oa_instance_comment` -UNION ALL SELECT 'oa_relation', COUNT(*) FROM `oa_relation` -UNION ALL SELECT 'oa_instance_event', COUNT(*) FROM `oa_instance_event` -UNION ALL SELECT 'oa_task', COUNT(*) FROM `oa_task` -UNION ALL SELECT 'oa_instance', COUNT(*) FROM `oa_instance` -UNION ALL SELECT 'oa_instance_draft', COUNT(*) FROM `oa_instance_draft`; diff --git a/docs/sql/migrations/2026-10-05-presign-order-menu.sql b/docs/sql/migrations/2026-10-05-presign-order-menu.sql new file mode 100644 index 00000000..ff19c12f --- /dev/null +++ b/docs/sql/migrations/2026-10-05-presign-order-menu.sql @@ -0,0 +1,73 @@ +-- ============================================================================= +-- 预签约订单菜单:插入 sys_menu,并授权给已有「采集协议单」的角色 +-- 测试库 zkxw-sc 已确认原先没有该菜单行。 +-- 菜单位置:采集协议单 之下、缴费信息/样本信息管理 之上 +-- id: 50000000000000000000000000000020 +-- perms: cell-storage:presign-order:list +-- ============================================================================= + +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + '50000000000000000000000000000020', + '预签约订单', + 'CEL00000000000000000000000000001', + 'C', + 'presign-order', + '/cell-storage/presign-order/index', + 'cell-storage:presign-order:list', + 1, + '1', + '1', + 'zkxw', + 3, + '细胞存储:预签约订单', + NULL, + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu m + WHERE m.del_flag = 0 + AND ( + m.id = '50000000000000000000000000000020' + OR m.perms = 'cell-storage:presign-order:list' + ) +); + +-- 已有采集协议单权限的角色补授权(幂等) +INSERT INTO sys_role_menu ( + id, role_id, menu_id, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + MD5(CONCAT(r.id, '|50000000000000000000000000000020')), + r.id, + '50000000000000000000000000000020', + IFNULL(NULLIF(r.tenant_no, ''), 'zkxw'), + 0, + 'grant presign-order to protocol-audit roles', + NULL, + '', + NOW(), + NULL, + NOW(), + 0 +FROM sys_role r +INNER JOIN sys_role_menu rm + ON rm.role_id = r.id + AND rm.menu_id = '50000000000000000000000000000003' + AND rm.del_flag = 0 +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x + WHERE x.role_id = r.id + AND x.menu_id = '50000000000000000000000000000020' + AND x.del_flag = 0 + ); diff --git a/docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql b/docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql index c96ba879..74043d62 100644 --- a/docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql +++ b/docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql @@ -87,11 +87,33 @@ WHERE NOT EXISTS ( ) ); --- 3) 可选:为已有角色补授权(将 super_admin 角色 id 替换为实际值后取消注释) --- INSERT INTO sys_role_menu (id, role_id, menu_id, tenant_no, create_time, update_time, del_flag) --- SELECT UUID(), '', '904b5173519411f1944300163e3bdd96', 'zkxw', NOW(), NOW(), 0 --- FROM DUAL --- WHERE NOT EXISTS ( --- SELECT 1 FROM sys_role_menu rm --- WHERE rm.menu_id = '904b5173519411f1944300163e3bdd96' AND rm.role_id = '' AND rm.del_flag = 0 --- ); +-- 3) 已有「采集协议单」权限的角色补授权(幂等) +INSERT INTO sys_role_menu ( + id, role_id, menu_id, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + MD5(CONCAT(r.id, '|904b5173519411f1944300163e3bdd96')), + r.id, + '904b5173519411f1944300163e3bdd96', + IFNULL(NULLIF(r.tenant_no, ''), 'zkxw'), + 0, + 'grant payment-info to protocol-audit roles', + NULL, + '', + NOW(), + NULL, + NOW(), + 0 +FROM sys_role r +INNER JOIN sys_role_menu rm + ON rm.role_id = r.id + AND rm.menu_id = '50000000000000000000000000000003' + AND rm.del_flag = 0 +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x + WHERE x.role_id = r.id + AND x.menu_id = '904b5173519411f1944300163e3bdd96' + AND x.del_flag = 0 + ); diff --git a/docs/sql/migrations/2026-10-06-hide-research-donation-sample-info-menu.sql b/docs/sql/migrations/2026-10-06-hide-research-donation-sample-info-menu.sql new file mode 100644 index 00000000..47586928 --- /dev/null +++ b/docs/sql/migrations/2026-10-06-hide-research-donation-sample-info-menu.sql @@ -0,0 +1,29 @@ +-- ============================================================================= +-- 下线「科研捐赠 → 样本信息管理」侧栏菜单(2026-10-06) +-- ============================================================================= +-- +-- 【主路径 — 前端固定菜单】 +-- 当前 Web 管理端使用 VITE_ACCESS_MODE=frontend,菜单树已从以下两处移除该子项: +-- - web-admin/src/router/modules/sqxp.ts +-- - backend/app/api/menu.py +-- 样本信息仍从「细胞存储 → 样本信息管理」进入(统一页含科研捐赠 Tab)。 +-- 前端模式环境 **无需执行** 本脚本;保留供 DB 菜单模式同步。 +-- +-- 科研捐赠目录 parent_id: RSC00000000000000000000000000001 +-- 样本信息 id: 60000000000000000000000000000004 +-- perms: research-donation:sample-info:list +-- visible: '0'=隐藏(权限码保留,统一页 Tab / 接口授权仍可用) +-- ============================================================================= + +UPDATE sys_menu +SET visible = '0', + update_time = NOW() +WHERE del_flag = 0 + AND ( + id = '60000000000000000000000000000004' + OR perms = 'research-donation:sample-info:list' + OR ( + path = 'sample-info' + AND parent_id = 'RSC00000000000000000000000000001' + ) + ); diff --git a/docs/sql/migrations/2026-10-08-customer-profile-expand.sql b/docs/sql/migrations/2026-10-08-customer-profile-expand.sql new file mode 100644 index 00000000..b0d75acf --- /dev/null +++ b/docs/sql/migrations/2026-10-08-customer-profile-expand.sql @@ -0,0 +1,10 @@ +-- 客户主档:通用个人档案字段 +-- Spec: docs/superpowers/specs/2026-10-08-customer-profile-expand-sync-design.md + +SET NAMES utf8mb4; + +ALTER TABLE `biz_customer` + ADD COLUMN `birth_ym` char(6) DEFAULT NULL COMMENT '出生年月YYYYMM' AFTER `age`, + ADD COLUMN `ethnic` varchar(32) DEFAULT NULL COMMENT '民族' AFTER `birth_ym`, + ADD COLUMN `email` varchar(128) DEFAULT NULL COMMENT '邮箱' AFTER `ethnic`, + ADD COLUMN `address` varchar(512) DEFAULT NULL COMMENT '家庭住址' AFTER `email`; diff --git a/docs/sql/migrations/2026-10-08-protocol-region-manager.sql b/docs/sql/migrations/2026-10-08-protocol-region-manager.sql new file mode 100644 index 00000000..4f6bd115 --- /dev/null +++ b/docs/sql/migrations/2026-10-08-protocol-region-manager.sql @@ -0,0 +1,10 @@ +-- 采集协议 / 科研捐赠:地区经理用户ID +-- Spec: docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md + +SET NAMES utf8mb4; + +ALTER TABLE `biz_protocol_collection_info` + ADD COLUMN `region_manager_user_id` varchar(64) DEFAULT NULL COMMENT '地区经理用户ID' AFTER `sales_user_id`; + +ALTER TABLE `biz_research_donation_collection` + ADD COLUMN `region_manager_user_id` varchar(64) DEFAULT NULL COMMENT '地区经理用户ID' AFTER `sales_user_id`; diff --git a/docs/sql/migrations/2026-10-08-rename-cell-storage-menu-to-storage-service.sql b/docs/sql/migrations/2026-10-08-rename-cell-storage-menu-to-storage-service.sql new file mode 100644 index 00000000..a8768edd --- /dev/null +++ b/docs/sql/migrations/2026-10-08-rename-cell-storage-menu-to-storage-service.sql @@ -0,0 +1,21 @@ +-- ============================================================================= +-- 侧栏二级菜单「细胞存储」更名为「存储服务」 +-- id: CEL00000000000000000000000000001 +-- 原因:该目录下统一承载细胞存储与科研捐赠业务入口 +-- +-- 【前端固定菜单】同步修改: +-- - web-admin/src/router/modules/sqxp.ts +-- - backend/app/api/menu.py +-- ============================================================================= + +UPDATE sys_menu +SET menu_name = '存储服务', + remark = CASE + WHEN remark IS NULL OR TRIM(remark) = '' THEN '存储服务:含细胞存储与科研捐赠统一业务入口' + WHEN remark LIKE '%细胞存储%' THEN REPLACE(remark, '细胞存储', '存储服务') + ELSE remark + END, + update_time = NOW() +WHERE del_flag = 0 + AND id = 'CEL00000000000000000000000000001' + AND menu_name = '细胞存储'; diff --git a/docs/sql/migrations/2026-10-08-sample-freight-no-fix-format.sql b/docs/sql/migrations/2026-10-08-sample-freight-no-fix-format.sql new file mode 100644 index 00000000..3a641056 --- /dev/null +++ b/docs/sql/migrations/2026-10-08-sample-freight-no-fix-format.sql @@ -0,0 +1,30 @@ +-- 若已按旧规则 YF+YYYY-MM-DD+序号 回填,纠正为 YF+YYYYMMDD+001~999 + +SET @grp := ''; +SET @seq := 0; + +UPDATE biz_sample_freight f +INNER JOIN ( + SELECT + id, + CONCAT( + 'YF', + DATE_FORMAT(create_time, '%Y%m%d'), + LPAD( + @seq := IF( + @grp = CONCAT(tenant_no, '|', DATE_FORMAT(create_time, '%Y%m%d')), + @seq + 1, + 1 + ), + 3, + '0' + ) + ) AS new_no, + @grp := CONCAT(tenant_no, '|', DATE_FORMAT(create_time, '%Y%m%d')) AS _grp + FROM biz_sample_freight + ORDER BY tenant_no ASC, create_time ASC, id ASC +) t ON f.id = t.id +SET f.freight_no = t.new_no; + +ALTER TABLE biz_sample_freight + MODIFY COLUMN freight_no VARCHAR(32) NOT NULL DEFAULT '' COMMENT '运费批次号 YF+YYYYMMDD+序号'; diff --git a/docs/sql/migrations/2026-10-08-sample-freight-no.sql b/docs/sql/migrations/2026-10-08-sample-freight-no.sql new file mode 100644 index 00000000..322ddf8b --- /dev/null +++ b/docs/sql/migrations/2026-10-08-sample-freight-no.sql @@ -0,0 +1,35 @@ +-- 运费批次号:YF + YYYYMMDD + 001~999(按租户、按日递增) + +ALTER TABLE biz_sample_freight + ADD COLUMN freight_no VARCHAR(32) NOT NULL DEFAULT '' COMMENT '运费批次号 YF+YYYYMMDD+序号' AFTER id; + +-- 按租户 + 创建日回填历史数据 +SET @grp := ''; +SET @seq := 0; + +UPDATE biz_sample_freight f +INNER JOIN ( + SELECT + id, + CONCAT( + 'YF', + DATE_FORMAT(create_time, '%Y%m%d'), + LPAD( + @seq := IF( + @grp = CONCAT(tenant_no, '|', DATE_FORMAT(create_time, '%Y%m%d')), + @seq + 1, + 1 + ), + 3, + '0' + ) + ) AS new_no, + @grp := CONCAT(tenant_no, '|', DATE_FORMAT(create_time, '%Y%m%d')) AS _grp + FROM biz_sample_freight + ORDER BY tenant_no ASC, create_time ASC, id ASC +) t ON f.id = t.id +SET f.freight_no = t.new_no +WHERE f.freight_no = '' OR f.freight_no IS NULL; + +ALTER TABLE biz_sample_freight + ADD UNIQUE KEY uk_sample_freight_tenant_no (tenant_no, freight_no); diff --git a/docs/sql/migrations/2026-10-09-payment-chase-menu.sql b/docs/sql/migrations/2026-10-09-payment-chase-menu.sql new file mode 100644 index 00000000..c2ba0e6a --- /dev/null +++ b/docs/sql/migrations/2026-10-09-payment-chase-menu.sql @@ -0,0 +1,74 @@ +-- ============================================================================= +-- 催缴信息菜单:插在缴费信息之下、样本信息管理之上 +-- id: 50000000000000000000000000000022 +-- perms / menuCode: cellStorage:paymentChase:list +-- ============================================================================= + +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + '50000000000000000000000000000022', + '催缴信息', + 'CEL00000000000000000000000000001', + 'C', + 'payment-chase', + '/cell-storage/payment-chase/index', + 'cellStorage:paymentChase:list', + 1, + '1', + '1', + 'zkxw', + 5, + '细胞存储:催缴信息(逾期∪近30天)', + NULL, + '', + NOW(), + NULL, + NOW(), + 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu m + WHERE m.del_flag = 0 + AND ( + m.id = '50000000000000000000000000000022' + OR m.perms = 'cellStorage:paymentChase:list' + ) +); + +-- 已有缴费信息权限的角色补授权(幂等) +INSERT INTO sys_role_menu ( + id, role_id, menu_id, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + MD5(CONCAT(r.id, '|50000000000000000000000000000022')), + r.id, + '50000000000000000000000000000022', + IFNULL(NULLIF(r.tenant_no, ''), 'zkxw'), + 0, + 'grant payment-chase to payment-info roles', + NULL, + '', + NOW(), + NULL, + NOW(), + 0 +FROM sys_role r +INNER JOIN sys_role_menu rm + ON rm.role_id = r.id + AND rm.del_flag = 0 +INNER JOIN sys_menu m + ON m.id = rm.menu_id + AND m.del_flag = 0 + AND m.perms = 'cellStorage:paymentInfo:list' +WHERE r.del_flag = 0 + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x + WHERE x.del_flag = 0 + AND x.role_id = r.id + AND x.menu_id = '50000000000000000000000000000022' + ); diff --git a/docs/sql/migrations/2026-10-10-protocol-deposit-transfer.sql b/docs/sql/migrations/2026-10-10-protocol-deposit-transfer.sql new file mode 100644 index 00000000..f4b803d5 --- /dev/null +++ b/docs/sql/migrations/2026-10-10-protocol-deposit-transfer.sql @@ -0,0 +1,8 @@ +-- 意向金不落协议侧快照表:正式协议通过 biz_collection_protocol.source_presign_order_id +-- 关联预签订单,读写同一份 biz_presign_transfer,保证财务审批/调整两边一致。 +-- +-- 若此前已执行过「创建 biz_protocol_deposit_transfer」的旧脚本,执行下方 DROP 清理。 + +SET NAMES utf8mb4; + +DROP TABLE IF EXISTS `biz_protocol_deposit_transfer`; diff --git a/docs/superpowers/plans/2026-10-05-unified-sample-info.md b/docs/superpowers/plans/2026-10-05-unified-sample-info.md new file mode 100644 index 00000000..829b9185 --- /dev/null +++ b/docs/superpowers/plans/2026-10-05-unified-sample-info.md @@ -0,0 +1,736 @@ +# 统一样本信息管理 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 细胞存储与科研捐赠共用一个「样本信息管理」页(两个来源 Tab、一条样本一行),列表只读展示采集/接收/质检/入库标识,细胞存储另展示证/保险已办理,详情里轻量办理证与保险;采集协议单去掉储存证、满月待办 Tab。 + +**Architecture:** 进度标识由现有接收单、入库质检工单、入库放行审核按 `tenant_no + sample_code` 派生,不建总表。证/保险仍写 `biz_protocol_storage_cert` / `biz_protocol_fullmoon_todo`(协议级),补 `handle_time`、`handler_user_id`。列表扩展现有两个 sample-collections 接口;办理走独立 PUT。前端两路由共用同一列表页。 + +**Tech Stack:** MySQL 5.7 / FastAPI + SQLAlchemy / pytest unittest / Vue 3 + Element Plus / pnpm + +**Design Doc:** `docs/superpowers/specs/2026-10-05-unified-sample-info-design.md` + +**Reuse:** `backend/app/services/protocol_sample_list_summary.py`(接收/质检摘要)、`qc_sms_notification._qc_status_label` 语义、`storage_entry_sms_notification._storage_status_label`、协议详情里已有的接收/质检/入库只读组件。 + +--- + +## File Structure Map + +### New files + +| File | Responsibility | +|------|----------------| +| `docs/sql/migrations/2026-10-05-sample-info-handle-fields.sql` | 两表 ALTER 增加办理时间、办理人 | +| `backend/app/services/sample_info_progress.py` | 采集/接收/质检/入库展示码、证/保险已办理判定、同步 status 日期 | +| `backend/tests/test_sample_info_progress.py` | 上述纯函数单测 | +| `backend/tests/test_sample_info_handle.py` | 办理 upsert 同步字段单测(mock db 或纯函数 + crud 辅助) | +| `web-admin/src/views/cell-storage/sample-info/UnifiedSampleInfoPage.vue` | 两来源 Tab + 筛选 + 列表 + 打开详情 | +| `web-admin/src/views/cell-storage/sample-info/SampleProgressTags.ts` | 列表 Tag 的 type/label 映射(可与 registry 合并,见 Task 7) | +| `web-admin/src/components/business/protocol-handle-panel/index.vue` | 证/保险办理表单(时间、人员、附件、备注、保存) | + +### Modified files + +| File | Change | +|------|--------| +| `backend/app/models/models.py` | `BizProtocolStorageCert`、`BizProtocolFullmoonTodo` 加列 | +| `backend/app/crud/collection_protocol.py` | dict/upsert 支持新字段;`PUT` 办理辅助 | +| `backend/app/crud/protocol_sample_collection.py` | 列表富化 + 进度筛选 | +| `backend/app/crud/research_donation_sample_collection.py` | 同上(无证/保险) | +| `backend/app/api/protocol_sample_collection.py` | 新 query 参数 | +| `backend/app/api/research_donation_sample_collection.py` | 新 query 参数 | +| `backend/app/api/collection_protocol.py` | `PUT /storage-certs/{id}`、`PUT /fullmoon-todos/{id}/handle`;bundle 返回办理字段 | +| `web-admin/src/api/protocol-sample-collection.ts` | 筛选参数 | +| `web-admin/src/api/research-donation-sample-collection.ts` | 筛选参数 | +| `web-admin/src/api/admin.ts` | 办理 PUT(或新 `sample-info-handle.ts`) | +| `web-admin/src/views/cell-storage/sample-info/index.vue` | 改为挂载统一页,默认细胞存储 | +| `web-admin/src/views/research-donation/sample-info/index.vue` | 挂载同一页,默认科研捐赠 | +| `web-admin/src/views/cell-storage/protocol-audit/index.vue` | 只留协议单 Tab | +| `web-admin/src/views/cell-storage/storage-cert/index.vue` | 跳转样本信息 | +| `web-admin/src/views/cell-storage/fullmoon-todo/index.vue` | 跳转样本信息 | +| `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` | 高亮样本;证/保险改为办理面板 | +| `web-admin/src/views/research-donation/list/DonationViewDialog.vue` | 高亮样本(进度组件已有则只加 highlight) | +| `web-admin/src/utils/status-tag-registry.ts` | 进度/办理 Tag | + +### Stop using as workbenches (delete after统一页可用,或先停引用) + +- `protocol-audit/StorageCertTab.vue`、`FullmoonTodoTab.vue` +- `SampleCollectionInfoTab.vue`、`SampleReceptionInfoTab.vue`、`SampleQcInfoTab.vue`、`StorageEntryInfoTab.vue` +- `research-donation/sample-info/DonationSampleCollectionInfoTab.vue` + +短信 API **不要删**。 + +--- + +### Task 1: SQL 与 Model 列 + +**Files:** +- Create: `docs/sql/migrations/2026-10-05-sample-info-handle-fields.sql` +- Modify: `backend/app/models/models.py`(`BizProtocolStorageCert` 约 L1597、`BizProtocolFullmoonTodo` 约 L3613) + +- [ ] **Step 1: 写迁移 SQL** + +MySQL 5.7,列加在业务字段末、公共字段前。`handle_date` 已存在于满月表,新列名为 `handle_time`(DATETIME)。 + +```sql +-- 储存证 / 满月保险轻量办理字段(2026-10-05) +ALTER TABLE `biz_protocol_storage_cert` + ADD COLUMN `handle_time` datetime DEFAULT NULL COMMENT '办理时间' AFTER `make_date`, + ADD COLUMN `handler_user_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '办理人用户ID' AFTER `handle_time`; + +ALTER TABLE `biz_protocol_fullmoon_todo` + ADD COLUMN `handle_time` datetime DEFAULT NULL COMMENT '办理时间' AFTER `handle_date`, + ADD COLUMN `handler_user_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '办理人用户ID' AFTER `handle_time`; +``` + +- [ ] **Step 2: Model 增加同名 Column** + +在 `make_date` 后、`tenant_no` 前: + +```python +handle_time = Column(DateTime, nullable=True, comment="办理时间") +handler_user_id = Column(String(64), nullable=True, comment="办理人用户ID") +``` + +满月表:`handle_date` 列之后同样两列。不要删除保险公司旧列。 + +- [ ] **Step 3: Commit** + +```bash +git add docs/sql/migrations/2026-10-05-sample-info-handle-fields.sql backend/app/models/models.py +git commit -m "$(cat <<'EOF' +feat(db): add handle_time and handler_user_id for storage cert and fullmoon todo. + +EOF +)" +``` + +--- + +### Task 2: 进度与已办理纯函数(TDD) + +**Files:** +- Create: `backend/app/services/sample_info_progress.py` +- Test: `backend/tests/test_sample_info_progress.py` + +在仓库根目录 `backend/` 下跑 pytest(已有 `tests/test_protocol_list_sample_reception_qc.py` 风格:`unittest` + 直接 import 纯函数)。 + +- [ ] **Step 1: 写失败测试** + +```python +"""样本信息管理:进度展示码与证/保险已办理判定。""" +import unittest +from datetime import datetime +from types import SimpleNamespace + +from app.services.sample_info_progress import ( + collection_progress, + insurance_handled, + qc_progress, + reception_progress, + storage_cert_handled, + storage_progress, + sync_handle_status_fields, +) + + +class TestCollectionProgress(unittest.TestCase): + def test_empty(self): + self.assertEqual(collection_progress(""), ("uncollected", "未采集")) + + def test_collected_includes_mid_states(self): + for st in ("ycj", "jcz", "yrk"): + self.assertEqual(collection_progress(st)[0], "collected") + self.assertEqual(collection_progress("ycj")[1], "已采集") + + def test_void(self): + self.assertEqual(collection_progress("yzf"), ("voided", "已作废")) + + +class TestReceptionProgress(unittest.TestCase): + def test_missing_or_pending(self): + self.assertEqual(reception_progress(None)[0], "none") + self.assertEqual(reception_progress({"acceptConclusion": "dqr"})[0], "none") + self.assertEqual(reception_progress({"acceptConclusion": ""})[0], "none") + + def test_accept_reject(self): + self.assertEqual(reception_progress({"acceptConclusion": "js"}), ("js", "已接收")) + self.assertEqual(reception_progress({"acceptConclusion": "bj"}), ("bj", "拒收")) + + +class TestQcProgress(unittest.TestCase): + def test_none(self): + self.assertEqual(qc_progress(None)[0], "incomplete") + + def test_complete_pass_or_fail(self): + self.assertEqual(qc_progress({"qcConclusion": "tg", "qcWorkOrderStatus": "ywc"})[0], "complete") + self.assertEqual(qc_progress({"qcConclusion": "btg", "qcWorkOrderStatus": "ywc"})[0], "complete") + + def test_void(self): + self.assertEqual(qc_progress({"qcWorkOrderStatus": "yzf"})[0], "voided") + + def test_in_progress(self): + self.assertEqual(qc_progress({"qcWorkOrderStatus": "ypz", "qcConclusion": ""})[0], "incomplete") + + +class TestStorageProgress(unittest.TestCase): + def test_none_or_pending(self): + self.assertEqual(storage_progress(None)[0], "none") + self.assertEqual(storage_progress({"storageStatus": "drk"})[0], "none") + + def test_in_and_reject(self): + self.assertEqual(storage_progress({"storageStatus": "yrk"})[0], "yrk") + self.assertEqual( + storage_progress({"storageStatus": "drk", "reviewConclusion": "btg"})[0], + "rejected", + ) + + +class TestHandleFlags(unittest.TestCase): + def test_cert_handle_time_wins(self): + row = SimpleNamespace(handle_time=datetime(2026, 10, 1, 8, 0), make_status="0", make_date="") + self.assertTrue(storage_cert_handled(row)) + + def test_cert_legacy_make_status(self): + row = SimpleNamespace(handle_time=None, make_status="1", make_date="") + self.assertTrue(storage_cert_handled(row)) + + def test_cert_legacy_make_date(self): + row = SimpleNamespace(handle_time=None, make_status="0", make_date="20260101") + self.assertTrue(storage_cert_handled(row)) + + def test_cert_empty(self): + self.assertFalse(storage_cert_handled(None)) + row = SimpleNamespace(handle_time=None, make_status="0", make_date="") + self.assertFalse(storage_cert_handled(row)) + + def test_insurance_legacy_todo_or_date(self): + self.assertTrue( + insurance_handled(SimpleNamespace(handle_time=None, todo_status="1", handle_date="")) + ) + self.assertTrue( + insurance_handled(SimpleNamespace(handle_time=None, todo_status="0", handle_date="20260131")) + ) + self.assertFalse( + insurance_handled(SimpleNamespace(handle_time=None, todo_status="0", handle_date="")) + ) + + +class TestSyncHandleStatus(unittest.TestCase): + def test_set_and_clear(self): + ht = datetime(2026, 10, 5, 15, 0, 0) + st, ymd = sync_handle_status_fields(ht) + self.assertEqual(st, "1") + self.assertEqual(ymd, "20261005") + st2, ymd2 = sync_handle_status_fields(None) + self.assertEqual(st2, "0") + self.assertEqual(ymd2, "") +``` + +- [ ] **Step 2: 跑测确认失败** + +Run: `cd backend && python -m pytest tests/test_sample_info_progress.py -v` + +Expected: FAIL(`sample_info_progress` 未定义) + +- [ ] **Step 3: 实现 `sample_info_progress.py`** + +约定展示码(列表 `xxxFlag` / `xxxFlagLabel`): + +| 领域 | code | 文案 | +|------|------|------| +| 采集 | `uncollected` / `collected` / `voided` | 未采集 / 已采集 / 已作废 | +| 接收 | `none` / `js` / `bj` | 未接收 / 已接收 / 拒收 | +| 质检 | `incomplete` / `complete` / `voided` | 未完成 / 已完成 / 已作废 | +| 入库 | `none` / `yrk` / `rejected` | 未入库 / 已入库 / 拒绝入库 | + +入库拒绝:与短信列表一致——`storage_status != yrk` 且 `review_conclusion == btg`。 + +```python +def collection_progress(sample_status: str) -> tuple: + st = (sample_status or "").strip() + if not st: + return "uncollected", "未采集" + if st == "yzf": + return "voided", "已作废" + if st in ("ycj", "jcz", "yrk"): + return "collected", "已采集" + return "uncollected", "未采集" + + +def reception_progress(reception: dict | None) -> tuple: + if not reception: + return "none", "未接收" + c = (reception.get("acceptConclusion") or "").strip() + if c == "js": + return "js", "已接收" + if c == "bj": + return "bj", "拒收" + return "none", "未接收" + + +def qc_progress(qc: dict | None) -> tuple: + if not qc: + return "incomplete", "未完成" + if (qc.get("qcWorkOrderStatus") or "").strip() == "yzf": + return "voided", "已作废" + conc = (qc.get("qcConclusion") or "").strip() + if conc in ("tg", "btg"): + return "complete", "已完成" + return "incomplete", "未完成" + + +def storage_progress(review: dict | None) -> tuple: + if not review: + return "none", "未入库" + if (review.get("storageStatus") or "").strip() == "yrk": + return "yrk", "已入库" + if (review.get("reviewConclusion") or "").strip() == "btg": + return "rejected", "拒绝入库" + return "none", "未入库" + + +def storage_cert_handled(row) -> bool: + if row is None: + return False + if getattr(row, "handle_time", None): + return True + if (getattr(row, "make_status", None) or "").strip() == "1": + return True + return bool((getattr(row, "make_date", None) or "").strip()) + + +def insurance_handled(row) -> bool: + if row is None: + return False + if getattr(row, "handle_time", None): + return True + if (getattr(row, "todo_status", None) or "").strip() == "1": + return True + return bool((getattr(row, "handle_date", None) or "").strip()) + + +def sync_handle_status_fields(handle_time): + if handle_time is None: + return "0", "" + return "1", handle_time.strftime("%Y%m%d") +``` + +把 `sync_handle_status_fields` 写清楚:`handle_time is None` → `("0", "")`,否则 `("1", YYYYMMDD)`。 + +- [ ] **Step 4: pytest 通过** + +Run: `cd backend && python -m pytest tests/test_sample_info_progress.py -v` + +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/services/sample_info_progress.py backend/tests/test_sample_info_progress.py +git commit -m "$(cat <<'EOF' +feat(backend): add sample info progress flag helpers. + +EOF +)" +``` + +--- + +### Task 3: 办理 upsert 同步字段(TDD) + +**Files:** +- Modify: `backend/app/crud/collection_protocol.py`(`_storage_cert_to_dict` L607、`upsert_storage_cert` L3348、`_fullmoon_todo_to_dict` L3434、`upsert_fullmoon_todo` L3476) +- Test: `backend/tests/test_sample_info_handle.py` + +- [ ] **Step 1: 测试 `apply_handle_payload` 纯逻辑**(可放在 `sample_info_progress.py`) + +```python +from datetime import datetime +from app.services.sample_info_progress import apply_handle_payload + +def test_apply_sets_status(): + out = apply_handle_payload( + handle_time=datetime(2026, 10, 5, 9, 0, 0), + handler_user_id="u1", + attachments="http://a.png", + remark="ok", + ) + assert out["handle_time"].year == 2026 + assert out["handler_user_id"] == "u1" + assert out["make_or_todo_status"] == "1" + assert out["legacy_date"] == "20261005" + +def test_apply_clear(): + out = apply_handle_payload(handle_time=None, handler_user_id="u1", attachments="", remark="") + assert out["handle_time"] is None + assert out["handler_user_id"] is None + assert out["make_or_todo_status"] == "0" + assert out["legacy_date"] == "" +``` + +规则:清空办理时间时 **同时清空** `handler_user_id`(与 spec 一致)。有办理时间时若 `handler_user_id` 为空,upsert 层抛 `ValueError("填写办理时间时必须指定办理人员")`。 + +- [ ] **Step 2: pytest 失败后实现 `apply_handle_payload`,再接到 upsert** + +`upsert_storage_cert` 增加字段:`handle_time`、`handler_user_id`;写入时调用 `sync_handle_status_fields` 设置 `make_status`、`make_date`。 + +`upsert_fullmoon_todo`:写入 `handle_time`、`handler_user_id`,同步 `todo_status`、`handle_date`。调用方 **不要** 传入 `insurance_company_*` 等键(exclude)。 + +`_storage_cert_to_dict` / `_fullmoon_todo_to_dict` 增加: + +```python +"handleTime": _fmt_dt(s.handle_time), +"handlerUserId": s.handler_user_id or "", +"handled": storage_cert_handled(s), # 保险用 insurance_handled +``` + +办理人姓名在 API 层批量查 `sys_user.nick_name`,CRUD dict 可只带 id。 + +- [ ] **Step 3: pytest 通过后 Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(backend): sync storage cert and insurance handle time with legacy status. + +EOF +)" +``` + +--- + +### Task 4: 列表富化与筛选 + +**Files:** +- Modify: `backend/app/crud/protocol_sample_collection.py` +- Modify: `backend/app/crud/research_donation_sample_collection.py` +- Modify: `backend/app/api/protocol_sample_collection.py` +- Modify: `backend/app/api/research_donation_sample_collection.py` +- Modify: `backend/app/services/protocol_sample_list_summary.py`(接收多条时按 `reception_time` 取最晚) + +**列表每行增加(camelCase):** + +- `collectionFlag`, `collectionFlagLabel` +- `receptionFlag`, `receptionFlagLabel`, `receptionTime`(可空) +- `qcFlag`, `qcFlagLabel`, `qcCompleteTime`(可空) +- `storageFlag`, `storageFlagLabel`, `storageTime`(可空) +- 仅细胞存储:`storageCertHandled`, `insuranceHandled`(bool) + +科研捐赠 **禁止** 返回证/保险 handled 字段。 + +**Query(两接口):** `collectionFlag`, `receptionFlag`, `qcFlag`, `storageFlag`;细胞存储另加 `storageCertHandled`、`insuranceHandled`(`1`/`0` 或 `true`/`false`)。 + +实现要点: + +1. 先按现有分页查出样本行。 +2. 收集本页 `sample_code`、`collection_protocol_id`。 +3. 接收:在 `get_list_reception_summary_map_by_sample_codes` 中同码多条时保留 `reception_time` 最大的一条(改循环:不要 `if code in out: continue` 先到先得;改为比较时间)。同时返回 `receptionTime`。 +4. 质检:复用 `get_list_qc_conclusion_map_by_sample_codes(..., reception_source=...)`。 +5. 入库:按 `tenant_no + sample_code + reception_source` 查 `BizStorageReleaseReview`,`del_flag=0`,同码多条取 `update_time`/`storage_time` 最晚。字段 `storageStatus`、`reviewConclusion`、`storageTime`。 +6. 证/保险:按本页 `protocol_id` 批量查两表,用 Task 2 的 handled。 +7. **筛选必须在 count/offset 之前生效**,避免分页错位:对 flag 过滤用 EXISTS / 子查询,或先筛 sample id 再 paginate。不要先 page 再在 Python 里丢掉行。 + +采集筛选 SQL: + +- `uncollected`:`sample_status` IS NULL OR `''` +- `collected`:`IN ('ycj','jcz','yrk')` +- `voided`:`= 'yzf'` + +保留旧参数 `sampleStatus` 时:若传入 `ycj` 等原始码仍过滤该码;新前端只传 `collectionFlag`。 + +- [ ] **Step 1: 补测接收 map 取最晚** + +在 `tests/test_protocol_list_sample_reception_qc.py` 增加:两条 mock `reception_time` 不同,断言留下较晚且 `js`。 + +- [ ] **Step 2: 实现 map + list enrich + API 参数** + +细胞存储 `list_protocol_sample_collections` 签名增加上述 filter。捐赠 crud 同样,无 cert 参数。 + +- [ ] **Step 3:** `cd backend && python -m pytest tests/test_protocol_list_sample_reception_qc.py tests/test_sample_info_progress.py -v` + +Expected: PASS + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(backend): enrich sample collection lists with progress and handle flags. + +EOF +)" +``` + +--- + +### Task 5: 办理 HTTP API 与详情 bundle + +**Files:** +- Modify: `backend/app/api/collection_protocol.py` + +- [ ] **Step 1: Pydantic body** + +```python +class StorageCertHandleBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + handle_time: Optional[datetime] = Field(None, alias="handleTime") + handler_user_id: Optional[str] = Field(None, alias="handlerUserId") + cert_image_urls: Optional[str] = Field(None, alias="certImageUrls") + remark: Optional[str] = None + +class FullmoonHandleBody(BaseModel): + model_config = ConfigDict(populate_by_name=True) + handle_time: Optional[datetime] = Field(None, alias="handleTime") + handler_user_id: Optional[str] = Field(None, alias="handlerUserId") + insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments") + handle_remark: Optional[str] = Field(None, alias="handleRemark") +``` + +- [ ] **Step 2: 路由** + +- `PUT /web/admin/storage-certs/{protocol_id}`:鉴权与协议更新相同(`require_admin_or_super_admin` 或样本信息 menu code 并集,与现有 storage-certs GET 对齐)。校验协议存在、租户、数据权限。`handle_time` 有值则 `handler_user_id` 必填。调用 `upsert_storage_cert`。 +- `PUT /web/admin/fullmoon-todos/{protocol_id}/handle`:同样。`model_dump` **只含** 上述四字段,禁止 spread 进保险公司字段。 +- 不删除 `PUT .../insurance`。 + +协议详情 bundle 里 `storageCert`、`fullmoonTodo` 带上 `handleTime`、`handlerUserId`、`handlerUserName`、`handled`。 + +- [ ] **Step 3: 无 DB 的校验测**(handler 缺失 → ValueError)放 `test_sample_info_handle.py`。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(api): add storage cert and fullmoon lightweight handle endpoints. + +EOF +)" +``` + +--- + +### Task 6: 前端 API 与 Tag 注册 + +**Files:** +- Modify: `web-admin/src/api/protocol-sample-collection.ts` +- Modify: `web-admin/src/api/research-donation-sample-collection.ts` +- Modify: `web-admin/src/api/admin.ts`(或新建 `web-admin/src/api/sample-info-handle.ts`) +- Modify: `web-admin/src/utils/status-tag-registry.ts` + +- [ ] **Step 1: list params 增加 flag 字段**(与后端 alias 一致)。 + +- [ ] **Step 2: 办理函数** + +```ts +export function fetchUpdateStorageCertHandle(protocolId: string, data: { + handleTime?: string | null + handlerUserId?: string | null + certImageUrls?: string + remark?: string +}) { + return request.put({ url: `/web/admin/storage-certs/${protocolId}`, params: data }) +} + +export function fetchUpdateFullmoonHandle(protocolId: string, data: { + handleTime?: string | null + handlerUserId?: string | null + insuranceAttachments?: string + handleRemark?: string +}) { + return request.put({ url: `/web/admin/fullmoon-todos/${protocolId}/handle`, params: data }) +} +``` + +- [ ] **Step 3: registry** + +```ts +export const sampleCollectionFlag: StatusRegistry = { + uncollected: { label: '未采集', tagType: 'info' }, + collected: { label: '已采集', tagType: 'success' }, + voided: { label: '已作废', tagType: 'danger' } +} +export const sampleReceptionFlag: StatusRegistry = { + none: { label: '未接收', tagType: 'info' }, + js: { label: '已接收', tagType: 'success' }, + bj: { label: '拒收', tagType: 'danger' } +} +export const sampleQcFlag: StatusRegistry = { + incomplete: { label: '未完成', tagType: 'warning' }, + complete: { label: '已完成', tagType: 'success' }, + voided: { label: '已作废', tagType: 'danger' } +} +export const sampleStorageFlag: StatusRegistry = { + none: { label: '未入库', tagType: 'info' }, + yrk: { label: '已入库', tagType: 'success' }, + rejected: { label: '拒绝入库', tagType: 'danger' } +} +export const handleDoneFlag: StatusRegistry = { + '1': { label: '已办理', tagType: 'success' }, + '0': { label: '未办理', tagType: 'info' } +} +``` + +导出 label/tagType getter,与文件内现有工厂一致。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): add sample info list params, handle APIs, and progress tags. + +EOF +)" +``` + +--- + +### Task 7: 统一列表页 + +**Files:** +- Create: `web-admin/src/views/cell-storage/sample-info/UnifiedSampleInfoPage.vue` +- Modify: `web-admin/src/views/cell-storage/sample-info/index.vue` +- Modify: `web-admin/src/views/research-donation/sample-info/index.vue` + +对照:`SampleCollectionInfoTab.vue` 的表格骨架 + `protocol-audit/index.vue` 的 pill Tab。UI:Fresh Mint、筛选无重复标题、按钮带 `el-icon`。 + +Props:`defaultSource: 'cell_storage' | 'research_donation'`。 + +- [ ] **Step 1: Tab 与权限** + +两 Tab 文案:细胞存储 / 科研捐赠。`query.tab` 同步。用当前路由 `meta.menuCode` 与用户菜单树判断 Tab 可见性(无科研捐赠权限则只显示细胞存储,反之亦然)。从细胞存储菜单进入默认 `cell_storage`。 + +- [ ] **Step 2: 筛选** + +关键词 placeholder:`姓名/样本编码/入库编码`。采集/接收/质检/入库下拉用 Task 2 的 code。细胞存储 Tab 增加储存证、保险:已办理 / 未办理。防抖 300ms 与现页一致。 + +- [ ] **Step 3: 表格列** + +与 spec §4.2:状态 Tag、样本编码用 `SampleCodeWithSpecialDisplay`。四个进度 +(存储 Tab)两列办理。操作列仅「查看」,`fixed="right"`,`min-width` ≥ 260。双击查看。分页 10/20/50。 + +细胞存储 `fetchProtocolSampleCollectionList`;捐赠 `fetchResearchDonationSampleCollectionList`。 + +- [ ] **Step 4: 查看** + +细胞存储:`ProtocolViewDialog`,`protocol-id = collectionProtocolId`,传入 `highlight-sample-code`。捐赠:`DonationViewDialog` + `researchDonationId`。`show-audit-history` 可保留;不要再靠 `show-storage-cert` 只读拼盘作为办理入口。 + +- [ ] **Step 5: 两个 index 只包一层** + +```vue + +``` + +科研捐赠:`default-source="research_donation"`。科研捐赠页的 `panel-top`「样本信息管理」标题:统一页自带 pill Tab 后,按 Fresh Mint **单标题**——标题放统一页 `h2` 一次即可,子 index 不要再套一个 h2。 + +- [ ] **Step 6: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): unify sample info management into two source tabs. + +EOF +)" +``` + +--- + +### Task 8: 办理面板 + 协议详情 + +**Files:** +- Create: `web-admin/src/components/business/protocol-handle-panel/index.vue` +- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` +- Modify: `web-admin/src/views/research-donation/list/DonationViewDialog.vue`(高亮当前样本) + +- [ ] **Step 1: `protocol-handle-panel`** + +`label-position="top"`,`.dialog-form-layout` 内嵌即可(本身是 section 不是独立 dialog)。字段:`el-date-picker` datetime `value-format="YYYY-MM-DD HH:mm:ss"`;办理人 `el-select` filterable,远程 `fetchUserList({ page: 1, pageSize: 50, keyword, status: '1' })`,空表单默认 `userStore` 当前用户 id/名。附件卡片对齐 `ProtocolFormDialog` 协议附件(`fetchUploadAttachment`,folder 用 `storage-cert` / `fullmoon-todo`)。备注 textarea。底栏「保存」Check 图标。有办理时间无办理人前端拦截。保存成功 `ElMessage.success` 并 `emit('saved')`。 + +两种 mode:`storage-cert` | `insurance`,调用对应 PUT。 + +- [ ] **Step 2: ProtocolViewDialog** + +- props:`highlightSampleCode?: string`。匹配的 `.sample-card` 加高亮 class(主色描边)。`onMounted`/`watch` bundle 就绪后 `scrollIntoView` 该卡片。 +- 替换基本信息里旧储存证只读行、客户下「新生儿保险信息」只读大段:改为两个 `protocol-handle-panel`(仅 `source` 为细胞存储详情时显示)。打开后用 bundle 预填;保存后 `reload` bundle。 +- 样本卡片已有接收/质检/入库只读,不重复造四段表单;确保入库组件在捐赠详情同样存在。 + +- [ ] **Step 3: DonationViewDialog** + +同样 `highlightSampleCode` + 滚动。不要加证/保险面板。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +feat(web-admin): add cert and insurance handle panels on protocol detail. + +EOF +)" +``` + +--- + +### Task 9: 撤协议单 Tab 与旧路由 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/protocol-audit/index.vue` +- Modify: `web-admin/src/views/cell-storage/storage-cert/index.vue` +- Modify: `web-admin/src/views/cell-storage/fullmoon-todo/index.vue` +- Modify: `web-admin/src/views/cell-storage/sample-reception-info/index.vue`(若仍指向旧 tab) +- Modify: `web-admin/src/views/cell-storage/storage-entry-info/index.vue` + +- [ ] **Step 1:** `protocol-audit/index.vue` 的 `entityTabs` 只留 `protocol`;删除 `StorageCertTab`/`FullmoonTodoTab` 引用。`?tab=storage-cert|fullmoon-todo` 时 `replace` 到 `CellStorageSampleInfo`。 + +- [ ] **Step 2:** 储存证/满月隐藏页: + +```ts +router.replace({ name: 'CellStorageSampleInfo', query: { ...route.query, tab: 'cell_storage' } }) +``` + +- [ ] **Step 3:** 确认样本信息页不再 import 四个旧 Tab。删除或停止引用 `StorageCertTab.vue`、`FullmoonTodoTab.vue`、四个 `Sample*InfoTab.vue`、`DonationSampleCollectionInfoTab.vue`(无引用后再删文件,避免死代码)。 + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +refactor(web-admin): remove storage cert and fullmoon tabs from protocol audit. + +EOF +)" +``` + +--- + +### Task 10: 图谱与回归 + +- [ ] **Step 1:** `./scripts/update-graph.sh all`(或分别 backend/frontend)。 + +- [ ] **Step 2:** `cd backend && python -m pytest tests/test_sample_info_progress.py tests/test_sample_info_handle.py tests/test_protocol_list_sample_reception_qc.py -v` + +- [ ] **Step 3:** 浏览器(有环境时):细胞存储样本信息四标识、多样本改证后两行已办理一致、清空办理时间变未办理、科研捐赠无证列、采集协议单无证/满月 Tab、旧储存证 URL 跳转。无浏览器则注明未做 UI 点验。 + +- [ ] **Step 4: Commit** 图谱(若有 diff) + +```bash +git commit -m "$(cat <<'EOF' +chore: update code graphs after unified sample info. + +EOF +)" +``` + +--- + +## Spec coverage + +| Spec | Task | +|------|------| +| 一页两 Tab、默认来源、权限 | 7 | +| 样本行、列、只读进度 | 2, 4, 7 | +| 证/保险已办理兼容旧数据 | 2, 3, 4 | +| 详情进度 + 办理四字段 | 5, 8(进度复用已有只读组件) | +| 独立 PUT、不同步保险公司字段 | 3, 5 | +| 协议单撤 Tab、旧路由 | 9 | +| 不发短信、不建总表 | 全程禁止 | +| 测试要点 | 2–5, 10 | + +## Placeholder / 一致性 + +- Flag code 全计划统一为 `uncollected|collected|voided`、`none|js|bj`、`incomplete|complete|voided`、`none|yrk|rejected`。 +- 办理清空时 handler 一并清空。 +- 列表筛选不得「先分页再过滤」。 diff --git a/docs/superpowers/plans/2026-10-08-customer-profile-expand-sync.md b/docs/superpowers/plans/2026-10-08-customer-profile-expand-sync.md index ef0aa06a..55e284c9 100644 --- a/docs/superpowers/plans/2026-10-08-customer-profile-expand-sync.md +++ b/docs/superpowers/plans/2026-10-08-customer-profile-expand-sync.md @@ -37,7 +37,7 @@ - Create: `docs/sql/migrations/2026-10-08-customer-profile-expand.sql` - Modify: `backend/app/models/models.py`(`BizCustomer`,约 327–347 行) -- [ ] **Step 1: 写迁移 SQL** +- [x] **Step 1: 写迁移 SQL** ```sql -- 客户主档:通用个人档案字段 @@ -52,7 +52,7 @@ ALTER TABLE `biz_customer` ADD COLUMN `address` varchar(512) DEFAULT NULL COMMENT '家庭住址' AFTER `email`; ``` -- [ ] **Step 2: ORM 增加字段** +- [x] **Step 2: ORM 增加字段** 在 `BizCustomer` 中 `age` 与 `phone_no` 之间(或与迁移 AFTER 顺序一致)增加: @@ -65,7 +65,7 @@ address = Column(String(512), nullable=True, comment="家庭住址") 确认文件顶部已 `from sqlalchemy import ... CHAR`(或与同文件其他 `CHAR` 用法一致)。 -- [ ] **Step 3: 本地执行迁移**(按 `.env.local` 库) +- [x] **Step 3: 本地执行迁移**(按 `.env.local` 库) ```bash mysql -h 127.0.0.1 -u -p < docs/sql/migrations/2026-10-08-customer-profile-expand.sql @@ -81,7 +81,7 @@ Expected: 无报错;`SHOW COLUMNS FROM biz_customer LIKE 'birth_ym';` 有行 - Modify: `backend/app/crud/customer.py` - Modify: `backend/app/api/basic_info.py`(`CustomerCreateBody` / `CustomerUpdateBody`) -- [ ] **Step 1: 扩展 `_row_to_dict`** +- [x] **Step 1: 扩展 `_row_to_dict`** 在 `age` 后增加: @@ -92,7 +92,7 @@ Expected: 无报错;`SHOW COLUMNS FROM biz_customer LIKE 'birth_ym';` 有行 "address": c.address or "", ``` -- [ ] **Step 2: 扩展 `create_customer` / `update_customer` 签名与赋值** +- [x] **Step 2: 扩展 `create_customer` / `update_customer` 签名与赋值** `create_customer` 增加可选参数: @@ -114,7 +114,7 @@ address=(address or "").strip() or None, `update_customer`:各参数 `is not None` 时写入;空串写 `None`(与 phone 等一致)。 -- [ ] **Step 3: 扩展 Pydantic Body** +- [x] **Step 3: 扩展 Pydantic Body** `CustomerCreateBody` / `CustomerUpdateBody` 增加: @@ -127,7 +127,7 @@ address: Optional[str] = None 创建/更新路由调用 `create_customer` / `update_customer` 时传入这四个字段(若路由内是 `body.xxx` 展开,一并补上)。 -- [ ] **Step 4: 快速冒烟(可选)** +- [x] **Step 4: 快速冒烟(可选)** 启动后端后 `POST /api/web/admin/customers` 带 `birthYm`/`ethnic`/`email`/`address`,`GET` 列表项应原样返回。 @@ -148,7 +148,7 @@ address: Optional[str] = None - `_create_role_customer` / `create_customer` 调用传入新字段。 - `_apply_direct_identity` 可改为调用 `apply_role_profile_overwrite`,或扩展为同样覆盖四字段。 -- [ ] **Step 1: 写失败单测(覆盖 + 空不覆盖 + 母亲扩展)** +- [x] **Step 1: 写失败单测(覆盖 + 空不覆盖 + 母亲扩展)** 在 `test_party_customer_sync.py` 追加: @@ -229,7 +229,7 @@ class TestApplyRoleProfileOverwrite(unittest.TestCase): self.assertEqual(linked.address, "新址") ``` -- [ ] **Step 2: 跑测确认失败** +- [x] **Step 2: 跑测确认失败** ```bash cd backend && source venv/bin/activate && pytest tests/test_party_customer_sync.py::TestApplyRoleProfileOverwrite -v @@ -237,7 +237,7 @@ cd backend && source venv/bin/activate && pytest tests/test_party_customer_sync. Expected: FAIL(`apply_role_profile_overwrite` 未定义或行为不符)。 -- [ ] **Step 3: 实现 `apply_role_profile_overwrite` 与角色映射** +- [x] **Step 3: 实现 `apply_role_profile_overwrite` 与角色映射** 示例实现骨架: @@ -287,7 +287,7 @@ def apply_role_profile_overwrite(target: Any, profile: Any) -> None: 同步调整 `_apply_direct_identity`,使姓名/电话/身份证/扩展字段走覆盖语义(与单测 `test_name_and_id_card_keeps_linked_id_when_not_found` 兼容:有值仍写入)。 -- [ ] **Step 4: 全量跑该文件** +- [x] **Step 4: 全量跑该文件** ```bash cd backend && source venv/bin/activate && pytest tests/test_party_customer_sync.py -v @@ -305,7 +305,7 @@ Expected: 全部 PASS(含原有 merge 空才补用例)。 - Modify: `web-admin/src/types/api.d.ts`(`Api.BizCustomer.CustomerItem`) - Modify: `web-admin/src/views/basic-info/customer/index.vue` -- [ ] **Step 1: 扩展 `CustomerItem`** +- [x] **Step 1: 扩展 `CustomerItem`** ```typescript birthYm: string @@ -314,7 +314,7 @@ email: string address: string ``` -- [ ] **Step 2: 表单增加四项** +- [x] **Step 2: 表单增加四项** 在 `formData` / `resetForm` / `handleEdit` / `handleSubmit` payload 增加 `birthYm`、`ethnic`、`email`、`address`。 @@ -359,7 +359,7 @@ address: string 复用 `@/api/ethnic` 的 `fetchEthnicList`(对齐 `ProtocolFormDialog` 的 `loadEthnicOptions`)。页面 `onMounted` 或打开弹窗时加载民族列表。 -- [ ] **Step 3: 查看弹窗 `viewSections` 基本信息增加** +- [x] **Step 3: 查看弹窗 `viewSections` 基本信息增加** ```typescript { label: '出生年月', value: formatYm(row.birthYm) }, // 可内联:YYYYMM → YYYY-MM @@ -370,7 +370,7 @@ address: string 列表列可不加(spec 允许);至少查看齐全。 -- [ ] **Step 4: 本地 UI 冒烟** +- [x] **Step 4: 本地 UI 冒烟** 打开 `/#/market/market-info/customer`:新增带四字段 → 保存 → 查看可见 → 编辑改邮箱再保存。 @@ -382,7 +382,7 @@ address: string - Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`(`onMotherCustomerSelect`) - Modify: `web-admin/src/views/research-donation/list/DonationFormDialog.vue`(同名逻辑) -- [ ] **Step 1: 母亲选中时回填** +- [x] **Step 1: 母亲选中时回填** 在现有赋值后增加(仅填主档有值的字段;若希望「只填表单空位」,用 `if (!formData.motherEmail) ...` 判断): @@ -416,13 +416,13 @@ if (!(formData.motherEthnic || '').trim() && ethnic) { 父亲侧无协议扩展字段源,无需回填四新字段。 -- [ ] **Step 2: 科研捐赠表单同样改 `onMotherCustomerSelect`** +- [x] **Step 2: 科研捐赠表单同样改 `onMotherCustomerSelect`** --- ### Task 6: 验收与图谱 -- [ ] **Step 1: 跑后端相关测试** +- [x] **Step 1: 跑后端相关测试** ```bash cd backend && source venv/bin/activate && pytest tests/test_party_customer_sync.py tests/test_customer_relations.py -v @@ -430,7 +430,7 @@ cd backend && source venv/bin/activate && pytest tests/test_party_customer_sync. Expected: PASS。 -- [ ] **Step 2: 按 spec §6 手工验收** +- [x] **Step 2: 按 spec §6 手工验收** 1. 协议填全母亲扩展字段保存 → 当事人有快照 + 客户主档一致。 2. 客户管理改电话 → 旧协议查看仍是当事人旧电话。 @@ -439,7 +439,7 @@ Expected: PASS。 5. 客户管理独立维护四字段。 6. 父亲同步不误清空客户已有邮箱/住址。 -- [ ] **Step 3: 更新知识图谱** +- [x] **Step 3: 更新知识图谱** ```bash ./scripts/update-graph.sh all diff --git a/docs/superpowers/plans/2026-10-08-freight-link-existing-enrich.md b/docs/superpowers/plans/2026-10-08-freight-link-existing-enrich.md new file mode 100644 index 00000000..162ab678 --- /dev/null +++ b/docs/superpowers/plans/2026-10-08-freight-link-existing-enrich.md @@ -0,0 +1,270 @@ +# 配置运费「关联已有」列表 enrichment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 让协议单与运费信息页「配置运费 → 关联已有」列表对齐「运费信息」主表信息密度(合并单元格 + 样本明细 + 创建人),便于用户判断关联哪一笔批次。 + +**Architecture:** 抽出 `flattenSampleFreights` 工具与 `SampleFreightPickTable` 选批表格组件;两处弹窗改为 `includeItems: true` 加载并嵌入该组件;按 `freightId` 单选,不改 create/link API。 + +**Tech Stack:** Vue 3 + Element Plus + TypeScript(web-admin);既有 `fetchSampleFreightList`。 + +**Spec:** `docs/superpowers/specs/2026-10-08-freight-link-existing-enrich-design.md` + +--- + +## File Structure + +| 文件 | 职责 | +|------|------| +| Create: `web-admin/src/utils/sample-freight-flatten.ts` | flatten + spanMethod | +| Create: `web-admin/src/components/business/sample-freight-pick-table/index.vue` | 关联已有选批表 | +| Modify: `web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue` | 嵌入选批表、加宽、keyword | +| Modify: `web-admin/src/components/business/sample-transport-form-section/index.vue` | 嵌入选批表、加宽、includeItems | +| Modify(可选): `web-admin/src/views/cell-storage/sample-freight/index.vue` | 改用公共 flatten(减少重复) | + +--- + +### Task 1: flatten 工具 + +**Files:** +- Create: `web-admin/src/utils/sample-freight-flatten.ts` + +- [x] **Step 1: 新增工具文件** + +```ts +import type { SampleFreight } from '@/api/sample-freight' + +export type SampleFreightFlatRow = { + rowKey: string + freightId: string + rowspan: number + isFirst: boolean + transportMode: string + transportModeLabel: string + freightAmount: number + linkedSampleCount: number + createBy?: string + createByName?: string + createTime: string + sampleSource?: string + protocolNo?: string + sampleCode?: string + sampleTypeName?: string + sampleId?: string + protocolId?: string + /** 原始批次,选中时回传 */ + freight: SampleFreight +} + +export function flattenSampleFreights(list: SampleFreight[]): SampleFreightFlatRow[] { + const rows: SampleFreightFlatRow[] = [] + for (const f of list) { + const items = f.items?.length ? f.items : [null] + items.forEach((it, idx) => { + rows.push({ + rowKey: `${f.id}|${it?.sampleId || idx}`, + freightId: f.id, + rowspan: idx === 0 ? items.length : 0, + isFirst: idx === 0, + transportMode: f.transportMode, + transportModeLabel: f.transportModeLabel || f.transportMode, + freightAmount: f.freightAmount, + linkedSampleCount: f.linkedSampleCount ?? (f.items?.length || 0), + createBy: f.createBy || '', + createByName: f.createByName || f.createBy || '', + createTime: f.createTime || '', + sampleSource: it?.sampleSource, + protocolNo: it?.protocolNo, + sampleCode: it?.sampleCode, + sampleTypeName: it?.sampleTypeName, + sampleId: it?.sampleId, + protocolId: it?.protocolId, + freight: f + }) + }) + } + return rows +} + +const MERGE_PROPS = new Set([ + 'selection', + 'radio', + 'transportMode', + 'freightAmount', + 'linkedSampleCount', + 'createByName', + 'createTime' +]) + +export function sampleFreightSpanMethod({ + column, + row +}: { + column: { type?: string; property?: string } + row: SampleFreightFlatRow +}): { rowspan: number; colspan: number } { + const prop = column.type === 'selection' ? 'selection' : column.property || '' + if (MERGE_PROPS.has(prop)) { + if (row.rowspan > 0) return { rowspan: row.rowspan, colspan: 1 } + return { rowspan: 0, colspan: 0 } + } + return { rowspan: 1, colspan: 1 } +} +``` + +- [x] **Step 2: 确认无 TS 错误后进入 Task 2**(本工具无独立单测;逻辑与主列表一致) + +--- + +### Task 2: SampleFreightPickTable 组件 + +**Files:** +- Create: `web-admin/src/components/business/sample-freight-pick-table/index.vue` + +- [x] **Step 1: 实现选批表格** + +Props: +- `list: SampleFreight[]` +- `loading?: boolean` +- `modelValue: string`(选中的 freightId) +- `maxHeight?: number | string`(默认 280) +- `keyword?: string`(可选,仅展示由父级控制检索时可不内置) + +Emits: +- `update:modelValue` +- `select` → `SampleFreight | null` + +行为: +- 内部 `flattenSampleFreights` + `sampleFreightSpanMethod` +- 第一列 radio(`prop="radio"`),仅 `isFirst` 行显示;点击任意行 `setCurrentRow` / 设置 modelValue 为该行 `freightId` +- 列:运输方式 Tag、运费、关联数、来源 Tag、协议单号、样本编码、样本类型、创建人、创建时间 +- 金额用 `formatMoneyDisplay` +- `@row-click` 选中整批 + +模板要点(radio 列): + +```vue + + + +``` + +`@row-click="(row) => emitSelect(row.freight)"`。 + +- [x] **Step 2: 肉眼确认组件可被 import** + +--- + +### Task 3: FreightConfigDialog 接入 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/sample-freight/FreightConfigDialog.vue` + +- [x] **Step 1: 弹窗 width 改为 `1020px`** + +- [x] **Step 2: 「关联已有」区替换为 keyword + SampleFreightPickTable** + +```vue + +``` + +- [x] **Step 3: `loadExistingFreights` 传 `includeItems: true` 与 `keyword`** + +```ts +const res = await fetchSampleFreightList({ + page: 1, + pageSize: 50, + includeItems: true, + keyword: existingKeyword.value || undefined +}) +``` + +- [x] **Step 4: 用 `selectedFreightId` + `selectedExisting` 同步;open/closed 时清空 keyword 与选中** + +- [x] **Step 5: 提交时仍用 `selectedExisting.value.id`(或由 id 从 list 找回)** + +--- + +### Task 4: SampleTransportFormSection 接入 + +**Files:** +- Modify: `web-admin/src/components/business/sample-transport-form-section/index.vue` + +- [x] **Step 1: 配置运费弹窗 width 改为 `1020px`** + +- [x] **Step 2: 「关联已有」旧 `el-table` 换成 `SampleFreightPickTable` + 可选 keyword(与 Task 3 一致)** + +- [x] **Step 3: `openFreightDialog` 中 list 请求加 `includeItems: true`** + +- [x] **Step 4: `selectedExistingFreight` 由 pick 的 `@select` / `v-model` 更新;`confirmFreight` 的 link 分支不变** + +--- + +### Task 5:(可选)主列表复用 flatten + +**Files:** +- Modify: `web-admin/src/views/cell-storage/sample-freight/index.vue` + +- [x] **Step 1: 删除本地 `flattenFreights` / 调整 `spanMethod`,改 import `flattenSampleFreights` + `sampleFreightSpanMethod`** +- [x] **Step 2: FlatRow 可复用 `SampleFreightFlatRow`(操作列仍本地;span 合并 props 若含 `actions`,在页面包装一层把 `actions` 加入 merge 集合,或扩展 util 支持 `extraMergeProps`)** + +若扩展成本高可跳过本 Task,验收不依赖此项。 + +--- + +### Task 6: 验收 + +- [x] **Step 1: 运费信息页 → 配置运费 → 关联已有:列齐全、合并正确、搜索可用、可单选并提交**(代码已接入,请浏览器手工点验) +- [x] **Step 2: 细胞存储协议单 → 配置运费 → 关联已有:同上**(共用组件已接入) +- [x] **Step 3: 科研捐赠协议单 → 配置运费 → 关联已有:同上(共用 SampleTransportFormSection)** +- [x] **Step 4: 运行 `./scripts/update-graph.sh frontend`** + +--- + +## Spec coverage checklist + +| Spec 要求 | Task | +|-----------|------| +| 对齐运费信息列(含创建人) | 2 | +| 两入口一致 | 3, 4 | +| includeItems=true | 3, 4 | +| 按 freightId 单选 | 2 | +| 弹窗加宽 | 3, 4 | +| 轻量 keyword | 3, 4 | +| 不改 create/link 规则 | 3, 4 保持现有提交 | +| 创建人后端已有 | 前置已完成 | + +--- + +## Self-review notes + +- 无 TBD/占位。 +- `SampleFreightFlatRow.freight` 保证选中后无需再查 map。 +- radio 列 `prop="radio"` 须在 MERGE_PROPS 中,与 selection 同行为。 diff --git a/docs/superpowers/plans/2026-10-08-protocol-market-specialist-region-manager.md b/docs/superpowers/plans/2026-10-08-protocol-market-specialist-region-manager.md new file mode 100644 index 00000000..8d3954e8 --- /dev/null +++ b/docs/superpowers/plans/2026-10-08-protocol-market-specialist-region-manager.md @@ -0,0 +1,425 @@ +# 采集协议单市场专员 / 地区经理联动 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 细胞存储与科研捐赠采集单将「销售人员」改为「市场专员」,按专员关联地区自动带出采集地区,并新增可改选的「地区经理」落库。 + +**Architecture:** 在两张采集信息表增加 `region_manager_user_id`;新增 `GET .../collection-regions/by-staff-user/{userId}` 返回用户绑定地区;前端表单联动对齐预签单 `applyDefaultRegionManager`。列表列改名,查看弹窗与协议审核卡片展示地区经理。 + +**Tech Stack:** MySQL 5.7、FastAPI + SQLAlchemy、Vue 3 + Element Plus、pytest + +**Spec:** `docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md` + +**Commits:** 本会话默认不自动 `git commit`(除非用户明确要求);完成各 Task 后用 checkbox 勾选即可。 + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| `docs/sql/migrations/2026-10-08-protocol-region-manager.sql` | 两表加 `region_manager_user_id` | +| `backend/app/models/models.py` | `BizProtocolCollectionInfo.region_manager_user_id` | +| `backend/app/models/research_donation.py` | `BizResearchDonationCollection.region_manager_user_id` | +| `backend/app/crud/collection_region.py` | `list_regions_by_staff_user` | +| `backend/app/api/basic_info.py` | HTTP 路由 `by-staff-user` | +| `backend/app/crud/collection_protocol.py` | 采集信息 dict / upsert / 列表与详情昵称 | +| `backend/app/crud/research_donation.py` | 同上 | +| `backend/tests/test_collection_regions_by_staff_user.py` | 按用户查地区纯函数/CRUD 单测(可用 mock Session 或 helper) | +| `web-admin/src/api/admin.ts` | `fetchRegionsByStaffUser` | +| `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` | 字段顺序 + 联动 | +| `web-admin/src/views/research-donation/list/DonationFormDialog.vue` | 同上 | +| `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` | 文案 + 地区经理 | +| `web-admin/src/views/research-donation/list/DonationViewDialog.vue` | 同上 | +| `web-admin/src/views/cell-storage/protocol/index.vue` | 列名 | +| `web-admin/src/views/research-donation/list/index.vue` | 列名 | +| `web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue` | 文案 + 地区经理 | +| `web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue` | 若有销售人员展示则同步;补地区经理 | + +--- + +### Task 1: SQL 迁移 + ORM 字段 + +**Files:** +- Create: `docs/sql/migrations/2026-10-08-protocol-region-manager.sql` +- Modify: `backend/app/models/models.py`(`BizProtocolCollectionInfo`) +- Modify: `backend/app/models/research_donation.py`(`BizResearchDonationCollection`) + +- [ ] **Step 1: 写迁移 SQL** + +```sql +-- 采集协议 / 科研捐赠:地区经理用户ID +-- Spec: docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md + +SET NAMES utf8mb4; + +ALTER TABLE `biz_protocol_collection_info` + ADD COLUMN `region_manager_user_id` varchar(64) DEFAULT NULL COMMENT '地区经理用户ID' AFTER `sales_user_id`; + +ALTER TABLE `biz_research_donation_collection` + ADD COLUMN `region_manager_user_id` varchar(64) DEFAULT NULL COMMENT '地区经理用户ID' AFTER `sales_user_id`; +``` + +- [ ] **Step 2: ORM 增加字段** + +在 `BizProtocolCollectionInfo` 中 `sales_user_id` 后增加: + +```python +region_manager_user_id = Column(String(64), nullable=True, comment="地区经理用户ID") +``` + +并将 `sales_user_id` 的 comment 改为 `"市场专员用户ID"`。 + +在 `BizResearchDonationCollection` 同样处理。 + +- [ ] **Step 3: 本地执行迁移**(按 `.env.local` 库) + +```bash +# 示例 +mysql -h 127.0.0.1 -u -p < docs/sql/migrations/2026-10-08-protocol-region-manager.sql +``` + +Expected: `SHOW COLUMNS` 两表均可见 `region_manager_user_id`。 + +--- + +### Task 2: `list_regions_by_staff_user` + 单测(TDD) + +**Files:** +- Create: `backend/tests/test_collection_regions_by_staff_user.py` +- Modify: `backend/app/crud/collection_region.py` +- Modify: `backend/app/api/basic_info.py` +- Modify: `web-admin/src/api/admin.ts` + +- [ ] **Step 1: 写失败测试(排序与映射 helper)** + +在 `collection_region.py` 将「行 → 地区 dict 列表」抽成可测纯函数更稳妥。先写: + +```python +# backend/tests/test_collection_regions_by_staff_user.py +import unittest + +from app.crud.collection_region import map_staff_region_rows_to_list + + +class RegionsByStaffUserTests(unittest.TestCase): + def test_map_preserves_order_and_fields(self): + rows = [ + ("r2", "华东"), + ("r1", "华南"), + ] + out = map_staff_region_rows_to_list(rows) + self.assertEqual( + out, + [{"id": "r2", "regionName": "华东"}, {"id": "r1", "regionName": "华南"}], + ) + + def test_map_skips_empty_id(self): + rows = [("", "无"), ("r1", "华南")] + out = map_staff_region_rows_to_list(rows) + self.assertEqual(out, [{"id": "r1", "regionName": "华南"}]) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: 跑测确认失败** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_collection_regions_by_staff_user.py -v +``` + +Expected: FAIL import / `map_staff_region_rows_to_list` 不存在。 + +- [ ] **Step 3: 实现 helper + CRUD 查询** + +```python +def map_staff_region_rows_to_list(rows: List[Tuple[str, str]]) -> List[Dict[str, Any]]: + out: List[Dict[str, Any]] = [] + for rid, name in rows: + rid = (rid or "").strip() + if not rid: + continue + out.append({"id": rid, "regionName": (name or "").strip()}) + return out + + +def list_regions_by_staff_user( + db: Session, user_id: str, tenant_no: Optional[str] = None +) -> List[Dict[str, Any]]: + uid = (user_id or "").strip() + if not uid: + return [] + q = ( + db.query(BizCollectionRegionStaff.region_id, BizCollectionRegion.region_name) + .join( + BizCollectionRegion, + BizCollectionRegion.id == BizCollectionRegionStaff.region_id, + ) + .filter( + BizCollectionRegionStaff.user_id == uid, + BizCollectionRegionStaff.del_flag == 0, + BizCollectionRegion.del_flag == 0, + ) + ) + if tenant_no: + q = q.filter( + BizCollectionRegionStaff.tenant_no == tenant_no, + BizCollectionRegion.tenant_no == tenant_no, + ) + q = q.order_by( + BizCollectionRegionStaff.order_num.asc(), + BizCollectionRegionStaff.create_time.desc(), + ) + return map_staff_region_rows_to_list([(r[0], r[1]) for r in q.all()]) +``` + +- [ ] **Step 4: 再跑测 PASS** + +```bash +pytest tests/test_collection_regions_by_staff_user.py -v +``` + +Expected: PASS。 + +- [ ] **Step 5: 注册 API** + +在 `basic_info.py` 中,放在 `@router.get("/collection-regions")` 附近、**先于**带 `{region_id}` 的路由亦可(路径静态段已足够区分): + +```python +@router.get("/collection-regions/by-staff-user/{user_id}") +async def list_regions_by_staff_user( + user_id: str, + user: dict = Depends(require_menu_code("biz:collectionRegion:list")), + db: Session = Depends(get_db), +): + tenant_no = _get_tenant_no(db, user) + items = region_svc.list_regions_by_staff_user(db, user_id=user_id, tenant_no=tenant_no) + return success_response({"list": items}) +``` + +- [ ] **Step 6: 前端 API** + +在 `web-admin/src/api/admin.ts` 的 staff API 附近增加: + +```typescript +export function fetchRegionsByStaffUser(userId: string) { + return request.get<{ list: Api.BizCollectionRegion.RegionItem[] }>({ + url: `/web/admin/collection-regions/by-staff-user/${userId}` + }) +} +``` + +(若 `RegionItem` 仅需 `id`/`regionName`,可复用;否则用内联类型 `{ id: string; regionName: string }`。) + +--- + +### Task 3: 后端采集信息读写 `regionManagerUserId` + +**Files:** +- Modify: `backend/app/crud/collection_protocol.py` +- Modify: `backend/app/crud/research_donation.py` +- Test: 可在现有 save-bundle 测试中补断言;若无 DB fixture,至少保证 `_collection_info_to_dict` / `_STR_FIELDS` 改全(手工用 grep 复核字段一致性) + +- [ ] **Step 1: `collection_protocol.py`** + +1. `_collection_info_to_dict` 增加: + `"regionManagerUserId": c.region_manager_user_id or ""` +2. `upsert_collection_info` 的 `_STR_FIELDS` 增加 `"region_manager_user_id"`;创建分支 setattr 同样包含该字段。 +3. 列表 enrichment(约 1051 / 1168 行附近):收集 `regionManagerUserId` 进 `all_user_ids`,设置 `d["regionManagerUserName"]`。 +4. 详情 enrichment(约 1281 行):同上填充 `ci_dict["regionManagerUserName"]`。 + +- [ ] **Step 2: `research_donation.py`** + +1. `_collection_to_dict` 增加 `regionManagerUserId`。 +2. `_COLL_STR_FIELDS` 增加 `"region_manager_user_id"`。 +3. 列表 / 详情昵称 map 同步 `regionManagerUserName`(与 `salesUserName` 同样路径)。 + +- [ ] **Step 3: grep 复核** + +```bash +rg "region_manager_user_id|regionManagerUserId" backend/app/crud/collection_protocol.py backend/app/crud/research_donation.py backend/app/models +``` + +Expected: to_dict、upsert/create/update 字段列表均出现。 + +--- + +### Task 4: `ProtocolFormDialog.vue` 联动 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` + +- [ ] **Step 1: 模板字段顺序与控件** + +采集信息区改为: + +1. 市场专员(原销售人员 select,label 改文案) +2. 地区经理(新 `el-select`,对齐预签单采集经理:remote `fetchUserList`、`regionManagerUserId`、`managerUserOptions`、`managerLoading`) +3. 采集地区 +4. 采集医院 … +其余不变。 + +- [ ] **Step 2: 状态** + +```typescript +const regionManagerUserId = ref(null) +const managerUserOptions = ref([]) +const managerLoading = ref(false) +const linkedRegionMode = ref(false) // true=专员关联地区限制下拉 +``` + +`formData` 增加 `regionManagerUserId: ''`;reset / 新建默认 / 编辑 hydrate 同步处理。 + +- [ ] **Step 3: 核心联动函数** + +参考预签单 `applyDefaultRegionManager`,并新增: + +```typescript +async function applyRegionsForSalesUser(userId: string | null, opts?: { preferKeepRegion?: boolean }) { + const uid = (userId || '').trim() + if (!uid) { + linkedRegionMode.value = false + formData.regionId = '' + formData.hospitalId = '' + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + regionOptions.value = [] + hospitalOptions.value = [] + return + } + const res: any = await fetchRegionsByStaffUser(uid) + const list = (res?.list || []) as Array<{ id: string; regionName: string }> + if (list.length > 0) { + linkedRegionMode.value = true + regionOptions.value = list as any + const keep = + opts?.preferKeepRegion && + formData.regionId && + list.some((x) => x.id === formData.regionId) + const nextId = keep ? formData.regionId : list[0].id + const regionChanged = nextId !== formData.regionId + formData.regionId = nextId + if (regionChanged) { + formData.hospitalId = '' + hospitalOptions.value = [] + await loadHospitalsByRegion(nextId, '') + await applyDefaultRegionManager(nextId) + } + } else { + linkedRegionMode.value = false + formData.regionId = '' + formData.hospitalId = '' + hospitalOptions.value = [] + regionOptions.value = [] + regionManagerUserId.value = null + formData.regionManagerUserId = '' + managerUserOptions.value = [] + } +} +``` + +`onSalesUserChange`:写完 `salesUserId` 后 `await applyRegionsForSalesUser(val)`。 + +`onRegionChange`:现有清医院逻辑后追加 `await applyDefaultRegionManager(val)`。 + +新建默认当前用户后:`await applyRegionsForSalesUser(uid, { preferKeepRegion: false })`。 + +编辑 hydrate:先填库内 `regionId`/`salesUserId`/`regionManagerUserId` 与 stub;**不要**在初次 hydrate 时强制覆盖经理。可设 `linkedRegionMode`:若专员有关联地区则加载 linked 列表并 preferKeepRegion。 + +- [ ] **Step 4: 地区下拉行为** + +- `linkedRegionMode === true`:去掉 remote,或 remote 仅本地 filter `regionOptions`;placeholder「请选择关联地区」。 +- `linkedRegionMode === false`:保持现有 `remoteSearchRegions`。 + +- [ ] **Step 5: 保存 payload** + +`collectionInfo` 增加 `regionManagerUserId: formData.regionManagerUserId || undefined`。 + +--- + +### Task 5: `DonationFormDialog.vue` 同步 + +**Files:** +- Modify: `web-admin/src/views/research-donation/list/DonationFormDialog.vue` + +- [ ] **Step 1–5:** 与 Task 4 **相同行为与字段名**(复制联动逻辑,保持两套弹窗一致;勿抽大 refactor,除非改动极小可复制函数)。 + +校验点:新建默认专员、多地区默认第一、改地区带经理、保存含 `regionManagerUserId`、编辑回显。 + +--- + +### Task 6: 查看弹窗 / 列表 / 审核卡片文案与展示 + +**Files:** +- Modify: `ProtocolViewDialog.vue`、`DonationViewDialog.vue` +- Modify: `protocol/index.vue`、`research-donation/list/index.vue` +- Modify: `DonationProtocolTab.vue`;检查 `ProtocolAuditTab.vue` 是否展示销售人员(若无销售人员字段则仅在有区域信息处补地区经理,或跳过) + +- [ ] **Step 1:** 所有「销售人员」label →「市场专员」。 + +- [ ] **Step 2:** 查看弹窗在市场专员旁增加「地区经理」展示(`regionManagerUserName`,无则 —)。 + +- [ ] **Step 3:** 列表列 `label="销售人员"` → `市场专员`;不加地区经理列。 + +- [ ] **Step 4:** `DonationProtocolTab`:销售人员→市场专员;增加地区经理 kv(`row.regionManagerUserName`)。确认列表 API 已回填该字段(Task 3)。 + +- [ ] **Step 5:** 细胞存储审核卡片若当前无销售人员行,按需在采集信息块增加市场专员/地区经理(与捐赠卡片对齐);若产品仅捐赠卡片有销售人员,至少捐赠侧完整。 + +--- + +### Task 7: 验证 + +- [ ] **Step 1: 后端测试** + +```bash +cd backend && source venv/bin/activate +pytest tests/test_collection_regions_by_staff_user.py tests/test_collection_region_staff_helpers.py -v +``` + +Expected: PASS。 + +- [ ] **Step 2: 字段一致性(若脚本适用)** + +```bash +python scripts/check_field_consistency.py +``` + +若脚本不覆盖这两张表,则人工确认 Create/Update/`_row_to_dict`/前端 form 含 `regionManagerUserId`。 + +- [ ] **Step 3: 手工冒烟** + +1. 采集地区模块:用户 A 绑定地区 R1、R2;R1 设负责人 L1。 +2. 细胞存储新建协议:选市场专员 A → 地区默认 R1,下拉仅 R1/R2;地区经理为 L1;可改选。 +3. 改地区到 R2 → 经理变为 R2 负责人。 +4. 保存再编辑:三项均回显。 +5. 科研捐赠重复 2–4。 +6. 专员无绑定时:地区可全量检索,经理随地区带出。 + +- [ ] **Step 4: 图谱增量(改完后)** + +```bash +./scripts/update-graph.sh all +``` + +--- + +## Spec coverage checklist + +| Spec 要求 | Task | +|-----------|------| +| 文案改名市场专员 | 4, 5, 6 | +| 按专员带地区、多地区默认第一可改 | 2, 4, 5 | +| 专员可检索全部用户 | 4, 5(保留 remote) | +| 地区经理默认可改选并落库 | 1, 3, 4, 5 | +| 细胞存储 + 科研捐赠一致 | 4, 5, 6 | +| by-staff-user API | 2 | +| 列表不加经理列、弹窗/卡片展示 | 6 | + +## Self-review notes + +- 无 TBD;路径与字段名 `regionManagerUserId` / `region_manager_user_id` 全文一致。 +- 路由静态段 `by-staff-user` 不与 `{region_id}` 冲突。 +- 编辑 hydrate 不得用联动覆盖已存经理(`preferKeepRegion` + 跳过初次 applyDefault)。 diff --git a/docs/superpowers/plans/2026-10-09-payment-info-excel-list-and-chase.md b/docs/superpowers/plans/2026-10-09-payment-info-excel-list-and-chase.md new file mode 100644 index 00000000..5d0655b2 --- /dev/null +++ b/docs/superpowers/plans/2026-10-09-payment-info-excel-list-and-chase.md @@ -0,0 +1,868 @@ +# 缴费信息 Excel 单列表 + 分改趸 + 催缴信息 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将【缴费信息】改为 Excel 式单列表(基础信息 + 固定 1~6 期),支持分改趸 Tag 与协议单内 `fq→fgd` 并期,并新增【催缴信息】菜单(逾期 ∪ 未来 30 天待催期次)。 + +**Architecture:** 后端用纯函数计算「期摘要」与「分改趸并期」结果,CRUD/API 负责持久化与列表聚合;缴费信息前端去掉 4 Tab,宽表消费增强后的 `GET /sample-payments`;催缴独立 `GET /payment-chase`;写确认仍走现有 `payment-transfers`。协议单 `SamplePaymentPanel` 增加 `fgd`,保存时检测模式切换触发并期。 + +**Tech Stack:** MySQL 5.7 · FastAPI · SQLAlchemy · Vue 3 · Element Plus · pnpm · pytest/unittest + +**Design spec:** `docs/superpowers/specs/2026-10-09-payment-info-excel-list-and-chase-design.md` +**UI Demo:** `docs/demos/2026-10-08-payment-info-excel-list-demo.html` + +--- + +## File Map + +| 文件 | 职责 | +|------|------| +| `backend/app/services/sample_payment_period_summary.py` | **新建** 单期摘要(实缴展示合计、摘要日期/方式/审核码) | +| `backend/tests/test_sample_payment_period_summary.py` | **新建** 期摘要单测 | +| `backend/app/services/sample_payment_fgd_convert.py` | **新建** `fq→fgd` 并期纯函数(输入 schedules+transfers 字典 → 输出变更指令) | +| `backend/tests/test_sample_payment_fgd_convert.py` | **新建** 并期单测 | +| `backend/app/crud/protocol_sample_payment.py` | 允许 `fgd`;保存时调用并期;列表补期摘要与协议/样本字段;催缴查询 | +| `backend/app/api/protocol_sample_payment.py` | `GET /payment-chase`;保存路径透传 `fgd` | +| `backend/tests/test_payment_chase_query.py` | **新建** 催缴纳入规则纯函数/查询辅助单测 | +| `web-admin/src/utils/status-tag-registry.ts` | `payMode.fgd`;催缴状态 Tag(可选) | +| `web-admin/src/api/protocol-sample-payment.ts` | 列表类型扩展;`fetchPaymentChaseList` | +| `web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue` | 分改趸选项 + 并期确认提示 | +| `web-admin/src/utils/protocol-payment-presets.ts` / `protocol-sample-payment.ts` | `PayMode` 含 `fgd`;分期类判断不含误伤 | +| `web-admin/src/views/cell-storage/payment-info/index.vue` | 去 Tab,挂载宽表 | +| `web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue` | **新建** Excel 宽表 + 期弹窗 | +| `web-admin/src/views/cell-storage/payment-chase/index.vue` | **新建** 催缴列表页 | +| `web-admin/src/router/modules/sqxp.ts` | 注册 `payment-chase`,紧挨缴费信息之后 | +| `backend/app/api/menu.py` | 静态菜单树插入催缴信息 | +| `docs/sql/migrations/2026-10-09-payment-chase-menu.sql` | **新建** DB 菜单 + 角色授权 | +| (可选保留)`PaymentBySampleTab.vue` / `PaymentInfoTab.vue` | 本页不再引用;勿删文件除非确认无其它入口 | + +--- + +## 依赖顺序 + +```text +Task 1 期摘要纯函数 + → Task 2 分改趸并期纯函数 + → Task 3 CRUD 允许 fgd + 保存并期落地 + → Task 4 sample-payments 列表宽表字段 + → Task 5 payment-chase API + → Task 6 前端 Tag + API + → Task 7 SamplePaymentPanel 分改趸 + → Task 8 缴费信息 Excel 宽表页 + → Task 9 催缴信息页 + 路由/菜单 + → Task 10 菜单 SQL + 图谱 + 冒烟 +``` + +--- + +### Task 1: 期摘要纯函数 + +**Files:** +- Create: `backend/app/services/sample_payment_period_summary.py` +- Create: `backend/tests/test_sample_payment_period_summary.py` + +- [ ] **Step 1: Write the failing tests** + +```python +"""单期展示摘要:实缴合计、日期、方式、财务审核码""" + +from decimal import Decimal +import unittest + +from app.services.sample_payment_period_summary import summarize_period + + +class TestPeriodSummary(unittest.TestCase): + def test_empty_transfers(self): + r = summarize_period( + due_amount=Decimal("1000"), + transfers=[], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("0")) + self.assertEqual(r["payDate"], "") + self.assertEqual(r["payTypeSummary"], "") + self.assertEqual(r["financeAudit"], "dfj") + + def test_multi_transfer_summary(self): + r = summarize_period( + due_amount=Decimal("5000"), + transfers=[ + { + "transferAmount": Decimal("3000"), + "transferType": "wxzz", + "transferTime": "2026-04-19 10:11:00", + "payStatus": "yjf", + }, + { + "transferAmount": Decimal("2000"), + "transferType": "yhkzz", + "transferTime": "2026-04-20 09:40:00", + "payStatus": "yjf", + }, + ], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("5000")) + self.assertEqual(r["payDate"], "2026-04-20") + self.assertEqual(r["payTypeSummary"], "多笔") + self.assertEqual(r["financeAudit"], "yjf") + + def test_confirming_wins(self): + r = summarize_period( + due_amount=Decimal("4960"), + transfers=[ + { + "transferAmount": Decimal("2000"), + "transferType": "pos", + "transferTime": "2026-06-18 15:20:00", + "payStatus": "jfqrz", + } + ], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("2000")) + self.assertEqual(r["financeAudit"], "jfqrz") +``` + +- [ ] **Step 2: Run to verify fail** + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_sample_payment_period_summary.py -v +``` + +Expected: FAIL import error + +- [ ] **Step 3: Implement** + +```python +# backend/app/services/sample_payment_period_summary.py +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Dict, Iterable, List, Mapping + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def _status(row: Mapping[str, Any], *keys: str) -> str: + for k in keys: + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +def summarize_period( + *, + due_amount: Any, + transfers: Iterable[Mapping[str, Any]], +) -> Dict[str, Any]: + """ + displayPaidAmount: yjf + jfqrz 合计(列表「实缴」展示) + financeAudit: jfqrz > (paid>=due 且无确认中 → yjf) > (0 dfj + payDate: 最近一笔 transferTime 的日期部分 + payTypeSummary: 0 笔空;1 笔用该笔 type;多笔「多笔」 + """ + trs: List[Mapping[str, Any]] = list(transfers or []) + display_paid = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in trs + if _status(t, "payStatus", "pay_status") in ("yjf", "jfqrz") + ), + Decimal("0"), + ) + due = _dec(due_amount) + has_jfqrz = any(_status(t, "payStatus", "pay_status") == "jfqrz" for t in trs) + confirmed = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in trs + if _status(t, "payStatus", "pay_status") == "yjf" + ), + Decimal("0"), + ) + if has_jfqrz: + audit = "jfqrz" + elif due > 0 and confirmed >= due: + audit = "yjf" + elif confirmed > 0: + audit = "partial" + else: + audit = "dfj" + + def _time_key(t: Mapping[str, Any]) -> str: + return str(t.get("transferTime") or t.get("transfer_time") or "") + + latest = max(trs, key=_time_key) if trs else None + pay_date = "" + if latest: + raw = _time_key(latest) + pay_date = raw[:10] if len(raw) >= 10 else raw + + types = [ + _status(t, "transferType", "transfer_type") + for t in trs + if _status(t, "transferType", "transfer_type") + ] + if len(types) == 0: + pay_type = "" + elif len(set(types)) == 1 and len(types) == 1: + pay_type = types[0] + else: + pay_type = "多笔" + + return { + "displayPaidAmount": display_paid, + "payDate": pay_date, + "payTypeSummary": pay_type, + "financeAudit": audit, + } +``` + +- [ ] **Step 4: Run tests PASS** + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_sample_payment_period_summary.py -v +``` + +- [ ] **Step 5: Commit**(仅当用户要求提交时执行) + +```bash +git add backend/app/services/sample_payment_period_summary.py backend/tests/test_sample_payment_period_summary.py +git commit -m "$(cat <<'EOF' +feat: add sample payment period summary helper for excel list cells + +EOF +)" +``` + +--- + +### Task 2: 分改趸并期纯函数 + +**Files:** +- Create: `backend/app/services/sample_payment_fgd_convert.py` +- Create: `backend/tests/test_sample_payment_fgd_convert.py` + +- [ ] **Step 1: Write the failing tests** + +```python +"""fq → fgd:软删后续期,剩余欠缴并入第一期未清期""" + +from decimal import Decimal +import unittest + +from app.services.sample_payment_fgd_convert import plan_fq_to_fgd_merge + + +class TestFgdConvert(unittest.TestCase): + def test_merge_into_period_3(self): + schedules = [ + {"id": "s1", "scheduleNo": 1, "dueAmount": Decimal("5000"), "scheduleStatus": "cleared"}, + {"id": "s2", "scheduleNo": 2, "dueAmount": Decimal("5000"), "scheduleStatus": "cleared"}, + {"id": "s3", "scheduleNo": 3, "dueAmount": Decimal("5000"), "scheduleStatus": "open"}, + {"id": "s4", "scheduleNo": 4, "dueAmount": Decimal("5000"), "scheduleStatus": "open"}, + {"id": "s5", "scheduleNo": 5, "dueAmount": Decimal("5000"), "scheduleStatus": "open"}, + {"id": "s6", "scheduleNo": 6, "dueAmount": Decimal("4800"), "scheduleStatus": "open"}, + ] + transfers = [ + {"scheduleId": "s1", "payStatus": "yjf", "transferAmount": Decimal("5000")}, + {"scheduleId": "s2", "payStatus": "yjf", "transferAmount": Decimal("5000")}, + ] + plan = plan_fq_to_fgd_merge(schedules=schedules, transfers=transfers) + self.assertEqual(plan["targetScheduleId"], "s3") + self.assertEqual(plan["newDueAmount"], Decimal("19800")) # 5000+5000+5000+4800 + self.assertEqual(set(plan["softDeleteScheduleIds"]), {"s4", "s5", "s6"}) + self.assertEqual(plan["newInstallmentCount"], 3) + + def test_reject_when_jfqrz(self): + schedules = [ + {"id": "s1", "scheduleNo": 1, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + {"id": "s2", "scheduleNo": 2, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + ] + transfers = [ + {"scheduleId": "s1", "payStatus": "jfqrz", "transferAmount": Decimal("500")}, + ] + with self.assertRaises(ValueError) as ctx: + plan_fq_to_fgd_merge(schedules=schedules, transfers=transfers) + self.assertIn("确认中", str(ctx.exception)) + + def test_reject_when_later_period_has_yjf(self): + schedules = [ + {"id": "s1", "scheduleNo": 1, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + {"id": "s2", "scheduleNo": 2, "dueAmount": Decimal("1000"), "scheduleStatus": "open"}, + ] + transfers = [ + {"scheduleId": "s2", "payStatus": "yjf", "transferAmount": Decimal("1000")}, + ] + with self.assertRaises(ValueError) as ctx: + plan_fq_to_fgd_merge(schedules=schedules, transfers=transfers) + self.assertIn("后续期", str(ctx.exception)) +``` + +说明:`newDueAmount` = 目标期已确认实缴 + 所有未清期欠缴之和。上例目标期无实缴,欠缴合计 5000+5000+5000+4800=19800。 + +- [ ] **Step 2: Run FAIL** + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_sample_payment_fgd_convert.py -v +``` + +- [ ] **Step 3: Implement** + +```python +# backend/app/services/sample_payment_fgd_convert.py +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Dict, Iterable, List, Mapping, Set + +from app.services.protocol_transfer_status import schedule_paid_and_arrears + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def _status(row: Mapping[str, Any], *keys: str) -> str: + for k in keys: + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +def plan_fq_to_fgd_merge( + *, + schedules: Iterable[Mapping[str, Any]], + transfers: Iterable[Mapping[str, Any]], +) -> Dict[str, Any]: + sch_list = [ + s + for s in schedules + if _status(s, "scheduleStatus", "schedule_status") != "void" + and not s.get("_deleted") + ] + sch_list.sort(key=lambda s: int(s.get("scheduleNo") or s.get("schedule_no") or 0)) + tr_list = list(transfers or []) + + if any(_status(t, "payStatus", "pay_status") == "jfqrz" for t in tr_list): + raise ValueError("存在缴费确认中的转账,请先完成财务确认") + + by_sch: Dict[str, List[Mapping[str, Any]]] = {} + for t in tr_list: + sid = str(t.get("scheduleId") or t.get("schedule_id") or "") + by_sch.setdefault(sid, []).append(t) + + def _arrears(s: Mapping[str, Any]) -> Decimal: + sid = str(s.get("id") or "") + confirmed = [ + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in by_sch.get(sid, []) + if _status(t, "payStatus", "pay_status") == "yjf" + ] + _paid, arr = schedule_paid_and_arrears( + due=s.get("dueAmount", s.get("due_amount")), + confirmed_transfers=confirmed, + ) + return _dec(arr) + + target = None + for s in sch_list: + if _arrears(s) > 0 or _status(s, "scheduleStatus", "schedule_status") != "cleared": + if _arrears(s) > 0: + target = s + break + if _status(s, "scheduleStatus", "schedule_status") != "cleared": + target = s + break + if target is None: + raise ValueError("没有可合并的未清期次") + + target_no = int(target.get("scheduleNo") or target.get("schedule_no") or 0) + later = [ + s + for s in sch_list + if int(s.get("scheduleNo") or s.get("schedule_no") or 0) > target_no + ] + for s in later: + sid = str(s.get("id") or "") + if any( + _status(t, "payStatus", "pay_status") == "yjf" for t in by_sch.get(sid, []) + ): + raise ValueError("后续期已有已确认转账,无法合并为分改趸") + + remain = sum((_arrears(s) for s in sch_list), Decimal("0")) + tid = str(target.get("id") or "") + target_confirmed = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in by_sch.get(tid, []) + if _status(t, "payStatus", "pay_status") == "yjf" + ), + Decimal("0"), + ) + new_due = target_confirmed + remain + soft_ids = [str(s.get("id") or "") for s in later] + kept = [s for s in sch_list if str(s.get("id") or "") not in set(soft_ids)] + + return { + "targetScheduleId": tid, + "newDueAmount": new_due, + "softDeleteScheduleIds": soft_ids, + "newInstallmentCount": len(kept), + } +``` + +若 `test_merge_into_period_3` 因「cleared 且欠缴=0 跳过」逻辑与 `_arrears` 口径不一致,以测试期望为准微调:目标期必须是第一期 `arrears>0` 的期。 + +- [ ] **Step 4: Run PASS** + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_sample_payment_fgd_convert.py -v +``` + +- [ ] **Step 5: Commit**(用户要求时) + +--- + +### Task 3: CRUD 允许 fgd + 并期落地 + +**Files:** +- Modify: `backend/app/crud/protocol_sample_payment.py`(`mode not in (...)` 加入 `fgd`;在 `_save_sample_payments` / `upsert` 路径于 `pay_mode` 从 `fq` 变为 `fgd` 时调用并期并写库) +- Modify: `backend/app/api/protocol_sample_payment.py`(Pydantic `pay_mode` 校验若有枚举则含 `fgd`) + +- [ ] **Step 1: 放宽枚举** + +将: + +```python +if mode not in ("dj", "fq", "djfq"): +``` + +改为: + +```python +if mode not in ("dj", "fq", "djfq", "fgd"): +``` + +API Body / Schema 中同类校验一并改。 + +- [ ] **Step 2: 增加应用并期函数** + +在 `protocol_sample_payment.py` 增加(示意): + +```python +def apply_fq_to_fgd_merge( + db: Session, + *, + sample_payment_id: str, + tenant_no: str, + user_id: Optional[str], +) -> None: + assert_sample_payment_editable(db, sample_payment_id=sample_payment_id, tenant_no=tenant_no) + sp = _get_sample_payment_or_raise(db, sample_payment_id=sample_payment_id, tenant_no=tenant_no) + schedules = list_active_schedules(...) # 现有查询未软删未 void + transfers = list_transfers_for_schedules(...) + plan = plan_fq_to_fgd_merge( + schedules=[_schedule_to_dict(...) for ...], # 或直接用 ORM 字段拼 dict + transfers=[...], + ) + # 更新 target.due_amount = plan["newDueAmount"] + # 软删 softDeleteScheduleIds 及其 dfj 转账 + # sp.pay_mode = "fgd"; sp.installment_count = plan["newInstallmentCount"] + db.flush() +``` + +在保存样本方案循环中:若库中原 `pay_mode=="fq"` 且请求 `pay_mode=="fgd"`,先 `apply_fq_to_fgd_merge`,再保存其余字段;**不要**对已缴期调用盲目 `replace_schedules` 全量重建。 + +- [ ] **Step 3: 手工或集成测** + +```bash +cd backend && source venv/bin/activate && python -m pytest tests/test_sample_payment_fgd_convert.py tests/test_sample_payment_period_summary.py -v +``` + +若已有保存相关测试,补一条「mock DB 调用 apply」或轻量集成测。 + +- [ ] **Step 4: Commit**(用户要求时) + +--- + +### Task 4: `GET /sample-payments` 宽表字段 + +**Files:** +- Modify: `backend/app/crud/protocol_sample_payment.py` → `list_sample_payments_page` 行字典 +- Modify: `backend/app/api/protocol_sample_payment.py`(若需) + +- [ ] **Step 1: 扩展列表行字段** + +每行在现有摘要上增加: + +```python +{ + # 已有: id, protocolNo, motherName, motherPhone, sampleCode, payMode, + # contractAmount, paidAmount, dueRemainAmount, progressStatus, canConfirm, ... + "signDate": "...", # 协议签署日 + "storageYears": "...", # 样本存储年限 + "sampleTypeName": "...", # 存储项目 + "discountAmount": None, # 无字段则 null + "receivableAmount": float(...), # 有效期 due 合计 + "remark": "...", + "periods": [ + # 长度固定 6;无期用 null + { + "scheduleId": "s3", + "scheduleNo": 3, + "dueAmount": 5000.0, + "displayPaidAmount": 2000.0, + "payDate": "2026-06-18", + "payTypeSummary": "pos", # 或字典 label 由前端转 + "financeAudit": "jfqrz", + }, + None, + ... + ], +} +``` + +实现要点: + +- 批量加载 schedules + transfers,用 `summarize_period` 填 `periods[scheduleNo-1]`(`scheduleNo` 1..6)。 +- 批量 JOIN/查询协议签署日、样本 `storage_years`、样本类型名、当事人手机(避免 N+1)。 +- `payMode=fgd` 可筛选。 + +- [ ] **Step 2: 详情接口**保持完整 `schedules[].transfers`,供弹窗使用(已有则只补缺失展示字段)。 + +- [ ] **Step 3: 跑相关单测 / 手工打一条 list 响应检查 `periods` 长度恒为 6** + +- [ ] **Step 4: Commit**(用户要求时) + +--- + +### Task 5: 催缴 API + +**Files:** +- Create: `backend/app/services/payment_chase.py`(纳入规则纯函数,便于测) +- Create: `backend/tests/test_payment_chase.py` +- Modify: `backend/app/crud/protocol_sample_payment.py` → `list_payment_chase_page` +- Modify: `backend/app/api/protocol_sample_payment.py` → `GET /payment-chase` + +- [ ] **Step 1: 纯函数单测** + +```python +from datetime import date +import unittest + +from app.services.payment_chase import is_chase_candidate + + +class TestChase(unittest.TestCase): + def test_overdue(self): + self.assertTrue( + is_chase_candidate( + due_date=date(2026, 9, 1), + today=date(2026, 10, 9), + arrears=Decimal("100"), + ) + ) + + def test_within_30(self): + self.assertTrue( + is_chase_candidate( + due_date=date(2026, 11, 1), + today=date(2026, 10, 9), + arrears=Decimal("100"), + ) + ) + + def test_beyond_30(self): + self.assertFalse( + is_chase_candidate( + due_date=date(2026, 11, 10), + today=date(2026, 10, 9), + arrears=Decimal("100"), + ) + ) + + def test_zero_arrears(self): + self.assertFalse( + is_chase_candidate( + due_date=date(2026, 9, 1), + today=date(2026, 10, 9), + arrears=Decimal("0"), + ) + ) +``` + +(`from decimal import Decimal` 写入测试文件顶部。) + +- [ ] **Step 2: 实现 `is_chase_candidate`** + +```python +# backend/app/services/payment_chase.py +from datetime import date, timedelta +from decimal import Decimal +from typing import Any + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def is_chase_candidate(*, due_date: date, today: date, arrears: Any) -> bool: + if _dec(arrears) <= 0: + return False + if due_date < today: + return True + return today <= due_date <= today + timedelta(days=30) +``` + +- [ ] **Step 3: CRUD 分页** + +`list_payment_chase_page(db, tenant_no, user, page, pageSize, keyword, chaseType, payMode, salesUserId)`: + +- 主查有效 schedule(未软删、非 void)JOIN sample_payment + protocol + party + collection_info(市场专员)。 +- SQL 侧尽量用 `due_date < today OR (due_date BETWEEN today AND today+30)`,欠缴在内存或子查询用已确认转账计算(与现 schedule 欠缴口径一致)。 +- `chaseType`: `overdue` | `upcoming` | 空=全部。 +- 排序:逾期天数 DESC,`due_date` ASC,`protocol_no`。 +- `apply_data_scope` 按协议。 + +行字段:`scheduleId`, `samplePaymentId`, `protocolId`, `protocolNo`, `motherName`, `motherPhone`, `sampleCode`, `salesUserName`, `payMode`, `scheduleNo`, `dueDate`, `dueAmount`, `paidAmount`, `arrearsAmount`, `chaseStatus`(`overdue`|`upcoming`), `daysOverdue` / `daysUntilDue`, `protocolStatus`。 + +- [ ] **Step 4: 路由**(注意放在 `/sample-payments/{id}` 之前或使用静态路径 `/payment-chase` 避免冲突) + +```python +@router.get("/payment-chase") +async def list_payment_chase(...): + ... +``` + +- [ ] **Step 5: pytest PASS + Commit**(用户要求时) + +--- + +### Task 6: 前端 Tag + API + +**Files:** +- Modify: `web-admin/src/utils/status-tag-registry.ts` +- Modify: `web-admin/src/api/protocol-sample-payment.ts` +- Modify: `web-admin/src/utils/protocol-payment-presets.ts`(`PayMode` 含 `fgd`;`isInstallmentPayMode`:**不要**把 `fgd` 当普通分期 UI,除非产品要同一套期数表单项——建单 `fgd` 需要期数配置时单独 `isMultiSchedulePayMode = fq|djfq|fgd`) + +- [ ] **Step 1: Tag** + +```ts +export const payMode: StatusRegistry = { + dj: { label: '趸交', tagType: 'primary' }, + fq: { label: '分期', tagType: 'warning' }, + djfq: { label: '趸交+分期', tagType: 'warning' }, + fgd: { label: '分改趸', tagType: 'warning' } +} + +export const paymentChaseStatus: StatusRegistry = { + overdue: { label: '已逾期', tagType: 'danger' }, + upcoming: { label: '即将到期', tagType: 'warning' } +} +``` + +- [ ] **Step 2: API** + +```ts +export function fetchPaymentChaseList(params?: { + page?: number + pageSize?: number + keyword?: string + chaseType?: string + payMode?: string + salesUserId?: string +}) { + return request.get>>({ + url: '/web/admin/payment-chase', + params + }) +} +``` + +- [ ] **Step 3: Commit**(用户要求时) + +--- + +### Task 7: SamplePaymentPanel 分改趸 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue` +- 同步只读展示处:`payMode === 'fq' ? '分期' : '趸交'` 等硬编码改为 `payModeLabel` + +- [ ] **Step 1: 单选增加分改趸** + +```vue + + 趸交 + 分期 + 分改趸 + +``` + +(若双样本仍有 `djfq`,保持现有入口,不删除。) + +- [ ] **Step 2: `fq` → `fgd` 确认** + +在 `onPayModeChange` 中:若原值 `fq` 且新值 `fgd`,`ElMessageBox.confirm('改为分改趸将合并后续未缴期至当前未清期,是否继续?')`,取消则回滚。 + +- [ ] **Step 3: 表单字段** + +`fgd` 显示期数/应缴配置(与分期相同的多期表单项),趸交仍单期。 + +- [ ] **Step 4: 浏览器冒烟** — 编辑协议改分改趸保存成功。 + +- [ ] **Step 5: Commit**(用户要求时) + +--- + +### Task 8: 缴费信息 Excel 宽表页 + +**Files:** +- Create: `web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue` +- Modify: `web-admin/src/views/cell-storage/payment-info/index.vue` +- 对照 Demo:`docs/demos/2026-10-08-payment-info-excel-list-demo.html` +- 复用:`PaymentReceiptConfirmDialog`、`ProtocolViewDialog`、`fetchSamplePaymentList` / `fetchSamplePaymentDetail` + +- [ ] **Step 1: 重写 `index.vue`** + +去掉 4 个 Pill Tab 与 `PaymentInfoTab` / `PaymentBySampleTab` 引用;仅渲染: + +```vue + +``` + +(若 `App.vue` `panelTitleBarTextRules` 需登记「缴费信息」则补上。) + +- [ ] **Step 2: 实现 `PaymentExcelList.vue`** + +- 筛选区对齐现按样本筛选项 + `fgd`。 +- `el-table` 或原生表:双行表头(基础 / 第1~6期 / 汇总);期列背景色参考 Demo CSS 变量。 +- `periods[i].displayPaidAmount` 用链接按钮;点击 → `fetchSamplePaymentDetail` → 取对应 `schedule.transfers` 打开弹窗(可内嵌简单转账列表 + 底部缴费确认)。 +- 左侧冻结列:用 `el-table` `fixed` 或容器 `overflow:auto` + sticky(与 Demo 一致即可)。 +- `?tab=` 旧参数:忽略或映射 `progressStatus` 筛选(spec §7)。 + +- [ ] **Step 3: 样式** — scoped SCSS,色值用 Fresh Mint + 期次浅色底,勿用小程序绿。 + +- [ ] **Step 4: 本地打开 `/#/market/cell-storage/payment-info` 冒烟**(横向滚动、点实缴、确认弹窗)。 + +- [ ] **Step 5: Commit**(用户要求时) + +--- + +### Task 9: 催缴信息页 + 路由 + +**Files:** +- Create: `web-admin/src/views/cell-storage/payment-chase/index.vue` +- Modify: `web-admin/src/router/modules/sqxp.ts` +- Modify: `backend/app/api/menu.py` + +- [ ] **Step 1: 路由**(紧接 `payment-info` 之后) + +```ts +{ + name: 'CellStoragePaymentChase', + path: 'payment-chase', + component: '/cell-storage/payment-chase/index', + meta: { + title: '催缴信息', + keepAlive: true, + menuCode: 'cellStorage:paymentChase:list' + } +} +``` + +- [ ] **Step 2: `menu.py` 静态树**在缴费信息节点后插入同结构项(`path: payment-chase`,`menuCode: cellStorage:paymentChase:list`,新 id 如 `50000000000000000000000000000022`)。 + +- [ ] **Step 3: 列表页**对齐 `unit/order`:筛选 + `list-table` + 分页;列见 spec §4;催缴状态用 `paymentChaseStatus` Tag;操作「协议单」打开 `ProtocolViewDialog`;可选「缴费信息」`router.push({ name: 'CellStoragePaymentInfo', query: { samplePaymentId } })`。 + +- [ ] **Step 4: 冒烟** `/#/market/cell-storage/payment-chase` + +- [ ] **Step 5: Commit**(用户要求时) + +--- + +### Task 10: 菜单 SQL + 图谱 + 回归 + +**Files:** +- Create: `docs/sql/migrations/2026-10-09-payment-chase-menu.sql` +- Run: `./scripts/update-graph.sh all`(或 backend + frontend) + +- [ ] **Step 1: SQL**(幂等 INSERT,parent 与缴费信息相同;`order_num` 介于缴费信息与样本信息之间;授权给已有 `cellStorage:paymentInfo:list` 的角色) + +参考 `docs/sql/migrations/2026-10-05-presign-order-menu.sql` 写法,perms 使用 `cellStorage:paymentChase:list`(与前端 `menuCode` 一致)。 + +- [ ] **Step 2: 后端测试** + +```bash +cd backend && source venv/bin/activate && python -m pytest \ + tests/test_sample_payment_period_summary.py \ + tests/test_sample_payment_fgd_convert.py \ + tests/test_payment_chase.py \ + tests/test_sample_payment_progress.py -v +``` + +- [ ] **Step 3: 前端** + +```bash +cd web-admin && pnpm lint +``` + +(有时间再 `pnpm build:typecheck`。) + +- [ ] **Step 4: 更新图谱** + +```bash +./scripts/update-graph.sh all +``` + +- [ ] **Step 5: 对照 spec 勾选验收** — Excel 宽表、分改趸建单/并期、催缴纳入规则、一期多笔弹窗。 + +--- + +## Spec 覆盖自检 + +| Spec 要求 | Task | +|-----------|------| +| 缴费信息去 4 Tab → Excel 6 期 | 8 | +| 点实缴看转账/审核 | 8(依赖 4 详情) | +| `fgd` Tag | 3, 6, 7 | +| 建单可选分改趸 | 7 | +| `fq→fgd` 并期软删后续期 | 2, 3 | +| 并期仅协议单 | 7(缴费页不提供) | +| 催缴菜单 + 逾期∪30天 | 5, 9, 10 | +| 催缴一行一期 | 5, 9 | +| data scope | 4, 5 | +| 保留 `djfq` | 3, 7 | +| Demo 对齐 | 8 | + +**占位符扫描:** 无 TBD;Commit 步骤标注「仅用户要求时」以符合仓库提交约定。 + +--- + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-10-09-payment-info-excel-list-and-chase.md`. + +**Two execution options:** + +1. **Subagent-Driven(推荐)** — 每个 Task 开新子代理,Task 间复查 +2. **Inline Execution** — 本会话按 `executing-plans` 连续做,设检查点 + +Which approach? diff --git a/docs/superpowers/plans/2026-10-09-payment-period-columns-and-schedule-edit.md b/docs/superpowers/plans/2026-10-09-payment-period-columns-and-schedule-edit.md new file mode 100644 index 00000000..828f66f4 --- /dev/null +++ b/docs/superpowers/plans/2026-10-09-payment-period-columns-and-schedule-edit.md @@ -0,0 +1,286 @@ +# 缴费期次列口径 + 应缴批次可编辑 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 缴费信息宽表每期展示应缴金额/应缴时间/已缴金额/已缴时间/欠缴金额;协议单按样本缴费支持单期改应缴金额与时间,且保存时各期合计必须等于合同金额。 + +**Architecture:** 期摘要逻辑集中在 `summarize_period`;列表 `_build_periods_array` 透出 `dueDate`/`arrearsAmount`;保存校验抽成纯函数供 CRUD 与前端共用思路;`SamplePaymentPanel` 单期编辑不触发 `rebuildSchedules`。 + +**Tech Stack:** FastAPI / SQLAlchemy / unittest;Vue 3 + Element Plus;现有 `PaymentExcelList` / `SamplePaymentPanel`。 + +**Spec:** `docs/superpowers/specs/2026-10-09-payment-period-columns-and-schedule-edit-design.md` + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| `backend/app/services/sample_payment_period_summary.py` | 已缴含 dfj;首笔 payDate;arrearsAmount | +| `backend/tests/test_sample_payment_period_summary.py` | 期摘要单测 | +| `backend/app/services/sample_payment_schedule_validate.py` | 新建:合同金额 vs 各期应缴合计校验 | +| `backend/tests/test_sample_payment_schedule_validate.py` | 校验单测 | +| `backend/app/crud/protocol_sample_payment.py` | periods 字段;save 时调校验 | +| `web-admin/.../PaymentExcelList.vue` | 宽表 5 子列 | +| `web-admin/.../SamplePaymentPanel.vue` | 期次可编辑 | +| `web-admin/src/utils/protocol-sample-payment.ts` | 前端合计校验 helper | +| `web-admin/.../ProtocolFormDialog.vue` | 提交前硬拦 | + +--- + +### Task 1: 期摘要口径(TDD) + +**Files:** +- Modify: `backend/app/services/sample_payment_period_summary.py` +- Modify: `backend/tests/test_sample_payment_period_summary.py` + +- [x] **Step 1: 扩展失败用例** + +在 `test_sample_payment_period_summary.py` 增加/改写: + +```python +def test_dfj_counts_as_display_paid(self): + r = summarize_period( + due_amount=Decimal("1000"), + transfers=[ + { + "transferAmount": Decimal("400"), + "transferType": "wxzz", + "transferTime": "2026-04-19 10:00:00", + "payStatus": "dfj", + } + ], + ) + self.assertEqual(r["displayPaidAmount"], Decimal("400")) + self.assertEqual(r["arrearsAmount"], Decimal("600")) + self.assertEqual(r["payDate"], "2026-04-19") + +def test_pay_date_is_first_transfer_not_latest(self): + r = summarize_period( + due_amount=Decimal("5000"), + transfers=[ + { + "transferAmount": Decimal("3000"), + "transferType": "wxzz", + "transferTime": "2026-04-19 10:11:00", + "payStatus": "yjf", + }, + { + "transferAmount": Decimal("2000"), + "transferType": "yhkzz", + "transferTime": "2026-04-20 09:40:00", + "payStatus": "yjf", + }, + ], + ) + self.assertEqual(r["payDate"], "2026-04-19") # 首笔,非 04-20 + self.assertEqual(r["arrearsAmount"], Decimal("0")) +``` + +并把原 `test_multi_transfer_summary` 的 `payDate` 期望从 `2026-04-20` 改为 `2026-04-19`;`test_empty_transfers` 断言 `arrearsAmount == Decimal("1000")`。 + +- [ ] **Step 2: 跑测确认失败** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_sample_payment_period_summary.py -v +``` + +Expected: FAIL(旧逻辑取最晚;缺 arrears;dfj 未计入) + +- [ ] **Step 3: 实现 `summarize_period`** + +- `displayPaidAmount`:`pay_status in ("dfj", "jfqrz", "yjf")` +- `payDate`:`trs[0]` 的时间日期部分(调用方已按 `create_time ASC` 传入) +- `arrearsAmount`:`max(due - display_paid, 0)` +- 保留 `payTypeSummary` / `financeAudit`(financeAudit 仍仅看 yjf/jfqrz 语义不变) + +- [ ] **Step 4: 跑测通过** + +```bash +pytest tests/test_sample_payment_period_summary.py -v +``` + +Expected: PASS + +--- + +### Task 2: periods 数组透出 dueDate / arrearsAmount + +**Files:** +- Modify: `backend/app/crud/protocol_sample_payment.py` → `_build_periods_array` + +- [ ] **Step 1: 在 periods 元素中增加字段** + +```python +periods.append( + { + "scheduleId": s.id, + "scheduleNo": no, + "dueAmount": float(s.due_amount) if s.due_amount is not None else None, + "dueDate": _fmt_ymd8(s.due_date), + "displayPaidAmount": float(summary["displayPaidAmount"]), + "arrearsAmount": float(summary["arrearsAmount"]), + "payDate": summary.get("payDate") or "", + "payTypeSummary": summary.get("payTypeSummary") or "", + "financeAudit": summary.get("financeAudit") or "dfj", + } +) +``` + +- [ ] **Step 2: 手动/现有列表接口冒烟**(有环境时);无环境则依赖 Task 1 单测 + 前端联调。 + +--- + +### Task 3: 保存校验纯函数(TDD) + +**Files:** +- Create: `backend/app/services/sample_payment_schedule_validate.py` +- Create: `backend/tests/test_sample_payment_schedule_validate.py` +- Modify: `backend/app/crud/protocol_sample_payment.py` → `save_sample_payments_for_protocol` + +- [ ] **Step 1: 写失败测试** + +```python +from decimal import Decimal +import unittest +from app.services.sample_payment_schedule_validate import ( + schedule_dues_match_contract, + schedule_dues_mismatch_message, +) + +class TestScheduleValidate(unittest.TestCase): + def test_match(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("1000"), + schedules=[{"dueAmount": 600}, {"dueAmount": 400}], + ) + self.assertTrue(ok) + self.assertIsNone(msg) + + def test_mismatch(self): + ok, msg = schedule_dues_match_contract( + contract_amount=Decimal("1000"), + schedules=[{"dueAmount": 600}, {"dueAmount": 300}], + ) + self.assertFalse(ok) + self.assertIn("900", msg or "") + self.assertIn("1000", msg or "") + + def test_skip_void(self): + ok, _ = schedule_dues_match_contract( + contract_amount=Decimal("500"), + schedules=[ + {"dueAmount": 500, "scheduleStatus": "open"}, + {"dueAmount": 200, "scheduleStatus": "void"}, + ], + ) + self.assertTrue(ok) +``` + +- [ ] **Step 2: 跑测 FAIL → 实现 → PASS** + +实现要点: + +```python +def schedule_dues_match_contract(*, contract_amount, schedules, tol=Decimal("0.015")): + # 排除 schedule_status == void;Σ dueAmount 与 contract 比较 + # 返回 (True, None) 或 (False, "样本合同金额 … 与各期应缴合计 … 不一致,请调整后再保存") +``` + +在 `save_sample_payments_for_protocol` 循环内、`sync_schedules_and_transfers` 之前(fgd merge 分支除外或 merge 后亦校验): + +```python +ok, msg = schedule_dues_match_contract( + contract_amount=raw.get("contract_amount", raw.get("contractAmount")), + schedules=raw.get("schedules") or [], +) +if not ok: + raise ValueError(msg) +``` + +fgd 并期后跳过前端 schedules 同步时:并期后库内数据由服务保证;可对 merge 后的 target 再读库校验,或并期路径单独算。**最小实现:** 仅对走 `sync_schedules_and_transfers` 的路径校验 payload;`fq→fgd` continue 前不校验 payload(并期改 due)。 + +--- + +### Task 4: 前端宽表列 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/payment-info/PaymentExcelList.vue` + +- [ ] **Step 1: 替换第 N 期子列模板** + +五列:应缴费金额(元)、应缴费时间、已缴费金额(可点)、已缴费时间、欠缴费金额。 + +绑定: + +- 应缴金额 → `periodDue` +- 应缴时间 → `periodField(..., 'dueDate')` 或专用 formatter +- 已缴费金额 → `periodPaid`(`displayPaidAmount`) +- 已缴费时间 → `periodField(..., 'payDate')` +- 欠缴 → `periodField` / `formatAmount(arrearsAmount)` + +- [ ] **Step 2: 扩展 `PeriodCell` 类型** 含 `dueDate?`、`arrearsAmount?`;移除对支付方式/财务审核列的依赖(弹窗保留)。 + +--- + +### Task 5: SamplePaymentPanel 单期可编辑 + +**Files:** +- Modify: `web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue` + +- [ ] **Step 1: 期头增加控件** + +在 `schedule-block__head` 旁或下方增加一行表单: + +- `AmountInput` → `sch.dueAmount`,`@update` 调 `patchSchedule(pIdx, sIdx, { dueAmount })`(**rebuild=false**) +- `el-date-picker` → `sch.dueDate`,`value-format="YYYYMMDD"` +- `:disabled="isScheduleDueLocked(sch)"`:`(sch.transfers||[]).some(t => t.payStatus==='yjf')` + +- [ ] **Step 2: `patchSchedule` 只改该期,不 `rebuildSchedules`** + +--- + +### Task 6: 前端保存硬拦 + +**Files:** +- Modify: `web-admin/src/utils/protocol-sample-payment.ts` +- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` + +- [ ] **Step 1: helper** + +```typescript +export function sampleScheduleDuesMismatchHint(pay: SamplePaymentLike): string | null { + const contract = Number(pay.contractAmount) + if (!Number.isFinite(contract)) return null + const schedules = (pay.schedules || []).filter( + (s) => String((s as any).scheduleStatus || 'open') !== 'void' + ) + const sum = schedules.reduce((s, x) => s + (Number(x.dueAmount) || 0), 0) + if (Math.abs(contract - sum) > 0.015) { + return `样本 ${pay.sampleCode || ''} 合同金额 ${contract.toFixed(2)} 与各期应缴合计 ${sum.toFixed(2)} 不一致,请调整后再保存` + } + return null +} +``` + +- [ ] **Step 2: `handleSubmit` v2 分支** 对每个 `samplePayments` 项调用;有 hint 则 `ElMessageBox.alert` / `ElMessage.warning` 并 `return`(硬拦)。 + +--- + +### Task 7: 验证与图谱 + +- [ ] `pytest tests/test_sample_payment_period_summary.py tests/test_sample_payment_schedule_validate.py -v` +- [ ] 前端相关文件无 lint 明显错误 +- [ ] `./scripts/update-graph.sh backend`(若改了 backend 模块结构) + +--- + +## Spec coverage + +| Spec | Task | +|------|------| +| §2 五列口径 | 1, 2, 4 | +| §2.2 summarize_period | 1 | +| §3.1 单期可编辑 / yjf 只读 / 不重建 | 5 | +| §3.3 前后端硬拦 | 3, 6 | +| §5 测试 | 1, 3, 7 | diff --git a/docs/superpowers/plans/2026-10-09-protocol-preferential-price-payment.md b/docs/superpowers/plans/2026-10-09-protocol-preferential-price-payment.md new file mode 100644 index 00000000..4af3343c --- /dev/null +++ b/docs/superpowers/plans/2026-10-09-protocol-preferential-price-payment.md @@ -0,0 +1,61 @@ +# 优惠价驱动缴费金额 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 废除采集协议单价目表默认金额;样本合同/合计/协议总默认均由优惠价驱动;分期金额手填且保存强校验。 + +**Architecture:** 纯函数在 `protocol-sample-payment.ts` 解析优惠价与校验;`SamplePaymentPanel` 生成空金额期次;`ProtocolFormDialog` 去掉 `resolvePaymentPreset` 套用;后端 schedule 校验补齐每期金额与应缴日必填。 + +**Tech Stack:** Vue 3 + TypeScript(web-admin)、FastAPI(backend)、node:test / pytest + +**Design spec:** `docs/superpowers/specs/2026-10-09-protocol-preferential-price-payment-design.md` + +--- + +## File map + +| File | Responsibility | +|------|----------------| +| `web-admin/src/utils/protocol-sample-payment.ts` | 优惠价解析、合计、分期必填校验文案;删除硬编码 preset | +| `web-admin/src/utils/protocol-sample-payment.test.ts` | 单测 | +| `web-admin/src/utils/protocol-payment-presets.ts` | 删除 PRESETS / resolvePaymentPreset | +| `web-admin/src/views/cell-storage/protocol/SamplePaymentPanel.vue` | 新建/改价/分期空金额 | +| `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` | 去掉价目套用;保存调新校验 | +| `backend/app/services/sample_payment_schedule_validate.py` | 各期金额+应缴日必填 | +| `backend/tests/test_sample_payment_schedule_validate.py` | 后端单测 | + +--- + +### Task 1: 工具函数与单测(去价目、分期必填) + +- [x] 改写 `resolveSampleContractAmount`:仅优惠价,无则 `null`(不再回落 22880) +- [x] 删除或掏空 `resolvePerSamplePreset` 硬编码 +- [x] 新增 `sampleScheduleRequiredFieldsHint(pay)`:分期每期金额与 dueDate +- [x] 更新 `protocol-sample-payment.test.ts`;`node --import tsx --test` 通过 + +### Task 2: SamplePaymentPanel + +- [x] `emptyDjPayment` / 改价同步:合同 = 优惠价 +- [x] 打开编辑:不强制用优惠价覆盖库内合同(仅改价 force) +- [x] 切分期:`dueAmount` 留空;dueDate 仍按公式 +- [x] 去掉对 `resolvePerSamplePreset` / 默认分期金额模板的依赖 + +### Task 3: ProtocolFormDialog + +- [x] 删除/停用 `applyPaymentPresetForMode`、样本数 watch 套价目、`resolvePaymentPreset` 引用 +- [x] v2 保存前调用分期必填校验 +- [x] 协议总金额跟随逻辑保持「默认同步可手改」 + +### Task 4: protocol-payment-presets 清理 + +- [x] 删除 `PRESETS` 与 `resolvePaymentPreset`;修复所有引用点 + +### Task 5: 后端校验 + +- [x] schedule validate:非 void 期 `due_amount` 与 `due_date` 必填 +- [x] pytest 覆盖 + +### Task 6: 验证与图谱 + +- [x] 前端单测 + 后端相关 pytest +- [x] `./scripts/update-graph.sh frontend`(若改后端则 `all`) diff --git a/docs/superpowers/plans/2026-10-10-payment-detail-responsive-dialog.md b/docs/superpowers/plans/2026-10-10-payment-detail-responsive-dialog.md new file mode 100644 index 00000000..66e0c267 --- /dev/null +++ b/docs/superpowers/plans/2026-10-10-payment-detail-responsive-dialog.md @@ -0,0 +1,135 @@ +# 缴费明细弹窗响应式宽度 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 增大缴费信息列表“缴费明细”按钮所打开弹窗的可用宽度,并让基础信息中的缴费方式、缴费进度 Tag 恢复内容自适应宽度。 + +**Architecture:** 保持 `PaymentExcelList.vue` 的按钮和弹窗调用链不变,只修改其复用组件 `sample-payment-detail-dialog/index.vue`。弹窗使用带最大值的视口响应式宽度;两个目标 Tag 使用专用类收缩,不影响批次和转账表格中的其他 Tag。 + +**Tech Stack:** Vue 3、TypeScript、Element Plus、SCSS、Node.js assert/tsx + +--- + +## 文件结构 + +- Create: `web-admin/scripts/test-payment-detail-dialog-layout.ts` — 组件布局源码契约检查。 +- Modify: `web-admin/src/components/business/sample-payment-detail-dialog/index.vue` — 响应式弹窗宽度及基础信息 Tag 收缩样式。 +- Update generated graph: `web-admin/graphify-out/*` — 按仓库规则同步前端代码知识图谱。 + +### Task 1: 锁定弹窗宽度和 Tag 宽度契约 + +**Files:** +- Create: `web-admin/scripts/test-payment-detail-dialog-layout.ts` +- Test: `web-admin/scripts/test-payment-detail-dialog-layout.ts` + +- [ ] **Step 1: 写入失败的源码契约检查** + +```typescript +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +const componentUrl = new URL( + '../src/components/business/sample-payment-detail-dialog/index.vue', + import.meta.url +) +const source = readFileSync(componentUrl, 'utf8') + +assert.match(source, /width="min\(1200px, 90vw\)"/) +assert.equal(source.match(/class="detail-tag"/g)?.length, 2) +assert.match( + source, + /\.detail-tag\s*\{[^}]*align-self:\s*flex-start;[^}]*width:\s*fit-content;/s +) + +console.log('payment detail dialog layout contract passed') +``` + +- [ ] **Step 2: 运行检查并确认按预期失败** + +Run: `cd web-admin && pnpm exec tsx scripts/test-payment-detail-dialog-layout.ts` + +Expected: FAIL,首个断言提示组件仍是 `width="1080px"`,未匹配响应式宽度。 + +### Task 2: 实现响应式弹窗和自适应 Tag + +**Files:** +- Modify: `web-admin/src/components/business/sample-payment-detail-dialog/index.vue:3-70` +- Modify: `web-admin/src/components/business/sample-payment-detail-dialog/index.vue:666-705` +- Test: `web-admin/scripts/test-payment-detail-dialog-layout.ts` + +- [ ] **Step 1: 将弹窗宽度改为响应式且限制最大值** + +将: + +```vue +width="1080px" +``` + +改为: + +```vue +width="min(1200px, 90vw)" +``` + +- [ ] **Step 2: 只给两个基础信息 Tag 添加专用类** + +缴费方式和缴费进度的两个 `` 均增加: + +```vue +class="detail-tag" +``` + +- [ ] **Step 3: 添加收缩样式** + +在组件 scoped SCSS 中增加: + +```scss +.detail-tag { + align-self: flex-start; + width: fit-content; +} +``` + +- [ ] **Step 4: 重跑源码契约检查** + +Run: `cd web-admin && pnpm exec tsx scripts/test-payment-detail-dialog-layout.ts` + +Expected: PASS,并输出 `payment detail dialog layout contract passed`。 + +- [ ] **Step 5: 运行前端验证** + +Run: `cd web-admin && pnpm exec eslint src/components/business/sample-payment-detail-dialog/index.vue scripts/test-payment-detail-dialog-layout.ts` + +Expected: PASS,无 ESLint 错误。 + +Run: `cd web-admin && pnpm build:typecheck` + +Expected: PASS,`vue-tsc --noEmit` 与 Vite production build 均成功。 + +### Task 3: 同步知识图谱并检查改动范围 + +**Files:** +- Update generated graph: `web-admin/graphify-out/*` + +- [ ] **Step 1: 更新前端知识图谱** + +Run: `./scripts/update-graph.sh frontend` + +Expected: 命令成功完成,前端图谱与组件源码一致。 + +- [ ] **Step 2: 检查差异与格式** + +Run: `git diff --check -- web-admin/src/components/business/sample-payment-detail-dialog/index.vue web-admin/scripts/test-payment-detail-dialog-layout.ts web-admin/graphify-out` + +Expected: PASS,无空白或补丁格式错误;差异仅包含本次组件、测试和生成图谱变更。 + +- [ ] **Step 3: 提交本次实现** + +```bash +git add web-admin/src/components/business/sample-payment-detail-dialog/index.vue \ + web-admin/scripts/test-payment-detail-dialog-layout.ts \ + web-admin/graphify-out +git commit -m "fix(web-admin): widen payment detail dialog responsively" +``` + +Expected: 仅提交本计划列出的实现、验证与图谱文件。 diff --git a/docs/superpowers/specs/2026-07-27-oa-auto-pass-initiator-approver-design.md b/docs/superpowers/specs/2026-07-27-oa-auto-pass-initiator-approver-design.md index da11b3a6..028ef9eb 100644 --- a/docs/superpowers/specs/2026-07-27-oa-auto-pass-initiator-approver-design.md +++ b/docs/superpowers/specs/2026-07-27-oa-auto-pass-initiator-approver-design.md @@ -31,7 +31,7 @@ **非目标** - 本期不实现「审批人去重」`approverDedup` 的引擎逻辑(UI 开关可继续存在,与本开关独立)。 -- 不改变 `direct_supervisor`「主管不能是发起人本人」的解析报错。 +- ~~不改变 `direct_supervisor`「主管不能是发起人本人」的解析报错。~~(已取消:允许主管配置为本人,如部门经理。) - 不新增独立 `event_type=auto_approve`(以任务意见留痕;后续若报表需要再补)。 - 发起预览 V1 不增加「将自动通过」旁注。 - 小程序端、抄送节点语义变更。 @@ -108,7 +108,7 @@ - 开关关闭 - 抄送节点(保持现有建即 done / 知会) - 本人不在该节点 assignee 列表 -- `direct_supervisor` 解析到本人仍抛错(不依赖本开关兜底) +- ~~`direct_supervisor` 解析到本人仍抛错~~(已取消:主管为本人时正常建待办;是否自动通过由本开关决定) - **加签**:仅追加他人 pending,不因加签对本节点发起人再跑一遍「自动整节点通过」 ### 5.4 与意见必填 diff --git a/docs/superpowers/specs/2026-10-05-unified-sample-info-design.md b/docs/superpowers/specs/2026-10-05-unified-sample-info-design.md new file mode 100644 index 00000000..a0b0796a --- /dev/null +++ b/docs/superpowers/specs/2026-10-05-unified-sample-info-design.md @@ -0,0 +1,249 @@ +# 统一样本信息管理 — 实现设计(方案 1) + +> 日期:2026-10-05 +> 状态:已确认(2026-10-05) +> 关联:细胞存储 / 科研捐赠「样本信息管理」;采集协议单「储存证 / 满月待办」收口 + +--- + +## 1. 背景 + +现状把样本进度拆在多处: + +- 细胞存储「样本信息管理」四个 Tab(采集 / 接收 / 质检 / 入库),后三 Tab 还承担短信提取与通知标记。 +- 科研捐赠「样本信息管理」只有采集列表。 +- 储存证、满月保险在「采集协议单」另外两个 Tab;保险还绑定保险公司、险种、保单号、投保单、发票。 + +市场侧无法在同一张样本列表上看完整进度。保险公司 / 险种菜单已隐藏,产品要求证与保险先做轻量办理。 + +--- + +## 2. 目标与非目标 + +### 2.1 目标 + +- 一个「样本信息管理」界面、两个来源 Tab(细胞存储 / 科研捐赠)。 +- **一条样本一行**;一份协议多个样本即多行。 +- 列表展示采集、接收、质检、入库进度标识(只读,由现有业务数据派生)。 +- 细胞存储额外展示储存证、保险「已办理 / 未办理」(协议级,同协议多行相同)。 +- 详情展示对应进度明细;细胞存储详情可办理储存证、保险(办理时间、办理人员、附件、备注)。 +- 采集协议单移除「储存证管理」「满月待办管理」Tab;旧路由跳转到样本信息。 + +### 2.2 非目标 + +- 不在本页改采集 / 接收 / 质检 / 入库状态(仍在质检、生产等菜单办理)。 +- 不在本页提取短信、标记已通知。 +- 不采集保险公司、险种、保单号、费用、投保单、发票;不恢复已隐藏的保险公司 / 体检机构菜单。 +- 不新建「样本进度总表」,不把两套协议样本合成一张混排物理表。 +- 不删除短信相关 API / 表;本页不再入口即可。 +- 不删除证 / 保险表上的旧保险字段;界面不展示、新保存不写入这些字段。 + +--- + +## 3. 菜单与页面 + +### 3.1 入口 + +| 菜单 | 路由 | 默认 Tab | +|------|------|----------| +| 细胞存储 → 样本信息管理 | `/market/cell-storage/sample-info` | `cell_storage` | +| 科研捐赠 → 样本信息管理 | `/market/research-donation/sample-info` | `research_donation` | + +两路由共用同一页面组件。`query.tab` 为 `cell_storage` | `research_donation`,与菜单默认一致,切换 Tab 时写入 query。 + +权限码保持现有:`cell-storage:sample-info:list`、`research-donation:sample-info:list`。用户只能看到有权限的 Tab;从无权限的菜单进入时不展示对应 Tab。 + +### 3.2 采集协议单 + +`protocol-audit` 仅保留「协议单管理」(审核、状态变更、增删改)。移除储存证、满月待办 Tab。 + +已隐藏路由: + +- `/market/cell-storage/storage-cert` → 跳转 `CellStorageSampleInfo?tab=cell_storage` +- `/market/cell-storage/fullmoon-todo` → 同上 +- 旧样本接收 / 入库 / `protocol-audit?tab=storage-cert|fullmoon-todo` → 同样落到统一样本信息页 + +### 3.3 UI + +- 列表对齐标准列表(筛选 → 表格 → 分页);进度与办理状态用 `el-tag`。 +- 操作列 `fixed="right"`,`min-width` ≥ 260px;本页主操作仅为「查看」。 +- 详情弹窗沿用 `ProtocolViewDialog` / `DonationViewDialog`,`label-position="top"`,无遮罩模糊。 +- 附件上传卡片化,对齐协议附件(`ProtocolFormDialog`)。 + +--- + +## 4. 列表 + +### 4.1 粒度与排序 + +- 数据源:`biz_protocol_sample`(细胞存储)/ `biz_research_donation_sample`(科研捐赠),`del_flag = 0`,当前租户 + 数据权限。 +- 默认排序与现采集列表一致:预产期空值靠后,其余预产期倒序,再主单 `create_time DESC`,再样本 id 倒序。 + +### 4.2 公共列 + +协议/捐赠状态、预产期、母亲姓名、新生儿姓名、样本编码(含特殊样本展示)、实验室入库编码、样本类型、采集标识、接收标识、质检标识、入库标识、操作。 + +细胞存储 Tab 在入库标识后增加:储存证办理、保险办理。 + +### 4.3 进度标识(只读,按租户 + 样本编码关联下游) + +**采集**(`sample_status`) + +| 展示 | 条件 | +|------|------| +| 未采集 | 空 | +| 已采集 | `ycj` / `jcz` / `yrk` | +| 已作废 | `yzf` | + +**接收**(该编码、对应来源下 `del_flag=0` 的接收单;多条时取接收时间最晚一条) + +| 展示 | 条件 | +|------|------| +| 未接收 | 无接收单,或结论为空 / `dqr` | +| 已接收 | `js` | +| 拒收 | `bj` | + +**质检**(该编码细胞存储或科研捐赠来源的入库质检) + +| 展示 | 条件 | +|------|------| +| 未完成 | 无工单,或待质检 / 质检中 | +| 已完成 | 已有结论:`tg` 或 `btg`(通过、不通过都算完成) | +| 已作废 | 工单作废 `yzf` | + +**入库** + +| 展示 | 条件 | +|------|------| +| 未入库 | 无入库结果,或待确认 `drk` | +| 已入库 | `yrk` | +| 拒绝入库 | `btg` | + +本页 **不得** 提供改这四类状态的控件。 + +### 4.4 储存证 / 保险办理标识(仅细胞存储,协议级) + +同一 `collection_protocol_id` 下所有样本行标识相同。 + +**已办理** 当且仅当以下任一成立: + +- 新字段 `handle_time` 非空;或 +- 兼容旧数据:储存证 `make_status = 1` 或 `make_date` 非空;保险 `todo_status = 1` 或旧 `handle_date` 非空。 + +否则 **未办理**。 + +不使用保险公司、险种、保单号、`insurance_subscribed` 判断。 + +### 4.5 筛选 + +- 关键词:姓名 / 样本编码 / 实验室入库编码(与现采集列表一致)。 +- 采集、接收、质检、入库:与上表展示枚举一致。 +- 细胞存储:储存证办理、保险办理(已办理 / 未办理)。 + +--- + +## 5. 详情 + +### 5.1 打开方式 + +查看 / 双击:细胞存储打开协议详情,科研捐赠打开捐赠详情。仍为整单。打开后滚动到「样本及运输」,高亮当前行样本。 + +### 5.2 样本进度(只读) + +每条样本卡片补充四段:采集(状态与采集/母血时间、地区医院医护)、接收(结论、时间、备注)、质检(状态/结论、完成时间)、入库(状态、时间)。无数据展示「—」。 + +科研捐赠无缴费、储存证、保险区块。 + +### 5.3 储存证 / 保险办理(仅细胞存储,可编辑) + +协议级两块,各含: + +| 字段 | 规则 | +|------|------| +| 办理时间 | 日期时间;非空则已办理 | +| 办理人员 | 用户选择;打开空表单时默认当前登录用户。填写了办理时间则办理人员必填 | +| 附件 | 可选;多文件逗号分隔 URL(证:`cert_image_urls`;保险:`insurance_attachments`) | +| 备注 | 可选(证:`remark`;保险:`handle_remark`) | + +每块独立「保存」。不校验协议必须为生效中。 + +保存时: + +- 有办理时间:写入 `handle_time`、`handler_user_id`、附件、备注;同步 `make_status`/`todo_status` = `1`;同步 `make_date` / 旧 `handle_date` 为该日 `YYYYMMDD`。 +- 办理时间为空:视为撤销办理——清空 `handle_time`、`handler_user_id`,status = `0`,日期字段清空;附件与备注按本次提交写入(允许一并清空)。 +- **不更新** `insurance_company_id`、险种、保单号、费用、生效失效、`insurance_subscribed`。 + +从样本信息打开的协议详情不再使用旧的只读储存证拼盘 / 满月保险只读区作为办理入口;办理只在上述两块。 + +--- + +## 6. 数据与接口 + +### 6.1 表变更 + +脚本:`docs/sql/migrations/2026-10-05-sample-info-handle-fields.sql`(名称以落地为准)。 + +`biz_protocol_storage_cert` 在业务字段末、公共字段前增加: + +| 列 | 类型 | 说明 | +|----|------|------| +| `handle_time` | DATETIME NULL | 办理时间 | +| `handler_user_id` | VARCHAR(64) NULL | 办理人 `sys_user.id` | + +`biz_protocol_fullmoon_todo` 同样增加 `handle_time`、`handler_user_id`。 + +Model / `_row_to_dict` 与 Create/Update 可写字段对齐(办理接口,非整单协议 CreateBody)。 + +### 6.2 列表 + +扩展: + +- `GET /web/admin/protocol-sample-collections/list` +- `GET /web/admin/research-donation-sample-collections/list` + +响应增加进度字段(code + label + 关键时间)。细胞存储增加 `storageCertHandled`、`insuranceHandled`(boolean)及办理时间(可选,供详情预填不是必须)。 + +查询参数增加进度与办理筛选。关联下游一律 `tenant_no` + `sample_code`,来源分别为 `cell_storage` / `research_donation`。 + +### 6.3 办理写入 + +- `PUT /web/admin/storage-certs/{protocolId}`:body 仅 `handleTime`、`handlerUserId`、`certImageUrls`、`remark`。 +- `PUT /web/admin/fullmoon-todos/{protocolId}/handle`:body 仅 `handleTime`、`handlerUserId`、`insuranceAttachments`、`handleRemark`。 + +租户校验、数据权限与协议更新一致。协议详情 bundle 返回办理字段及办理人姓名。 + +现有 `PUT .../fullmoon-todos/{id}/insurance` 可保留但前端样本信息页不调用;协议整单更新不再作为本页证/保险保存路径。 + +### 6.4 前端主要改动(落地时按此拆文件) + +- `web-admin/src/views/cell-storage/sample-info/index.vue`:两来源 Tab + 统一列表。 +- 科研捐赠 `sample-info/index.vue`:复用或重定向到同一实现。 +- `protocol-audit/index.vue`:去掉证 / 满月 Tab。 +- `ProtocolViewDialog.vue`:样本进度摘要 + 证/保险办理表单。 +- `DonationViewDialog.vue`:样本进度摘要。 +- 可删除或停用:`StorageCertTab.vue`、`FullmoonTodoTab.vue`、四个旧进度 Tab 作为独立工作台(采集列表逻辑并入统一页)。 + +--- + +## 7. 测试要点 + +- 细胞存储 / 科研捐赠列表:无下游 → 未接收 / 质检未完成 / 未入库;有 `js`/`tg`/`yrk` → 对应已完成类标识。 +- 多样本协议:两行;改证/保险后两行办理标识同时变化。 +- 办理保存 / 清空办理时间:标识与 status、日期同步字段一致。 +- 旧 `make_status=1` 且新 `handle_time` 空:列表仍为已办理。 +- 租户隔离与数据权限:不可见他户协议样本。 +- 科研捐赠列表响应不含证/保险办理列。 + +--- + +## 8. 决策摘要 + +| 项 | 结论 | +|----|------| +| 架构 | 方案 1:一页两 Tab,进度派生,不建总表 | +| 行粒度 | 样本行 | +| 证/保险入口 | 只在样本详情;协议单 Tab 撤销(选项 A) | +| 证/保险字段 | 仅时间、人员、附件、备注 | +| 短信 | 本页不做 | +| 已办理 | 以 `handle_time` 为主,兼容旧制作/办理日期与 status | +| 质检「已完成」 | 通过或不通过都算完成 | diff --git a/docs/superpowers/specs/2026-10-08-freight-link-existing-enrich-design.md b/docs/superpowers/specs/2026-10-08-freight-link-existing-enrich-design.md new file mode 100644 index 00000000..6296a131 --- /dev/null +++ b/docs/superpowers/specs/2026-10-08-freight-link-existing-enrich-design.md @@ -0,0 +1,155 @@ +# 配置运费「关联已有」列表 enrichment — 实现设计 + +> 日期:2026-10-08 +> 状态:已确认(2026-10-08) +> 关联: +> - `docs/superpowers/specs/2026-10-07-sample-freight-batch-design.md` +> - `docs/superpowers/specs/2026-10-08-sample-freight-menu-design.md` +> - Demo:`docs/demos/2026-10-08-freight-link-existing-demo.html` + +--- + +## 1. 背景与问题 + +采集协议单(细胞存储 / 科研捐赠)的「配置运费」弹窗中,「关联已有」仅展示: + +- 运输方式、运费(元)、关联样本数、创建时间 + +多笔同运输方式、同金额的批次无法区分,用户难以判断应关联哪一笔。 + +「运费信息」菜单页(`/#/market/cell-storage/sample-freight`)已用合并单元格 + 样本明细列,信息完整,业务确认希望「关联已有」对齐该列表。 + +--- + +## 2. 已确认决策 + +| 项 | 结论 | +|----|------| +| 展示形态 | 对齐「运费信息」列表:批次字段 rowspan + 样本明细行 | +| 覆盖入口 | ① 协议单 `SampleTransportFormSection`「配置运费」;②「运费信息」页 `FreightConfigDialog`「关联已有」 | +| 创建人 | 列表展示创建人(昵称);运费信息主列表已落地 `createBy` / `createByName` | +| 后端 | 复用 `GET /web/admin/sample-freights?includeItems=true`;关联已有路径须传 `includeItems=true` | +| 新接口 | 不新增 | + +--- + +## 3. 目标与非目标 + +### 3.1 目标 + +- 「关联已有」表格与「运费信息」主列表信息密度一致,便于识别批次。 +- 按**运费批次**单选一笔;点任一样本明细行即选中该批次。 +- 两入口 UI 一致(可抽公共表格组件,避免复制)。 + +### 3.2 非目标 + +- 不改新建运费、link/unlink、编辑运费业务规则。 +- 不改运费信息主列表筛选/分页/工具栏行为(创建人列已单独落地)。 +- 不引入快递单号等物流扩展字段。 +- 不做完整筛选区(弹窗高度有限);仅可选轻量关键词。 + +--- + +## 4. UI 规格 + +### 4.1 弹窗 + +- `dialog-form-layout`;无 backdrop blur;`append-to-body`。 +- 「关联已有」场景建议宽度 **960~1080px**(相对现状 720/860 加宽)。 +- Tab:新建运费 | 关联已有(行为不变)。 + +### 4.2 「关联已有」表格列 + +与运费信息主列表对齐(去掉操作列,增加单选): + +| 列 | 合并(按 freightId rowspan) | 说明 | +|----|------------------------------|------| +| 选择 | ✓ | 单选;点该批次任一行选中整笔 | +| 运输方式 | ✓ | `el-tag` | +| 运费(元) | ✓ | | +| 关联数 | ✓ | | +| 来源 | 否 | 细胞存储 / 科研捐赠 Tag | +| 协议单号 | 否 | | +| 样本编码 | 否 | | +| 样本类型 | 否 | | +| 创建人 | ✓ | `createByName \|\| createBy \|\| —` | +| 创建时间 | ✓ | | + +- **不展示**操作列(编辑 / 取消关联)。 +- 无 item 的批次:仍展示一行批次摘要,样本列显示 `—`。 +- 表格 `max-height` 可滚;`stripe` + `list-table`,对齐 Fresh Mint。 + +### 4.3 轻量检索(建议) + +弹窗内可选:关键词输入(运输方式 / 协议单号 / 样本编码),调用现有 list `keyword` 参数。 +不做运输方式/来源/日期完整筛选条。 + +### 4.4 协议单内 vs 运费页弹窗差异 + +| | 协议单内 | 运费信息页 | +|--|----------|------------| +| 选中样本来源 | 当前协议单已勾选样本 | 弹窗内样本多选表 | +| 关联已有表 | 本规格 | 本规格(同一组件) | +| 确定后 | create/link 或草稿(现有逻辑) | create/link 后刷新主列表 | + +--- + +## 5. 数据与前端结构 + +### 5.1 API + +``` +GET /web/admin/sample-freights?page=1&pageSize=50&includeItems=true&keyword?= +``` + +响应批次字段须含(已有或近期已补): + +- `transportMode` / `transportModeLabel` / `freightAmount` / `linkedSampleCount` +- `createBy` / `createByName` / `createTime` +- `items[]`:`sampleSource`、`sampleId`、`sampleCode`、`protocolId`、`protocolNo`、`sampleTypeName` + +### 5.2 前端 flatten + +复用运费信息页逻辑(可抽到工具或公共组件): + +- `flattenFreights(list)` → 扁平行 + `rowspan` / `isFirst` / `freightId` +- `span-method` 合并:选择、运输方式、运费、关联数、创建人、创建时间 + +选中态:维护 `selectedFreightId`;`current-change` 或 radio 以 `freightId` 为准(非样本行 key)。 + +### 5.3 建议文件 + +| 文件 | 职责 | +|------|------| +| 新建(建议)`web-admin/src/components/business/sample-freight-pick-table/index.vue` | 关联已有选批表格 | +| `sample-transport-form-section/index.vue` | 协议单弹窗改用公共表;`includeItems: true` | +| `sample-freight/FreightConfigDialog.vue` | 同上 | +| `sample-freight/index.vue` | 已含创建人;可改为复用 flatten 工具(可选,非必须) | + +若不愿抽组件:允许两处各改表格,但列定义与 flatten **必须一致**。 + +--- + +## 6. 验收标准 + +1. 细胞存储协议单「配置运费 → 关联已有」可见来源、协议单号、样本编码、样本类型、创建人、创建时间,批次列合并正确。 +2. 科研捐赠协议单同上。 +3. 运费信息页「配置运费 → 关联已有」同上。 +4. 点击某批次下任一样本行,确定后将该批次关联到选中样本(协议单内)或弹窗勾选样本(运费页)。 +5. 同运输方式、同金额的两笔批次可凭协议/样本/创建人区分。 +6. 新建运费 Tab 与 link API 行为无回归。 + +--- + +## 7. 已落地前置 + +- 运费信息主列表「创建人」列 + 后端 `_row_to_dict` 返回 `createBy` / `createByName`(本会话已改)。 + +--- + +## 8. 实现顺序(概要) + +1. (可选)抽出 `flatten` + 选批表格组件。 +2. 改造 `FreightConfigDialog`「关联已有」。 +3. 改造 `SampleTransportFormSection`「关联已有」(细胞存储 + 科研捐赠共用该组件)。 +4. 手工验收三入口;必要时补前端类型字段。 diff --git a/docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md b/docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md new file mode 100644 index 00000000..742510e8 --- /dev/null +++ b/docs/superpowers/specs/2026-10-08-protocol-market-specialist-region-manager-design.md @@ -0,0 +1,128 @@ +# 采集协议单:市场专员 / 采集地区 / 地区经理联动 + +**日期:** 2026-10-08 +**状态:** 已确认待实现 +**范围:** 细胞存储采集协议单 + 科研捐赠采集单(协议审核页两个 Tab 共用的表单/列表/查看) + +## 背景 + +采集地区人员配置在「采集地区及医院」模块(`biz_collection_region_staff`): + +| 模块用语 | 业务用语 | +|---------|---------| +| 工作人员 | 市场专员 | +| 负责人(`is_leader=1`) | 地区经理 | + +采集协议单原字段「销售人员」需更名为「市场专员」,并按人员关联地区自动带出采集地区;再按所选地区带出地区经理并落库。 + +预签单(`PresignOrderFormDialog`)已有「采集经理 + 按地区负责人默认带出、可改选」模式,本设计对齐该行为。 + +## 目标 + +1. UI 文案:`销售人员` → `市场专员`(表单、列表列、查看弹窗、协议审核卡片展示)。 +2. 选中市场专员后,采集地区选项收缩为其在 `biz_collection_region_staff` 中绑定的地区;多地区时默认第一项,可改选。 +3. 市场专员仍可按姓名 remote 检索**全部系统用户**(不强制必须是已配置工作人员)。 +4. 市场专员后新增「地区经理」:按当前采集地区负责人默认带出,可改选任意用户;必须写入采集信息表。 +5. 细胞存储与科研捐赠两套采集单行为一致。 + +## 非目标 + +- 不改采集地区人员配置模块本身。 +- 不做 HTML Demo。 +- 不限制市场专员只能选自工作人员列表。 +- 不重构预签单(仅复用其交互模式)。 + +## 数据模型 + +### 新增列 + +| 表 | 列 | 说明 | +|----|-----|------| +| `biz_protocol_collection_info` | `region_manager_user_id` VARCHAR(64) NULL | 地区经理用户 ID | +| `biz_research_donation_collection` | `region_manager_user_id` VARCHAR(64) NULL | 同上 | + +迁移脚本放在 `docs/sql/migrations/2026-10-08-protocol-region-manager.sql`。 + +ORM / CRUD / CreateBody·UpdateBody / `_row_to_dict` / 前端 form 与提交体同步增加 `regionManagerUserId`;详情与列表填充 `regionManagerUserName`(由用户表昵称解析)。 + +既有 `sales_user_id` 字段保留,仅前端与注释语义改为「市场专员」。 + +## API + +### 新增:按用户查关联采集地区 + +`GET /api/admin/collection-regions/by-staff-user/{userId}` + +- 租户隔离:仅当前用户 `tenant_no`。 +- 查询 `biz_collection_region_staff`(`del_flag=0`)join 地区(`del_flag=0`)。 +- 排序:`order_num ASC`,再 `create_time DESC`(与地区人员列表习惯一致)。 +- 响应:`{ list: [{ id, regionName }] }`;无绑定时 `list: []`。 + +既有 `GET /collection-regions/{regionId}/staff` 继续用于按地区取负责人(与预签单一致)。 + +## 前端联动(`ProtocolFormDialog` + `DonationFormDialog`) + +### 字段顺序(采集信息区) + +市场专员 → 地区经理 → 采集地区 → 采集医院 → 采集医护 → 样本/母血采集时间 → … + +### 市场专员 + +- Remote 检索全部启用用户(现状保留)。 +- 新建默认当前登录人;默认后触发一次「按专员刷新关联地区」。 + +### 市场专员变更 → 采集地区 + +1. 调用 `by-staff-user` 取关联地区列表 `linkedRegions`。 +2. **有关联:** + - 采集地区下拉选项 = `linkedRegions`(不再用全量 remote 检索,改为本地 filter 即可)。 + - 若当前已选地区仍在 `linkedRegions` 中则保留,否则默认第一项。 + - 若地区值发生变化:清空医院并重载;触发地区经理默认带出。地区未变则不强制改医院/经理。 +3. **无关联:** + - 清空采集地区与医院。 + - 恢复全量地区 remote 检索(与改前一致),用户可手工选任意地区。 + - 清空地区经理(待用户选地区后再带出)。 + +### 采集地区变更 → 地区经理 + +- 清空医院并重载该地区医院。 +- 拉取该地区 staff:优先 `isLeader`,否则取列表第一人;写入 `regionManagerUserId` 并保证 options 中有 stub。 +- 地区经理控件:remote 检索全部用户、可 clear、可改选(对齐预签单「采集经理」)。 + +### 保存 + +提交 `collectionInfo`(或等价结构)时包含: + +- `salesUserId` +- `regionManagerUserId` +- `regionId` / `hospitalId` / … + +## 展示同步 + +以下位置文案与回显一并调整(有则改): + +| 位置 | 调整 | +|------|------| +| `ProtocolFormDialog` / `DonationFormDialog` | label + 地区经理字段 + 联动 | +| `ProtocolViewDialog` / `DonationViewDialog` | 销售人员→市场专员;增加地区经理 | +| 协议列表 `protocol/index.vue`、科研捐赠 `research-donation/list/index.vue` | 「销售人员」列改名为「市场专员」;列表不加「地区经理」列 | +| `protocol-audit` 卡片(`ProtocolAuditTab` / `DonationProtocolTab`) | 销售人员→市场专员;卡片增加地区经理展示 | + +**列表列策略:** 列表只改名「市场专员」;地区经理在查看弹窗与审核卡片展示。 + +## 错误与边界 + +- 市场专员清空:清空关联地区约束,恢复全量地区检索;清空地区/医院/地区经理(与「无关联」一致)。 +- 地区无任何 staff:地区经理留空,可手工选。 +- 编辑回填:以库中已存 `regionId` / `salesUserId` / `regionManagerUserId` 为准;hydrate options stub;进入编辑后若用户改市场专员再走联动逻辑。 +- 历史数据无 `region_manager_user_id`:展示为空,编辑保存时可补全。 + +## 测试 + +- 后端:`by-staff-user` 有/无绑定、租户隔离;采集信息 create/update/get 含 `regionManagerUserId`。 +- 前端手工:新建默认专员→带地区;多地区默认第一可改;改地区→经理变化可改选;科研捐赠与细胞存储一致;编辑回显不丢经理。 + +## 参考实现 + +- 地区经理默认带出:`web-admin/src/views/cell-storage/presign-order/PresignOrderFormDialog.vue` 中 `applyDefaultRegionManager` +- 人员表:`biz_collection_region_staff` / `backend/app/crud/collection_region.py` diff --git a/docs/superpowers/specs/2026-10-09-payment-info-excel-list-and-chase-design.md b/docs/superpowers/specs/2026-10-09-payment-info-excel-list-and-chase-design.md new file mode 100644 index 00000000..0d585c83 --- /dev/null +++ b/docs/superpowers/specs/2026-10-09-payment-info-excel-list-and-chase-design.md @@ -0,0 +1,244 @@ +# 缴费信息 Excel 单列表 + 分改趸 + 催缴信息 + +> 状态:已确认 + +> 日期:2026-10-09 +> 界面模式:P02 复杂业务列表(缴费宽表 / 催缴标准列表) +> 参考:Excel 缴费台账截图、`docs/demos/2026-10-08-payment-info-excel-list-demo.html`、现 `payment-info` / `SamplePaymentPanel` + +## 1. 背景与目标 + +### 1.1 现状 + +【缴费信息】含四个 Pill Tab(已缴费 / 缴费确认中 / 缴费成功 / 按样本),运营扫读成本高,且列表难以对齐财务 Excel「一行一样本 + 多期横排」习惯。 + +数据模型已支持: + +``` +样本缴费方案 1 ──N 应缴批次(期) ──N 转账笔 +``` + +一期多笔在「按样本详情」可见,但流程 Tab 按转账散开,不成组。 + +缴费方式目前主要为 `dj`(趸交)、`fq`(分期)、以及双样本预设 `djfq`(趸交+分期),缺少业务上的「分改趸」标识与中途并期能力。 + +市场侧缺少「近月应缴 + 已逾期」的催缴工作台。 + +### 1.2 目标 + +1. 【缴费信息】改为**单列表**(方案 1):基础信息 + 固定第 1~6 期色块 + 汇总;点击某期「实缴金额」弹窗查看转账与财务审核。 +2. 新增缴费方式 Tag **`fgd`(分改趸)**:建单可选;中途可由分期改来并自动并期(入口仅在采集协议单缴费方案)。 +3. 新增侧栏菜单 **【催缴信息】**:展示未来 30 天内应缴未清 + 已逾期未清的期次,供市场专员催缴。 + +### 1.3 已确认决策 + +| 项 | 选择 | +|----|------| +| 缴费信息行粒度 | 一行 = 一个样本缴费方案 | +| 列表形态 | Excel 定宽 6 期列(方案 1) | +| 缴费方式语义 | Tag 标识;真实结构以有效期数为准 | +| 分期 | 最多 6 期 | +| 趸交 | 通常 1 期 | +| 分改趸 | 建单可选;中途 `fq→fgd` 并期后常见 2~3 期 | +| 中途并期规则 | 软删合并目标期之后的后续期,剩余未缴并入第一期欠缴>0 的期 | +| 中途入口 | 仅采集协议单 · 编辑缴费方案 | +| 改造范围 | 完整闭环:列表 + 建单 `fgd` + 中途并期 + 催缴菜单 | +| 催缴纳入 | 未来 30 天待缴期 ∪ 已逾期未缴清期 | +| 催缴行粒度 | 一行 = 一个待催期(样本×期次) | + +## 2. 信息架构与菜单 + +``` +… → 预签约订单 → 【缴费信息】→ 【催缴信息】→ 样本信息管理 → … +``` + +| 菜单 | 路由建议 | 权限码建议 | +|------|----------|------------| +| 缴费信息 | `/#/market/cell-storage/payment-info`(沿用现路由) | 沿用 `cellStorage:paymentInfo:list` | +| 催缴信息 | `/#/market/cell-storage/payment-chase` | 新增 `cellStorage:paymentChase:list` | + +- 缴费信息:去掉四个 Tab,单一宽表。 +- 催缴信息:独立页面与权限;默认授权给市场相关角色(具体角色在菜单 SQL / 实施时配置)。 +- 中途改分改趸:不在缴费信息/催缴信息写 Tag。 + +## 3. 缴费信息(Excel 宽表) + +### 3.1 页面形态 + +- 筛选:关键词、缴费方式(含分改趸)、缴费进度、应缴日区间(可选)。 +- 左侧基础列 sticky 冻结;期次区横向滚动。 +- 操作:查看协议单;有欠缴时可「缴费确认」(复用现有转账确认弹窗)。 +- Demo:`docs/demos/2026-10-08-payment-info-excel-list-demo.html`。 + +### 3.2 列映射 + +**基础信息** + +| 列 | 字段 | +|----|------| +| 序号 | 分页序号 | +| 姓名 / 手机号 | `motherName` / `motherPhone` | +| 合同编号 | `protocolNo` | +| 合同签署日期 | 协议签署日(列表 JOIN 补齐) | +| 存储年限 | 样本 `storageYears` | +| 存储项目 | 样本类型名 `sampleTypeName` | +| 合同总额 | `contractAmount` | +| 缴费方式 | `payMode` Tag:`dj` / `fq` / `fgd`(`djfq` 若存在仍展示) | +| 优惠金额 | 有则展示;无则 `—`(不强造新业务规则) | +| 应收总额 | 未作废期应缴合计(或与现应收口径对齐) | + +**第 N 期(N=1…6)** + +| 子列 | 口径 | +|------|------| +| 应缴金额 | 该期 `dueAmount`;无期 → `—` | +| 实缴金额 | 该期转账展示合计(含 `yjf` + `jfqrz`);**可点击** | +| 缴费日期 | 多笔取最近一笔转账日(摘要) | +| 支付方式 | 多笔显示「多笔」或最近一笔方式 | +| 财务审核 | 期级摘要 Tag(确认中 / 已通过 / 待缴费 / 部分到账) | + +**汇总** + +| 列 | 口径 | +|----|------| +| 累计实缴 | 已确认 `yjf` 合计(对齐现 `paidAmount`) | +| 待缴金额 | `dueRemainAmount` | +| 备注 | 方案 `remark` | + +无 schedule 的期位显示 `—`。 + +### 3.3 实缴金额弹窗 + +- 标题:客户 · 协议号 · 第 N 期。 +- 摘要:应缴 / 实缴合计 / 欠缴 / 笔数。 +- 明细:每笔金额、方式、时间、凭证、`payStatus`、财务审核时间线。 +- 可确认时底部「缴费确认」→ 复用 `PaymentReceiptConfirmDialog`。 +- 本弹窗不提供改分改趸。 + +### 3.4 与旧 Tab + +原流程三状态由行内 Tag + 筛选「缴费进度」替代。旧转账列表接口可保留给其它页面;本页主数据为样本方案 + 期摘要聚合。 + +## 4. 催缴信息 + +### 4.1 纳入规则 + +未作废、未软删、欠缴 > 0 的应缴批次,且: + +1. `dueDate` ∈ `[今天, 今天+30 天]`,或 +2. `dueDate` < 今天(已逾期)。 + +趸交 / 分期 / 分改趸均按期次纳入。 + +### 4.2 列表 + +- 模式:P02 标准复杂列表(非 6 期宽表)。 +- 默认排序:逾期优先(逾期天数 DESC)→ 应缴日 ASC → 协议号。 +- 列:协议号、客户/手机、样本编码、市场专员、缴费方式、期次、应缴日(逾期高亮)、应缴/已缴/欠缴、催缴状态 Tag(已逾期 / N 天内到期)、协议状态、操作(查看协议单;可选深链到缴费信息该样本)。 +- 本页不做缴费确认、不改分改趸。 + +### 4.3 筛选与权限 + +- 关键词、催缴类型(全部/仅逾期/仅未来 30 天)、市场专员、缴费方式。 +- 数据范围:与采集协议单一致(`apply_data_scope`)。 + +## 5. 分改趸规则与 API + +### 5.1 枚举 + +| 码 | Tag | 说明 | +|----|-----|------| +| `dj` | 趸交 | 通常 1 期 | +| `fq` | 分期 | 最多 6 期 | +| `fgd` | 分改趸 | 新建 | +| `djfq` | 趸交+分期 | 保留,本需求不改语义 | + +`pay_mode` 仅为 Tag;有效 schedule 条数决定实际期结构。 + +### 5.2 建单选分改趸 + +- 协议单样本缴费方案增加与趸交/分期同级选项。 +- 允许配置 2~6 期;生成规则复用分期 schedule 生成能力。 +- 保存 `pay_mode=fgd`。 + +### 5.3 中途 `fq` → `fgd`(仅协议单) + +算法(例:原 6 期,前 2 期已缴清): + +1. 有效期按 `schedule_no` 排序。 +2. 第一期欠缴 > 0 记为合并目标期 `T`。 +3. 计算全局剩余欠缴,将未清金额并入 `T` 的应缴,使总欠缴与合同应收关系不变。 +4. 软删 `schedule_no > T` 的后续期(`del_flag=1`);其下仅 `dfj` 草稿转账一并软删。 +5. 更新 `pay_mode=fgd`,`installment_count` = 保留有效期数。 + +实现上「删除后续期」= 软删除,禁止物理删库。 + +### 5.4 约束 + +- 存在 `jfqrz` 转账 → 禁止改模式/并期(对齐现可编辑校验)。 +- 拟软删的后续期若已有 `yjf` → 拒绝并提示。 +- 本需求明确支持的自动并期路径:`fq` → `fgd`。其它模式切换不承诺自动并期。 +- 数据权限与协议单编辑一致。 + +### 5.5 API + +| 接口 | 变更 | +|------|------| +| 协议内保存样本缴费方案 | 接受 `fgd`;`fq→fgd` 走并期 service,避免盲目重建抹掉已缴期 | +| `GET .../sample-payments` | 宽表字段:签署日、年限、项目、期摘要 1~6、汇总;支持 `fgd` | +| `GET .../sample-payments/{id}` | 完整 schedules + transfers(补展示字段) | +| `GET .../payment-chase`(名称可微调) | 催缴分页 | +| 转账写接口 | 语义不变 | + +并期逻辑放在 `app/services/`(建议 `sample_payment_fgd_convert.py`),附单测。 + +### 5.6 前端落点 + +| 位置 | 改动 | +|------|------| +| 协议表单 `SamplePaymentPanel` | 分改趸选项;`fq→fgd` 保存前提示并期 | +| `payment-info` | 去 Tab → Excel 宽表 + 实缴弹窗 | +| 新页 `payment-chase` | 催缴列表 | +| 路由 + 菜单 SQL | 缴费信息下挂催缴信息 | +| `status-tag-registry` | `fgd` | + +### 5.7 用户提示 + +- 确认中不可改:存在缴费确认中的转账,请先完成财务确认。 +- 并期成功:已改为分改趸,后续未缴期已合并至第 N 期。 + +## 6. 测试要点 + +### 6.1 后端 + +- `pay_mode=fgd` 可创建 2~6 期方案。 +- `fq→fgd`:已缴清 2 期时软删 4~6,第 3 期应缴吸收剩余欠缴;总已确认实缴不变。 +- 存在 `jfqrz` 时 `fq→fgd` 失败。 +- 后续期已有 `yjf` 时并期失败。 +- 催缴查询:边界日今天、今天+30 纳入;今天+31 不纳入;逾期未清纳入;欠缴=0 不纳入。 +- 宽表列表期摘要与详情树金额一致;租户与 data scope 生效。 + +### 6.2 前端 + +- 缴费信息无 4 Tab;趸交仅第 1 期有值;分期最多 6 期有值。 +- 点击实缴打开多笔转账弹窗并可发起确认。 +- 协议单可选分改趸;`fq→fgd` 有确认提示。 +- 催缴菜单可见、列表排序与筛选符合 §4。 + +### 6.3 非目标(本变更不做) + +- 催缴页内直接改分改趸或财务确认。 +- 物理删除 schedule。 +- 改造 `djfq` 双样本预设语义。 +- 财务确认独立页流程大改。 + +## 7. 风险与兼容 + +- 宽表列多,需冻结列与横向滚动;小屏体验依赖横向滚。 +- 列表接口需批量补齐协议/样本字段,注意 N+1。 +- 历史数据无 `fgd`:仅 `dj`/`fq`/`djfq`,展示不受影响。 +- 旧书签 `?tab=dfj|jfqrz|yjf|by-sample`:进入缴费信息单列表并**忽略 tab**(不再分 Tab);若 `tab` 为进度码,可选地预填「缴费进度」筛选项,无匹配则忽略。 + +## 8. 附录:Demo + +- `docs/demos/2026-10-08-payment-info-excel-list-demo.html` diff --git a/docs/superpowers/specs/2026-10-09-payment-period-columns-and-schedule-edit-design.md b/docs/superpowers/specs/2026-10-09-payment-period-columns-and-schedule-edit-design.md new file mode 100644 index 00000000..5919512e --- /dev/null +++ b/docs/superpowers/specs/2026-10-09-payment-period-columns-and-schedule-edit-design.md @@ -0,0 +1,115 @@ +# 缴费信息期次列口径 + 协议单应缴批次可编辑 + +> 状态:已确认 +> 日期:2026-10-09 +> 界面模式:P02 复杂业务列表(缴费宽表);协议表单内嵌按样本缴费面板 +> 关联:`docs/superpowers/specs/2026-10-09-payment-info-excel-list-and-chase-design.md` + +## 1. 背景与目标 + +### 1.1 现状 + +1. 【缴费信息】Excel 宽表每期子列为:应缴、实缴、缴费日期、支付方式、财务审核。 + - 实缴仅计 `yjf` + `jfqrz`,不含转账笔「已缴费」(`dfj`)。 + - 缴费日期取多笔中**最晚**一笔。 + - 未单独展示应缴费时间、欠缴费金额。 + +2. 【采集协议单 · 细胞存储】按样本缴费(`SamplePaymentPanel`)的「应缴批次」由期数/间隔/开始日公式生成,每期应缴金额与应缴时间只读展示,无法单期微调。 + +### 1.2 目标 + +1. 缴费信息列表每一期展示五列业务口径字段(见 §2)。 +2. 协议单编辑态支持动态调整每一期的应缴费时间、应缴费金额(见 §3)。 +3. 保存时强制:各期应缴合计 = 该样本合同金额(见 §3.3)。 + +## 2. 缴费信息宽表 · 期次列 + +### 2.1 列定义(第 N 期,N=1…6) + +| 子列 | 口径 | +|------|------| +| 应缴费金额(元) | 该期 `dueAmount`;无期 → `—` | +| 应缴费时间 | 该期 `dueDate`(展示为 `YYYY-MM-DD`);无期 / 无日期 → `—` | +| 已缴费金额 | 该期转账笔中 `pay_status ∈ {dfj, jfqrz, yjf}` 的 `transfer_amount` 合计;无转账 → `0` 或与现展示一致的金额格式;无期 → `—` | +| 已缴费时间 | 该期转账记录中**第一条**的 `transferTime` 日期部分;无转账 → `—`;无期 → `—` | +| 欠缴费金额 | `max(应缴费金额 − 已缴费金额, 0)`;无期 → `—` | + +说明: + +- 「已缴费 / 确认中 / 缴费完成」对应 UI 文案与码值:`dfj`(已缴费)、`jfqrz`(缴费确认中)、`yjf`(缴费成功/完成)。退费相关状态(`tfqrz` / `ytf`)不计入已缴费金额。 +- 「第一条」指该期转账列表的持久化/查询顺序中的首条(与详情弹窗列表顺序一致),**不是**按时间最晚。若需稳定顺序:同序下按 `create_time ASC`、`id ASC`。 +- 点击「已缴费金额」(有金额或有转账时)仍打开现有期次转账明细弹窗;支付方式、财务审核仅在弹窗内展示,不再作为宽表子列。 + +### 2.2 后端摘要 `summarize_period` + +调整 / 补充返回字段: + +| 字段 | 规则 | +|------|------| +| `displayPaidAmount` | `dfj` + `jfqrz` + `yjf` 转账金额合计 | +| `payDate` | 首笔转账时间的日期部分(非最近一笔) | +| `arrearsAmount` | `max(due − displayPaidAmount, 0)` | +| `payTypeSummary` / `financeAudit` | 保留现有逻辑(供弹窗或其它消费方),宽表不再展示 | + +`_build_periods_array` 每期额外输出: + +- `dueDate`(格式与列表约定一致,建议 `YYYY-MM-DD` 或现有 8 位统一前端格式化) +- `arrearsAmount` + +前端 `PaymentExcelList.vue` 按上表改列绑定。 + +## 3. 协议单 · 应缴批次可编辑 + +### 3.1 交互 + +在 `SamplePaymentPanel` 每个「第 N 期」区域提供可编辑控件: + +- **应缴费金额(元)** +- **应缴费时间**(日期) + +约束: + +- 若该期存在任一转账笔 `pay_status = yjf`(财务已确认),该期应缴金额、应缴时间**只读**。 +- 仅改某一期的应缴金额/时间时,**不得**触发 `rebuildSchedules` 整表重建。 +- 变更「缴费模式 / 合同金额 / 期数 / 每期间隔 / 分期开始时间」时,仍可按现有公式重建批次(可能覆盖手工调整,属预期)。 +- **合同金额不因各期应缴合计自动改写**(决策 A)。 + +### 3.2 数据流 + +- 编辑写入 `samplePayments[].schedules[].dueAmount` / `dueDate`。 +- 保存协议时原样提交;后端 `sync_schedules_and_transfers` / `replace_schedules` 已支持写入 `dueAmount`/`dueDate`,无需新表字段。 + +### 3.3 保存校验(硬拦截) + +对每个样本缴费方案(未作废期): + +``` +Σ(dueAmount) 必须等于 contractAmount +``` + +- 前端:提交前校验;不一致则 `ElMessage` / `ElMessageBox` 提示差额,**阻止保存**。 +- 后端:保存 `samplePayments` 时同样校验,失败返回业务错误(防绕过前端)。 +- 比较使用 Decimal,避免浮点误差(允许极小精度差如 `0.01` 元内视为相等,或按项目金额惯例四舍五入到分后比较)。 + +## 4. 非目标 + +- 催缴信息列表「应缴 / 已缴 / 欠缴」与本节缴费信息期次口径对齐(已缴含 `dfj`+`jfqrz`+`yjf`,欠缴=max(应缴−已缴,0));纳入规则仍用对齐后的欠缴>0。 +- 不恢复缴费信息四个流程 Tab。 +- 不改转账笔状态机枚举。 + +## 5. 测试要点 + +1. `summarize_period`:仅 `dfj` 计入已缴;首笔时间非最晚笔;欠缴计算。 +2. 列表 periods 含 `dueDate`、`arrearsAmount`,`displayPaidAmount` 含 `dfj`。 +3. 面板:改单期金额/日期不重建其它期;有 `yjf` 的期只读。 +4. 保存:合计 ≠ 合同金额前后端均拒绝;相等可保存成功。 + +## 6. 已确认决策 + +| 项 | 选择 | +|----|------| +| 合同金额与各期关系 | 各期独立改,不自动改写合同金额 | +| 合计不一致 | 必须改到一致才能保存(前后端硬拦) | +| 已缴费金额含状态 | `dfj` + `jfqrz` + `yjf` | +| 已缴费时间 | 首笔转账时间(非最晚) | +| 宽表期次子列 | 五列:应缴金额/应缴时间/已缴金额/已缴时间/欠缴金额 | diff --git a/docs/superpowers/specs/2026-10-09-protocol-preferential-price-payment-design.md b/docs/superpowers/specs/2026-10-09-protocol-preferential-price-payment-design.md new file mode 100644 index 00000000..d7fe6513 --- /dev/null +++ b/docs/superpowers/specs/2026-10-09-protocol-preferential-price-payment-design.md @@ -0,0 +1,123 @@ +# 采集协议单 · 优惠价驱动缴费金额 — 设计说明 + +**日期**:2026-10-09 +**状态**:已确认 +**范围**:`web-admin` 采集协议单新增/编辑(细胞存储)、相关工具函数与保存校验;后端按样本缴费保存校验对齐 +**取代**:`2026-07-06-protocol-payment-defaults-design.md` 中的「样本数 × 缴费模式」硬编码价目表默认合同金额逻辑 + +--- + +## 1. 目标 + +1. **废除价目表**:不再使用 `22880 / 25880 / 37880 / 41000 / 44000` 及各期写死拆分作为合同/应缴默认值。 +2. **优惠价为唯一金额源**(新建与变更时): + - 样本合同金额 = 该样本优惠价 `price` + - 样本合同合计 = Σ 各有效样本合同金额 + - 协议总金额默认 = 样本合同合计 +3. **分期不默认金额**:按「期数 + 间隔 + 开始日」生成期次与应缴日;每期金额留空手填;保存时每期金额与应缴时间必填,且各期之和 = 该样本合同金额。 +4. **历史已核对数据不被重算**:打开编辑以库内缴费为准;仅用户改优惠价时同步该样本合同。 + +--- + +## 2. 已确认产品决策 + +| # | 议题 | 决策 | +|---|------|------| +| D1 | 价目表 | **全部去掉**(新旧一律不再走价目表代码路径) | +| D2 | 协议总金额 | **默认同步合计,可手改**;与合计不一致仅提示,不阻断 | +| D3 | 分期期次生成 | 仍用期数 + 间隔 + 开始日;**金额空**;应缴日可预填可改 | +| D4 | 历史打开 | **以库内缴费为准**;改优惠价才同步该样本合同 | +| D5 | 实现范围 | **前端去价目 + 统一按优惠价;后端保存校验对齐**(方案 B);不做历史金额迁移脚本 | + +--- + +## 3. 金额规则 + +### 3.1 样本层 + +| 字段 | 规则 | +|------|------| +| 原价格 `originalPrice` | 仍可由样本类型 `storagePrice` 带出;不参与合同默认 | +| 优惠价格 `price` | 用户设置;**合同金额来源** | +| 样本合同金额 | = `price`(新建方案时;或用户改 `price` 后同步) | +| 趸交应缴 | 单期 `dueAmount` = 合同金额 | +| 分期各期 | 金额**不默认**;用户填写;Σ = 合同金额;每期须有应缴时间 | + +### 3.2 协议层 + +| 字段 | 规则 | +|------|------| +| 样本合同合计 | Σ 各样本 `contractAmount`(只读展示) | +| 协议总金额 | 未手改时跟随合计;手改后可偏离,仅弱提示 | + +### 3.3 优惠价变更 + +- 无已财务确认(`yjf`)转账时:将该样本 `contractAmount` 更新为新优惠价。 +- 趸交:同步唯一期 `dueAmount`。 +- 分期:不自动改写各期金额;保存前须各期之和仍等于新合同,否则阻断并提示用户调整。 + +### 3.4 编辑打开 + +- **不**用当前优惠价覆盖库内 `contractAmount` / 各期 `dueAmount`。 +- 新建样本缴费方案时:合同 = 当时优惠价(无优惠价则合同为空,提示补价后再配缴费)。 + +--- + +## 4. 校验 + +### 4.1 前端保存(v2 按样本) + +| 规则 | 强度 | +|------|------| +| 有效样本须有样本编码(已有按样本方案时) | 阻断 | +| 分期/分改趸:每期 `dueAmount` 有值且 > 0 | 阻断 | +| 分期/分改趸:每期 `dueDate` 非空 | 阻断 | +| 未作废期应缴合计 = 该样本合同金额 | 阻断 | +| 协议总金额 ≠ 样本合同合计 | 仅提示 | +| 样本合同与优惠价不一致(可选弱提示) | 仅提示(允许历史核对后手改合同的边界;新建以优惠价写入) | + +### 4.2 后端 + +与前端阻断项对齐:`sample_payment_schedule_validate`(或等价)校验各期必填与合计 = 合同;不依赖价目表常量。 + +--- + +## 5. 删除 / 停用范围 + +| 项 | 处理 | +|----|------| +| `protocol-payment-presets.ts` 中 `PRESETS` 及 `resolvePaymentPreset` | 删除价目;保留 `getActiveSampleCount` / `availablePayModes` / `isInstallmentPayMode` 等非金额工具(若仍被引用) | +| `resolvePerSamplePreset` / 硬编码 22880 等 | 删除;改由优惠价解析 | +| `ProtocolFormDialog` v1:`applyPaymentPresetForMode`、样本数变化套价目 | 删除套用逻辑;v1 编辑仅展示库内值,不再填默认合同 | +| 新建 | 一律 `paymentModelVersion=2`,不出现整单价目填充 | + +**不做**:历史协议金额批量迁移;强制把 v1 明细拆成按样本(本期)。 + +--- + +## 6. UI 行为摘要(SamplePaymentPanel) + +1. 样本增减:无方案则新建趸交方案,`contractAmount = price`(无 price 则 null)。 +2. 切换趸交:合同 = 优惠价,一期应缴 = 合同。 +3. 切换分期/分改趸:保留/写入期数间隔开始日;生成期次行时 **dueAmount 为空**;dueDate 按公式预填。 +4. 合同金额字段:可展示;与优惠价联动策略见 §3.3;用户仍可手改合同(保存时各期之和须等于合同)。 +5. 去掉「套用默认分期金额模板」的一切入口。 + +--- + +## 7. 非目标 + +- 意向金自动冲抵样本合同。 +- 强制协议总金额 = 样本合计。 +- 历史 v1 → v2 自动拆分迁移。 +- 价目表配置化(本期直接删除硬编码,不换成配置表)。 + +--- + +## 8. 验收要点 + +1. 两样本优惠价 20000 + 22000 → 样本合同分别为 20000/22000,合计与默认协议总金额均为 42000。 +2. 分期生成后各期金额为空;不填金额或应缴日无法保存。 +3. 各期填满且之和 ≠ 合同 → 无法保存。 +4. 打开历史单金额与打开前一致;改某一优惠价后仅该样本合同(及趸交应缴)更新。 +5. 代码与测试中不再出现作为默认合同的 22880/25880/37880/41000/44000 套用路径。 diff --git a/web-admin/graphify-out/.graphify_analysis.json b/web-admin/graphify-out/.graphify_analysis.json index edf0f15d..cd41c4da 100644 --- a/web-admin/graphify-out/.graphify_analysis.json +++ b/web-admin/graphify-out/.graphify_analysis.json @@ -143,7 +143,9 @@ "api_admin_fetchinsuranceprojectprices", "api_admin_fetchnotdeletedregions", "api_admin_fetchpilotapplicationdetail", + "api_admin_fetchpilotcheckupactualitems", "api_admin_fetchpilotcheckuppendingpatients", + "api_admin_fetchpilotcheckupreserveitems", "api_admin_fetchpilotdeliverydetail", "api_admin_fetchpilotdeliverylistbyapplication", "api_admin_fetchpilotdeliverymaterials", @@ -302,6 +304,13 @@ "api_infusion_schedule_handover_fetchinfusionscheduleoutboundhandover", "api_infusion_schedule_handover_saveinfusionscheduleoutboundhandover", "api_infusion_schedule_handover_ts", + "api_leave_fetchcreateleave", + "api_leave_fetchdeleteleave", + "api_leave_fetchleavedetail", + "api_leave_fetchleavelist", + "api_leave_fetchsubmitleave", + "api_leave_fetchupdateleave", + "api_leave_ts", "api_material_stock_check_fetchcancelmaterialstockcheck", "api_material_stock_check_fetchcompletematerialstockcheck", "api_material_stock_check_fetchcreatematerialstockcheck", @@ -517,6 +526,14 @@ "api_sample_reception_fetchsamplereceptionsubmit", "api_sample_reception_fetchsamplereceptionupdate", "api_sample_reception_ts", + "api_sample_sms_notification_fetchgeneratesms", + "api_sample_sms_notification_fetchsamplesmsnotificationlist", + "api_sample_sms_notification_fetchtogglesmsnotified", + "api_sample_sms_notification_ts", + "api_storage_entry_sms_notification_fetchgeneratestorageentrysms", + "api_storage_entry_sms_notification_fetchstorageentrysmslist", + "api_storage_entry_sms_notification_fetchtogglestorageentrysmsnotified", + "api_storage_entry_sms_notification_ts", "api_storage_release_review_batchreviewstoragerelease", "api_storage_release_review_batchstoresample", "api_storage_release_review_fetchstoragereleasereviewdetail", @@ -548,6 +565,7 @@ "api_workflow_fetchversionlist", "api_workflow_ts", "cell_product_type_projectpricesystemtab_if", + "checkup_management_checkupdetaildialog_loaddata", "components_approvaldialog_handlesubmit", "components_approvaldialog_loadformdata", "infusion_schedule_outbound_chain_view_index_loaddetail", @@ -567,8 +585,11 @@ "views_system_workflow_components_approvaldialog_vue" ], "1": [ + "api_admin_fetchdepttree", + "api_admin_fetchuserlist", "api_business_fetchuploadattachment", - "api_oaapproval_fetchoarelatedsummaries", + "api_oaapproval_fetchoaflowpreview", + "api_oaapproval_fetchoatemplatedetail", "art_fireworks_effect_index_fireworksystem", "art_fireworks_effect_index_fireworksystem_constructor", "art_fireworks_effect_index_fireworksystem_createfirework", @@ -581,10 +602,31 @@ "checkup_management_checkupmarkcheckeddialog_handlereportupload", "clean_room_index_walk", "components_core_layouts_art_fireworks_effect_index_vue", - "core_iframeroutemanager_iframeroutemanager_add", - "core_iframeroutemanager_iframeroutemanager_save", - "core_routevalidator_routevalidator_loglayouterror", - "env_detect_record_index_if", + "components_oa_approval_oa_condition_fields_ts", + "components_oa_approval_oastartformdialog_vue", + "drafts_index_openfordraft", + "flow_tree_flowbranch_branchconditionlines", + "infusion_schedule_management_infusionschedulehandoverdialog_remotesearchhandoverusers", + "oa_approval_oa_condition_fields_collectconditionreferencedfields", + "oa_approval_oa_condition_fields_collectformconditionreferencedfields", + "oa_approval_oa_condition_fields_flattenformrules", + "oa_approval_oa_condition_fields_formatconditionpreview", + "oa_approval_oa_condition_fields_formatconditionpreviewlines", + "oa_approval_oa_condition_fields_formatconditionvaluetext", + "oa_approval_oa_condition_fields_listconditioncandidates", + "oa_approval_oa_condition_fields_normalizeschemaconditionrequired", + "oa_approval_oa_condition_fields_opsforfieldtype", + "oa_approval_oa_condition_fields_walkrules", + "oa_approval_oainstancedetaildialog_applyschema", + "oa_approval_oastartformdialog_closedialog", + "oa_approval_oastartformdialog_flowneedsinitiatorselect", + "oa_approval_oastartformdialog_loadflowpreview", + "oa_approval_oastartformdialog_loadusers", + "oa_approval_oastartformdialog_ondialogclosed", + "oa_approval_oastartformdialog_onpick", + "oa_approval_oastartformdialog_openforapp", + "oa_approval_oastartformdialog_resetflowpreviewstate", + "oa_approval_oastartformdialog_scheduleflowpreview", "pilot_application_applicationdetaildialog_async", "pilot_application_applicationdetaildialog_initdata", "pilot_application_applicationdetaildialog_validate", @@ -595,58 +637,55 @@ "production_work_order_dispatchtaskdialog_resolvecreatedtaskid", "production_work_order_dispatchtaskdialog_seedrouteoptionsfromitems", "production_work_order_stepdetailpanel_for", - "protocol_protocolrefundrecorddialog_onfilechange", "qc_work_order_qcworkordertasksublist_canfillallthird", "qc_work_order_qcworkordertasksublist_companyprogresstagtype", "qc_work_order_qcworkordertasksublist_companyprogresstext", "qc_work_order_qcworkordertasksublist_displayitemkey", "qc_work_order_qcworkordertasksublist_if", "qc_work_order_qcworkordertasksublist_iscurrentuserassignee", + "role_index_walk", + "start_index_isstartroute", + "start_index_ondialogsaved", + "start_index_ondialogsubmitted", + "start_index_onpick", + "start_index_openhistory", + "start_index_queryopenkey", + "start_index_restorestarttabtitle", + "start_index_tryopenfromquery", "storage_facility_index_push", - "utils_oa_approval_print_buildformrows", - "utils_oa_approval_print_buildoaprintfilename", - "utils_oa_approval_print_buildoaprintmodel", - "utils_oa_approval_print_canvastopdf", - "utils_oa_approval_print_ensureplainobject", - "utils_oa_approval_print_exportoaapprovalpdf", - "utils_oa_approval_print_extracturls", - "utils_oa_approval_print_flow_buildflowitems", - "utils_oa_approval_print_flow_isccprogressitem", - "utils_oa_approval_print_flow_peopledisplaynames", - "utils_oa_approval_print_flow_progressnodestatustext", - "utils_oa_approval_print_flow_progresspersonlinesfromitem", - "utils_oa_approval_print_flow_statustext", - "utils_oa_approval_print_flow_ts", - "utils_oa_approval_print_formatfieldvalue", - "utils_oa_approval_print_formatnow", - "utils_oa_approval_print_formatscalar", - "utils_oa_approval_print_formattripitineraryforprint", - "utils_oa_approval_print_formattriplegforprint", - "utils_oa_approval_print_ishttpurl", - "utils_oa_approval_print_israngefieldrule", - "utils_oa_approval_print_related_assemblerelatedprintblocks", - "utils_oa_approval_print_related_buildrelatedprintblocksfrommap", - "utils_oa_approval_print_related_collectreachableids", - "utils_oa_approval_print_related_datepart", - "utils_oa_approval_print_related_loadsummariesintomap", - "utils_oa_approval_print_related_normalizerelatedidsfromformvalue", - "utils_oa_approval_print_related_placeholdersummary", - "utils_oa_approval_print_related_titleof", - "utils_oa_approval_print_related_ts", - "utils_oa_approval_print_renderpagelabelcanvas", - "utils_oa_approval_print_resolvestamp", - "utils_oa_approval_print_ts", - "utils_oa_approval_print_walkrules", + "utils_oa_confirmation_kit_runtime_applyconfirmationkitdatelinkage", + "utils_oa_confirmation_kit_runtime_applyconfirmationkitdefaults", + "utils_oa_confirmation_kit_runtime_asrules", + "utils_oa_confirmation_kit_runtime_clearconfirmationdatemanualandrelink", + "utils_oa_confirmation_kit_runtime_ensureconfirmationkitvisibility", + "utils_oa_confirmation_kit_runtime_findconfirmationkitbyhireorprobation", + "utils_oa_confirmation_kit_runtime_isconfirmationdatefield", + "utils_oa_confirmation_kit_runtime_isemptyvalue", + "utils_oa_confirmation_kit_runtime_kitchildren", + "utils_oa_confirmation_kit_runtime_listconfirmationkitbindings", + "utils_oa_confirmation_kit_runtime_listmanualconfirmationdatefields", + "utils_oa_confirmation_kit_runtime_markconfirmationdatemanual", + "utils_oa_confirmation_kit_runtime_resolvebinding", + "utils_oa_confirmation_kit_runtime_ts", + "utils_oa_confirmation_kit_runtime_walk", "utils_oa_dept_value_formatdeptdisplay", "utils_oa_dept_value_isdeptvalueitem", "utils_oa_dept_value_normalizedeptmodelvalue", "utils_oa_dept_value_ts", - "utils_oa_form_value_display_formatformvaluedisplay", - "utils_oa_form_value_display_formatregionlikedisplay", - "utils_oa_form_value_display_formatunknownobjectdisplay", - "utils_oa_form_value_display_ts", + "utils_oa_flow_preview_refresh_addfield", + "utils_oa_flow_preview_refresh_addformulafields", + "utils_oa_flow_preview_refresh_buildflowpreviewrequestkey", + "utils_oa_flow_preview_refresh_collectconditionfields", + "utils_oa_flow_preview_refresh_nodetype", + "utils_oa_flow_preview_refresh_pickrelevantformdata", + "utils_oa_flow_preview_refresh_shouldshowflowpreviewspinner", + "utils_oa_flow_preview_refresh_shouldskipflowpreviewrequest", + "utils_oa_flow_preview_refresh_sortkeysdeep", + "utils_oa_flow_preview_refresh_ts", + "utils_oa_form_create_upload_ensureoauploadruleprops", "utils_oa_leave_kit_runtime_applyleavekitdurationstoformdata", "utils_oa_leave_kit_runtime_asrules", + "utils_oa_leave_kit_runtime_ensureleavekitruleprops", "utils_oa_leave_kit_runtime_findleavekitbystartorend", "utils_oa_leave_kit_runtime_isleavedurationfield", "utils_oa_leave_kit_runtime_listleavekitbindings", @@ -673,6 +712,7 @@ "utils_oa_overtime_kit_runtime_applyovertimekitdefaults", "utils_oa_overtime_kit_runtime_applyovertimekitdurationstoformdata", "utils_oa_overtime_kit_runtime_asrules", + "utils_oa_overtime_kit_runtime_ensureovertimekitruleprops", "utils_oa_overtime_kit_runtime_findovertimekitbystartorend", "utils_oa_overtime_kit_runtime_hasovertimekit", "utils_oa_overtime_kit_runtime_isemptypersonvalue", @@ -682,54 +722,40 @@ "utils_oa_overtime_kit_runtime_resolvebinding", "utils_oa_overtime_kit_runtime_ts", "utils_oa_overtime_kit_runtime_walk", + "utils_oa_owner_field_defaults_applyownerfielddefaults", + "utils_oa_owner_field_defaults_buildcurrentownerdeptsync", + "utils_oa_owner_field_defaults_buildcurrentownerperson", + "utils_oa_owner_field_defaults_ensureownerfieldruleprops", + "utils_oa_owner_field_defaults_flattenrules", + "utils_oa_owner_field_defaults_isemptyownervalue", "utils_oa_owner_field_defaults_isownerdeptvalue", "utils_oa_owner_field_defaults_isownerpersonvalue", + "utils_oa_owner_field_defaults_resolvecurrentownerdept", + "utils_oa_owner_field_defaults_ts", + "utils_oa_owner_field_defaults_walkrules", "utils_oa_person_value_extractpersonids", "utils_oa_person_value_formatpersondisplay", "utils_oa_person_value_ispersonvalueitem", "utils_oa_person_value_normalizepersonmodelvalue", "utils_oa_person_value_ts", - "utils_oa_post_value_extractpostids", - "utils_oa_post_value_formatpostdisplay", - "utils_oa_post_value_ispostvalueitem", - "utils_oa_post_value_normalizepostmodelvalue", - "utils_oa_post_value_ts", - "utils_oa_progress_person_lines_displayname", - "utils_oa_progress_person_lines_formatprogresspersonlines", - "utils_oa_progress_person_lines_pushline", - "utils_oa_progress_person_lines_statusfallbacklabel", - "utils_oa_progress_person_lines_toneforstatus", - "utils_oa_progress_person_lines_transfertargetname", - "utils_oa_progress_person_lines_ts", - "utils_oa_receive_account_value_formatreceiveaccountdisplay", - "utils_oa_receive_account_value_formatreceiveaccountline", - "utils_oa_receive_account_value_isreceiveaccountsnapshot", - "utils_oa_receive_account_value_isreceiveaccounttype", - "utils_oa_receive_account_value_normalizereceiveaccountmodelvalue", - "utils_oa_receive_account_value_optionlabelfromrow", - "utils_oa_receive_account_value_snapshotfromrow", - "utils_oa_receive_account_value_trimsnapshot", - "utils_oa_receive_account_value_ts", + "utils_oa_resignation_kit_runtime_applyresignationkitdefaults", + "utils_oa_resignation_kit_runtime_asrules", + "utils_oa_resignation_kit_runtime_isempty", + "utils_oa_resignation_kit_runtime_listresignationkitbindings", + "utils_oa_resignation_kit_runtime_resolvebinding", + "utils_oa_resignation_kit_runtime_ts", + "utils_oa_resignation_kit_runtime_walk", "utils_oa_trip_kit_runtime_applytripkitdaystoformdata", "utils_oa_trip_kit_runtime_asrules", "utils_oa_trip_kit_runtime_listtripkits", "utils_oa_trip_kit_runtime_resolvetripkitfields", "utils_oa_trip_kit_runtime_ts", "utils_oa_trip_kit_runtime_walk", - "utils_oa_upload_file_type_formatoauploadpathsforprint", "utils_production_task_set_mapworkordertaskstosetitems", "utils_production_task_set_resolveworkorderlabstoragecode", "utils_production_task_set_reverttasknameplaceholders", "utils_production_task_set_ts", - "utils_protocol_audit_flatten_asrecord", - "utils_protocol_audit_flatten_extractcollectdate", - "utils_protocol_audit_flatten_firstpaytimelines", - "utils_protocol_audit_flatten_flattenprotocolauditrows", - "utils_protocol_audit_flatten_readsampleitems", - "utils_protocol_audit_flatten_str", - "utils_protocol_audit_flatten_ts", - "utils_protocol_audit_flatten_uniqueprotocolsfromflatrows", - "utils_protocol_payment_installment_defaultinstallmentratios", + "utils_protocol_payment_installment_addmonthsymd", "utils_qc_company_task_group_allthirdpartycardsubtitle", "utils_qc_company_task_group_buildtasklistdisplayitems", "utils_qc_company_task_group_companygroupprogress", @@ -741,15 +767,6 @@ "utils_qc_company_task_group_trimid", "utils_qc_company_task_group_ts", "utils_qc_company_task_group_uniqueassigneenames", - "utils_qc_item_param_options_assignqcitemparamrowkeys", - "utils_qc_item_param_options_formatqcparamoptiontext", - "utils_qc_item_param_options_hydrateqcitemparamsforform", - "utils_qc_item_param_options_onqcitemparamtypechange", - "utils_qc_item_param_options_parseqcparamoptions", - "utils_qc_item_param_options_parseqcparamoptiontext", - "utils_qc_item_param_options_serializeqcitemparamsforsave", - "utils_qc_item_param_options_ts", - "utils_qc_item_param_options_validateqcitemparamsbeforesave", "utils_qc_work_order_card_meta_buildcellstoragegroupcontext", "utils_qc_work_order_card_meta_builddonationgroupcontext", "utils_qc_work_order_card_meta_buildgroupcontextsubtitle", @@ -757,7 +774,6 @@ "utils_qc_work_order_card_meta_buildoutboundpilotgroupcontext", "utils_qc_work_order_card_meta_formatdetectmethods", "utils_qc_work_order_card_meta_formatoutboundschedulesortno", - "utils_qc_work_order_card_meta_formatoutboundscheduletotalcount", "utils_qc_work_order_card_meta_formatqctarget", "utils_qc_work_order_card_meta_formattempcellproductfield", "utils_qc_work_order_card_meta_getcardmetaconfig", @@ -771,26 +787,24 @@ "utils_qc_work_order_card_meta_resolveprotocolgroupstatus", "utils_qc_work_order_card_meta_resolvetemporaryworkordercardtitle", "utils_qc_work_order_card_meta_ts", - "utils_qc_work_order_selection_expand_collectinboundsamplesiblingids", - "utils_qc_work_order_selection_expand_expandinboundworkorderidsforexport", - "utils_qc_work_order_selection_expand_isinboundsampleworkorder", - "utils_qc_work_order_selection_expand_resolveinboundsampleselectionkey", - "utils_qc_work_order_selection_expand_ts", - "utils_rmb_upper_tormbupper", - "utils_rmb_upper_ts", - "utils_storage_slot_options_validateuniquestorageslotids", + "utils_tree_path_buildpathlabelmapfromtreeroots", + "views_oa_approval_drafts_index_vue", + "views_oa_approval_start_index_vue", "views_operations_clean_room_index_vue", "views_pilot_service_pilot_application_applicationdetaildialog_vue", "views_production_production_work_order_dispatchtaskdialog_vue", "views_production_production_work_order_stepdetailpanel_vue", - "views_quality_inspection_env_detect_record_index_vue", "views_quality_inspection_qc_work_order_qcworkorderbatchsublist_vue", - "views_quality_inspection_qc_work_order_qcworkordertasksublist_vue" + "views_quality_inspection_qc_work_order_qcworkordertasksublist_vue", + "views_system_role_index_vue" ], "2": [ "api_protocol_sample_payment_fetchsamplepaymentdetail", "components_business_payment_receipt_confirm_dialog_index_vue", + "components_common_decimal_input_index_vue", "components_oa_approval_oatripitinerary_vue", + "decimal_input_index_handlefocus", + "decimal_input_index_handleinput", "oa_approval_oatripitinerary_if", "payment_info_paymentbysampletab_buildpreviewrow", "payment_info_paymentbysampletab_findearliestconfirmableschedule", @@ -828,12 +842,13 @@ "protocol_protocolformdialog_sumactivesamplepreferentialprices", "protocol_protocolformdialog_syncprotocoltotalfromsamples", "protocol_protocolformdialog_togglepaymentdetailexpand", - "protocol_samplepaymentpanel_fullfileurl", - "protocol_samplepaymentpanel_if", - "protocol_samplepaymentpanel_isimageurl", "utils_chinese_numeral_parse_parsebirthorder", "utils_chinese_numeral_parse_parsechinesenumeral", "utils_chinese_numeral_parse_ts", + "utils_decimal_input_format_formatdecimaldisplaynatural", + "utils_decimal_input_format_parsedecimaltonumber", + "utils_decimal_input_format_sanitizedecimalinput", + "utils_decimal_input_format_ts", "utils_expected_cell_count_formatexpectedcellcountdisplay", "utils_expected_cell_count_ts", "utils_numeric_input_parse_extractnumericpart", @@ -926,6 +941,7 @@ "utils_protocol_payment_installment_calcinstallmentdueamountsbyratios", "utils_protocol_payment_installment_calcunpaidamountsbyratios", "utils_protocol_payment_installment_defaultinstallmentamounts", + "utils_protocol_payment_installment_defaultinstallmentratios", "utils_protocol_payment_installment_finalizeinstallmentratios", "utils_protocol_payment_installment_isinstallmentamountsvalid", "utils_protocol_payment_installment_isinstallmentratiosvalid", @@ -972,6 +988,7 @@ "utils_protocol_sample_payment_sumsamplecontractamounts", "utils_protocol_sample_payment_sumtransferamountbystatuses", "utils_protocol_sample_payment_ts", + "utils_qc_work_order_card_meta_formatoutboundscheduletotalcount", "utils_storage_slot_range_fill_applyslotidstoemptylines", "utils_storage_slot_range_fill_copyfirstproductlineattrs", "utils_storage_slot_range_fill_isslotavailable", @@ -982,167 +999,9 @@ "utils_storage_slot_range_fill_sortboxslots", "utils_storage_slot_range_fill_ts", "views_cell_storage_payment_info_paymentbysampletab_vue", - "views_cell_storage_protocol_protocolformdialog_vue", - "views_cell_storage_protocol_samplepaymentpanel_vue" + "views_cell_storage_protocol_protocolformdialog_vue" ], "3": [ - "api_production_task_fetchaddtaskstep", - "api_production_task_fetchcopytaskstep", - "api_production_task_fetchcreateproductiontask", - "api_production_task_fetchcreateproductiontasksfromset", - "api_production_task_fetchdeleteproductiontask", - "api_production_task_fetchdeletetaskstep", - "api_production_task_fetchdispatchproductiontasksteps", - "api_production_task_fetchforwardproductiontask", - "api_production_task_fetchproductiontaskdetail", - "api_production_task_fetchproductiontaskeditorinit", - "api_production_task_fetchproductiontasklist", - "api_production_task_fetchproductiontasksbyworkorderids", - "api_production_task_fetchreassignproductiontask", - "api_production_task_fetchreordertasksteps", - "api_production_task_fetchreplacestepconfiguredreagents", - "api_production_task_fetchreplacestepconsumables", - "api_production_task_fetchreplacestepdatacollection", - "api_production_task_fetchreplacestepoutputs", - "api_production_task_fetchreplacestepreagents", - "api_production_task_fetchsettaskinputs", - "api_production_task_fetchsettaskoutputs", - "api_production_task_fetchspawntemporaryworkorderfromtask", - "api_production_task_fetchupdateproductiontask", - "api_production_task_fetchupdateproductiontaskstatus", - "api_production_task_fetchupdatetaskstep", - "api_production_task_groupproductiontasksbyworkorderid", - "api_production_task_scopequery", - "api_production_task_ts", - "api_production_waste_output_fetchproductionwasteoutputlist", - "api_production_waste_output_ts", - "api_sample_production_reception_fetchproductionreceptionbysamplecode", - "api_sample_production_reception_fetchproductionreceptionsubmit", - "api_sample_production_reception_fetchproductionreceptionupdate", - "api_sample_production_reception_ts", - "api_sample_storage_cert_qc_fetchsamplestoragecertqc", - "api_sample_storage_cert_qc_savesamplestoragecertqc", - "api_sample_storage_cert_qc_ts", - "attachment_zip_progress_dialog_index_handledialogclose", - "components_business_attachment_zip_progress_dialog_index_vue", - "components_business_storage_cert_qc_dialog_index_vue", - "components_business_work_order_remark_dialog_index_vue", - "components_core_layouts_art_settings_panel_widget_settingitem_vue", - "components_oa_approval_flow_tree_flowbranch_vue", - "components_oa_approval_flow_tree_types_ts", - "components_oa_approval_oaflowpreview_vue", - "core_iframeroutemanager_iframeroutemanager_clear", - "flow_tree_flowbranch_branchname", - "flow_tree_flowbranch_nodesummary", - "flow_tree_flowbranch_onadd", - "flow_tree_flowbranch_onaddingroup", - "flow_tree_types_createdefaultflow", - "flow_tree_types_createendnode", - "flow_tree_types_createnode", - "flow_tree_types_ensureflowroot", - "flow_tree_types_findparent", - "flow_tree_types_insertbetween", - "flow_tree_types_newid", - "flow_tree_types_normalizeccassigneetypes", - "flow_tree_types_removechildnode", - "flow_tree_types_removefromsubtree", - "flow_tree_types_summarizeapprover", - "infusion_schedule_management_schedulecanceldialog_handlesubmit", - "oa_approval_oaflowpreview_avatarchar", - "oa_approval_oaflowpreview_avatarcolor", - "oa_approval_oaflowpreview_avatarinitial", - "oa_approval_oaflowpreview_chiptitle", - "oa_approval_oaflowpreview_confirmpicker", - "oa_approval_oaflowpreview_isinitiatorselect", - "oa_approval_oaflowpreview_islocalselected", - "oa_approval_oaflowpreview_localpeople", - "oa_approval_oaflowpreview_memberoptionlabel", - "oa_approval_oaflowpreview_memberoptionsub", - "oa_approval_oaflowpreview_onmemberpick", - "oa_approval_oaflowpreview_onpickerclosed", - "oa_approval_oaflowpreview_openpicker", - "oa_approval_oaflowpreview_removepickermember", - "oa_approval_oaflowpreview_removeuser", - "oa_approval_oaflowpreview_resolveuser", - "oa_approval_oaflowpreview_signbadge", - "oa_approval_oaflowpreview_signbadgetext", - "oa_approval_oaflowpreview_updateselect", - "production_work_order_completeproductiontaskdialog_handleclose", - "production_work_order_completeproductiontaskdialog_handlesubmit", - "production_work_order_dispatchstepdialog_mergeduseroptions", - "production_work_order_dispatchstepdialog_onuserselectvisible", - "production_work_order_dispatchstepdialog_remotesearchusers", - "production_work_order_edittaskinfodialog_handleclose", - "production_work_order_edittaskinfodialog_handlesubmit", - "production_work_order_edittaskinfodialog_loadtaskdetail", - "production_work_order_forwardtaskdialog_handleclose", - "production_work_order_forwardtaskdialog_handlesubmit", - "production_work_order_forwardtaskdialog_onuserselectvisible", - "production_work_order_forwardtaskdialog_remotesearchusers", - "production_work_order_samplereceptiondialog_buildpayload", - "production_work_order_samplereceptiondialog_ensuredefaulthandler", - "production_work_order_samplereceptiondialog_handleclosed", - "production_work_order_samplereceptiondialog_initdialog", - "production_work_order_samplereceptiondialog_loadassignees", - "production_work_order_samplereceptiondialog_onhandlerchange", - "production_work_order_samplereceptiondialog_populatefromproduction", - "production_work_order_samplereceptiondialog_protocolstatustagtypebysource", - "production_work_order_samplereceptiondialog_resetprodform", - "production_work_order_samplereceptiondialog_searchassignees", - "production_work_order_samplereceptiondialog_submitproduction", - "production_work_order_samplereceptiondialog_todaydate", - "production_work_order_taskexecutiondialog_handleforwardsuccess", - "production_work_order_taskexecutiondialog_handlespawntemporaryworkorderclick", - "production_work_order_taskexecutiondialog_if", - "production_work_order_taskexecutiondialog_onsteplistdrop", - "production_work_order_taskexecutiondialog_openforwarddialog", - "production_work_order_taskexecutiondialog_savetaskattachmentsmanual", - "production_work_order_taskexecutiondialog_savetaskequipmentmanual", - "production_work_order_taskexecutiondialog_savetaskoutputsmanual", - "production_work_order_taskexecutiondialog_stepseqclass", - "production_work_order_taskexecutiondialog_submitspawntemporary", - "storage_cert_qc_dialog_index_applydetail", - "storage_cert_qc_dialog_index_displayvalue", - "storage_cert_qc_dialog_index_handlesave", - "storage_cert_qc_dialog_index_loaddetail", - "storage_cert_qc_dialog_index_resetformkeys", - "utils_production_complete_work_order_gate_checkcompleteproductionworkordergate", - "utils_production_complete_work_order_gate_logic_classifytasksforcompleteworkordergate", - "utils_production_complete_work_order_gate_logic_findtaskstepgateissue", - "utils_production_complete_work_order_gate_logic_needsproductionreceptiongate", - "utils_production_complete_work_order_gate_logic_ts", - "utils_production_complete_work_order_gate_ts", - "utils_production_labels_extractproductiontasksteps", - "utils_production_step_assignee_fetchproductionstepassigneeusers", - "utils_production_step_assignee_invalidateproductionstepassigneeusercache", - "utils_production_step_assignee_productionstepassigneeuserlistparams", - "utils_production_step_assignee_ts", - "utils_storage_cert_qc_fields_resolvestoragecertqctypekey", - "utils_storage_cert_qc_fields_supportsstoragecertqc", - "utils_storage_cert_qc_fields_ts", - "views_pilot_service_infusion_schedule_management_schedulecanceldialog_vue", - "views_production_production_work_order_completeproductiontaskdialog_vue", - "views_production_production_work_order_dispatchqcworkorderdialog_vue", - "views_production_production_work_order_dispatchstepdialog_vue", - "views_production_production_work_order_edittaskinfodialog_vue", - "views_production_production_work_order_forwardtaskdialog_vue", - "views_production_production_work_order_index_vue", - "views_production_production_work_order_productionworkordertabbar_vue", - "views_production_production_work_order_samplereceptiondialog_vue", - "views_production_production_work_order_tabs_productionworkordertabpanel_vue", - "views_production_production_work_order_taskexecutiondialog_vue", - "views_production_waste_output_index_vue", - "waste_output_index_handlereset", - "waste_output_index_handlesearch", - "waste_output_index_handleview", - "waste_output_index_hastaskid", - "waste_output_index_loaddata", - "widget_settingitem_emit", - "widget_settingitem_if", - "work_order_remark_dialog_index_handleclose", - "work_order_remark_dialog_index_handlesubmit" - ], - "4": [ "api_production_task_fetchmyproductiontasks", "api_qc_task_fetchcompleteqctask", "api_qc_task_fetchcreateqctask", @@ -1153,6 +1012,7 @@ "api_qc_task_fetchstartqctask", "api_qc_task_fetchupdateqctaskattachments", "api_qc_task_ts", + "components_business_sample_qc_batch_readonly_index_vue", "composables_inbound_view_sections_buildtemporaryviewsections", "composables_useinboundsiblingnavigation_buildinsertedsiblingcard", "composables_useinboundsiblingnavigation_fetchminetasksforworkorders", @@ -1160,6 +1020,15 @@ "composables_useinboundsiblingnavigation_useinboundsiblingnavigation", "composables_useqcworkordertabpage_shouldapplytriggersourcefilter", "composables_useqcworkordertabpage_useqcworkordertabpage", + "composables_useqcworkordertaskviewsections_appendtaskresultsections", + "composables_useqcworkordertaskviewsections_batchtasksforpanelslot", + "composables_useqcworkordertaskviewsections_buildconfiguredonlysection", + "composables_useqcworkordertaskviewsections_isbatchtaskpanelslot", + "composables_useqcworkordertaskviewsections_loadenrichedqcworkordertasks", + "composables_useqcworkordertaskviewsections_materialrowsfromapi", + "composables_useqcworkordertaskviewsections_taskattachmentslotname", + "composables_useqcworkordertaskviewsections_taskmaterialslotname", + "composables_useqcworkordertaskviewsections_useqcworkordertaskslothelpers", "pilot_application_applicationviewdialog_for", "process_processroutetab_if", "process_processroutetab_set", @@ -1168,10 +1037,9 @@ "qc_work_order_qcworkorderprotocolgroup_sampletabstatustagtype", "qc_work_order_qcworkorderprotocolgroup_sampletypelabel", "qc_work_order_qcworkorderprotocolgroup_workorderstatustagtype", - "qc_work_order_qcworkordersamplegroup_handlesiblinglinkclick", - "qc_work_order_qcworkordersamplegroup_workorderstatustagtype", - "qc_work_order_qcworkordersamplegroup_workordertabtitle", - "qc_work_order_qcworkordersinglecard_resolveinboundworkordercardtitle", + "sample_qc_batch_readonly_index_ensureactivebatchkey", + "sample_qc_batch_readonly_index_loadbatches", + "sample_qc_batch_readonly_index_syncsections", "source_chain_chaintoflow_annotateforkedges", "source_chain_chaintoflow_chainapitoelements", "source_chain_chaintoflow_haschaingraphcontent", @@ -1219,6 +1087,17 @@ "utils_production_work_order_tabs_toworkorderlistquery", "utils_production_work_order_tabs_ts", "utils_production_work_order_tabs_workordermatchestab", + "utils_qc_inspection_batch_batchtaskspanelslot", + "utils_qc_inspection_batch_buildbatchsectionsfrombatches", + "utils_qc_inspection_batch_buildbatchsectionsfromtasks", + "utils_qc_inspection_batch_createemptyinspectionbatch", + "utils_qc_inspection_batch_fetchqcinspectionbatches", + "utils_qc_inspection_batch_fetchqcinspectionbatchesbatch", + "utils_qc_inspection_batch_loadqcinspectionbatches", + "utils_qc_inspection_batch_resolvebatchsections", + "utils_qc_inspection_batch_resolvebatchsectiontasks", + "utils_qc_inspection_batch_resolvedefaultqcbatchname", + "utils_qc_inspection_batch_ts", "utils_qc_mine_task_groups_buildminecardlistmodel", "utils_qc_mine_task_groups_buildminetablerows", "utils_qc_mine_task_groups_buildpresettasksmap", @@ -1270,7 +1149,6 @@ "utils_qc_work_order_inbound_sample_cards_resolveprotocolsiblinglinksforgroup", "utils_qc_work_order_inbound_sample_cards_resolveprotocolsiblingsamplesforgroup", "utils_qc_work_order_inbound_sample_cards_sampletypenameforgroup", - "utils_qc_work_order_inbound_sample_cards_scrolltoinboundsamplecard", "utils_qc_work_order_inbound_sample_cards_ts", "utils_qc_work_order_tabs_filtertimelabelfortab", "utils_qc_work_order_tabs_getqcworkordertab", @@ -1290,10 +1168,139 @@ "views_production_process_processroutetab_vue", "views_quality_inspection_qc_work_order_composables_useinboundsiblingnavigation_ts", "views_quality_inspection_qc_work_order_composables_useqcworkordertabpage_ts", - "views_quality_inspection_qc_work_order_qcworkordercardlist_vue", - "views_quality_inspection_qc_work_order_qcworkorderprotocolgroup_vue", - "views_quality_inspection_qc_work_order_qcworkordersamplegroup_vue", - "views_quality_inspection_qc_work_order_qcworkordersinglecard_vue" + "views_quality_inspection_qc_work_order_composables_useqcworkordertaskviewsections_ts", + "views_quality_inspection_qc_work_order_qcworkorderprotocolgroup_vue" + ], + "4": [ + "api_admin_batchsavepilotcheckup", + "api_admin_deletepilotcheckupactualitem", + "api_admin_fetchcheckupinstitutionlist", + "api_admin_fetchcheckupinstitutionpackagedetail", + "api_admin_fetchcheckupinstitutionpackages", + "api_admin_fetchpilotcheckupdetail", + "api_admin_fetchpilotcheckuplist", + "api_admin_fetchupdatepostboundroles", + "api_admin_markpilotcheckupchecked", + "api_admin_savepilotcheckup", + "api_admin_savepilotcheckupactualitems", + "api_admin_savepilotcheckupreserveitems", + "checkup_management_checkup_reserve_items_actualitemexists", + "checkup_management_checkup_reserve_items_appendcheckupactualitems", + "checkup_management_checkup_reserve_items_appendcheckupreserveitems", + "checkup_management_checkup_reserve_items_dedupecheckupactualitems", + "checkup_management_checkup_reserve_items_dedupecheckupreserveitems", + "checkup_management_checkup_reserve_items_findduplicateactualitemnames", + "checkup_management_checkup_reserve_items_findduplicatereserveitemnames", + "checkup_management_checkup_reserve_items_issameactualitem", + "checkup_management_checkup_reserve_items_issamereserveitem", + "checkup_management_checkup_reserve_items_mapreserverowstoactualitems", + "checkup_management_checkup_reserve_items_normalizereserveprojectname", + "checkup_management_checkup_reserve_items_parseprojectfee", + "checkup_management_checkup_reserve_items_reserveitemexists", + "checkup_management_checkup_reserve_items_reserveitemkey", + "checkup_management_checkup_reserve_items_sumreserveitemsfee", + "checkup_management_checkup_reserve_items_toreservecandidate", + "checkup_management_checkupdetaildialog_addactualitem", + "checkup_management_checkupdetaildialog_deleteactualitem", + "checkup_management_checkupdetaildialog_downloadreport", + "checkup_management_checkupdetaildialog_fullfileurl", + "checkup_management_checkupdetaildialog_handlemarkchecked", + "checkup_management_checkupdetaildialog_handlesave", + "checkup_management_checkupdetaildialog_isimagefile", + "checkup_management_checkupdetaildialog_removereport", + "checkup_management_checkupdetaildialog_saveactualitem", + "checkup_management_checkupfilldialog_confirmpackageselect", + "checkup_management_checkupfilldialog_confirmprojectselect", + "checkup_management_checkupfilldialog_ensureinstitutioninoptions", + "checkup_management_checkupfilldialog_handleopenpackageselector", + "checkup_management_checkupfilldialog_handleopenprojectselector", + "checkup_management_checkupfilldialog_handlepackageview", + "checkup_management_checkupfilldialog_handlesubmit", + "checkup_management_checkupfilldialog_isprojectalreadyinreserve", + "checkup_management_checkupfilldialog_loadeditdata", + "checkup_management_checkupfilldialog_loadinstitutioncatalog", + "checkup_management_checkupfilldialog_oninstitutionchange", + "checkup_management_checkupfilldialog_oninstitutionselectvisible", + "checkup_management_checkupfilldialog_parselist", + "checkup_management_checkupfilldialog_projectstatuslabel", + "checkup_management_checkupfilldialog_projectstatustagtype", + "checkup_management_checkupfilldialog_remotesearchinstitutions", + "checkup_management_checkupfilldialog_requireinstitution", + "checkup_management_checkupmarkcheckeddialog_confirmpackageselect", + "checkup_management_checkupmarkcheckeddialog_confirmprojectselect", + "checkup_management_checkupmarkcheckeddialog_downloadreport", + "checkup_management_checkupmarkcheckeddialog_formatnowdatetime", + "checkup_management_checkupmarkcheckeddialog_fullfileurl", + "checkup_management_checkupmarkcheckeddialog_handleopenpackageselector", + "checkup_management_checkupmarkcheckeddialog_handleopenprojectselector", + "checkup_management_checkupmarkcheckeddialog_handlepackageview", + "checkup_management_checkupmarkcheckeddialog_handlesubmit", + "checkup_management_checkupmarkcheckeddialog_isimagefile", + "checkup_management_checkupmarkcheckeddialog_isprojectalreadyinactual", + "checkup_management_checkupmarkcheckeddialog_loaddata", + "checkup_management_checkupmarkcheckeddialog_loadinstitutioncatalog", + "checkup_management_checkupmarkcheckeddialog_parseattachmenturls", + "checkup_management_checkupmarkcheckeddialog_parselist", + "checkup_management_checkupmarkcheckeddialog_projectstatuslabel", + "checkup_management_checkupmarkcheckeddialog_projectstatustagtype", + "checkup_management_checkupmarkcheckeddialog_removereport", + "checkup_management_checkupmarkcheckeddialog_requireinstitution", + "checkup_management_checkupregisterdialog_confirmpackageselect", + "checkup_management_checkupregisterdialog_confirmprojectselect", + "checkup_management_checkupregisterdialog_handleopenpackageselector", + "checkup_management_checkupregisterdialog_handleopenprojectselector", + "checkup_management_checkupregisterdialog_handlepackageview", + "checkup_management_checkupregisterdialog_handlesubmit", + "checkup_management_checkupregisterdialog_loadinstitutioncatalog", + "checkup_management_checkupregisterdialog_oninstitutionchange", + "checkup_management_checkupregisterdialog_requireinstitution", + "checkup_management_checkupregisterdialog_reserveitemspayload", + "components_business_dialog_form_body_index_vue", + "pilot_application_applicationdetaildialog_if", + "pilot_application_checkupmanagementtab_caneditactualcheckupinfo", + "pilot_application_checkupmanagementtab_caneditreserve", + "pilot_application_checkupmanagementtab_canfillcheckupinfo", + "pilot_application_checkupmanagementtab_canmarkchecked", + "pilot_application_checkupmanagementtab_handlebatchreservefromlist", + "pilot_application_checkupmanagementtab_handleeditactual", + "pilot_application_checkupmanagementtab_handleeditreserve", + "pilot_application_checkupmanagementtab_handlefill", + "pilot_application_checkupmanagementtab_handlemarkchecked", + "pilot_application_checkupmanagementtab_handlereset", + "pilot_application_checkupmanagementtab_handlesearch", + "pilot_application_checkupmanagementtab_handleselectionchange", + "pilot_application_checkupmanagementtab_handleview", + "pilot_application_checkupmanagementtab_hasprimarycheckupactions", + "pilot_application_checkupmanagementtab_isrowselectable", + "pilot_application_checkupmanagementtab_loaddata", + "pilot_application_checkupmanagementtab_onbatchreservesuccess", + "pilot_application_checkupmanagementtab_onfillsuccess", + "pilot_application_checkupmanagementtab_oninstitutionselectvisible", + "pilot_application_checkupmanagementtab_remotesearchinstitutions", + "post_index_collectallpostbinddeptids", + "post_index_collectpostbinddeptsubtreeids", + "post_index_expandpostbinddepttree", + "post_index_findpostbinddeptnodebyid", + "post_index_handlepostrolesubmit", + "presign_order_presignorderformdialog_attachmentstourls", + "presign_order_presignorderformdialog_fullfileurl", + "presign_order_presignorderformdialog_if", + "presign_order_presignorderformdialog_isimageurl", + "presign_order_presignorderformdialog_istransferlocked", + "presign_order_presignorderformdialog_makeuserstub", + "presign_order_presignorderformdialog_normalizetransfer", + "presign_order_presignorderformdialog_todayymd", + "presign_order_presignorderformdialog_userstubfromstore", + "views_cell_storage_presign_order_presignorderformdialog_vue", + "views_pilot_service_checkup_management_checkup_reserve_items_ts", + "views_pilot_service_checkup_management_checkupdetaildialog_vue", + "views_pilot_service_checkup_management_checkupfilldialog_vue", + "views_pilot_service_checkup_management_checkupmarkcheckeddialog_vue", + "views_pilot_service_checkup_management_checkupregisterdialog_vue", + "views_pilot_service_checkup_management_checkupreserveitemstable_vue", + "views_pilot_service_pilot_application_checkupmanagementtab_vue", + "views_production_production_material_productionmaterialconfiguredcatalogdialog_vue", + "views_system_post_index_vue" ], "5": [ "api_oaapproval_fetchaddsignoatask", @@ -1366,7 +1373,6 @@ "done_index_opendetail", "done_index_statustext", "done_index_statustype", - "drafts_index_openfordraft", "mentions_index_buildfilterparams", "mentions_index_currentprintername", "mentions_index_handlereset", @@ -1393,7 +1399,6 @@ "oa_approval_oainstancedetaildialog_statustext", "oa_approval_oainstancedetaildialog_statustype", "oa_approval_oaprogresschain_avatarchar", - "oa_approval_oastartformdialog_openforapp", "pending_index_buildfilterparams", "pending_index_handlereset", "pending_index_handlesearch", @@ -1416,28 +1421,146 @@ "receive_accounts_index_regiontext", "receive_accounts_index_requirewhenbank", "receive_accounts_index_resetform", - "start_index_isstartroute", - "start_index_ondialogsaved", - "start_index_ondialogsubmitted", - "start_index_onpick", - "start_index_openhistory", - "start_index_queryopenkey", - "start_index_restorestarttabtitle", - "start_index_tryopenfromquery", "utils_oa_menu_badge_collectvisibleoamenubadgecodes", "utils_oa_menu_badge_refreshoamenubadges", "utils_oa_menu_badge_resolveoamenubadgecode", "utils_oa_menu_badge_ts", "views_oa_approval_cc_index_vue", "views_oa_approval_done_index_vue", - "views_oa_approval_drafts_index_vue", "views_oa_approval_mentions_index_vue", "views_oa_approval_mine_index_vue", "views_oa_approval_pending_index_vue", - "views_oa_approval_receive_accounts_index_vue", - "views_oa_approval_start_index_vue" + "views_oa_approval_receive_accounts_index_vue" ], "6": [ + "category_index_catch", + "category_index_collectids", + "category_index_handlecategorytableselectionchange", + "category_index_handleview", + "components_business_system_view_dialog_index_vue", + "composables_inbound_view_sections_buildinboundbasicsection", + "composables_inbound_view_sections_buildinboundcellstorageviewsections", + "composables_inbound_view_sections_buildinboundresearchdonationviewsections", + "composables_inbound_view_sections_buildreceptionsection", + "composables_inbound_view_sections_buildtransportsection", + "composables_inbound_view_sections_formatmetric", + "composables_inbound_view_sections_resolveinboundprotocolstatus", + "composables_production_view_sections_buildbasefields", + "composables_production_view_sections_buildinboundbasicsection", + "composables_production_view_sections_buildinboundcellstorageproductionviewsections", + "composables_production_view_sections_buildinboundresearchdonationproductionviewsections", + "composables_production_view_sections_buildoutboundbasicexperimentproductionviewsections", + "composables_production_view_sections_buildoutboundclinicalproductionviewsections", + "composables_production_view_sections_buildoutbounddetail", + "composables_production_view_sections_buildoutboundpilotproductionviewsections", + "composables_production_view_sections_buildoutboundsections", + "composables_production_view_sections_buildreceptionsection", + "composables_production_view_sections_buildtemporaryproductionviewsections", + "composables_production_view_sections_buildtransportsections", + "composables_production_view_sections_formatmetric", + "composables_production_view_sections_isoutboundproductionview", + "composables_production_view_sections_sourcetag", + "composables_production_view_sections_wostatustag", + "composables_useinboundqcviewdialog_useinboundqcviewdialog", + "composables_useoutboundqcviewdialog_useoutboundqcviewdialog", + "payment_info_paymentbysampletab_formatduedate", + "reagent_index_isstocklow", + "reagent_index_sourcetypelabel", + "reagent_index_sourcetypetagtype", + "reagent_index_truncatetext", + "sample_payment_detail_dialog_index_formatduedate", + "storage_release_review_storagereleaseviewdialog_formatmetric", + "storage_release_review_storagereleaseviewdialog_protocolstatusdisplay", + "storage_release_review_storagereleaseviewdialog_sideconclusiondisplay", + "storage_release_review_storagereleaseviewdialog_sideconclusiontag", + "utils_clinical_payment_flow_steps_buildpaymentflowsteps", + "utils_clinical_payment_flow_steps_dash", + "utils_clinical_payment_flow_steps_formatamount", + "utils_clinical_payment_flow_steps_hasfinancereceipt", + "utils_clinical_payment_flow_steps_hasfinancerefund", + "utils_clinical_payment_flow_steps_hasreceiptsubmit", + "utils_clinical_payment_flow_steps_hasrefundsubmit", + "utils_clinical_payment_flow_steps_receiptsubmittime", + "utils_clinical_payment_flow_steps_resolvestepstate", + "utils_clinical_payment_flow_steps_ts", + "utils_datetime_display_formatdateonly", + "utils_datetime_display_formatdatetimedisplay", + "utils_datetime_display_formatdisplayordash", + "utils_datetime_display_lookslikedatetime", + "utils_datetime_display_ts", + "utils_mask_sensitive_maskidcard", + "utils_mask_sensitive_ts", + "utils_oa_approval_print_formatdateonly", + "utils_outbound_qc_pilot_view_sections_buildbasicexperimentapplicationfields", + "utils_outbound_qc_pilot_view_sections_builddefaultapplicationfields", + "utils_outbound_qc_pilot_view_sections_buildoutboundpilotcontextsections", + "utils_outbound_qc_pilot_view_sections_isoutboundpilotserviceqctask", + "utils_outbound_qc_pilot_view_sections_resolveoutboundviewvariant", + "utils_outbound_qc_pilot_view_sections_ts", + "utils_production_work_order_card_meta_protocolstatuslabelfortab", + "utils_sample_production_reception_fields_formatprocesstimedate", + "utils_sample_production_reception_fields_formatproductionfieldvalue", + "utils_sample_production_reception_fields_infectiousfourtestlabel", + "utils_sample_production_reception_fields_resolveproductionreceptionfields", + "utils_sample_production_reception_fields_ts", + "utils_sample_reception_view_sections_buildproductionreceptionviewsection", + "utils_sample_reception_view_sections_buildqcreceptionviewfields", + "utils_sample_reception_view_sections_ts", + "utils_status_tag_registry_applicationstatuslabel", + "utils_status_tag_registry_applicationstatustagtype", + "utils_status_tag_registry_biztypelabel", + "utils_status_tag_registry_biztypetagtype", + "utils_status_tag_registry_cellproductstatuslabel", + "utils_status_tag_registry_cellproductstatustagtype", + "utils_status_tag_registry_checkupstatuslabel", + "utils_status_tag_registry_checkupstatustagtype", + "utils_status_tag_registry_createlabelgetter", + "utils_status_tag_registry_createtagtypegetter", + "utils_status_tag_registry_donationstatusdisplaylabel", + "utils_status_tag_registry_feestatuslabel", + "utils_status_tag_registry_feestatustagtype", + "utils_status_tag_registry_infusionmethodlabel", + "utils_status_tag_registry_infusionstatuslabel", + "utils_status_tag_registry_infusionstatustagtype", + "utils_status_tag_registry_isinactiveprotocolstatus", + "utils_status_tag_registry_joinworkorderstatusfilter", + "utils_status_tag_registry_matchesworkorderstatusfilter", + "utils_status_tag_registry_normalizescheduleproductstatuscode", + "utils_status_tag_registry_outboundmaterialcellstatuslabel", + "utils_status_tag_registry_outboundmaterialcellstatustagtype", + "utils_status_tag_registry_productstatuslabel", + "utils_status_tag_registry_productstatustagtype", + "utils_status_tag_registry_protocolstatusdisplaylabel", + "utils_status_tag_registry_protocolstatusdisplaylabelbysource", + "utils_status_tag_registry_qcconclusiondisplaylabel", + "utils_status_tag_registry_qcconclusiondisplaytagtype", + "utils_status_tag_registry_qcstatuslabel", + "utils_status_tag_registry_qcstatustagtype", + "utils_status_tag_registry_releasereviewconclusionlabel", + "utils_status_tag_registry_releasereviewconclusiontagtype", + "utils_status_tag_registry_schedulestatuslabel", + "utils_status_tag_registry_schedulestatustagtype", + "utils_status_tag_registry_ts", + "utils_status_tag_registry_txnstatuslabel", + "utils_status_tag_registry_txnstatustagtype", + "utils_status_tag_registry_txntypelabel", + "utils_status_tag_registry_txntypetagtype", + "utils_void_type_dict_buildfallbackoptions", + "utils_void_type_dict_ensurevoidtypedictloading", + "utils_void_type_dict_getvoidtypeoptions", + "utils_void_type_dict_loadvoidtypedict", + "utils_void_type_dict_resolvevoidtypelabel", + "utils_void_type_dict_ts", + "views_pilot_service_checkup_management_checkupviewdialog_vue", + "views_production_production_work_order_composables_production_view_sections_ts", + "views_production_reagent_index_vue", + "views_quality_inspection_qc_work_order_composables_inbound_view_sections_ts", + "views_quality_inspection_qc_work_order_composables_useinboundqcviewdialog_ts", + "views_quality_inspection_qc_work_order_composables_useoutboundqcviewdialog_ts", + "views_quality_inspection_storage_release_review_storagereleaseviewdialog_vue", + "views_system_category_index_vue" + ], + "7": [ "amount_input_index_handleblur", "amount_input_index_handlefocus", "amount_input_index_handleinput", @@ -1480,6 +1603,7 @@ "channel_channelviewdialog_formatprice", "channel_channelviewdialog_fullfileurl", "channel_channelviewdialog_normalizeagreementrow", + "clinical_payment_confirm_index_formatamount", "clinical_payment_confirm_refundconfirmtab_formatamount", "clinical_payment_confirm_timeline_index_formatprotocolamount", "clinical_payment_confirm_timeline_index_handleattachmentpreview", @@ -1490,6 +1614,7 @@ "components_business_clinical_payment_confirm_timeline_index_vue", "components_business_sample_payment_detail_dialog_index_vue", "components_common_amount_input_index_vue", + "composables_production_view_sections_buildfeesection", "payment_confirm_index_buildconfirmedparams", "payment_confirm_index_buildpendingparams", "payment_confirm_index_cardsubtitle", @@ -1510,24 +1635,19 @@ "payment_confirm_index_submitconfirm", "payment_confirm_index_submitreject", "payment_info_paymentbysampletab_formatamount", - "presign_order_presignorderformdialog_attachmentstourls", - "presign_order_presignorderformdialog_fullfileurl", - "presign_order_presignorderformdialog_if", - "presign_order_presignorderformdialog_isimageurl", - "presign_order_presignorderformdialog_istransferlocked", - "presign_order_presignorderformdialog_makeuserstub", - "presign_order_presignorderformdialog_normalizetransfer", - "presign_order_presignorderformdialog_todayymd", - "presign_order_presignorderformdialog_userstubfromstore", "protocol_protocolformdialog_ensuretransfertypeoptions", "protocol_protocolformdialog_fmtmoney", "protocol_protocolrefundrecorddialog_fmtmoney", "protocol_protocolrefundrecorddialog_loadrecords", "protocol_protocolrefundrecorddialog_onclosed", + "protocol_protocolrefundrecorddialog_onfilechange", "protocol_protocolrefundrecorddialog_onsave", "protocol_protocolrefundrecorddialog_opencreate", "protocol_protocolrefundrecorddialog_openedit", "protocol_protocolrefundrecorddialog_resetform", + "protocol_samplepaymentpanel_fullfileurl", + "protocol_samplepaymentpanel_if", + "protocol_samplepaymentpanel_isimageurl", "sample_payment_detail_dialog_index_addtransfer", "sample_payment_detail_dialog_index_attachmentstourls", "sample_payment_detail_dialog_index_ensuretransferattachments", @@ -1538,7 +1658,6 @@ "sample_payment_detail_dialog_index_istransferfieldslocked", "sample_payment_detail_dialog_index_loaddetail", "sample_payment_detail_dialog_index_notifychanged", - "sample_payment_detail_dialog_index_number", "sample_payment_detail_dialog_index_paymentschedulestatuslabel", "sample_payment_detail_dialog_index_removetransfer", "sample_payment_detail_dialog_index_savetransferrow", @@ -1560,137 +1679,12 @@ "utils_transfer_type_dict_ts", "views_cell_storage_payment_confirm_index_vue", "views_cell_storage_payment_confirm_invoicerequesttab_vue", - "views_cell_storage_presign_order_presignorderformdialog_vue", "views_cell_storage_protocol_protocolrefundrecorddialog_vue", + "views_cell_storage_protocol_samplepaymentpanel_vue", "views_pilot_service_channel_channelserviceordertab_vue", "views_pilot_service_channel_channelviewdialog_vue", "views_pilot_service_channel_index_vue" ], - "7": [ - "api_admin_batchsavepilotcheckup", - "api_admin_deletepilotcheckupactualitem", - "api_admin_fetchcheckupinstitutionlist", - "api_admin_fetchcheckupinstitutionpackagedetail", - "api_admin_fetchcheckupinstitutionpackages", - "api_admin_fetchpilotcheckupactualitems", - "api_admin_fetchpilotcheckupdetail", - "api_admin_fetchpilotcheckuplist", - "api_admin_fetchpilotcheckupreserveitems", - "api_admin_markpilotcheckupchecked", - "api_admin_savepilotcheckup", - "api_admin_savepilotcheckupactualitems", - "api_admin_savepilotcheckupreserveitems", - "api_production_material_fetchmyproductionmaterialconsumptions", - "checkup_management_checkup_reserve_items_actualitemexists", - "checkup_management_checkup_reserve_items_appendcheckupactualitems", - "checkup_management_checkup_reserve_items_appendcheckupreserveitems", - "checkup_management_checkup_reserve_items_dedupecheckupactualitems", - "checkup_management_checkup_reserve_items_dedupecheckupreserveitems", - "checkup_management_checkup_reserve_items_findduplicateactualitemnames", - "checkup_management_checkup_reserve_items_findduplicatereserveitemnames", - "checkup_management_checkup_reserve_items_issameactualitem", - "checkup_management_checkup_reserve_items_issamereserveitem", - "checkup_management_checkup_reserve_items_mapreserverowstoactualitems", - "checkup_management_checkup_reserve_items_normalizereserveprojectname", - "checkup_management_checkup_reserve_items_parseprojectfee", - "checkup_management_checkup_reserve_items_reserveitemexists", - "checkup_management_checkup_reserve_items_reserveitemkey", - "checkup_management_checkup_reserve_items_sumreserveitemsfee", - "checkup_management_checkup_reserve_items_toreservecandidate", - "checkup_management_checkupdetaildialog_addactualitem", - "checkup_management_checkupdetaildialog_deleteactualitem", - "checkup_management_checkupdetaildialog_downloadreport", - "checkup_management_checkupdetaildialog_fullfileurl", - "checkup_management_checkupdetaildialog_handlemarkchecked", - "checkup_management_checkupdetaildialog_handlesave", - "checkup_management_checkupdetaildialog_isimagefile", - "checkup_management_checkupdetaildialog_loaddata", - "checkup_management_checkupdetaildialog_removereport", - "checkup_management_checkupdetaildialog_saveactualitem", - "checkup_management_checkupfilldialog_confirmpackageselect", - "checkup_management_checkupfilldialog_confirmprojectselect", - "checkup_management_checkupfilldialog_ensureinstitutioninoptions", - "checkup_management_checkupfilldialog_handleopenpackageselector", - "checkup_management_checkupfilldialog_handleopenprojectselector", - "checkup_management_checkupfilldialog_handlepackageview", - "checkup_management_checkupfilldialog_handlesubmit", - "checkup_management_checkupfilldialog_isprojectalreadyinreserve", - "checkup_management_checkupfilldialog_loadeditdata", - "checkup_management_checkupfilldialog_loadinstitutioncatalog", - "checkup_management_checkupfilldialog_oninstitutionchange", - "checkup_management_checkupfilldialog_oninstitutionselectvisible", - "checkup_management_checkupfilldialog_parselist", - "checkup_management_checkupfilldialog_projectstatuslabel", - "checkup_management_checkupfilldialog_projectstatustagtype", - "checkup_management_checkupfilldialog_remotesearchinstitutions", - "checkup_management_checkupfilldialog_requireinstitution", - "checkup_management_checkupmarkcheckeddialog_confirmpackageselect", - "checkup_management_checkupmarkcheckeddialog_confirmprojectselect", - "checkup_management_checkupmarkcheckeddialog_downloadreport", - "checkup_management_checkupmarkcheckeddialog_fullfileurl", - "checkup_management_checkupmarkcheckeddialog_handleopenpackageselector", - "checkup_management_checkupmarkcheckeddialog_handleopenprojectselector", - "checkup_management_checkupmarkcheckeddialog_handlepackageview", - "checkup_management_checkupmarkcheckeddialog_handlesubmit", - "checkup_management_checkupmarkcheckeddialog_isimagefile", - "checkup_management_checkupmarkcheckeddialog_isprojectalreadyinactual", - "checkup_management_checkupmarkcheckeddialog_loaddata", - "checkup_management_checkupmarkcheckeddialog_loadinstitutioncatalog", - "checkup_management_checkupmarkcheckeddialog_parseattachmenturls", - "checkup_management_checkupmarkcheckeddialog_parselist", - "checkup_management_checkupmarkcheckeddialog_projectstatuslabel", - "checkup_management_checkupmarkcheckeddialog_projectstatustagtype", - "checkup_management_checkupmarkcheckeddialog_removereport", - "checkup_management_checkupmarkcheckeddialog_requireinstitution", - "checkup_management_checkupregisterdialog_confirmpackageselect", - "checkup_management_checkupregisterdialog_confirmprojectselect", - "checkup_management_checkupregisterdialog_handleopenpackageselector", - "checkup_management_checkupregisterdialog_handleopenprojectselector", - "checkup_management_checkupregisterdialog_handlepackageview", - "checkup_management_checkupregisterdialog_handlesubmit", - "checkup_management_checkupregisterdialog_loadinstitutioncatalog", - "checkup_management_checkupregisterdialog_oninstitutionchange", - "checkup_management_checkupregisterdialog_requireinstitution", - "checkup_management_checkupregisterdialog_reserveitemspayload", - "components_business_dialog_form_body_index_vue", - "my_production_material_usage_index_handletabchange", - "pilot_application_applicationdetaildialog_if", - "pilot_application_checkupmanagementtab_caneditactualcheckupinfo", - "pilot_application_checkupmanagementtab_caneditreserve", - "pilot_application_checkupmanagementtab_canfillcheckupinfo", - "pilot_application_checkupmanagementtab_canmarkchecked", - "pilot_application_checkupmanagementtab_handlebatchreservefromlist", - "pilot_application_checkupmanagementtab_handleeditactual", - "pilot_application_checkupmanagementtab_handleeditreserve", - "pilot_application_checkupmanagementtab_handlefill", - "pilot_application_checkupmanagementtab_handlemarkchecked", - "pilot_application_checkupmanagementtab_handlereset", - "pilot_application_checkupmanagementtab_handlesearch", - "pilot_application_checkupmanagementtab_handleselectionchange", - "pilot_application_checkupmanagementtab_handleview", - "pilot_application_checkupmanagementtab_hasprimarycheckupactions", - "pilot_application_checkupmanagementtab_isrowselectable", - "pilot_application_checkupmanagementtab_loaddata", - "pilot_application_checkupmanagementtab_onbatchreservesuccess", - "pilot_application_checkupmanagementtab_onfillsuccess", - "pilot_application_checkupmanagementtab_oninstitutionselectvisible", - "pilot_application_checkupmanagementtab_remotesearchinstitutions", - "production_material_materialconsumptiontab_displayconsumptionqty", - "production_material_materialconsumptiontab_handlereset", - "production_material_materialconsumptiontab_handlesearch", - "production_material_materialconsumptiontab_loaddata", - "views_pilot_service_checkup_management_checkup_reserve_items_ts", - "views_pilot_service_checkup_management_checkupdetaildialog_vue", - "views_pilot_service_checkup_management_checkupfilldialog_vue", - "views_pilot_service_checkup_management_checkupmarkcheckeddialog_vue", - "views_pilot_service_checkup_management_checkupregisterdialog_vue", - "views_pilot_service_checkup_management_checkupreserveitemstable_vue", - "views_pilot_service_pilot_application_checkupmanagementtab_vue", - "views_production_my_production_material_usage_index_vue", - "views_production_production_material_materialconsumptiontab_vue", - "views_production_production_material_productionmaterialconfiguredcatalogdialog_vue", - "views_production_production_material_productionmaterialusagetab_vue" - ], "8": [ "api_admin_fetchcheckupinstitutioninvoices", "api_admin_fetchcheckupinstitutionprojects", @@ -1698,19 +1692,16 @@ "api_admin_fetchcreatecheckupinstitutioninvoice", "api_admin_fetchcreatecheckupinstitutionproject", "api_admin_fetchcreateitemunit", + "api_admin_fetchcreatetenant", "api_admin_fetchdeleteitemunit", + "api_admin_fetchdeletetenant", "api_admin_fetchitemunitlist", + "api_admin_fetchtenantlist", "api_admin_fetchupdatecheckupinstitution", "api_admin_fetchupdatecheckupinstitutioninvoice", "api_admin_fetchupdatecheckupinstitutionproject", "api_admin_fetchupdateitemunit", - "api_env_detect_item_fetchcreateenvdetectitem", - "api_env_detect_item_fetchdeleteenvdetectitem", - "api_env_detect_item_fetchenvdetectitemdetail", - "api_env_detect_item_fetchenvdetectitemlist", - "api_env_detect_item_fetchenvdetectitemsactive", - "api_env_detect_item_fetchupdateenvdetectitem", - "api_env_detect_item_ts", + "api_admin_fetchupdatetenant", "api_outbound_release_review_submitoutboundreleasereview", "art_screen_lock_index_disabledevtools", "art_screen_lock_index_handledialogopen", @@ -1720,7 +1711,6 @@ "art_screen_lock_index_ismobile", "art_screen_lock_index_openlockscreen", "art_screen_lock_index_verifypassword", - "cell_sample_type_index_handlebatchdelete", "checkup_institution_index_handleadd", "checkup_institution_index_handleadditem", "checkup_institution_index_handledelete", @@ -1757,17 +1747,6 @@ "core_routevalidator_routevalidator_resolvepath", "core_routevalidator_routevalidator_validate", "dept_index_catch", - "env_detect_item_index_handleadd", - "env_detect_item_index_handlebatchdelete", - "env_detect_item_index_handledelete", - "env_detect_item_index_handleedit", - "env_detect_item_index_handlereset", - "env_detect_item_index_handlesearch", - "env_detect_item_index_handleselectionchange", - "env_detect_item_index_handlesubmit", - "env_detect_item_index_handleview", - "env_detect_item_index_loaddata", - "env_detect_item_index_resetform", "list_donationauditdialog_handlesubmit", "list_donationauditdialog_resetform", "list_donationstatuschangedialog_handlesubmit", @@ -1784,6 +1763,21 @@ "storage_release_review_index_handlebatchstore", "storage_release_review_index_onreviewsubmitted", "storage_release_review_index_openreviewdialog", + "tenant_index_handleadd", + "tenant_index_handlebatchdelete", + "tenant_index_handledelete", + "tenant_index_handledropdowncommand", + "tenant_index_handleedit", + "tenant_index_handlereset", + "tenant_index_handlesearch", + "tenant_index_handleselectionchange", + "tenant_index_handlesubmit", + "tenant_index_handleview", + "tenant_index_isdefaulttenantno", + "tenant_index_isdefaulttenantrow", + "tenant_index_loaddata", + "tenant_index_resetform", + "tenant_index_rowselectable", "unit_index_handleadd", "unit_index_handlebatchdelete", "unit_index_handledelete", @@ -1804,21 +1798,217 @@ "views_cell_storage_protocol_protocolstatuschangedialog_vue", "views_pilot_service_checkup_institution_checkuppackagepanel_vue", "views_pilot_service_checkup_institution_index_vue", - "views_quality_inspection_env_detect_item_index_vue", "views_quality_inspection_storage_release_review_index_vue", "views_research_donation_list_donationauditdialog_vue", "views_research_donation_list_donationstatuschangedialog_vue", "views_system_product_index_vue", + "views_system_tenant_index_vue", "views_system_unit_index_vue" ], "9": [ - "api_admin_fetchgetmenulist", + "api_production_waste_output_fetchproductionwasteoutputlist", + "api_production_waste_output_ts", + "api_sample_production_reception_fetchproductionreceptionbysamplecode", + "api_sample_production_reception_fetchproductionreceptionsubmit", + "api_sample_production_reception_fetchproductionreceptionupdate", + "api_sample_production_reception_ts", + "api_sample_storage_cert_qc_fetchsamplestoragecertqc", + "api_sample_storage_cert_qc_savesamplestoragecertqc", + "api_sample_storage_cert_qc_ts", + "attachment_zip_progress_dialog_index_handledialogclose", + "components_business_attachment_zip_progress_dialog_index_vue", + "components_business_storage_cert_qc_dialog_index_vue", + "components_business_work_order_remark_dialog_index_vue", + "components_core_layouts_art_settings_panel_widget_settingitem_vue", + "components_oa_approval_flow_tree_flowbranch_vue", + "components_oa_approval_oaflowpreview_vue", + "core_iframeroutemanager_iframeroutemanager_clear", + "flow_tree_flowbranch_branchname", + "flow_tree_flowbranch_onadd", + "flow_tree_flowbranch_onaddingroup", + "infusion_schedule_management_schedulecanceldialog_handlesubmit", + "modules_user_search_handlereset", + "modules_user_search_handlesearch", + "oa_approval_oaflowpreview_avatarchar", + "oa_approval_oaflowpreview_avatarcolor", + "oa_approval_oaflowpreview_avatarinitial", + "oa_approval_oaflowpreview_chiptitle", + "oa_approval_oaflowpreview_confirmpicker", + "oa_approval_oaflowpreview_isinitiatorselect", + "oa_approval_oaflowpreview_islocalselected", + "oa_approval_oaflowpreview_localpeople", + "oa_approval_oaflowpreview_memberoptionlabel", + "oa_approval_oaflowpreview_memberoptionsub", + "oa_approval_oaflowpreview_onmemberpick", + "oa_approval_oaflowpreview_onpickerclosed", + "oa_approval_oaflowpreview_openpicker", + "oa_approval_oaflowpreview_removepickermember", + "oa_approval_oaflowpreview_removeuser", + "oa_approval_oaflowpreview_resolveuser", + "oa_approval_oaflowpreview_signbadge", + "oa_approval_oaflowpreview_signbadgetext", + "oa_approval_oaflowpreview_updateselect", + "production_work_order_completeproductiontaskdialog_handleclose", + "production_work_order_completeproductiontaskdialog_handlesubmit", + "production_work_order_dispatchstepdialog_mergeduseroptions", + "production_work_order_dispatchstepdialog_onuserselectvisible", + "production_work_order_dispatchstepdialog_remotesearchusers", + "production_work_order_samplereceptiondialog_buildpayload", + "production_work_order_samplereceptiondialog_ensuredefaulthandler", + "production_work_order_samplereceptiondialog_handleclosed", + "production_work_order_samplereceptiondialog_initdialog", + "production_work_order_samplereceptiondialog_loadassignees", + "production_work_order_samplereceptiondialog_onhandlerchange", + "production_work_order_samplereceptiondialog_populatefromproduction", + "production_work_order_samplereceptiondialog_protocolstatustagtypebysource", + "production_work_order_samplereceptiondialog_resetprodform", + "production_work_order_samplereceptiondialog_searchassignees", + "production_work_order_samplereceptiondialog_submitproduction", + "production_work_order_samplereceptiondialog_todaydate", + "production_work_order_taskexecutiondialog_handleforwardsuccess", + "production_work_order_taskexecutiondialog_handlespawntemporaryworkorderclick", + "production_work_order_taskexecutiondialog_if", + "production_work_order_taskexecutiondialog_onsteplistdrop", + "production_work_order_taskexecutiondialog_openforwarddialog", + "production_work_order_taskexecutiondialog_savetaskattachmentsmanual", + "production_work_order_taskexecutiondialog_savetaskequipmentmanual", + "production_work_order_taskexecutiondialog_savetaskoutputsmanual", + "production_work_order_taskexecutiondialog_stepseqclass", + "production_work_order_taskexecutiondialog_submitspawntemporary", + "storage_cert_qc_dialog_index_applydetail", + "storage_cert_qc_dialog_index_displayvalue", + "storage_cert_qc_dialog_index_handlesave", + "storage_cert_qc_dialog_index_loaddetail", + "storage_cert_qc_dialog_index_resetformkeys", + "utils_production_step_assignee_fetchproductionstepassigneeusers", + "utils_production_step_assignee_invalidateproductionstepassigneeusercache", + "utils_production_step_assignee_productionstepassigneeuserlistparams", + "utils_production_step_assignee_ts", + "utils_storage_cert_qc_fields_resolvestoragecertqctypekey", + "utils_storage_cert_qc_fields_supportsstoragecertqc", + "utils_storage_cert_qc_fields_ts", + "views_pilot_service_infusion_schedule_management_schedulecanceldialog_vue", + "views_production_production_work_order_completeproductiontaskdialog_vue", + "views_production_production_work_order_dispatchqcworkorderdialog_vue", + "views_production_production_work_order_dispatchstepdialog_vue", + "views_production_production_work_order_index_vue", + "views_production_production_work_order_productionworkordertabbar_vue", + "views_production_production_work_order_samplereceptiondialog_vue", + "views_production_production_work_order_tabs_productionworkordertabpanel_vue", + "views_production_production_work_order_taskexecutiondialog_vue", + "views_production_waste_output_index_vue", + "views_system_user_modules_user_search_vue", + "waste_output_index_displaycell", + "waste_output_index_handlereset", + "waste_output_index_handlesearch", + "waste_output_index_handleview", + "waste_output_index_hastaskid", + "waste_output_index_loaddata", + "widget_settingitem_emit", + "widget_settingitem_if", + "work_order_remark_dialog_index_handleclose", + "work_order_remark_dialog_index_handlesubmit" + ], + "10": [ "components_core_layouts_art_settings_panel_composables_usesettingshandlers_ts", "components_core_layouts_art_settings_panel_composables_usesettingspanel_ts", "components_core_layouts_art_settings_panel_composables_usesettingsstate_ts", + "components_oa_approval_flow_tree_types_ts", "composables_usesettingshandlers_usesettingshandlers", "composables_usesettingspanel_usesettingspanel", "composables_usesettingsstate_usesettingsstate", + "core_iframeroutemanager_iframeroutemanager_add", + "core_routepermissionvalidator_routepermissionvalidator", + "core_routepermissionvalidator_routepermissionvalidator_buildmenupathset", + "core_routepermissionvalidator_routepermissionvalidator_checkpathprefix", + "core_routepermissionvalidator_routepermissionvalidator_haspermission", + "core_routepermissionvalidator_routepermissionvalidator_isdynamicroutematch", + "core_routepermissionvalidator_routepermissionvalidator_matchroute", + "core_routepermissionvalidator_routepermissionvalidator_validatepath", + "core_routevalidator_routevalidator_loglayouterror", + "core_useceremony_useceremony", + "core_usetheme_initializetheme", + "core_usetheme_usetheme", + "env_detect_record_index_if", + "flow_tree_flowbranch_nodesummary", + "flow_tree_types_createdefaultflow", + "flow_tree_types_createendnode", + "flow_tree_types_createnode", + "flow_tree_types_ensureflowroot", + "flow_tree_types_findparent", + "flow_tree_types_insertbetween", + "flow_tree_types_newid", + "flow_tree_types_normalizeccassigneetypes", + "flow_tree_types_removechildnode", + "flow_tree_types_removefromsubtree", + "flow_tree_types_summarizeapprover", + "hooks_core_useceremony_ts", + "hooks_core_usetheme_ts", + "qc_work_order_qcworkordersamplegroup_handlesiblinglinkclick", + "qc_work_order_qcworkordersamplegroup_workorderstatustagtype", + "qc_work_order_qcworkordersamplegroup_workordertabtitle", + "qc_work_order_qcworkordersinglecard_resolveinboundworkordercardtitle", + "router_core_routepermissionvalidator_ts", + "ui_animation_themeanimation", + "ui_animation_toggletheme", + "ui_animation_toggletransition", + "utils_protocol_audit_flatten_asrecord", + "utils_protocol_audit_flatten_extractcollectdate", + "utils_protocol_audit_flatten_firstpaytimelines", + "utils_protocol_audit_flatten_flattenprotocolauditrows", + "utils_protocol_audit_flatten_readsampleitems", + "utils_protocol_audit_flatten_str", + "utils_protocol_audit_flatten_ts", + "utils_protocol_audit_flatten_uniqueprotocolsfromflatrows", + "utils_qc_item_param_options_assignqcitemparamrowkeys", + "utils_qc_item_param_options_formatqcparamoptiontext", + "utils_qc_item_param_options_hydrateqcitemparamsforform", + "utils_qc_item_param_options_onqcitemparamtypechange", + "utils_qc_item_param_options_parseqcparamoptions", + "utils_qc_item_param_options_parseqcparamoptiontext", + "utils_qc_item_param_options_serializeqcitemparamsforsave", + "utils_qc_item_param_options_ts", + "utils_qc_item_param_options_validateqcitemparamsbeforesave", + "utils_qc_work_order_inbound_sample_cards_scrolltoinboundsamplecard", + "utils_qc_work_order_selection_expand_collectinboundsamplesiblingids", + "utils_qc_work_order_selection_expand_expandinboundworkorderidsforexport", + "utils_qc_work_order_selection_expand_isinboundsampleworkorder", + "utils_qc_work_order_selection_expand_resolveinboundsampleselectionkey", + "utils_qc_work_order_selection_expand_ts", + "utils_storage_slot_options_buildfullslotcodefromflat", + "utils_storage_slot_options_buildnodecodepathfromflat", + "utils_storage_slot_options_collectleaffacilitynodesintreeorder", + "utils_storage_slot_options_comparestoragefacilityslots", + "utils_storage_slot_options_countoutputsmissingstorageslot", + "utils_storage_slot_options_decodeslotlayoutorder", + "utils_storage_slot_options_ensurestorageslotsinoptions", + "utils_storage_slot_options_filterstorageslotselectoptions", + "utils_storage_slot_options_getlaststorageslotsearchquery", + "utils_storage_slot_options_getstoragefacilityordernum", + "utils_storage_slot_options_getstorageslotordernum", + "utils_storage_slot_options_isoutputmissingstorageslot", + "utils_storage_slot_options_isstorageslotavailable", + "utils_storage_slot_options_isstorageslotselectoptiondisabled", + "utils_storage_slot_options_loadstorageslotselectoptions", + "utils_storage_slot_options_mapremoteslotselectoption", + "utils_storage_slot_options_mapslotstoselectoptions", + "utils_storage_slot_options_parselegacyrowcolslotcode", + "utils_storage_slot_options_parsestorageslotcode", + "utils_storage_slot_options_sortstoragefacilityslots", + "utils_storage_slot_options_ts", + "utils_storage_slot_options_validateuniquestorageslotids", + "utils_tree_path_buildtreepathlabelfromflat", + "utils_tree_path_findtreeselectshortlabel", + "utils_tree_path_resolvetreepathdisplay", + "utils_tree_path_ts", + "utils_ui_animation_ts", + "views_quality_inspection_env_detect_record_index_vue", + "views_quality_inspection_qc_work_order_qcworkordercardlist_vue", + "views_quality_inspection_qc_work_order_qcworkordersamplegroup_vue", + "views_quality_inspection_qc_work_order_qcworkordersinglecard_vue" + ], + "11": [ + "api_admin_fetchgetmenulist", "core_componentloader_componentloader", "core_componentloader_componentloader_constructor", "core_componentloader_componentloader_createemptycomponent", @@ -1848,13 +2038,6 @@ "core_menuprocessor_menuprocessor_resolvedefaultredirect", "core_menuprocessor_menuprocessor_validatemenulist", "core_menuprocessor_menuprocessor_validatemenupaths", - "core_routepermissionvalidator_routepermissionvalidator", - "core_routepermissionvalidator_routepermissionvalidator_buildmenupathset", - "core_routepermissionvalidator_routepermissionvalidator_checkpathprefix", - "core_routepermissionvalidator_routepermissionvalidator_haspermission", - "core_routepermissionvalidator_routepermissionvalidator_isdynamicroutematch", - "core_routepermissionvalidator_routepermissionvalidator_matchroute", - "core_routepermissionvalidator_routepermissionvalidator_validatepath", "core_routeregistry_routeregistry", "core_routeregistry_routeregistry_constructor", "core_routeregistry_routeregistry_getremoveroutefns", @@ -1872,10 +2055,7 @@ "core_routetransformer_routetransformer_transform", "core_useappmode_useappmode", "core_useauth_useauth", - "core_useceremony_useceremony", "core_usecommon_usecommon", - "core_usetheme_initializetheme", - "core_usetheme_usetheme", "guards_aftereach_setupaftereachguard", "guards_beforeeach_closeloading", "guards_beforeeach_getpendingloading", @@ -1892,9 +2072,7 @@ "guards_beforeeach_setupbeforeeachguard", "hooks_core_useappmode_ts", "hooks_core_useauth_ts", - "hooks_core_useceremony_ts", "hooks_core_usecommon_ts", - "hooks_core_usetheme_ts", "http_error_ishttperror", "navigation_route_getfirstmenupath", "navigation_route_getfirstsecondlevelmenupath", @@ -1907,7 +2085,6 @@ "router_core_componentloader_ts", "router_core_iframeroutemanager_ts", "router_core_menuprocessor_ts", - "router_core_routepermissionvalidator_ts", "router_core_routeregistry_ts", "router_core_routetransformer_ts", "router_guards_aftereach_ts", @@ -1916,9 +2093,6 @@ "router_index_ts", "router_sidebar_menu_filter_filtersidebarroutes", "router_sidebar_menu_filter_issidebarnavigableroute", - "ui_animation_themeanimation", - "ui_animation_toggletheme", - "ui_animation_toggletransition", "utils_navigation_route_ts", "utils_navigation_worktab_ts", "utils_router_configurenprogress", @@ -1926,94 +2100,10 @@ "utils_router_resolvesaferedirecttarget", "utils_router_setpagetitle", "utils_router_sidebar_menu_filter_ts", - "utils_router_ts", - "utils_ui_animation_ts" + "utils_router_ts" ], - "10": [ - "api_admin_fetchcellsampletypelist", - "api_admin_fetchcreatecellsampletype", - "api_admin_fetchcreatemenu", - "api_admin_fetchcreatetenant", - "api_admin_fetchdeletecellsampletype", - "api_admin_fetchdeletemenu", - "api_admin_fetchdeletetenant", - "api_admin_fetchmenutree", - "api_admin_fetchtenantlist", - "api_admin_fetchupdatecellsampletype", - "api_admin_fetchupdatemenu", - "api_admin_fetchupdatetenant", - "category_index_catch", - "category_index_collectids", - "category_index_handlecategorytableselectionchange", - "category_index_handleview", - "cell_sample_type_index_handleadd", - "cell_sample_type_index_handledelete", - "cell_sample_type_index_handledropdowncommand", - "cell_sample_type_index_handleedit", - "cell_sample_type_index_handlereset", - "cell_sample_type_index_handlesearch", - "cell_sample_type_index_handlesubmit", - "cell_sample_type_index_handletypelogochange", - "cell_sample_type_index_handletypelogoexceed", - "cell_sample_type_index_handleview", - "cell_sample_type_index_isrowselected", - "cell_sample_type_index_loaddata", - "cell_sample_type_index_resetform", - "cell_sample_type_index_togglerowselection", - "cell_sample_type_index_typelogourl", - "cell_sample_type_index_uploadtypelogofile", - "components_business_system_view_dialog_index_vue", - "composables_useinboundqcviewdialog_useinboundqcviewdialog", - "composables_useoutboundqcviewdialog_useoutboundqcviewdialog", - "dept_index_get", - "dept_index_postorder", - "dept_index_preorder", - "menu_index_applytreeexpand", - "menu_index_handleadd", - "menu_index_handleaddchild", - "menu_index_handledelete", - "menu_index_handledialogclose", - "menu_index_handledropdowncommand", - "menu_index_handleedit", - "menu_index_handlereset", - "menu_index_handlesearch", - "menu_index_handleselectionchange", - "menu_index_handlesubmit", - "menu_index_handleview", - "menu_index_loaddata", - "menu_index_resetform", - "menu_index_toggleexpand", - "reagent_index_isstocklow", - "reagent_index_sourcetypelabel", - "reagent_index_sourcetypetagtype", - "reagent_index_truncatetext", - "tenant_index_handleadd", - "tenant_index_handlebatchdelete", - "tenant_index_handledelete", - "tenant_index_handledropdowncommand", - "tenant_index_handleedit", - "tenant_index_handlereset", - "tenant_index_handlesearch", - "tenant_index_handleselectionchange", - "tenant_index_handlesubmit", - "tenant_index_handleview", - "tenant_index_isdefaulttenantno", - "tenant_index_isdefaulttenantrow", - "tenant_index_loaddata", - "tenant_index_resetform", - "tenant_index_rowselectable", - "views_basic_info_cell_sample_type_index_vue", - "views_basic_info_cell_sample_type_samplecodetab_vue", - "views_pilot_service_checkup_management_checkupviewdialog_vue", - "views_production_reagent_index_vue", - "views_quality_inspection_qc_work_order_composables_useinboundqcviewdialog_ts", - "views_quality_inspection_qc_work_order_composables_useoutboundqcviewdialog_ts", - "views_system_category_index_vue", - "views_system_dept_index_vue", - "views_system_menu_index_vue", - "views_system_tenant_index_vue" - ], - "11": [ + "12": [ + "api_admin_fetchdictdataoptions", "api_business_fetchattachmentdownloadurl", "components_business_infusion_schedule_outbound_chain_view_index_vue", "components_business_outbound_material_readonly_panel_index_vue", @@ -2029,6 +2119,7 @@ "infusion_schedule_outbound_chain_view_index_handleattachmentdownload", "infusion_schedule_outbound_chain_view_index_handleattachmentpreview", "infusion_schedule_outbound_chain_view_index_hasinfusionconfirm", + "material_index_loaddictoptions", "oa_approval_oafcupload_data", "oa_approval_oafcupload_handledownload", "oa_approval_oafcupload_handlepreview", @@ -2059,6 +2150,9 @@ "outbound_release_review_outboundreleasereviewdialog_uniquejoined", "payment_confirm_index_handleattachmentdownload", "payment_info_paymentbysampletab_handleattachdownload", + "sample_freight_freighteditdialog_loadtransportmodeoptions", + "sample_freight_freighteditdialog_onclosed", + "sample_freight_freighteditdialog_open", "utils_attachment_zip_ensureuniquezipentryname", "utils_attachment_zip_fetchattachmentblob", "utils_attachment_zip_formatzipdatestamp", @@ -2093,11 +2187,13 @@ "utils_qc_work_order_attachment_export_taskfoldername", "utils_qc_work_order_attachment_export_ts", "utils_qc_work_order_attachment_export_workorderfoldername", + "views_cell_storage_sample_freight_freighteditdialog_vue", "views_pilot_service_outbound_release_review_outboundreleasereviewdialog_vue", "views_quality_inspection_qc_work_order_composables_useqcviewattachmenthelpers_ts" ], - "12": [ + "13": [ "api_collection_protocol_fetchcollectionprotocollist", + "api_production_work_order_fetchproductionworkorderqcinfo", "api_sample_info_handle_fetchsampleinfoquickregister", "api_sample_info_handle_fetchupdatefullmoonhandle", "api_sample_info_handle_fetchupdatestoragecerthandle", @@ -2124,6 +2220,7 @@ "list_donationviewdialog_sampletypename", "list_donationviewdialog_sitename", "list_donationviewdialog_unitname", + "production_work_order_productionworkorderqcviewdialog_loaddetail", "protocol_finance_confirm_fields_index_for", "protocol_handle_panel_index_applyinitial", "protocol_handle_panel_index_handlesave", @@ -2176,162 +2273,7 @@ "views_quality_inspection_qc_work_order_view_dialogs_view_dialog_registry_ts", "views_research_donation_list_donationviewdialog_vue" ], - "13": [ - "composables_production_view_sections_buildbasefields", - "composables_production_view_sections_buildfeesection", - "composables_production_view_sections_buildinboundbasicsection", - "composables_production_view_sections_buildinboundcellstorageproductionviewsections", - "composables_production_view_sections_buildinboundresearchdonationproductionviewsections", - "composables_production_view_sections_buildoutboundbasicexperimentproductionviewsections", - "composables_production_view_sections_buildoutboundclinicalproductionviewsections", - "composables_production_view_sections_buildoutbounddetail", - "composables_production_view_sections_buildoutboundpilotproductionviewsections", - "composables_production_view_sections_buildoutboundsections", - "composables_production_view_sections_buildreceptionsection", - "composables_production_view_sections_buildtemporaryproductionviewsections", - "composables_production_view_sections_buildtransportsections", - "composables_production_view_sections_formatmetric", - "composables_production_view_sections_isoutboundproductionview", - "composables_production_view_sections_sourcetag", - "composables_production_view_sections_wostatustag", - "storage_release_review_storagereleaseviewdialog_formatmetric", - "storage_release_review_storagereleaseviewdialog_protocolstatusdisplay", - "storage_release_review_storagereleaseviewdialog_sideconclusiondisplay", - "storage_release_review_storagereleaseviewdialog_sideconclusiontag", - "utils_mask_sensitive_maskidcard", - "utils_mask_sensitive_ts", - "utils_outbound_qc_pilot_view_sections_buildbasicexperimentapplicationfields", - "utils_outbound_qc_pilot_view_sections_builddefaultapplicationfields", - "utils_outbound_qc_pilot_view_sections_buildoutboundpilotcontextsections", - "utils_outbound_qc_pilot_view_sections_isoutboundpilotserviceqctask", - "utils_outbound_qc_pilot_view_sections_resolveoutboundviewvariant", - "utils_outbound_qc_pilot_view_sections_ts", - "utils_production_work_order_card_meta_protocolstatuslabelfortab", - "utils_status_tag_registry_applicationstatuslabel", - "utils_status_tag_registry_applicationstatustagtype", - "utils_status_tag_registry_biztypelabel", - "utils_status_tag_registry_biztypetagtype", - "utils_status_tag_registry_cellproductstatuslabel", - "utils_status_tag_registry_cellproductstatustagtype", - "utils_status_tag_registry_checkupstatuslabel", - "utils_status_tag_registry_checkupstatustagtype", - "utils_status_tag_registry_createlabelgetter", - "utils_status_tag_registry_createtagtypegetter", - "utils_status_tag_registry_donationstatusdisplaylabel", - "utils_status_tag_registry_feestatuslabel", - "utils_status_tag_registry_feestatustagtype", - "utils_status_tag_registry_infusionmethodlabel", - "utils_status_tag_registry_infusionstatuslabel", - "utils_status_tag_registry_infusionstatustagtype", - "utils_status_tag_registry_isinactiveprotocolstatus", - "utils_status_tag_registry_joinworkorderstatusfilter", - "utils_status_tag_registry_matchesworkorderstatusfilter", - "utils_status_tag_registry_normalizescheduleproductstatuscode", - "utils_status_tag_registry_outboundmaterialcellstatuslabel", - "utils_status_tag_registry_outboundmaterialcellstatustagtype", - "utils_status_tag_registry_productstatuslabel", - "utils_status_tag_registry_productstatustagtype", - "utils_status_tag_registry_protocolstatusdisplaylabel", - "utils_status_tag_registry_protocolstatusdisplaylabelbysource", - "utils_status_tag_registry_qcconclusiondisplaylabel", - "utils_status_tag_registry_qcconclusiondisplaytagtype", - "utils_status_tag_registry_qcstatuslabel", - "utils_status_tag_registry_qcstatustagtype", - "utils_status_tag_registry_releasereviewconclusionlabel", - "utils_status_tag_registry_releasereviewconclusiontagtype", - "utils_status_tag_registry_schedulestatuslabel", - "utils_status_tag_registry_schedulestatustagtype", - "utils_status_tag_registry_ts", - "utils_status_tag_registry_txnstatuslabel", - "utils_status_tag_registry_txnstatustagtype", - "utils_status_tag_registry_txntypelabel", - "utils_status_tag_registry_txntypetagtype", - "utils_void_type_dict_buildfallbackoptions", - "utils_void_type_dict_ensurevoidtypedictloading", - "utils_void_type_dict_getvoidtypeoptions", - "utils_void_type_dict_loadvoidtypedict", - "utils_void_type_dict_resolvevoidtypelabel", - "utils_void_type_dict_ts", - "views_production_production_work_order_composables_production_view_sections_ts", - "views_quality_inspection_storage_release_review_storagereleaseviewdialog_vue" - ], "14": [ - "components_business_sample_qc_batch_readonly_index_vue", - "composables_inbound_view_sections_buildinboundbasicsection", - "composables_inbound_view_sections_buildinboundcellstorageviewsections", - "composables_inbound_view_sections_buildinboundresearchdonationviewsections", - "composables_inbound_view_sections_buildreceptionsection", - "composables_inbound_view_sections_buildtransportsection", - "composables_inbound_view_sections_formatmetric", - "composables_inbound_view_sections_resolveinboundprotocolstatus", - "composables_useqcworkordertaskviewsections_appendtaskresultsections", - "composables_useqcworkordertaskviewsections_batchtasksforpanelslot", - "composables_useqcworkordertaskviewsections_buildconfiguredonlysection", - "composables_useqcworkordertaskviewsections_isbatchtaskpanelslot", - "composables_useqcworkordertaskviewsections_loadenrichedqcworkordertasks", - "composables_useqcworkordertaskviewsections_materialrowsfromapi", - "composables_useqcworkordertaskviewsections_taskattachmentslotname", - "composables_useqcworkordertaskviewsections_taskmaterialslotname", - "composables_useqcworkordertaskviewsections_useqcworkordertaskslothelpers", - "payment_info_paymentbysampletab_formatduedate", - "sample_payment_detail_dialog_index_formatduedate", - "sample_qc_batch_readonly_index_ensureactivebatchkey", - "sample_qc_batch_readonly_index_loadbatches", - "sample_qc_batch_readonly_index_syncsections", - "utils_clinical_payment_flow_steps_buildpaymentflowsteps", - "utils_clinical_payment_flow_steps_dash", - "utils_clinical_payment_flow_steps_formatamount", - "utils_clinical_payment_flow_steps_hasfinancereceipt", - "utils_clinical_payment_flow_steps_hasfinancerefund", - "utils_clinical_payment_flow_steps_hasreceiptsubmit", - "utils_clinical_payment_flow_steps_hasrefundsubmit", - "utils_clinical_payment_flow_steps_receiptsubmittime", - "utils_clinical_payment_flow_steps_resolvestepstate", - "utils_clinical_payment_flow_steps_ts", - "utils_datetime_display_formatdateonly", - "utils_datetime_display_formatdatetimedisplay", - "utils_datetime_display_formatdisplayordash", - "utils_datetime_display_lookslikedatetime", - "utils_datetime_display_ts", - "utils_oa_approval_print_formatdateonly", - "utils_protocol_payment_flow_steps_buildprotocolpaymentflowsteps", - "utils_protocol_payment_flow_steps_dash", - "utils_protocol_payment_flow_steps_financereceipttime", - "utils_protocol_payment_flow_steps_formatamount", - "utils_protocol_payment_flow_steps_hasfinancereceipt", - "utils_protocol_payment_flow_steps_hasfinancerefund", - "utils_protocol_payment_flow_steps_hasreceiptsubmit", - "utils_protocol_payment_flow_steps_hasrefundsubmit", - "utils_protocol_payment_flow_steps_receiptsubmittime", - "utils_protocol_payment_flow_steps_refundfinancetime", - "utils_protocol_payment_flow_steps_refundsubmittime", - "utils_protocol_payment_flow_steps_resolveattachmenturls", - "utils_protocol_payment_flow_steps_resolvestepstate", - "utils_protocol_payment_flow_steps_ts", - "utils_qc_inspection_batch_batchtaskspanelslot", - "utils_qc_inspection_batch_buildbatchsectionsfrombatches", - "utils_qc_inspection_batch_buildbatchsectionsfromtasks", - "utils_qc_inspection_batch_createemptyinspectionbatch", - "utils_qc_inspection_batch_fetchqcinspectionbatches", - "utils_qc_inspection_batch_fetchqcinspectionbatchesbatch", - "utils_qc_inspection_batch_loadqcinspectionbatches", - "utils_qc_inspection_batch_resolvebatchsections", - "utils_qc_inspection_batch_resolvebatchsectiontasks", - "utils_qc_inspection_batch_resolvedefaultqcbatchname", - "utils_qc_inspection_batch_ts", - "utils_sample_production_reception_fields_formatprocesstimedate", - "utils_sample_production_reception_fields_formatproductionfieldvalue", - "utils_sample_production_reception_fields_infectiousfourtestlabel", - "utils_sample_production_reception_fields_resolveproductionreceptionfields", - "utils_sample_production_reception_fields_ts", - "utils_sample_reception_view_sections_buildproductionreceptionviewsection", - "utils_sample_reception_view_sections_buildqcreceptionviewfields", - "utils_sample_reception_view_sections_ts", - "views_quality_inspection_qc_work_order_composables_inbound_view_sections_ts", - "views_quality_inspection_qc_work_order_composables_useqcworkordertaskviewsections_ts", - "waste_output_index_displaycell" - ], - "15": [ "api_admin_fetchcollectionregionlist", "api_admin_fetchcreatecollectionregion", "api_admin_fetchcreatehospital", @@ -2406,6 +2348,79 @@ "views_pilot_service_pilot_application_infusionscheduletab_vue", "views_quality_inspection_qc_work_order_tabs_qcworkordertabpanel_vue" ], + "15": [ + "api_auth_fetchchangepassword", + "api_auth_fetchgetmenucodes", + "api_auth_fetchgetuserinfo", + "api_auth_fetchlogin", + "api_auth_fetchlogout", + "api_auth_fetchrefreshtoken", + "api_auth_fetchupdateprofile", + "api_auth_fetchuploadprofileavatar", + "api_auth_fetchwxqrcreate", + "api_auth_fetchwxqrstatus", + "api_auth_ts", + "art_screen_lock_index_tologin", + "components_core_layouts_art_header_bar_widget_userprofiledialog_vue", + "guards_beforeeach_fetchuserinfo", + "http_crypto_base64tobytes", + "http_crypto_bytestobase64", + "http_crypto_decryptjson", + "http_crypto_derivekey", + "http_crypto_encryptjson", + "http_crypto_frombase64url", + "http_crypto_hextobytes", + "http_crypto_randomnonce", + "http_crypto_sha256hex", + "http_crypto_tobase64url", + "http_error_geterrormessage", + "http_error_handleerror", + "http_error_httperror", + "http_error_httperror_constructor", + "http_error_httperror_tologdata", + "http_error_showerror", + "http_error_showsuccess", + "http_index_createhttperror", + "http_index_decryptresponseifneeded", + "http_index_delay", + "http_index_getencryptskipreason", + "http_index_getmaxretriesforconfig", + "http_index_handleunauthorizedresponse", + "http_index_ispathexcluded", + "http_index_logout", + "http_index_normalizerequestdata", + "http_index_onrefreshed", + "http_index_onrefreshfailed", + "http_index_parseuseridfromtoken", + "http_index_request", + "http_index_resetunauthorizederror", + "http_index_resolveuserid", + "http_index_retryrequest", + "http_index_shouldretry", + "http_index_subscribetokenrefresh", + "http_index_triggerlogoutwithdebounce", + "utils_http_crypto_ts", + "utils_http_error_ts", + "utils_http_index_ts", + "utils_loginrsa_buildwebencryptedloginbody", + "utils_loginrsa_ts", + "utils_user_avatar_defaultavatarpathbyrole", + "utils_user_avatar_hascustomuseravatar", + "utils_user_avatar_joinpublicasset", + "utils_user_avatar_normalizeavatarstoragekey", + "utils_user_avatar_pickuploadavatarurl", + "utils_user_avatar_pickuseravatarraw", + "utils_user_avatar_resolvestoragefileurl", + "utils_user_avatar_resolveuseravatarfrominfo", + "utils_user_avatar_resolveuseravatarurl", + "utils_user_avatar_ts", + "widget_userprofiledialog_handleavatarchange", + "widget_userprofiledialog_handleclearavatar", + "widget_userprofiledialog_handleclose", + "widget_userprofiledialog_handlesubmit", + "widget_userprofiledialog_loadprofile", + "widget_userprofiledialog_revokeavatarlocalpreview" + ], "16": [ "api_material_fetchbatchdiscardmaterial", "api_material_fetchbatchstockinmaterial", @@ -2425,7 +2440,6 @@ "api_material_usage_fetchmaterialusagelist", "api_material_usage_fetchmymaterialusagelist", "api_material_usage_ts", - "checkup_management_checkupmarkcheckeddialog_formatnowdatetime", "components_business_outbound_material_form_sections_index_vue", "infusion_schedule_management_infusionschedulehandoverdialog_applydefaulthandoveruser", "infusion_schedule_management_infusionschedulehandoverdialog_formathandoveruserlabel", @@ -2478,6 +2492,72 @@ "views_pilot_service_infusion_schedule_management_infusionschedulehandoverdialog_vue" ], "17": [ + "api_oaapproval_fetchoarelatedsummaries", + "core_iframeroutemanager_iframeroutemanager_save", + "utils_oa_approval_print_buildformrows", + "utils_oa_approval_print_buildoaprintfilename", + "utils_oa_approval_print_buildoaprintmodel", + "utils_oa_approval_print_canvastopdf", + "utils_oa_approval_print_ensureplainobject", + "utils_oa_approval_print_exportoaapprovalpdf", + "utils_oa_approval_print_extracturls", + "utils_oa_approval_print_flow_buildflowitems", + "utils_oa_approval_print_flow_isccprogressitem", + "utils_oa_approval_print_flow_peopledisplaynames", + "utils_oa_approval_print_flow_progressnodestatustext", + "utils_oa_approval_print_flow_progresspersonlinesfromitem", + "utils_oa_approval_print_flow_statustext", + "utils_oa_approval_print_flow_ts", + "utils_oa_approval_print_formatfieldvalue", + "utils_oa_approval_print_formatnow", + "utils_oa_approval_print_formatscalar", + "utils_oa_approval_print_formattripitineraryforprint", + "utils_oa_approval_print_formattriplegforprint", + "utils_oa_approval_print_ishttpurl", + "utils_oa_approval_print_israngefieldrule", + "utils_oa_approval_print_related_assemblerelatedprintblocks", + "utils_oa_approval_print_related_buildrelatedprintblocksfrommap", + "utils_oa_approval_print_related_collectreachableids", + "utils_oa_approval_print_related_datepart", + "utils_oa_approval_print_related_loadsummariesintomap", + "utils_oa_approval_print_related_normalizerelatedidsfromformvalue", + "utils_oa_approval_print_related_placeholdersummary", + "utils_oa_approval_print_related_titleof", + "utils_oa_approval_print_related_ts", + "utils_oa_approval_print_renderpagelabelcanvas", + "utils_oa_approval_print_resolvestamp", + "utils_oa_approval_print_ts", + "utils_oa_approval_print_walkrules", + "utils_oa_form_value_display_formatformvaluedisplay", + "utils_oa_form_value_display_formatregionlikedisplay", + "utils_oa_form_value_display_formatunknownobjectdisplay", + "utils_oa_form_value_display_ts", + "utils_oa_post_value_extractpostids", + "utils_oa_post_value_formatpostdisplay", + "utils_oa_post_value_ispostvalueitem", + "utils_oa_post_value_normalizepostmodelvalue", + "utils_oa_post_value_ts", + "utils_oa_progress_person_lines_displayname", + "utils_oa_progress_person_lines_formatprogresspersonlines", + "utils_oa_progress_person_lines_pushline", + "utils_oa_progress_person_lines_statusfallbacklabel", + "utils_oa_progress_person_lines_toneforstatus", + "utils_oa_progress_person_lines_transfertargetname", + "utils_oa_progress_person_lines_ts", + "utils_oa_receive_account_value_formatreceiveaccountdisplay", + "utils_oa_receive_account_value_formatreceiveaccountline", + "utils_oa_receive_account_value_isreceiveaccountsnapshot", + "utils_oa_receive_account_value_isreceiveaccounttype", + "utils_oa_receive_account_value_normalizereceiveaccountmodelvalue", + "utils_oa_receive_account_value_optionlabelfromrow", + "utils_oa_receive_account_value_snapshotfromrow", + "utils_oa_receive_account_value_trimsnapshot", + "utils_oa_receive_account_value_ts", + "utils_oa_upload_file_type_formatoauploadpathsforprint", + "utils_rmb_upper_tormbupper", + "utils_rmb_upper_ts" + ], + "18": [ "api_business_batchsubmitclinicalreceipt", "api_business_batchsubmitclinicalrefund", "api_business_confirmclinicalreceipt", @@ -2506,7 +2586,6 @@ "api_business_ts", "clinical_payment_confirm_index_buildconfirmedparams", "clinical_payment_confirm_index_buildpendingparams", - "clinical_payment_confirm_index_formatamount", "clinical_payment_confirm_index_formattabbadgecount", "clinical_payment_confirm_index_fullfileurl", "clinical_payment_confirm_index_handleconfirmedreset", @@ -2541,68 +2620,6 @@ "views_pilot_service_clinical_payment_confirm_index_vue", "views_pilot_service_clinical_payment_confirm_refundconfirmtab_vue" ], - "18": [ - "api_admin_fetchdepttree", - "api_admin_fetchuserlist", - "api_oaapproval_fetchoaflowpreview", - "api_oaapproval_fetchoatemplatedetail", - "components_oa_approval_oastartformdialog_vue", - "infusion_schedule_management_infusionschedulehandoverdialog_remotesearchhandoverusers", - "oa_approval_oainstancedetaildialog_applyschema", - "oa_approval_oastartformdialog_closedialog", - "oa_approval_oastartformdialog_flowneedsinitiatorselect", - "oa_approval_oastartformdialog_loadflowpreview", - "oa_approval_oastartformdialog_loadusers", - "oa_approval_oastartformdialog_ondialogclosed", - "oa_approval_oastartformdialog_onpick", - "oa_approval_oastartformdialog_resetflowpreviewstate", - "oa_approval_oastartformdialog_scheduleflowpreview", - "utils_oa_confirmation_kit_runtime_applyconfirmationkitdatelinkage", - "utils_oa_confirmation_kit_runtime_applyconfirmationkitdefaults", - "utils_oa_confirmation_kit_runtime_asrules", - "utils_oa_confirmation_kit_runtime_clearconfirmationdatemanualandrelink", - "utils_oa_confirmation_kit_runtime_ensureconfirmationkitvisibility", - "utils_oa_confirmation_kit_runtime_findconfirmationkitbyhireorprobation", - "utils_oa_confirmation_kit_runtime_isconfirmationdatefield", - "utils_oa_confirmation_kit_runtime_isemptyvalue", - "utils_oa_confirmation_kit_runtime_kitchildren", - "utils_oa_confirmation_kit_runtime_listconfirmationkitbindings", - "utils_oa_confirmation_kit_runtime_listmanualconfirmationdatefields", - "utils_oa_confirmation_kit_runtime_markconfirmationdatemanual", - "utils_oa_confirmation_kit_runtime_resolvebinding", - "utils_oa_confirmation_kit_runtime_ts", - "utils_oa_confirmation_kit_runtime_walk", - "utils_oa_flow_preview_refresh_addfield", - "utils_oa_flow_preview_refresh_addformulafields", - "utils_oa_flow_preview_refresh_buildflowpreviewrequestkey", - "utils_oa_flow_preview_refresh_collectconditionfields", - "utils_oa_flow_preview_refresh_nodetype", - "utils_oa_flow_preview_refresh_pickrelevantformdata", - "utils_oa_flow_preview_refresh_shouldshowflowpreviewspinner", - "utils_oa_flow_preview_refresh_shouldskipflowpreviewrequest", - "utils_oa_flow_preview_refresh_sortkeysdeep", - "utils_oa_flow_preview_refresh_ts", - "utils_oa_form_create_upload_ensureoauploadruleprops", - "utils_oa_leave_kit_runtime_ensureleavekitruleprops", - "utils_oa_overtime_kit_runtime_ensureovertimekitruleprops", - "utils_oa_owner_field_defaults_applyownerfielddefaults", - "utils_oa_owner_field_defaults_buildcurrentownerdeptsync", - "utils_oa_owner_field_defaults_buildcurrentownerperson", - "utils_oa_owner_field_defaults_ensureownerfieldruleprops", - "utils_oa_owner_field_defaults_flattenrules", - "utils_oa_owner_field_defaults_isemptyownervalue", - "utils_oa_owner_field_defaults_resolvecurrentownerdept", - "utils_oa_owner_field_defaults_ts", - "utils_oa_owner_field_defaults_walkrules", - "utils_oa_resignation_kit_runtime_applyresignationkitdefaults", - "utils_oa_resignation_kit_runtime_asrules", - "utils_oa_resignation_kit_runtime_isempty", - "utils_oa_resignation_kit_runtime_listresignationkitbindings", - "utils_oa_resignation_kit_runtime_resolvebinding", - "utils_oa_resignation_kit_runtime_ts", - "utils_oa_resignation_kit_runtime_walk", - "utils_protocol_payment_installment_addmonthsymd" - ], "19": [ "api_qc_inspection_item_template_fetchcreateqcinspectionitemtemplate", "api_qc_inspection_item_template_fetchdeleteqcinspectionitemtemplate", @@ -2663,64 +2680,6 @@ "views_quality_inspection_qc_work_order_qcworkorderconfigurebatches_vue" ], "20": [ - "api_admin_fetchdictdataoptions", - "api_qc_work_order_fetchconfigureqcworkorder", - "api_qc_work_order_fetchcreateqcworkorder", - "api_qc_work_order_fetchdeleteqcworkorder", - "api_qc_work_order_fetchmineqcworkordertabunfinishedcounts", - "api_qc_work_order_fetchoutboundqcdetail", - "api_qc_work_order_fetchqcworkorderdetail", - "api_qc_work_order_fetchqcworkorderlist", - "api_qc_work_order_fetchqcworkordersbysample", - "api_qc_work_order_fetchqcworkordertabunfinishedcounts", - "api_qc_work_order_fetchupdateqcworkorder", - "api_qc_work_order_formatqcworkordertabbadgecount", - "api_qc_work_order_ts", - "api_sample_freight_fetchcreatesamplefreight", - "api_sample_freight_fetchlinksamplefreight", - "api_sample_freight_fetchsamplefreightdetail", - "api_sample_freight_fetchsamplefreightlist", - "api_sample_freight_fetchsamplefreightsampleoptions", - "api_sample_freight_fetchunlinksamplefreight", - "api_sample_freight_fetchupdatesamplefreight", - "api_sample_freight_ts", - "art_page_content_index_reload", - "components_business_sample_freight_pick_table_index_vue", - "components_core_layouts_art_page_content_index_vue", - "material_index_loaddictoptions", - "my_qc_task_index_handletabchange", - "my_qc_task_index_reloadtabcounts", - "navigation_jump_handlemenujump", - "navigation_jump_openexternallink", - "sample_freight_freightconfigdialog_clearsampleselection", - "sample_freight_freightconfigdialog_handlesamplesearch", - "sample_freight_freightconfigdialog_handlesubmit", - "sample_freight_freightconfigdialog_issampleselectable", - "sample_freight_freightconfigdialog_loadexistingfreights", - "sample_freight_freightconfigdialog_loadtransportmodeoptions", - "sample_freight_freightconfigdialog_onclosed", - "sample_freight_freightconfigdialog_onpickfreight", - "sample_freight_freightconfigdialog_onsamplepagechange", - "sample_freight_freightconfigdialog_onsamplepagesizechange", - "sample_freight_freightconfigdialog_onsampleselectionchange", - "sample_freight_freightconfigdialog_open", - "sample_freight_freightconfigdialog_reloadsampleoptions", - "sample_freight_freightconfigdialog_toitems", - "sample_freight_freighteditdialog_handlesubmit", - "sample_freight_freighteditdialog_loadtransportmodeoptions", - "sample_freight_freighteditdialog_onclosed", - "sample_freight_freighteditdialog_open", - "utils_navigation_jump_ts", - "utils_qc_menu_badge_collectvisibleqcmenubadgecodes", - "utils_qc_menu_badge_refreshqcmenubadges", - "utils_qc_menu_badge_resolveqcmenubadgecode", - "utils_qc_menu_badge_ts", - "views_cell_storage_sample_freight_freightconfigdialog_vue", - "views_cell_storage_sample_freight_freighteditdialog_vue", - "views_quality_inspection_my_qc_task_index_vue", - "views_quality_inspection_qc_work_order_qcworkordertabbar_vue" - ], - "21": [ "api_admin_completepilotapplication", "api_admin_deletepilotapplication", "api_admin_fetchcreateexhibitionappointment", @@ -2775,56 +2734,7 @@ "views_operations_exhibition_appointment_index_vue", "views_pilot_service_pilot_application_pilotapplicationtab_vue" ], - "22": [ - "api_admin_fetchupdatepostboundroles", - "components_oa_approval_oa_condition_fields_ts", - "flow_tree_flowbranch_branchconditionlines", - "oa_approval_oa_condition_fields_collectconditionreferencedfields", - "oa_approval_oa_condition_fields_collectformconditionreferencedfields", - "oa_approval_oa_condition_fields_flattenformrules", - "oa_approval_oa_condition_fields_formatconditionpreview", - "oa_approval_oa_condition_fields_formatconditionpreviewlines", - "oa_approval_oa_condition_fields_formatconditionvaluetext", - "oa_approval_oa_condition_fields_listconditioncandidates", - "oa_approval_oa_condition_fields_normalizeschemaconditionrequired", - "oa_approval_oa_condition_fields_opsforfieldtype", - "oa_approval_oa_condition_fields_walkrules", - "post_index_collectallpostbinddeptids", - "post_index_collectpostbinddeptsubtreeids", - "post_index_expandpostbinddepttree", - "post_index_findpostbinddeptnodebyid", - "post_index_handlepostrolesubmit", - "role_index_walk", - "utils_storage_slot_options_buildfullslotcodefromflat", - "utils_storage_slot_options_buildnodecodepathfromflat", - "utils_storage_slot_options_collectleaffacilitynodesintreeorder", - "utils_storage_slot_options_comparestoragefacilityslots", - "utils_storage_slot_options_countoutputsmissingstorageslot", - "utils_storage_slot_options_decodeslotlayoutorder", - "utils_storage_slot_options_ensurestorageslotsinoptions", - "utils_storage_slot_options_filterstorageslotselectoptions", - "utils_storage_slot_options_getlaststorageslotsearchquery", - "utils_storage_slot_options_getstoragefacilityordernum", - "utils_storage_slot_options_getstorageslotordernum", - "utils_storage_slot_options_isoutputmissingstorageslot", - "utils_storage_slot_options_isstorageslotavailable", - "utils_storage_slot_options_isstorageslotselectoptiondisabled", - "utils_storage_slot_options_loadstorageslotselectoptions", - "utils_storage_slot_options_mapremoteslotselectoption", - "utils_storage_slot_options_mapslotstoselectoptions", - "utils_storage_slot_options_parselegacyrowcolslotcode", - "utils_storage_slot_options_parsestorageslotcode", - "utils_storage_slot_options_sortstoragefacilityslots", - "utils_storage_slot_options_ts", - "utils_tree_path_buildpathlabelmapfromtreeroots", - "utils_tree_path_buildtreepathlabelfromflat", - "utils_tree_path_findtreeselectshortlabel", - "utils_tree_path_resolvetreepathdisplay", - "utils_tree_path_ts", - "views_system_post_index_vue", - "views_system_role_index_vue" - ], - "23": [ + "21": [ "api_qc_material_fetchbatchdiscardqcmaterial", "api_qc_material_fetchbatchstockinqcmaterial", "api_qc_material_fetchbatchstockoutqcmaterial", @@ -2872,9 +2782,52 @@ "views_quality_inspection_qc_material_qcmaterialstockchecktab_vue", "views_quality_inspection_qc_material_qcmaterialusagetab_vue" ], - "24": [ - "modules_user_search_handlereset", - "modules_user_search_handlesearch", + "22": [ + "api_production_task_fetchproductiontasklist", + "production_work_order_task_step_editor_persistence_createemptysnapshots", + "production_work_order_task_step_editor_persistence_normalizeconfiguredreagents", + "production_work_order_task_step_editor_persistence_normalizeconsumables", + "production_work_order_task_step_editor_persistence_normalizedatacollections", + "production_work_order_task_step_editor_persistence_normalizereagents", + "production_work_order_task_step_editor_persistence_normalizestepoutputs", + "production_work_order_task_step_editor_persistence_scopetostepid", + "production_work_order_task_step_editor_persistence_serializeattachments", + "production_work_order_task_step_editor_persistence_serializeequipmentids", + "production_work_order_task_step_editor_persistence_serializeinputs", + "production_work_order_task_step_editor_persistence_serializeoutputs", + "production_work_order_task_step_editor_persistence_serializestep", + "production_work_order_usetaskstepeditor_fromapiflag", + "production_work_order_usetaskstepeditor_normalizestepsubtables", + "production_work_order_usetaskstepeditor_parseordernum", + "production_work_order_usetaskstepeditor_parsetaskattachmentsfromurls", + "production_work_order_usetaskstepeditor_toapiflag", + "production_work_order_usetaskstepeditor_usetaskstepeditor", + "utils_production_complete_work_order_gate_checkcompleteproductionworkordergate", + "utils_production_complete_work_order_gate_logic_classifytasksforcompleteworkordergate", + "utils_production_complete_work_order_gate_logic_findtaskstepgateissue", + "utils_production_complete_work_order_gate_logic_needsproductionreceptiongate", + "utils_production_complete_work_order_gate_logic_ts", + "utils_production_complete_work_order_gate_ts", + "utils_production_labels_computeworkorderplannedstarttimerange", + "utils_production_labels_extractproductiontasksteps", + "utils_production_labels_formatchinesedateymd", + "utils_production_labels_formatplannedstartdate", + "utils_production_labels_normalizeplannedstartdate", + "utils_production_labels_productionconclusionlabel", + "utils_production_labels_productionconclusiontagtype", + "utils_production_labels_productionordersourcetag", + "utils_production_labels_productionstepassigneedisplayname", + "utils_production_labels_productionstepassigneelabels", + "utils_production_labels_productionstepassigneetext", + "utils_production_labels_productionstepplannedstarttext", + "utils_production_labels_productionstepstatustag", + "utils_production_labels_productiontasktypeforworkorder", + "utils_production_labels_productiontasktypetag", + "utils_production_labels_ts", + "views_production_production_work_order_task_step_editor_persistence_ts", + "views_production_production_work_order_usetaskstepeditor_ts" + ], + "23": [ "socket_index_websocketclient", "socket_index_websocketclient_calculatereconnectdelay", "socket_index_websocketclient_clearalltimers", @@ -2916,52 +2869,9 @@ "table_tablecache_tablecache_log", "table_tablecache_tablecache_set", "utils_socket_index_ts", - "utils_table_tablecache_ts", - "views_system_user_modules_user_search_vue" + "utils_table_tablecache_ts" ], - "25": [ - "art_screen_lock_index_tologin", - "http_crypto_base64tobytes", - "http_crypto_bytestobase64", - "http_crypto_decryptjson", - "http_crypto_derivekey", - "http_crypto_encryptjson", - "http_crypto_frombase64url", - "http_crypto_hextobytes", - "http_crypto_randomnonce", - "http_crypto_sha256hex", - "http_crypto_tobase64url", - "http_error_geterrormessage", - "http_error_handleerror", - "http_error_httperror", - "http_error_httperror_constructor", - "http_error_httperror_tologdata", - "http_error_showerror", - "http_error_showsuccess", - "http_index_createhttperror", - "http_index_decryptresponseifneeded", - "http_index_delay", - "http_index_getencryptskipreason", - "http_index_getmaxretriesforconfig", - "http_index_handleunauthorizedresponse", - "http_index_ispathexcluded", - "http_index_logout", - "http_index_normalizerequestdata", - "http_index_onrefreshed", - "http_index_onrefreshfailed", - "http_index_parseuseridfromtoken", - "http_index_request", - "http_index_resetunauthorizederror", - "http_index_resolveuserid", - "http_index_retryrequest", - "http_index_shouldretry", - "http_index_subscribetokenrefresh", - "http_index_triggerlogoutwithdebounce", - "utils_http_crypto_ts", - "utils_http_error_ts", - "utils_http_index_ts" - ], - "26": [ + "24": [ "locales_index_getdefaultlanguage", "locales_index_ts", "storage_storage_checkstoragecompatibility", @@ -3000,7 +2910,45 @@ "utils_storage_storage_key_manager_ts", "utils_storage_storage_ts" ], - "27": [ + "25": [ + "api_production_task_fetchaddtaskstep", + "api_production_task_fetchcopytaskstep", + "api_production_task_fetchcreateproductiontask", + "api_production_task_fetchcreateproductiontasksfromset", + "api_production_task_fetchdeleteproductiontask", + "api_production_task_fetchdeletetaskstep", + "api_production_task_fetchdispatchproductiontasksteps", + "api_production_task_fetchforwardproductiontask", + "api_production_task_fetchproductiontaskdetail", + "api_production_task_fetchproductiontaskeditorinit", + "api_production_task_fetchproductiontasksbyworkorderids", + "api_production_task_fetchreassignproductiontask", + "api_production_task_fetchreordertasksteps", + "api_production_task_fetchreplacestepconfiguredreagents", + "api_production_task_fetchreplacestepconsumables", + "api_production_task_fetchreplacestepdatacollection", + "api_production_task_fetchreplacestepoutputs", + "api_production_task_fetchreplacestepreagents", + "api_production_task_fetchsettaskinputs", + "api_production_task_fetchsettaskoutputs", + "api_production_task_fetchspawntemporaryworkorderfromtask", + "api_production_task_fetchupdateproductiontask", + "api_production_task_fetchupdateproductiontaskstatus", + "api_production_task_fetchupdatetaskstep", + "api_production_task_groupproductiontasksbyworkorderid", + "api_production_task_scopequery", + "api_production_task_ts", + "production_work_order_edittaskinfodialog_handleclose", + "production_work_order_edittaskinfodialog_handlesubmit", + "production_work_order_edittaskinfodialog_loadtaskdetail", + "production_work_order_forwardtaskdialog_handleclose", + "production_work_order_forwardtaskdialog_handlesubmit", + "production_work_order_forwardtaskdialog_onuserselectvisible", + "production_work_order_forwardtaskdialog_remotesearchusers", + "views_production_production_work_order_edittaskinfodialog_vue", + "views_production_production_work_order_forwardtaskdialog_vue" + ], + "26": [ "api_production_work_order_fetchcompleteproductionworkorder", "api_production_work_order_fetchconfigureproductionworkorder", "api_production_work_order_fetchcreateproductionworkorder", @@ -3012,7 +2960,6 @@ "api_production_work_order_fetchproductionworkorderdetail", "api_production_work_order_fetchproductionworkorderlist", "api_production_work_order_fetchproductionworkorderqcdispatchoptions", - "api_production_work_order_fetchproductionworkorderqcinfo", "api_production_work_order_fetchproductionworkordertabstepcounts", "api_production_work_order_fetchproductionworkordertabstepcountsdeduped", "api_production_work_order_fetchupdateproductionworkorder", @@ -3021,7 +2968,6 @@ "api_production_work_order_isproductionworkordertabstepbadgezero", "api_production_work_order_sumproductionworkordertabstepcounts", "api_production_work_order_ts", - "production_work_order_productionworkorderqcviewdialog_loaddetail", "utils_production_menu_badge_collectvisibleproductionmenubadgecodes", "utils_production_menu_badge_refreshproductionmenubadges", "utils_production_menu_badge_resolveproductionmenubadgecode", @@ -3039,44 +2985,7 @@ "utils_production_work_order_step_badge_todayymd", "utils_production_work_order_step_badge_ts" ], - "28": [ - "production_work_order_task_step_editor_persistence_createemptysnapshots", - "production_work_order_task_step_editor_persistence_normalizeconfiguredreagents", - "production_work_order_task_step_editor_persistence_normalizeconsumables", - "production_work_order_task_step_editor_persistence_normalizedatacollections", - "production_work_order_task_step_editor_persistence_normalizereagents", - "production_work_order_task_step_editor_persistence_normalizestepoutputs", - "production_work_order_task_step_editor_persistence_scopetostepid", - "production_work_order_task_step_editor_persistence_serializeattachments", - "production_work_order_task_step_editor_persistence_serializeequipmentids", - "production_work_order_task_step_editor_persistence_serializeinputs", - "production_work_order_task_step_editor_persistence_serializeoutputs", - "production_work_order_task_step_editor_persistence_serializestep", - "production_work_order_usetaskstepeditor_fromapiflag", - "production_work_order_usetaskstepeditor_normalizestepsubtables", - "production_work_order_usetaskstepeditor_parseordernum", - "production_work_order_usetaskstepeditor_parsetaskattachmentsfromurls", - "production_work_order_usetaskstepeditor_toapiflag", - "production_work_order_usetaskstepeditor_usetaskstepeditor", - "utils_production_labels_computeworkorderplannedstarttimerange", - "utils_production_labels_formatchinesedateymd", - "utils_production_labels_formatplannedstartdate", - "utils_production_labels_normalizeplannedstartdate", - "utils_production_labels_productionconclusionlabel", - "utils_production_labels_productionconclusiontagtype", - "utils_production_labels_productionordersourcetag", - "utils_production_labels_productionstepassigneedisplayname", - "utils_production_labels_productionstepassigneelabels", - "utils_production_labels_productionstepassigneetext", - "utils_production_labels_productionstepplannedstarttext", - "utils_production_labels_productionstepstatustag", - "utils_production_labels_productiontasktypeforworkorder", - "utils_production_labels_productiontasktypetag", - "utils_production_labels_ts", - "views_production_production_work_order_task_step_editor_persistence_ts", - "views_production_production_work_order_usetaskstepeditor_ts" - ], - "29": [ + "27": [ "api_oaapproval_fetchoaapplist", "api_oaapproval_fetchoagrouplist", "api_oaapproval_fetchpublishoaapp", @@ -3111,40 +3020,7 @@ "views_oa_approval_apps_index_vue", "views_oa_approval_designer_index_vue" ], - "30": [ - "api_auth_fetchchangepassword", - "api_auth_fetchgetmenucodes", - "api_auth_fetchgetuserinfo", - "api_auth_fetchlogin", - "api_auth_fetchlogout", - "api_auth_fetchrefreshtoken", - "api_auth_fetchupdateprofile", - "api_auth_fetchuploadprofileavatar", - "api_auth_fetchwxqrcreate", - "api_auth_fetchwxqrstatus", - "api_auth_ts", - "components_core_layouts_art_header_bar_widget_userprofiledialog_vue", - "guards_beforeeach_fetchuserinfo", - "utils_loginrsa_buildwebencryptedloginbody", - "utils_loginrsa_ts", - "utils_user_avatar_defaultavatarpathbyrole", - "utils_user_avatar_hascustomuseravatar", - "utils_user_avatar_joinpublicasset", - "utils_user_avatar_normalizeavatarstoragekey", - "utils_user_avatar_pickuploadavatarurl", - "utils_user_avatar_pickuseravatarraw", - "utils_user_avatar_resolvestoragefileurl", - "utils_user_avatar_resolveuseravatarfrominfo", - "utils_user_avatar_resolveuseravatarurl", - "utils_user_avatar_ts", - "widget_userprofiledialog_handleavatarchange", - "widget_userprofiledialog_handleclearavatar", - "widget_userprofiledialog_handleclose", - "widget_userprofiledialog_handlesubmit", - "widget_userprofiledialog_loadprofile", - "widget_userprofiledialog_revokeavatarlocalpreview" - ], - "31": [ + "28": [ "api_ethnic_fetchethniclist", "api_ethnic_ts", "components_business_sample_transport_form_section_index_vue", @@ -3177,6 +3053,86 @@ "utils_chinese_datetime_parse_ts", "views_research_donation_list_donationformdialog_vue" ], + "29": [ + "api_qc_work_order_fetchconfigureqcworkorder", + "api_qc_work_order_fetchcreateqcworkorder", + "api_qc_work_order_fetchdeleteqcworkorder", + "api_qc_work_order_fetchmineqcworkordertabunfinishedcounts", + "api_qc_work_order_fetchoutboundqcdetail", + "api_qc_work_order_fetchqcworkorderdetail", + "api_qc_work_order_fetchqcworkorderlist", + "api_qc_work_order_fetchqcworkordersbysample", + "api_qc_work_order_fetchqcworkordertabunfinishedcounts", + "api_qc_work_order_fetchupdateqcworkorder", + "api_qc_work_order_formatqcworkordertabbadgecount", + "api_qc_work_order_ts", + "art_page_content_index_reload", + "components_core_layouts_art_page_content_index_vue", + "my_qc_task_index_handletabchange", + "my_qc_task_index_reloadtabcounts", + "navigation_jump_handlemenujump", + "navigation_jump_openexternallink", + "utils_navigation_jump_ts", + "utils_qc_menu_badge_collectvisibleqcmenubadgecodes", + "utils_qc_menu_badge_refreshqcmenubadges", + "utils_qc_menu_badge_resolveqcmenubadgecode", + "utils_qc_menu_badge_ts", + "views_quality_inspection_my_qc_task_index_vue", + "views_quality_inspection_qc_work_order_qcworkordertabbar_vue" + ], + "30": [ + "api_sample_freight_fetchcreatesamplefreight", + "api_sample_freight_fetchlinksamplefreight", + "api_sample_freight_fetchsamplefreightdetail", + "api_sample_freight_fetchsamplefreightlist", + "api_sample_freight_fetchsamplefreightsampleoptions", + "api_sample_freight_fetchunlinksamplefreight", + "api_sample_freight_fetchupdatesamplefreight", + "api_sample_freight_ts", + "components_business_sample_freight_pick_table_index_vue", + "sample_freight_freightconfigdialog_clearsampleselection", + "sample_freight_freightconfigdialog_handlesamplesearch", + "sample_freight_freightconfigdialog_handlesubmit", + "sample_freight_freightconfigdialog_issampleselectable", + "sample_freight_freightconfigdialog_loadexistingfreights", + "sample_freight_freightconfigdialog_loadtransportmodeoptions", + "sample_freight_freightconfigdialog_onclosed", + "sample_freight_freightconfigdialog_onpickfreight", + "sample_freight_freightconfigdialog_onsamplepagechange", + "sample_freight_freightconfigdialog_onsamplepagesizechange", + "sample_freight_freightconfigdialog_onsampleselectionchange", + "sample_freight_freightconfigdialog_open", + "sample_freight_freightconfigdialog_reloadsampleoptions", + "sample_freight_freightconfigdialog_toitems", + "sample_freight_freighteditdialog_handlesubmit", + "views_cell_storage_sample_freight_freightconfigdialog_vue" + ], + "31": [ + "api_admin_fetchcreatemenu", + "api_admin_fetchdeletemenu", + "api_admin_fetchmenutree", + "api_admin_fetchupdatemenu", + "dept_index_get", + "dept_index_postorder", + "dept_index_preorder", + "menu_index_applytreeexpand", + "menu_index_handleadd", + "menu_index_handleaddchild", + "menu_index_handledelete", + "menu_index_handledialogclose", + "menu_index_handledropdowncommand", + "menu_index_handleedit", + "menu_index_handlereset", + "menu_index_handlesearch", + "menu_index_handleselectionchange", + "menu_index_handlesubmit", + "menu_index_handleview", + "menu_index_loaddata", + "menu_index_resetform", + "menu_index_toggleexpand", + "views_system_dept_index_vue", + "views_system_menu_index_vue" + ], "32": [ "api_outbound_release_review_batchreviewoutboundrelease", "api_outbound_release_review_deletereleasereview", @@ -3204,6 +3160,31 @@ "views_pilot_service_outbound_release_review_outboundreleasereviewcardlist_vue" ], "33": [ + "api_admin_fetchcellsampletypelist", + "api_admin_fetchcreatecellsampletype", + "api_admin_fetchdeletecellsampletype", + "api_admin_fetchupdatecellsampletype", + "cell_sample_type_index_handleadd", + "cell_sample_type_index_handlebatchdelete", + "cell_sample_type_index_handledelete", + "cell_sample_type_index_handledropdowncommand", + "cell_sample_type_index_handleedit", + "cell_sample_type_index_handlereset", + "cell_sample_type_index_handlesearch", + "cell_sample_type_index_handlesubmit", + "cell_sample_type_index_handletypelogochange", + "cell_sample_type_index_handletypelogoexceed", + "cell_sample_type_index_handleview", + "cell_sample_type_index_isrowselected", + "cell_sample_type_index_loaddata", + "cell_sample_type_index_resetform", + "cell_sample_type_index_togglerowselection", + "cell_sample_type_index_typelogourl", + "cell_sample_type_index_uploadtypelogofile", + "views_basic_info_cell_sample_type_index_vue", + "views_basic_info_cell_sample_type_samplecodetab_vue" + ], + "34": [ "business_highlight_addcopybutton", "business_highlight_highlightcode", "business_highlight_insertlinenumbers", @@ -3227,7 +3208,7 @@ "directives_index_setupglobdirectives", "directives_index_ts" ], - "34": [ + "35": [ "oa_fc_host_app_applyflagtorule", "oa_fc_host_app_applyschema", "oa_fc_host_app_countrules", @@ -3248,7 +3229,28 @@ "oa_fc_host_app_vue", "oa_fc_host_main_ts" ], - "35": [ + "36": [ + "api_env_detect_item_fetchcreateenvdetectitem", + "api_env_detect_item_fetchdeleteenvdetectitem", + "api_env_detect_item_fetchenvdetectitemdetail", + "api_env_detect_item_fetchenvdetectitemlist", + "api_env_detect_item_fetchenvdetectitemsactive", + "api_env_detect_item_fetchupdateenvdetectitem", + "api_env_detect_item_ts", + "env_detect_item_index_handleadd", + "env_detect_item_index_handlebatchdelete", + "env_detect_item_index_handledelete", + "env_detect_item_index_handleedit", + "env_detect_item_index_handlereset", + "env_detect_item_index_handlesearch", + "env_detect_item_index_handleselectionchange", + "env_detect_item_index_handlesubmit", + "env_detect_item_index_handleview", + "env_detect_item_index_loaddata", + "env_detect_item_index_resetform", + "views_quality_inspection_env_detect_item_index_vue" + ], + "37": [ "components_oa_approval_oaconfirmationkit_vue", "components_oa_approval_oaleavekit_vue", "components_oa_approval_oaovertimekit_vue", @@ -3267,7 +3269,7 @@ "utils_oa_form_create_trip_kit_setupoatripkit", "utils_oa_form_create_trip_kit_ts" ], - "36": [ + "38": [ "core_usetable_usetable", "core_usetable_usetableimpl", "core_usetablecolumns_getcolumnchecks", @@ -3286,7 +3288,7 @@ "table_tableutils_updatepaginationfromresponse", "utils_table_tableutils_ts" ], - "37": [ + "39": [ "core_usechart_usechart", "core_usechart_usechartcomponent", "core_usechart_usechartops", @@ -3304,7 +3306,23 @@ "ui_colors_setelementthemecolor", "utils_ui_colors_ts" ], - "38": [ + "40": [ + "utils_protocol_payment_flow_steps_buildprotocolpaymentflowsteps", + "utils_protocol_payment_flow_steps_dash", + "utils_protocol_payment_flow_steps_financereceipttime", + "utils_protocol_payment_flow_steps_formatamount", + "utils_protocol_payment_flow_steps_hasfinancereceipt", + "utils_protocol_payment_flow_steps_hasfinancerefund", + "utils_protocol_payment_flow_steps_hasreceiptsubmit", + "utils_protocol_payment_flow_steps_hasrefundsubmit", + "utils_protocol_payment_flow_steps_receiptsubmittime", + "utils_protocol_payment_flow_steps_refundfinancetime", + "utils_protocol_payment_flow_steps_refundsubmittime", + "utils_protocol_payment_flow_steps_resolveattachmenturls", + "utils_protocol_payment_flow_steps_resolvestepstate", + "utils_protocol_payment_flow_steps_ts" + ], + "41": [ "api_admin_fetchserviceordertxnlist", "channel_service_order_finance_index_financesubmitattachmentdisplayfiles", "channel_service_order_finance_index_financesubmitremarkdisplay", @@ -3320,7 +3338,7 @@ "channel_service_order_finance_index_txntypehasproducts", "views_pilot_service_channel_service_order_finance_index_vue" ], - "39": [ + "42": [ "utils_sop_print_buildprintdocumenthtml", "utils_sop_print_cleanupprintiframe", "utils_sop_print_embedsopimagesasdataurls", @@ -3335,7 +3353,7 @@ "utils_sop_print_setupsopeditorinteractions", "utils_sop_print_ts" ], - "40": [ + "43": [ "form_validator_getpasswordstrength", "form_validator_trimspaces", "form_validator_validateaccount", @@ -3350,7 +3368,7 @@ "form_validator_validateurl", "utils_form_validator_ts" ], - "41": [ + "44": [ "art_work_tab_index_cleanupeventlisteners", "art_work_tab_index_handletouchend", "art_work_tab_index_handletouchmove", @@ -3361,7 +3379,7 @@ "art_work_tab_index_setupeventlisteners", "components_core_layouts_art_work_tab_index_vue" ], - "42": [ + "45": [ "art_count_to_index_clamp", "art_count_to_index_currentvalue", "art_count_to_index_formatnumber", @@ -3372,7 +3390,18 @@ "art_count_to_index_targetvalue", "components_core_text_effect_art_count_to_index_vue" ], - "43": [ + "46": [ + "api_production_material_fetchmyproductionmaterialconsumptions", + "my_production_material_usage_index_handletabchange", + "production_material_materialconsumptiontab_displayconsumptionqty", + "production_material_materialconsumptiontab_handlereset", + "production_material_materialconsumptiontab_handlesearch", + "production_material_materialconsumptiontab_loaddata", + "views_production_my_production_material_usage_index_vue", + "views_production_production_material_materialconsumptiontab_vue", + "views_production_production_material_productionmaterialusagetab_vue" + ], + "47": [ "history_index_if", "history_index_indexmethod", "history_index_isactivehistoryinstance", @@ -3383,7 +3412,7 @@ "history_index_syncworktabtitle", "views_oa_approval_history_index_vue" ], - "44": [ + "48": [ "view_dialogs_view_dialog_registry_getproductionworkorderviewdialogcomponent", "views_production_production_work_order_view_dialogs_inboundcellstorageproductionviewdialog_vue", "views_production_production_work_order_view_dialogs_inboundresearchdonationproductionviewdialog_vue", @@ -3394,7 +3423,7 @@ "views_production_production_work_order_view_dialogs_temporaryproductionviewdialog_vue", "views_production_production_work_order_view_dialogs_view_dialog_registry_ts" ], - "45": [ + "49": [ "tabs_tab_registry_getqcworkordertabcomponent", "views_quality_inspection_qc_work_order_tabs_alltab_vue", "views_quality_inspection_qc_work_order_tabs_inboundcellstoragetab_vue", @@ -3405,7 +3434,7 @@ "views_quality_inspection_qc_work_order_tabs_tab_registry_ts", "views_quality_inspection_qc_work_order_tabs_temporarytab_vue" ], - "46": [ + "50": [ "sys_error_handle_isignorablescripterror", "sys_error_handle_normalizeerrormessage", "sys_error_handle_registerpromiseerrorhandler", @@ -3415,7 +3444,7 @@ "sys_error_handle_vueerrorhandler", "utils_sys_error_handle_ts" ], - "47": [ + "51": [ "core_usetableheight_tableheightcalculator", "core_usetableheight_tableheightcalculator_calculate", "core_usetableheight_tableheightcalculator_calculateoffset", @@ -3425,7 +3454,7 @@ "core_usetableheight_usetableheight", "hooks_core_usetableheight_ts" ], - "48": [ + "52": [ "tabs_tab_registry_getproductionworkordertabcomponent", "views_production_production_work_order_tabs_inboundcellstoragetab_vue", "views_production_production_work_order_tabs_inboundresearchdonationtab_vue", @@ -3435,7 +3464,7 @@ "views_production_production_work_order_tabs_tab_registry_ts", "views_production_production_work_order_tabs_temporarytab_vue" ], - "49": [ + "53": [ "app_vue", "main_ts", "src_app_refreshbuttonautoicons", @@ -3444,16 +3473,7 @@ "src_main_masksecret", "utils_sys_console_ts" ], - "50": [ - "components_common_decimal_input_index_vue", - "decimal_input_index_handlefocus", - "decimal_input_index_handleinput", - "utils_decimal_input_format_formatdecimaldisplaynatural", - "utils_decimal_input_format_parsedecimaltonumber", - "utils_decimal_input_format_sanitizedecimalinput", - "utils_decimal_input_format_ts" - ], - "51": [ + "54": [ "api_qc_material_month_template_fetchconfirmqcmonthimport", "api_qc_material_month_template_fetchcreateqcmonthtemplate", "api_qc_material_month_template_fetchdeleteqcmonthtemplate", @@ -3462,16 +3482,7 @@ "api_qc_material_month_template_fetchupdateqcmonthtemplate", "api_qc_material_month_template_ts" ], - "52": [ - "api_leave_fetchcreateleave", - "api_leave_fetchdeleteleave", - "api_leave_fetchleavedetail", - "api_leave_fetchleavelist", - "api_leave_fetchsubmitleave", - "api_leave_fetchupdateleave", - "api_leave_ts" - ], - "53": [ + "55": [ "utils_qc_work_order_unfinished_badge_formatqcworkorderunfinishedbadgecount", "utils_qc_work_order_unfinished_badge_isunfinishedqcworkorder", "utils_qc_work_order_unfinished_badge_qcworkorderunfinishedbadgetitle", @@ -3479,69 +3490,57 @@ "utils_qc_work_order_unfinished_badge_sumqcworkordergroupunfinishedbadgecount", "utils_qc_work_order_unfinished_badge_ts" ], - "54": [ + "56": [ "utils_equipment_select_equipmentoptionlabel", "utils_equipment_select_joinequipmentids", "utils_equipment_select_parseequipmentids", "utils_equipment_select_syncequipmentnamesfromoptions", "utils_equipment_select_ts" ], - "55": [ + "57": [ "utils_production_task_editor_cache_clearproductiontaskeditorreferencecache", "utils_production_task_editor_cache_getcachedproductiontaskeditorreference", "utils_production_task_editor_cache_hascachedproductiontaskeditorreference", "utils_production_task_editor_cache_setcachedproductiontaskeditorreference", "utils_production_task_editor_cache_ts" ], - "56": [ + "58": [ "art_global_search_index_focusinput", "art_global_search_index_handlekeydown", "art_global_search_index_if", "art_global_search_index_search", "components_core_layouts_art_global_search_index_vue" ], - "57": [ + "59": [ "composables_useattachmentdropzone_readdroppedfiles", "composables_useattachmentdropzone_ts", "composables_useattachmentdropzone_useattachmentdropzone", "composables_useattachmentdropzone_useindexedattachmentdropzone" ], - "58": [ + "60": [ "utils_qc_inbound_lab_storage_sort_comparelabstoragethencreatetimedesc", "utils_qc_inbound_lab_storage_sort_resolvegrouplabstoragecode", "utils_qc_inbound_lab_storage_sort_resolvegroupmaxcreatetime", "utils_qc_inbound_lab_storage_sort_ts" ], - "59": [ + "61": [ "utils_oa_progress_merge_comments_iscurrentpending", "utils_oa_progress_merge_comments_isnotreached", "utils_oa_progress_merge_comments_mergeprogresswithcomments", "utils_oa_progress_merge_comments_ts" ], - "60": [ + "62": [ "art_sidebar_menu_index_if", "art_sidebar_menu_index_logsidebarmenu", "components_core_layouts_art_menus_art_sidebar_menu_index_vue", "components_core_layouts_art_menus_art_sidebar_menu_widget_sidebarsubmenu_vue" ], - "61": [ + "63": [ "components_core_layouts_art_header_bar_index_vue", "components_core_layouts_art_header_bar_widget_artusermenu_vue", "widget_artusermenu_openpassworddialog", "widget_artusermenu_openprofiledialog" ], - "62": [ - "api_storage_entry_sms_notification_fetchgeneratestorageentrysms", - "api_storage_entry_sms_notification_fetchstorageentrysmslist", - "api_storage_entry_sms_notification_fetchtogglestorageentrysmsnotified", - "api_storage_entry_sms_notification_ts" - ], - "63": [ - "api_sample_sms_notification_fetchgeneratesms", - "api_sample_sms_notification_fetchsamplesmsnotificationlist", - "api_sample_sms_notification_fetchtogglesmsnotified", - "api_sample_sms_notification_ts" - ], "64": [ "production_work_order_workordertasksublist_displaytaskname", "production_work_order_workordertasksublist_ensureactivetaskkey", @@ -3575,80 +3574,80 @@ "utils_sample_freight_flatten_ts" ], "70": [ - "utils_copy_text_copytexttoclipboard", - "utils_copy_text_copytextwithfallback", - "utils_copy_text_ts" - ], - "71": [ - "components_oa_approval_oamoneyfield_vue", - "utils_oa_form_create_money_setupoamoneyfield", - "utils_oa_form_create_money_ts" - ], - "72": [ - "utils_protocol_source_attachment_getsourceprotocolattachment", - "utils_protocol_source_attachment_normalizeattachmentfiles", - "utils_protocol_source_attachment_ts" - ], - "73": [ - "components_oa_approval_oadeptselect_vue", - "utils_oa_form_create_dept_setupoadeptselect", - "utils_oa_form_create_dept_ts" - ], - "74": [ - "form_responsive_calculateresponsivespan", - "form_responsive_createresponsivespancalculator", - "utils_form_responsive_ts" - ], - "75": [ - "art_line_chart_index_clearanimationtimers", - "art_line_chart_index_if", - "components_core_charts_art_line_chart_index_vue" - ], - "76": [ - "art_text_scroll_index_handlecontentclick", - "art_text_scroll_index_measuresizes", - "components_core_text_effect_art_text_scroll_index_vue" - ], - "77": [ - "components_business_production_task_view_dialog_index_vue", - "production_task_view_dialog_index_taskstatuslabel", - "production_task_view_dialog_index_tasktypelabel" - ], - "78": [ - "components_business_order_operate_dialog_bridge_index_vue", - "order_operate_dialog_bridge_index_handlemessage", - "order_operate_dialog_bridge_index_postopen" - ], - "79": [ - "components_oa_approval_oapostselect_vue", - "utils_oa_form_create_post_setupoapostselect", - "utils_oa_form_create_post_ts" - ], - "80": [ - "components_oa_approval_oapersonselect_vue", - "utils_oa_form_create_person_setupoapersonselect", - "utils_oa_form_create_person_ts" - ], - "81": [ - "components_oa_approval_oareceiveaccountselect_vue", - "utils_oa_form_create_receive_account_setupoareceiveaccountselect", - "utils_oa_form_create_receive_account_ts" - ], - "82": [ - "components_oa_approval_oarelatedapproval_vue", - "utils_oa_form_create_related_approval_setupoarelatedapproval", - "utils_oa_form_create_related_approval_ts" - ], - "83": [ "components_oa_approval_oaownerpersonselect_vue", "utils_oa_form_create_owner_person_setupoaownerpersonselect", "utils_oa_form_create_owner_person_ts" ], - "84": [ + "71": [ + "utils_copy_text_copytexttoclipboard", + "utils_copy_text_copytextwithfallback", + "utils_copy_text_ts" + ], + "72": [ "components_oa_approval_oaownerdeptselect_vue", "utils_oa_form_create_owner_dept_setupoaownerdeptselect", "utils_oa_form_create_owner_dept_ts" ], + "73": [ + "components_oa_approval_oamoneyfield_vue", + "utils_oa_form_create_money_setupoamoneyfield", + "utils_oa_form_create_money_ts" + ], + "74": [ + "components_oa_approval_oarelatedapproval_vue", + "utils_oa_form_create_related_approval_setupoarelatedapproval", + "utils_oa_form_create_related_approval_ts" + ], + "75": [ + "utils_protocol_source_attachment_getsourceprotocolattachment", + "utils_protocol_source_attachment_normalizeattachmentfiles", + "utils_protocol_source_attachment_ts" + ], + "76": [ + "components_oa_approval_oadeptselect_vue", + "utils_oa_form_create_dept_setupoadeptselect", + "utils_oa_form_create_dept_ts" + ], + "77": [ + "form_responsive_calculateresponsivespan", + "form_responsive_createresponsivespancalculator", + "utils_form_responsive_ts" + ], + "78": [ + "art_line_chart_index_clearanimationtimers", + "art_line_chart_index_if", + "components_core_charts_art_line_chart_index_vue" + ], + "79": [ + "art_text_scroll_index_handlecontentclick", + "art_text_scroll_index_measuresizes", + "components_core_text_effect_art_text_scroll_index_vue" + ], + "80": [ + "components_business_production_task_view_dialog_index_vue", + "production_task_view_dialog_index_taskstatuslabel", + "production_task_view_dialog_index_tasktypelabel" + ], + "81": [ + "components_business_order_operate_dialog_bridge_index_vue", + "order_operate_dialog_bridge_index_handlemessage", + "order_operate_dialog_bridge_index_postopen" + ], + "82": [ + "components_oa_approval_oapostselect_vue", + "utils_oa_form_create_post_setupoapostselect", + "utils_oa_form_create_post_ts" + ], + "83": [ + "components_oa_approval_oapersonselect_vue", + "utils_oa_form_create_person_setupoapersonselect", + "utils_oa_form_create_person_ts" + ], + "84": [ + "components_oa_approval_oareceiveaccountselect_vue", + "utils_oa_form_create_receive_account_setupoareceiveaccountselect", + "utils_oa_form_create_receive_account_ts" + ], "85": [ "core_uselayoutheight_useautolayoutheight", "core_uselayoutheight_uselayoutheight", @@ -4622,63 +4621,63 @@ "3": 0.02, "4": 0.02, "5": 0.02, - "6": 0.02, - "7": 0.03, + "6": 0.03, + "7": 0.02, "8": 0.02, - "9": 0.02, + "9": 0.03, "10": 0.03, - "11": 0.04, - "12": 0.03, - "13": 0.04, - "14": 0.05, - "15": 0.04, + "11": 0.03, + "12": 0.04, + "13": 0.03, + "14": 0.04, + "15": 0.05, "16": 0.04, - "17": 0.04, - "18": 0.06, + "17": 0.05, + "18": 0.05, "19": 0.05, - "20": 0.05, + "20": 0.06, "21": 0.06, "22": 0.06, - "23": 0.06, + "23": 0.1, "24": 0.09, - "25": 0.08, - "26": 0.09, - "27": 0.08, + "25": 0.09, + "26": 0.08, + "27": 0.09, "28": 0.08, "29": 0.09, - "30": 0.1, - "31": 0.08, + "30": 0.12, + "31": 0.13, "32": 0.1, - "33": 0.12, - "34": 0.2, - "35": 0.12, + "33": 0.13, + "34": 0.12, + "35": 0.2, "36": 0.16, - "37": 0.27, - "38": 0.19, - "39": 0.24, - "40": 0.15, - "41": 0.25, - "42": 0.22, - "43": 0.28, - "44": 0.36, + "37": 0.12, + "38": 0.16, + "39": 0.27, + "40": 0.33, + "41": 0.19, + "42": 0.24, + "43": 0.15, + "44": 0.25, "45": 0.22, - "46": 0.39, - "47": 0.36, - "48": 0.25, - "49": 0.33, - "50": 0.38, - "51": 0.29, - "52": 0.29, - "53": 0.4, - "54": 0.5, + "46": 0.28, + "47": 0.28, + "48": 0.36, + "49": 0.22, + "50": 0.39, + "51": 0.36, + "52": 0.25, + "53": 0.33, + "54": 0.29, "55": 0.4, "56": 0.5, - "57": 0.5, + "57": 0.4, "58": 0.5, - "59": 0.67, - "60": 0.67, - "61": 0.5, - "62": 0.5, + "59": 0.5, + "60": 0.5, + "61": 0.67, + "62": 0.67, "63": 0.5, "64": 0.5, "65": 0.5, @@ -4686,12 +4685,12 @@ "67": 1.0, "68": 0.67, "69": 0.67, - "70": 1.0, - "71": 0.67, - "72": 1.0, + "70": 0.67, + "71": 1.0, + "72": 0.67, "73": 0.67, "74": 0.67, - "75": 0.67, + "75": 1.0, "76": 0.67, "77": 0.67, "78": 0.67, @@ -5018,7 +5017,7 @@ { "id": "payment_info_paymentbysampletab_number", "label": "Number()", - "degree": 100 + "degree": 103 }, { "id": "widget_settingitem_emit", @@ -5105,7 +5104,7 @@ "source": "handleBatchDelete()", "target": "withFullscreenLoading()", "source_files": [ - "views/system/tenant/index.vue", + "views/basic-info/cell-sample-type/index.vue", "utils/fullscreen-loading.ts" ], "confidence": "INFERRED", diff --git a/web-admin/graphify-out/.graphify_extract.json b/web-admin/graphify-out/.graphify_extract.json index 12642ef3..1a34ddc1 100644 --- a/web-admin/graphify-out/.graphify_extract.json +++ b/web-admin/graphify-out/.graphify_extract.json @@ -9576,145 +9576,138 @@ "source_file": "components/business/sample-payment-detail-dialog/index.vue", "source_location": "L1" }, - { - "id": "sample_payment_detail_dialog_index_number", - "label": "Number()", - "file_type": "code", - "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L55" - }, { "id": "sample_payment_detail_dialog_index_formatamount", "label": "formatAmount()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L363" + "source_location": "L52" }, { "id": "sample_payment_detail_dialog_index_formatduedate", "label": "formatDueDate()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L369" + "source_location": "L376" }, { "id": "sample_payment_detail_dialog_index_paymentschedulestatuslabel", "label": "paymentScheduleStatusLabel()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L377" + "source_location": "L384" }, { "id": "sample_payment_detail_dialog_index_istransferfieldslocked", "label": "isTransferFieldsLocked()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L398" + "source_location": "L405" }, { "id": "sample_payment_detail_dialog_index_schedulecanaddtransfer", "label": "scheduleCanAddTransfer()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L403" + "source_location": "L410" }, { "id": "sample_payment_detail_dialog_index_fullfileurl", "label": "fullFileUrl()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L408" + "source_location": "L415" }, { "id": "sample_payment_detail_dialog_index_isimageurl", "label": "isImageUrl()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L414" + "source_location": "L421" }, { "id": "sample_payment_detail_dialog_index_ensuretransferattachments", "label": "ensureTransferAttachments()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L418" + "source_location": "L425" }, { "id": "sample_payment_detail_dialog_index_transferrowkey", "label": "transferRowKey()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L432" + "source_location": "L439" }, { "id": "sample_payment_detail_dialog_index_attachmentstourls", "label": "attachmentsToUrls()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L436" + "source_location": "L443" }, { "id": "sample_payment_detail_dialog_index_validatetransferrow", "label": "validateTransferRow()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L444" + "source_location": "L451" }, { "id": "sample_payment_detail_dialog_index_ensuretransfertypes", "label": "ensureTransferTypes()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L461" + "source_location": "L468" }, { "id": "sample_payment_detail_dialog_index_loaddetail", "label": "loadDetail()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L466" + "source_location": "L473" }, { "id": "sample_payment_detail_dialog_index_notifychanged", "label": "notifyChanged()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L490" + "source_location": "L497" }, { "id": "sample_payment_detail_dialog_index_addtransfer", "label": "addTransfer()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L494" + "source_location": "L501" }, { "id": "sample_payment_detail_dialog_index_savetransferrow", "label": "saveTransferRow()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L513" + "source_location": "L520" }, { "id": "sample_payment_detail_dialog_index_submittransfer", "label": "submitTransfer()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L548" + "source_location": "L555" }, { "id": "sample_payment_detail_dialog_index_withdrawtransfer", "label": "withdrawTransfer()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L563" + "source_location": "L570" }, { "id": "sample_payment_detail_dialog_index_removetransfer", "label": "removeTransfer()", "file_type": "code", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L581" + "source_location": "L588" }, { "id": "components_business_sample_production_info_cards_index_vue", @@ -46234,11 +46227,11 @@ }, { "source": "components_business_sample_payment_detail_dialog_index_vue", - "target": "sample_payment_detail_dialog_index_number", + "target": "sample_payment_detail_dialog_index_formatamount", "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L55", + "source_location": "L52", "weight": 1.0 }, { @@ -46247,7 +46240,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L299", + "source_location": "L306", "weight": 1.0 }, { @@ -46256,7 +46249,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L300", + "source_location": "L307", "weight": 1.0 }, { @@ -46265,7 +46258,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L301", + "source_location": "L308", "weight": 1.0 }, { @@ -46274,7 +46267,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L302", + "source_location": "L309", "weight": 1.0 }, { @@ -46283,7 +46276,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L303", + "source_location": "L310", "weight": 1.0 }, { @@ -46292,7 +46285,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L304", + "source_location": "L311", "weight": 1.0 }, { @@ -46301,7 +46294,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L310", + "source_location": "L317", "weight": 1.0 }, { @@ -46310,7 +46303,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L316", + "source_location": "L323", "weight": 1.0 }, { @@ -46319,7 +46312,7 @@ "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L324", + "source_location": "L331", "weight": 1.0 }, { @@ -46328,7 +46321,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L363", + "source_location": "L370", "weight": 1.0 }, { @@ -46337,7 +46330,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L369", + "source_location": "L376", "weight": 1.0 }, { @@ -46346,7 +46339,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L377", + "source_location": "L384", "weight": 1.0 }, { @@ -46355,7 +46348,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L398", + "source_location": "L405", "weight": 1.0 }, { @@ -46364,7 +46357,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L403", + "source_location": "L410", "weight": 1.0 }, { @@ -46373,7 +46366,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L408", + "source_location": "L415", "weight": 1.0 }, { @@ -46382,7 +46375,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L414", + "source_location": "L421", "weight": 1.0 }, { @@ -46391,7 +46384,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L418", + "source_location": "L425", "weight": 1.0 }, { @@ -46400,7 +46393,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L432", + "source_location": "L439", "weight": 1.0 }, { @@ -46409,7 +46402,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L436", + "source_location": "L443", "weight": 1.0 }, { @@ -46418,7 +46411,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L444", + "source_location": "L451", "weight": 1.0 }, { @@ -46427,7 +46420,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L461", + "source_location": "L468", "weight": 1.0 }, { @@ -46436,7 +46429,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L466", + "source_location": "L473", "weight": 1.0 }, { @@ -46445,7 +46438,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L490", + "source_location": "L497", "weight": 1.0 }, { @@ -46454,7 +46447,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L494", + "source_location": "L501", "weight": 1.0 }, { @@ -46463,7 +46456,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L513", + "source_location": "L520", "weight": 1.0 }, { @@ -46472,7 +46465,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L548", + "source_location": "L555", "weight": 1.0 }, { @@ -46481,7 +46474,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L563", + "source_location": "L570", "weight": 1.0 }, { @@ -46490,16 +46483,7 @@ "relation": "contains", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L581", - "weight": 1.0 - }, - { - "source": "sample_payment_detail_dialog_index_schedulecanaddtransfer", - "target": "sample_payment_detail_dialog_index_number", - "relation": "calls", - "confidence": "EXTRACTED", - "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L405", + "source_location": "L588", "weight": 1.0 }, { @@ -46508,16 +46492,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L437", - "weight": 1.0 - }, - { - "source": "sample_payment_detail_dialog_index_validatetransferrow", - "target": "sample_payment_detail_dialog_index_number", - "relation": "calls", - "confidence": "EXTRACTED", - "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L449", + "source_location": "L444", "weight": 1.0 }, { @@ -46526,7 +46501,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L481", + "source_location": "L488", "weight": 1.0 }, { @@ -46535,16 +46510,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L496", - "weight": 1.0 - }, - { - "source": "sample_payment_detail_dialog_index_addtransfer", - "target": "sample_payment_detail_dialog_index_number", - "relation": "calls", - "confidence": "EXTRACTED", - "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L497", + "source_location": "L503", "weight": 1.0 }, { @@ -46553,7 +46519,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L516", + "source_location": "L523", "weight": 1.0 }, { @@ -46562,7 +46528,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L521", + "source_location": "L528", "weight": 1.0 }, { @@ -46571,7 +46537,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L522", + "source_location": "L529", "weight": 1.0 }, { @@ -46580,7 +46546,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L529", + "source_location": "L536", "weight": 1.0 }, { @@ -46589,7 +46555,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L541", + "source_location": "L548", "weight": 1.0 }, { @@ -46598,7 +46564,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L542", + "source_location": "L549", "weight": 1.0 }, { @@ -46607,7 +46573,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L556", + "source_location": "L563", "weight": 1.0 }, { @@ -46616,7 +46582,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L557", + "source_location": "L564", "weight": 1.0 }, { @@ -46625,7 +46591,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L574", + "source_location": "L581", "weight": 1.0 }, { @@ -46634,7 +46600,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L575", + "source_location": "L582", "weight": 1.0 }, { @@ -46643,7 +46609,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L584", + "source_location": "L591", "weight": 1.0 }, { @@ -46652,7 +46618,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L597", + "source_location": "L604", "weight": 1.0 }, { @@ -46661,7 +46627,7 @@ "relation": "calls", "confidence": "EXTRACTED", "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L598", + "source_location": "L605", "weight": 1.0 }, { @@ -82807,7 +82773,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L365", + "source_location": "L372", "weight": 1.0 }, { @@ -82817,7 +82783,27 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L374", + "source_location": "L381", + "weight": 1.0 + }, + { + "source": "sample_payment_detail_dialog_index_schedulecanaddtransfer", + "target": "payment_info_paymentbysampletab_number", + "relation": "calls", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "components/business/sample-payment-detail-dialog/index.vue", + "source_location": "L412", + "weight": 1.0 + }, + { + "source": "sample_payment_detail_dialog_index_validatetransferrow", + "target": "payment_info_paymentbysampletab_number", + "relation": "calls", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "components/business/sample-payment-detail-dialog/index.vue", + "source_location": "L456", "weight": 1.0 }, { @@ -82827,7 +82813,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L462", + "source_location": "L469", "weight": 1.0 }, { @@ -82837,7 +82823,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L463", + "source_location": "L470", "weight": 1.0 }, { @@ -82847,7 +82833,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L475", + "source_location": "L482", "weight": 1.0 }, { @@ -82857,7 +82843,17 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L491", + "source_location": "L498", + "weight": 1.0 + }, + { + "source": "sample_payment_detail_dialog_index_addtransfer", + "target": "payment_info_paymentbysampletab_number", + "relation": "calls", + "confidence": "INFERRED", + "confidence_score": 0.8, + "source_file": "components/business/sample-payment-detail-dialog/index.vue", + "source_location": "L504", "weight": 1.0 }, { @@ -82867,7 +82863,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L501", + "source_location": "L508", "weight": 1.0 }, { @@ -82877,7 +82873,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L533", + "source_location": "L540", "weight": 1.0 }, { @@ -82887,7 +82883,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L536", + "source_location": "L543", "weight": 1.0 }, { @@ -82897,7 +82893,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L555", + "source_location": "L562", "weight": 1.0 }, { @@ -82907,7 +82903,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L572", + "source_location": "L579", "weight": 1.0 }, { @@ -82917,7 +82913,7 @@ "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "components/business/sample-payment-detail-dialog/index.vue", - "source_location": "L596", + "source_location": "L603", "weight": 1.0 }, { diff --git a/web-admin/graphify-out/GRAPH_REPORT.md b/web-admin/graphify-out/GRAPH_REPORT.md index d92471e6..4b76b454 100644 --- a/web-admin/graphify-out/GRAPH_REPORT.md +++ b/web-admin/graphify-out/GRAPH_REPORT.md @@ -1,11 +1,11 @@ # Graph Report - src (2026-10-10) ## Corpus Check -- Large corpus: 1026 files · ~680,861 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only. +- Large corpus: 1026 files · ~680,890 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only. ## Summary -- 3840 nodes · 5657 edges · 61 communities detected -- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 1350 edges (avg confidence: 0.8) +- 3839 nodes · 5656 edges · 61 communities detected +- Extraction: 76% EXTRACTED · 24% INFERRED · 0% AMBIGUOUS · INFERRED: 1353 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Community Hubs (Navigation) @@ -49,24 +49,24 @@ - [[_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 43|Community 43]] +- [[_COMMUNITY_Community 42|Community 42]] +- [[_COMMUNITY_Community 44|Community 44]] - [[_COMMUNITY_Community 46|Community 46]] - [[_COMMUNITY_Community 47|Community 47]] -- [[_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 59|Community 59]] -- [[_COMMUNITY_Community 60|Community 60]] +- [[_COMMUNITY_Community 58|Community 58]] +- [[_COMMUNITY_Community 61|Community 61]] - [[_COMMUNITY_Community 62|Community 62]] -- [[_COMMUNITY_Community 63|Community 63]] - [[_COMMUNITY_Community 67|Community 67]] -- [[_COMMUNITY_Community 70|Community 70]] -- [[_COMMUNITY_Community 72|Community 72]] +- [[_COMMUNITY_Community 71|Community 71]] +- [[_COMMUNITY_Community 75|Community 75]] - [[_COMMUNITY_Community 88|Community 88]] - [[_COMMUNITY_Community 91|Community 91]] - [[_COMMUNITY_Community 92|Community 92]] @@ -74,7 +74,7 @@ ## God Nodes (most connected - your core abstractions) 1. `get()` - 339 edges 2. `push()` - 128 edges -3. `Number()` - 100 edges +3. `Number()` - 103 edges 4. `emit()` - 36 edges 5. `WebSocketClient` - 26 edges 6. `set()` - 22 edges @@ -93,239 +93,239 @@ - `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED] views/operations/exhibition-appointment/index.vue → utils/fullscreen-loading.ts - `handleBatchDelete()` --calls--> `withFullscreenLoading()` [INFERRED] - views/system/tenant/index.vue → utils/fullscreen-loading.ts + views/basic-info/cell-sample-type/index.vue → utils/fullscreen-loading.ts ## Communities ### Community 0 - "Community 0" Cohesion: 0.0 -Nodes (207): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+199 more) +Nodes (214): fetchAccessLogList(), fetchActiveRegions(), fetchCategoryList(), fetchCategoryParameters(), fetchCellProductPriceList(), fetchCellProductSpecList(), fetchCellProductTypeList(), fetchChannelAccount() (+206 more) ### Community 1 - "Community 1" Cohesion: 0.02 -Nodes (158): fetchUploadAttachment(), fetchOaRelatedSummaries(), FireworkSystem, handleReportUpload(), handleReportUpload(), walk(), if(), async() (+150 more) +Nodes (153): fetchDeptTree(), fetchUserList(), fetchUploadAttachment(), fetchOaFlowPreview(), fetchOaTemplateDetail(), FireworkSystem, handleReportUpload(), handleReportUpload() (+145 more) ### Community 2 - "Community 2" Cohesion: 0.02 -Nodes (137): fetchSamplePaymentDetail(), if(), buildPreviewRow(), findEarliestConfirmableSchedule(), fullFileUrl(), handleAttachPreview(), handleReset(), handleSearch() (+129 more) +Nodes (142): fetchSamplePaymentDetail(), handleFocus(), handleInput(), if(), buildPreviewRow(), findEarliestConfirmableSchedule(), fullFileUrl(), handleAttachPreview() (+134 more) ### Community 3 - "Community 3" Cohesion: 0.02 -Nodes (91): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchProductionTaskList() (+83 more) +Nodes (106): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), buildTemporaryViewSections(), buildInsertedSiblingCard(), fetchMineTasksForWorkOrders(), resolveShowProtocolPaymentAtGroup() (+98 more) ### Community 4 - "Community 4" Cohesion: 0.02 -Nodes (95): fetchMyProductionTasks(), fetchMyQcTaskList(), fetchQcTaskDetail(), fetchQcTaskList(), buildTemporaryViewSections(), buildInsertedSiblingCard(), fetchMineTasksForWorkOrders(), resolveShowProtocolPaymentAtGroup() (+87 more) +Nodes (89): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupDetail(), fetchPilotCheckupList(), fetchUpdatePostBoundRoles() (+81 more) ### Community 5 - "Community 5" Cohesion: 0.02 -Nodes (80): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaFormAttachments() (+72 more) +Nodes (71): fetchBatchDeleteOaReceiveAccounts(), fetchBatchMarkOaCcTasksRead(), fetchCreateOaReceiveAccount(), fetchDeleteOaReceiveAccount(), fetchMarkOaCcTaskRead(), fetchOaAppDraft(), fetchOaCcUnreadCount(), fetchOaFormAttachments() (+63 more) ### Community 6 - "Community 6" -Cohesion: 0.02 -Nodes (79): handleBlur(), handleFocus(), handleInput(), fetchProtocolInvoicePendingCount(), fetchConfirmPresignTransfer(), fetchPresignOrderDetail(), fetchPresignOrderList(), fetchPresignTransferReceiptConfirmedList() (+71 more) +Cohesion: 0.03 +Nodes (71): collectIds(), buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), buildBaseFields() (+63 more) ### Community 7 - "Community 7" -Cohesion: 0.03 -Nodes (91): batchSavePilotCheckup(), deletePilotCheckupActualItem(), fetchCheckupInstitutionList(), fetchCheckupInstitutionPackageDetail(), fetchCheckupInstitutionPackages(), fetchPilotCheckupActualItems(), fetchPilotCheckupDetail(), fetchPilotCheckupList() (+83 more) +Cohesion: 0.02 +Nodes (82): handleBlur(), handleFocus(), handleInput(), fetchProtocolInvoicePendingCount(), fetchConfirmPresignTransfer(), fetchPresignOrderDetail(), fetchPresignOrderList(), fetchPresignTransferReceiptConfirmedList() (+74 more) ### Community 8 - "Community 8" Cohesion: 0.02 -Nodes (79): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchCreateItemUnit(), fetchDeleteItemUnit(), fetchItemUnitList() (+71 more) +Nodes (81): fetchCheckupInstitutionInvoices(), fetchCheckupInstitutionProjects(), fetchCreateCheckupInstitution(), fetchCreateCheckupInstitutionInvoice(), fetchCreateCheckupInstitutionProject(), fetchCreateItemUnit(), fetchCreateTenant(), fetchDeleteItemUnit() (+73 more) ### Community 9 - "Community 9" -Cohesion: 0.02 -Nodes (42): fetchGetMenuList(), useSettingsHandlers(), useSettingsPanel(), useSettingsState(), ComponentLoader, IframeRouteManager, MenuProcessor, RoutePermissionValidator (+34 more) +Cohesion: 0.03 +Nodes (49): fetchProductionWasteOutputList(), fetchProductionReceptionBySampleCode(), fetchProductionReceptionSubmit(), fetchProductionReceptionUpdate(), fetchSampleStorageCertQc(), saveSampleStorageCertQc(), handleDialogClose(), onAdd() (+41 more) ### Community 10 - "Community 10" Cohesion: 0.03 -Nodes (55): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchCreateMenu(), fetchCreateTenant(), fetchDeleteCellSampleType(), fetchDeleteMenu(), fetchDeleteTenant(), fetchMenuTree() (+47 more) +Nodes (59): useSettingsHandlers(), useSettingsPanel(), useSettingsState(), RoutePermissionValidator, useCeremony(), useTheme(), if(), nodeSummary() (+51 more) ### Community 11 - "Community 11" -Cohesion: 0.04 -Nodes (50): fetchAttachmentDownloadUrl(), qcAttachmentFullUrl(), qcDownloadAttachment(), qcPreviewAttachment(), handleDownloadAttachment(), attachmentFullUrl(), dash(), handleAttachmentDownload() (+42 more) +Cohesion: 0.03 +Nodes (33): fetchGetMenuList(), ComponentLoader, IframeRouteManager, MenuProcessor, RouteRegistry, RouteTransformer, useAppMode(), useAuth() (+25 more) ### Community 12 - "Community 12" -Cohesion: 0.03 -Nodes (31): fetchCollectionProtocolList(), fetchUpdateFullmoonHandle(), fetchUpdateStorageCertHandle(), dash(), fmtMoney(), auditorDisplay(), dash(), ensureUnitsInOptions() (+23 more) +Cohesion: 0.04 +Nodes (54): fetchDictDataOptions(), fetchAttachmentDownloadUrl(), qcAttachmentFullUrl(), qcDownloadAttachment(), qcPreviewAttachment(), handleDownloadAttachment(), attachmentFullUrl(), dash() (+46 more) ### Community 13 - "Community 13" -Cohesion: 0.04 -Nodes (44): buildBaseFields(), buildFeeSection(), buildInboundBasicSection(), buildInboundCellStorageProductionViewSections(), buildInboundResearchDonationProductionViewSections(), buildOutboundBasicExperimentProductionViewSections(), buildOutboundClinicalProductionViewSections(), buildOutboundDetail() (+36 more) +Cohesion: 0.03 +Nodes (33): fetchCollectionProtocolList(), fetchProductionWorkOrderQcInfo(), fetchUpdateFullmoonHandle(), fetchUpdateStorageCertHandle(), dash(), fmtMoney(), auditorDisplay(), dash() (+25 more) ### Community 14 - "Community 14" -Cohesion: 0.05 -Nodes (55): buildInboundBasicSection(), buildInboundCellStorageViewSections(), buildInboundResearchDonationViewSections(), buildReceptionSection(), buildTransportSection(), resolveInboundProtocolStatus(), appendTaskResultSections(), batchTasksForPanelSlot() (+47 more) - -### Community 15 - "Community 15" Cohesion: 0.04 Nodes (57): fetchCollectionRegionList(), fetchCreateCollectionRegion(), fetchCreateHospital(), fetchCreateRegionStaff(), fetchDeleteCollectionRegion(), fetchDeleteHospital(), fetchDeleteRegionStaff(), fetchHospitalList() (+49 more) +### Community 15 - "Community 15" +Cohesion: 0.05 +Nodes (54): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), toLogin() (+46 more) + ### Community 16 - "Community 16" Cohesion: 0.04 -Nodes (41): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+33 more) +Nodes (40): fetchDeleteMaterialBatch(), fetchMaterialBatchList(), fetchMaterialBatchOptions(), fetchMaterialList(), fetchMaterialOptions(), fetchUpdateMaterialBatch(), fetchMaterialUsageList(), fetchMyMaterialUsageList() (+32 more) ### Community 17 - "Community 17" -Cohesion: 0.04 -Nodes (35): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+27 more) +Cohesion: 0.05 +Nodes (54): fetchOaRelatedSummaries(), buildFormRows(), buildOaPrintFilename(), buildOaPrintModel(), canvasToPdf(), ensurePlainObject(), exportOaApprovalPdf(), extractUrls() (+46 more) ### Community 18 - "Community 18" -Cohesion: 0.06 -Nodes (51): fetchDeptTree(), fetchUserList(), fetchOaFlowPreview(), fetchOaTemplateDetail(), remoteSearchHandoverUsers(), applySchema(), flowNeedsInitiatorSelect(), loadFlowPreview() (+43 more) +Cohesion: 0.05 +Nodes (34): fetchClinicalPaymentDetail(), fetchClinicalPayments(), fetchClinicalReceiptConfirmed(), fetchClinicalReceiptPending(), fetchClinicalRefundConfirmed(), fetchClinicalRefundPending(), fetchOrderAdjusts(), fetchOrderDetail() (+26 more) ### Community 19 - "Community 19" Cohesion: 0.05 Nodes (39): fetchCreateQcInspectionItemTemplate(), fetchDeleteQcInspectionItemTemplate(), fetchQcInspectionItemTemplateDetail(), fetchQcInspectionItemTemplateList(), fetchQcInspectionItemTemplatesActive(), fetchUpdateQcInspectionItemTemplate(), fetchQcLabTestItemList(), fetchQcLabTestItemsActive() (+31 more) ### Community 20 - "Community 20" -Cohesion: 0.05 -Nodes (33): fetchDictDataOptions(), fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), fetchCreateSampleFreight() (+25 more) - -### Community 21 - "Community 21" Cohesion: 0.06 Nodes (43): completePilotApplication(), deletePilotApplication(), fetchCreateExhibitionAppointment(), fetchDeleteExhibitionAppointment(), fetchExhibitionAppointmentList(), fetchPilotApplicationList(), fetchUpdateExhibitionAppointment(), startPilotApplication() (+35 more) -### Community 22 - "Community 22" -Cohesion: 0.06 -Nodes (34): fetchUpdatePostBoundRoles(), branchConditionLines(), collectConditionReferencedFields(), collectFormConditionReferencedFields(), flattenFormRules(), formatConditionPreview(), formatConditionPreviewLines(), listConditionCandidates() (+26 more) - -### Community 23 - "Community 23" +### Community 21 - "Community 21" Cohesion: 0.06 Nodes (25): fetchQcMaterialBatchDetail(), fetchQcMaterialBatchList(), fetchQcMaterialBatchOptions(), fetchQcMaterialList(), fetchQcMaterialOptions(), fetchUpdateQcMaterialBatch(), fetchMarkQcMaterialUsageFullyUsed(), financeAttachmentsToUrlString() (+17 more) +### Community 22 - "Community 22" +Cohesion: 0.06 +Nodes (26): fetchProductionTaskList(), createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments() (+18 more) + +### Community 23 - "Community 23" +Cohesion: 0.1 +Nodes (2): WebSocketClient, TableCache + ### Community 24 - "Community 24" Cohesion: 0.09 -Nodes (4): handleReset(), handleSearch(), WebSocketClient, TableCache - -### Community 25 - "Community 25" -Cohesion: 0.08 -Nodes (32): toLogin(), base64ToBytes(), bytesToBase64(), decryptJson(), deriveKey(), encryptJson(), fromBase64Url(), hexToBytes() (+24 more) - -### Community 26 - "Community 26" -Cohesion: 0.09 Nodes (8): getDefaultLanguage(), checkStorageCompatibility(), StorageConfig, getSystemStorage(), getSysVersion(), StorageKeyManager, StorageCompatibilityManager, validateStorageData() +### Community 25 - "Community 25" +Cohesion: 0.09 +Nodes (29): fetchAddTaskStep(), fetchCopyTaskStep(), fetchDeleteTaskStep(), fetchDispatchProductionTaskSteps(), fetchForwardProductionTask(), fetchProductionTaskDetail(), fetchProductionTaskEditorInit(), fetchReassignProductionTask() (+21 more) + +### Community 26 - "Community 26" +Cohesion: 0.08 +Nodes (22): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderTabStepCounts(), fetchProductionWorkOrderTabStepCountsDeduped() (+14 more) + ### Community 27 - "Community 27" -Cohesion: 0.08 -Nodes (24): fetchMineProductionWorkOrderTabStepCounts(), fetchMineProductionWorkOrderTabStepCountsDeduped(), fetchProductionWorkOrderCellProductOptions(), fetchProductionWorkOrderDetail(), fetchProductionWorkOrderList(), fetchProductionWorkOrderQcDispatchOptions(), fetchProductionWorkOrderQcInfo(), fetchProductionWorkOrderTabStepCounts() (+16 more) - -### Community 28 - "Community 28" -Cohesion: 0.08 -Nodes (20): createEmptySnapshots(), normalizeConfiguredReagents(), normalizeConsumables(), normalizeDataCollections(), normalizeReagents(), normalizeStepOutputs(), serializeAttachments(), serializeEquipmentIds() (+12 more) - -### Community 29 - "Community 29" Cohesion: 0.09 Nodes (22): fetchOaAppList(), fetchOaGroupList(), fetchPublishOaApp(), loadData(), loadGroups(), resetForm(), onSuccess(), goBack() (+14 more) -### Community 30 - "Community 30" -Cohesion: 0.1 -Nodes (22): fetchGetMenuCodes(), fetchGetUserInfo(), fetchLogin(), fetchRefreshToken(), fetchUpdateProfile(), fetchUploadProfileAvatar(), fetchWxQrStatus(), fetchUserInfo() (+14 more) - -### Community 31 - "Community 31" +### Community 28 - "Community 28" Cohesion: 0.08 Nodes (17): fetchEthnicList(), applyDonationBundleToForm(), createEmptyTransport(), ensureEthnicOptionInList(), handleOpenForAdd(), loadEthnicOptions(), normalizeSampleRow(), resetForm() (+9 more) +### Community 29 - "Community 29" +Cohesion: 0.09 +Nodes (12): fetchMineQcWorkOrderTabUnfinishedCounts(), fetchOutboundQcDetail(), fetchQcWorkOrderDetail(), fetchQcWorkOrderList(), fetchQcWorkOrdersBySample(), fetchQcWorkOrderTabUnfinishedCounts(), reload(), reloadTabCounts() (+4 more) + +### Community 30 - "Community 30" +Cohesion: 0.12 +Nodes (17): fetchCreateSampleFreight(), fetchLinkSampleFreight(), fetchSampleFreightDetail(), fetchSampleFreightList(), fetchSampleFreightSampleOptions(), fetchUpdateSampleFreight(), clearSampleSelection(), handleSampleSearch() (+9 more) + +### Community 31 - "Community 31" +Cohesion: 0.13 +Nodes (19): fetchCreateMenu(), fetchDeleteMenu(), fetchMenuTree(), fetchUpdateMenu(), postorder(), preorder(), applyTreeExpand(), handleAdd() (+11 more) + ### Community 32 - "Community 32" Cohesion: 0.1 Nodes (12): fetchOutboundReleaseMaterial(), fetchOutboundReleaseReviewDetail(), fetchOutboundReleaseReviewList(), fetchReleaseReviewList(), handleReset(), handleSearch(), handleTabChange(), handleToggleSelect() (+4 more) ### Community 33 - "Community 33" +Cohesion: 0.13 +Nodes (19): fetchCellSampleTypeList(), fetchCreateCellSampleType(), fetchDeleteCellSampleType(), fetchUpdateCellSampleType(), handleAdd(), handleBatchDelete(), handleDelete(), handleDropdownCommand() (+11 more) + +### Community 34 - "Community 34" Cohesion: 0.12 Nodes (15): addCopyButton(), highlightCode(), insertLineNumbers(), isBlockProcessed(), markBlockAsProcessed(), processBlock(), setupHighlightDirective(), setupRippleDirective() (+7 more) -### Community 34 - "Community 34" +### Community 35 - "Community 35" Cohesion: 0.2 Nodes (14): applyFlagToRule(), applySchema(), countRules(), getActiveRule(), getDesigner(), getSetupState(), onMessage(), readFlag() (+6 more) -### Community 35 - "Community 35" +### Community 36 - "Community 36" +Cohesion: 0.16 +Nodes (14): fetchCreateEnvDetectItem(), fetchDeleteEnvDetectItem(), fetchEnvDetectItemDetail(), fetchEnvDetectItemList(), fetchEnvDetectItemsActive(), fetchUpdateEnvDetectItem(), handleAdd(), handleBatchDelete() (+6 more) + +### Community 37 - "Community 37" Cohesion: 0.12 Nodes (6): setupOaConfirmationKit(), ensureOaKitMenu(), setupOaLeaveKit(), setupOaOvertimeKit(), setupOaResignationKit(), setupOaTripKit() -### Community 36 - "Community 36" +### Community 38 - "Community 38" Cohesion: 0.16 Nodes (10): useTable(), useTableImpl(), getColumnChecks(), useTableColumns(), createErrorHandler(), createSmartDebounce(), defaultResponseAdapter(), extractPagination() (+2 more) -### Community 37 - "Community 37" +### Community 39 - "Community 39" Cohesion: 0.27 Nodes (14): useChart(), useChartComponent(), useChartOps(), colourBlend(), getCssVar(), getDarkColor(), getLightColor(), handleElementThemeColor() (+6 more) -### Community 38 - "Community 38" +### Community 40 - "Community 40" +Cohesion: 0.33 +Nodes (13): buildProtocolPaymentFlowSteps(), dash(), financeReceiptTime(), formatAmount(), hasFinanceReceipt(), hasFinanceRefund(), hasReceiptSubmit(), hasRefundSubmit() (+5 more) + +### Community 41 - "Community 41" Cohesion: 0.19 Nodes (6): fetchServiceOrderTxnList(), handleReset(), handleSearch(), handleTabChange(), loadData(), switchTab() -### Community 39 - "Community 39" +### Community 42 - "Community 42" Cohesion: 0.24 Nodes (10): buildPrintDocumentHtml(), cleanupPrintIframe(), embedSopImagesAsDataUrls(), getSopA4ContentHeightPx(), getSopA4EditorHeightPx(), normalizeSopCheckboxes(), prepareSopHtmlForPrint(), printSopHtml() (+2 more) -### Community 41 - "Community 41" +### Community 44 - "Community 44" Cohesion: 0.25 Nodes (2): handleTouchEnd(), setTransition() -### Community 43 - "Community 43" +### Community 46 - "Community 46" +Cohesion: 0.28 +Nodes (5): fetchMyProductionMaterialConsumptions(), displayConsumptionQty(), handleReset(), handleSearch(), loadData() + +### Community 47 - "Community 47" Cohesion: 0.28 Nodes (4): isActiveHistoryInstance(), isUploadPreviewLabel(), previewColumnMinWidth(), syncWorktabTitle() -### Community 46 - "Community 46" +### Community 50 - "Community 50" Cohesion: 0.39 Nodes (6): isIgnorableScriptError(), normalizeErrorMessage(), registerPromiseErrorHandler(), registerResourceErrorHandler(), scriptErrorHandler(), setupErrorHandle() -### Community 47 - "Community 47" +### Community 51 - "Community 51" Cohesion: 0.36 Nodes (1): TableHeightCalculator -### Community 49 - "Community 49" +### Community 53 - "Community 53" Cohesion: 0.33 Nodes (2): logApiCryptoBootInfo(), maskSecret() -### Community 50 - "Community 50" -Cohesion: 0.38 -Nodes (4): handleFocus(), handleInput(), parseDecimalToNumber(), sanitizeDecimalInput() - -### Community 51 - "Community 51" +### Community 54 - "Community 54" Cohesion: 0.29 Nodes (1): fetchQcMonthTemplateList() -### Community 52 - "Community 52" -Cohesion: 0.29 -Nodes (2): fetchLeaveDetail(), fetchLeaveList() - -### Community 53 - "Community 53" +### Community 55 - "Community 55" Cohesion: 0.4 Nodes (2): isUnfinishedQcWorkOrder(), resolveQcWorkOrderUnfinishedBadgeCount() -### Community 54 - "Community 54" +### Community 56 - "Community 56" Cohesion: 0.5 Nodes (2): parseEquipmentIds(), syncEquipmentNamesFromOptions() -### Community 56 - "Community 56" +### Community 58 - "Community 58" Cohesion: 0.5 Nodes (2): focusInput(), handleKeydown() -### Community 59 - "Community 59" +### Community 61 - "Community 61" Cohesion: 0.67 Nodes (2): isCurrentPending(), isNotReached() -### Community 60 - "Community 60" +### Community 62 - "Community 62" Cohesion: 0.67 Nodes (2): if(), logSidebarMenu() -### Community 62 - "Community 62" -Cohesion: 0.5 -Nodes (1): fetchStorageEntrySmsList() - -### Community 63 - "Community 63" -Cohesion: 0.5 -Nodes (1): fetchSampleSmsNotificationList() - ### Community 67 - "Community 67" Cohesion: 1.0 Nodes (2): getSettingDefaults(), resetToDefaults() -### Community 70 - "Community 70" +### Community 71 - "Community 71" Cohesion: 1.0 Nodes (2): copyTextToClipboard(), copyTextWithFallback() -### Community 72 - "Community 72" +### Community 75 - "Community 75" Cohesion: 1.0 Nodes (2): getSourceProtocolAttachment(), normalizeAttachmentFiles() @@ -342,35 +342,31 @@ Cohesion: 1.0 Nodes (2): emptyTabCounts(), useQcWorkOrderTabUnfinishedCounts() ## Knowledge Gaps -- **Thin community `Community 41`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue` +- **Thin community `Community 23`** (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 47`** (8 nodes): `TableHeightCalculator`, `.calculate()`, `.calculateOffset()`, `.calculatePaginationOffset()`, `.constructor()`, `.getHeaderHeight()`, `useTableHeight()`, `useTableHeight.ts` +- **Thin community `Community 44`** (9 nodes): `cleanupEventListeners()`, `handleTouchEnd()`, `handleTouchMove()`, `handleTouchStart()`, `handleWheelScroll()`, `if()`, `setTransition()`, `setupEventListeners()`, `index.vue` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 49`** (7 nodes): `App.vue`, `main.ts`, `refreshButtonAutoIcons()`, `refreshPanelTitleIcons()`, `logApiCryptoBootInfo()`, `maskSecret()`, `console.ts` +- **Thin community `Community 51`** (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 51`** (7 nodes): `fetchConfirmQcMonthImport()`, `fetchCreateQcMonthTemplate()`, `fetchDeleteQcMonthTemplate()`, `fetchPreviewQcMonthImport()`, `fetchQcMonthTemplateList()`, `fetchUpdateQcMonthTemplate()`, `qc-material-month-template.ts` +- **Thin community `Community 53`** (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 52`** (7 nodes): `fetchCreateLeave()`, `fetchDeleteLeave()`, `fetchLeaveDetail()`, `fetchLeaveList()`, `fetchSubmitLeave()`, `fetchUpdateLeave()`, `leave.ts` +- **Thin community `Community 54`** (7 nodes): `fetchConfirmQcMonthImport()`, `fetchCreateQcMonthTemplate()`, `fetchDeleteQcMonthTemplate()`, `fetchPreviewQcMonthImport()`, `fetchQcMonthTemplateList()`, `fetchUpdateQcMonthTemplate()`, `qc-material-month-template.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 53`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts` +- **Thin community `Community 55`** (6 nodes): `formatQcWorkOrderUnfinishedBadgeCount()`, `isUnfinishedQcWorkOrder()`, `qcWorkOrderUnfinishedBadgeTitle()`, `resolveQcWorkOrderUnfinishedBadgeCount()`, `sumQcWorkOrderGroupUnfinishedBadgeCount()`, `qc-work-order-unfinished-badge.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 54`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts` +- **Thin community `Community 56`** (5 nodes): `equipmentOptionLabel()`, `joinEquipmentIds()`, `parseEquipmentIds()`, `syncEquipmentNamesFromOptions()`, `equipment-select.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 56`** (5 nodes): `focusInput()`, `handleKeydown()`, `if()`, `search()`, `index.vue` +- **Thin community `Community 58`** (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 59`** (4 nodes): `isCurrentPending()`, `isNotReached()`, `mergeProgressWithComments()`, `oa-progress-merge-comments.ts` +- **Thin community `Community 61`** (4 nodes): `isCurrentPending()`, `isNotReached()`, `mergeProgressWithComments()`, `oa-progress-merge-comments.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 60`** (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 62`** (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 63`** (4 nodes): `fetchGenerateSms()`, `fetchSampleSmsNotificationList()`, `fetchToggleSmsNotified()`, `sample-sms-notification.ts` +- **Thin community `Community 62`** (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 67`** (3 nodes): `getSettingDefaults()`, `resetToDefaults()`, `setting.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 70`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts` +- **Thin community `Community 71`** (3 nodes): `copyTextToClipboard()`, `copyTextWithFallback()`, `copy-text.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. -- **Thin community `Community 72`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts` +- **Thin community `Community 75`** (3 nodes): `getSourceProtocolAttachment()`, `normalizeAttachmentFiles()`, `protocol-source-attachment.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. - **Thin community `Community 88`** (3 nodes): `emptyTabCounts()`, `useProductionWorkOrderTabStepCounts()`, `useProductionWorkOrderTabStepCounts.ts` Too small to be a meaningful cluster - may be noise or needs more connections extracted. @@ -382,17 +378,17 @@ Nodes (2): emptyTabCounts(), useQcWorkOrderTabUnfinishedCounts() ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ -- **Why does `push()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 11`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 24`, `Community 25`, `Community 27`, `Community 28`, `Community 29`?** - _High betweenness centrality (0.262) - this node is a cross-community bridge._ -- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 12`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 23`, `Community 27`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 38`, `Community 51`, `Community 52`, `Community 62`, `Community 63`?** - _High betweenness centrality (0.256) - this node is a cross-community bridge._ -- **Why does `Number()` connect `Community 2` to `Community 0`, `Community 1`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 12`, `Community 13`, `Community 14`, `Community 16`, `Community 18`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 28`, `Community 31`, `Community 37`, `Community 50`?** - _High betweenness centrality (0.187) - this node is a cross-community bridge._ +- **Why does `get()` connect `Community 0` to `Community 1`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 15`, `Community 16`, `Community 17`, `Community 18`, `Community 19`, `Community 20`, `Community 21`, `Community 22`, `Community 25`, `Community 26`, `Community 27`, `Community 28`, `Community 29`, `Community 30`, `Community 31`, `Community 32`, `Community 33`, `Community 36`, `Community 41`, `Community 46`, `Community 54`?** + _High betweenness centrality (0.264) - this node is a cross-community bridge._ +- **Why does `push()` connect `Community 1` to `Community 0`, `Community 2`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 19`, `Community 20`, `Community 22`, `Community 23`, `Community 25`, `Community 26`, `Community 27`, `Community 29`?** + _High betweenness centrality (0.239) - this node is a cross-community bridge._ +- **Why does `Number()` connect `Community 2` to `Community 0`, `Community 1`, `Community 3`, `Community 5`, `Community 6`, `Community 7`, `Community 8`, `Community 10`, `Community 13`, `Community 15`, `Community 16`, `Community 17`, `Community 21`, `Community 22`, `Community 28`, `Community 30`, `Community 39`, `Community 40`, `Community 46`?** + _High betweenness centrality (0.192) - this node is a cross-community bridge._ - **Are the 338 inferred relationships involving `get()` (e.g. with `resolveStorageSlotRangeFill()` and `buildNodeCodePathFromFlat()`) actually correct?** _`get()` has 338 INFERRED edges - model-reasoned connections that need verification._ - **Are the 127 inferred relationships involving `push()` (e.g. with `refreshQcMenuBadges()` and `collectLeafFacilityNodesInTreeOrder()`) actually correct?** _`push()` has 127 INFERRED edges - model-reasoned connections that need verification._ -- **Are the 95 inferred relationships involving `Number()` (e.g. with `slotOrderNum()` and `isSlotAvailable()`) actually correct?** - _`Number()` has 95 INFERRED edges - model-reasoned connections that need verification._ +- **Are the 98 inferred relationships involving `Number()` (e.g. with `slotOrderNum()` and `isSlotAvailable()`) actually correct?** + _`Number()` has 98 INFERRED edges - model-reasoned connections that need verification._ - **Are the 35 inferred relationships involving `emit()` (e.g. with `updateSamples()` and `notifyChanged()`) actually correct?** _`emit()` has 35 INFERRED edges - model-reasoned connections that need verification._ \ No newline at end of file diff --git a/web-admin/graphify-out/graph.html b/web-admin/graphify-out/graph.html index 2bb058fc..323fb383 100644 --- a/web-admin/graphify-out/graph.html +++ b/web-admin/graphify-out/graph.html @@ -57,12 +57,12 @@
-
3840 nodes · 5657 edges · 387 communities
+
3839 nodes · 5656 edges · 387 communities
@@ -489,7 +633,7 @@ } .v-timeline-field--full { - flex: 1 1 220px; + flex: 1 1 100%; } @media (max-width: 560px) { @@ -523,4 +667,14 @@ line-height: 1.4; } } + + .preview-container { + img, + video { + display: block; + max-width: 100%; + max-height: 70vh; + margin: 0 auto; + } + } diff --git a/web-admin/src/components/business/outbound-material-form-sections/index.vue b/web-admin/src/components/business/outbound-material-form-sections/index.vue index ed6a38ce..51b913a4 100644 --- a/web-admin/src/components/business/outbound-material-form-sections/index.vue +++ b/web-admin/src/components/business/outbound-material-form-sections/index.vue @@ -363,8 +363,9 @@ @@ -406,7 +407,9 @@ Document, Download, View, - VideoPlay + VideoPlay, + Check, + Close } from '@element-plus/icons-vue' import { fetchUploadAttachment } from '@/api/business' import { useIndexedAttachmentDropZone } from '@/composables/useAttachmentDropZone' diff --git a/web-admin/src/components/business/protocol-handle-panel/index.vue b/web-admin/src/components/business/protocol-handle-panel/index.vue new file mode 100644 index 00000000..5df54d01 --- /dev/null +++ b/web-admin/src/components/business/protocol-handle-panel/index.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/web-admin/src/components/business/sample-code-with-special-display/index.vue b/web-admin/src/components/business/sample-code-with-special-display/index.vue index 317b842e..c0b90c4e 100644 --- a/web-admin/src/components/business/sample-code-with-special-display/index.vue +++ b/web-admin/src/components/business/sample-code-with-special-display/index.vue @@ -59,6 +59,9 @@ align-items: center; flex-wrap: wrap; gap: 6px; + /* 避免换行后撑满单元格导致在 align=center 列内左对齐 */ + width: fit-content; + max-width: 100%; vertical-align: middle; } diff --git a/web-admin/src/components/business/sample-freight-pick-table/index.vue b/web-admin/src/components/business/sample-freight-pick-table/index.vue new file mode 100644 index 00000000..d5af7ca8 --- /dev/null +++ b/web-admin/src/components/business/sample-freight-pick-table/index.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/web-admin/src/components/business/sample-payment-detail-dialog/index.vue b/web-admin/src/components/business/sample-payment-detail-dialog/index.vue index 2c4bc75c..51442dc0 100644 --- a/web-admin/src/components/business/sample-payment-detail-dialog/index.vue +++ b/web-admin/src/components/business/sample-payment-detail-dialog/index.vue @@ -3,7 +3,7 @@
- + @@ -693,6 +700,11 @@ font-weight: 400; } + .detail-tag { + align-self: flex-start; + width: fit-content; + } + .amount-text { font-weight: 600; color: var(--primary-dark, #059669); @@ -734,6 +746,17 @@ } } + .transfer-action-stack { + display: inline-flex; + flex-direction: column; + align-items: center; + gap: 4px; + + :deep(.el-button + .el-button) { + margin-left: 0; + } + } + .muted-dash { color: var(--mint-text-muted, #94a3b8); } diff --git a/web-admin/src/components/business/sample-production-info-cards/index.vue b/web-admin/src/components/business/sample-production-info-cards/index.vue index b617ddc2..70e1d2a6 100644 --- a/web-admin/src/components/business/sample-production-info-cards/index.vue +++ b/web-admin/src/components/business/sample-production-info-cards/index.vue @@ -199,11 +199,13 @@ } .sample-card__section-label { - font-size: 11px; + margin: 0 0 10px; + padding-left: 10px; + font-size: 13px; font-weight: 600; - color: var(--mint-text-muted, #334155); - margin-bottom: 10px; - letter-spacing: 0.3px; + line-height: 1.4; + color: #334155; + border-left: 3px solid #10b981; } .sample-production-empty, diff --git a/web-admin/src/components/business/sample-qc-info-grouped-panel/index.vue b/web-admin/src/components/business/sample-qc-info-grouped-panel/index.vue index 20d7d07b..be19564f 100644 --- a/web-admin/src/components/business/sample-qc-info-grouped-panel/index.vue +++ b/web-admin/src/components/business/sample-qc-info-grouped-panel/index.vue @@ -103,11 +103,13 @@ } .sample-card__section-label { - margin-bottom: 10px; - font-size: 11px; + margin: 0 0 10px; + padding-left: 10px; + font-size: 13px; font-weight: 600; - color: var(--mint-text-muted, #334155); - letter-spacing: 0.3px; + line-height: 1.4; + color: #334155; + border-left: 3px solid #10b981; } .sample-qc-grouped-panel { @@ -124,9 +126,9 @@ } .sample-qc-grouped-panel__subtitle { + margin: 0 0 8px; padding-left: 10px; - margin: 0; - font-size: 12px; + font-size: 13px; font-weight: 600; line-height: 1.4; color: #334155; diff --git a/web-admin/src/components/business/sample-reception-dual-readonly-panel/index.vue b/web-admin/src/components/business/sample-reception-dual-readonly-panel/index.vue index 2fbcbb26..d3dece0b 100644 --- a/web-admin/src/components/business/sample-reception-dual-readonly-panel/index.vue +++ b/web-admin/src/components/business/sample-reception-dual-readonly-panel/index.vue @@ -2,7 +2,7 @@