diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index 99fd7cfc..fb85d7ed 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -43,6 +43,7 @@ from app.api.mini_menu import router as mini_menu_router from app.api.market_mobile import router as market_mobile_router from app.api.sample_storage_cert_qc import router as sample_storage_cert_qc_router from app.api.customer_service_dict import router as customer_service_dict_router +from app.api.sample_cert_insurance import router as sample_cert_insurance_router __all__ = [ "public_router", @@ -89,4 +90,5 @@ __all__ = [ "market_mobile_router", "sample_storage_cert_qc_router", "customer_service_dict_router", + "sample_cert_insurance_router", ] diff --git a/backend/app/api/sample_cert_insurance.py b/backend/app/api/sample_cert_insurance.py new file mode 100644 index 00000000..a33528da --- /dev/null +++ b/backend/app/api/sample_cert_insurance.py @@ -0,0 +1,210 @@ +"""样本级储存证 / 保险办理 API""" +from typing import Optional + +from fastapi import APIRouter, Depends +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 sample_insurance as insurance_crud +from app.crud import sample_storage_cert as cert_crud +from app.models.models import BizProtocolSample, SysUser + +router = APIRouter(prefix="/web/admin", tags=["样本储存证保险"]) + +_MENU = require_any_menu_code("cell-storage:sample-info:list") + + +def _get_tenant_no(db: Session, user: dict) -> str: + user_id = user.get("user_id") + if not user_id: + raise PermissionError("用户身份信息缺失,无法确定租户") + sys_user = db.query(SysUser).filter(SysUser.id == user_id).first() + if not sys_user or not sys_user.tenant_no: + raise PermissionError("用户租户信息缺失") + return sys_user.tenant_no + + +def _get_current_user_id(user: dict) -> str: + user_id = user.get("user_id") + if not user_id: + raise PermissionError("用户身份信息缺失") + return str(user_id) + + +def _resolve_protocol_id(db: Session, tenant_no: str, sample_code: str) -> Optional[str]: + row = ( + db.query(BizProtocolSample) + .filter( + BizProtocolSample.tenant_no == tenant_no, + BizProtocolSample.sample_code == sample_code, + BizProtocolSample.del_flag == 0, + ) + .first() + ) + return (row.collection_protocol_id or "").strip() or None if row else None + + +class SampleHandleBody(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") + insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments") + remark: Optional[str] = None + handle_remark: Optional[str] = Field(None, alias="handleRemark") + mail_cert_time: Optional[str] = Field(None, alias="mailCertTime") + policy_send_time: Optional[str] = Field(None, alias="policySendTime") + collection_protocol_id: Optional[str] = Field(None, alias="collectionProtocolId") + + +@router.get("/sample-storage-certs/{sample_code}") +async def get_sample_storage_cert( + sample_code: str, + db: Session = Depends(get_db), + user: dict = Depends(_MENU), +): + try: + tenant_no = _get_tenant_no(db, user) + code = (sample_code or "").strip() + row = cert_crud.get_by_sample_code(db, tenant_no, code) + if row is None: + return success_response( + { + "sampleCode": code, + "storageCertHandled": False, + "handleTime": "", + "handlerUserId": "", + "certImageUrls": "", + "mailCertTime": "", + "remark": "", + } + ) + return success_response(cert_crud.row_to_dict(row)) + except PermissionError as e: + return error_response(403, str(e)) + except Exception as e: + return error_response(400, str(e)) + + +@router.put("/sample-storage-certs/{sample_code}") +async def put_sample_storage_cert( + sample_code: str, + body: SampleHandleBody, + db: Session = Depends(get_db), + user: dict = Depends(_MENU), +): + try: + tenant_no = _get_tenant_no(db, user) + user_id = _get_current_user_id(user) + code = (sample_code or "").strip() + pid = (body.collection_protocol_id or "").strip() or _resolve_protocol_id( + db, tenant_no, code + ) + ht = parse_datetime(body.handle_time) if body.handle_time else None + mail_raw = body.mail_cert_time + update_mail = mail_raw is not None + mail_t = parse_datetime(mail_raw) if mail_raw and str(mail_raw).strip() else None + row = cert_crud.upsert_handle( + db, + tenant_no=tenant_no, + sample_code=code, + collection_protocol_id=pid, + user_id=user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + cert_image_urls=body.cert_image_urls or "", + remark=body.remark or "", + mail_cert_time=mail_t, + update_mail_cert_time=update_mail, + ) + db.commit() + return success_response(cert_crud.row_to_dict(row), message="保存成功") + except PermissionError as e: + db.rollback() + return error_response(403, str(e)) + except ValueError as e: + db.rollback() + return error_response(400, str(e)) + except Exception as e: + db.rollback() + return error_response(400, str(e)) + + +@router.get("/sample-insurances/{sample_code}") +async def get_sample_insurance( + sample_code: str, + db: Session = Depends(get_db), + user: dict = Depends(_MENU), +): + try: + tenant_no = _get_tenant_no(db, user) + code = (sample_code or "").strip() + row = insurance_crud.get_by_sample_code(db, tenant_no, code) + if row is None: + return success_response( + { + "sampleCode": code, + "insuranceHandled": False, + "handleTime": "", + "handlerUserId": "", + "insuranceAttachments": "", + "policySendTime": "", + "handleRemark": "", + } + ) + return success_response(insurance_crud.row_to_dict(row)) + except PermissionError as e: + return error_response(403, str(e)) + except Exception as e: + return error_response(400, str(e)) + + +@router.put("/sample-insurances/{sample_code}") +async def put_sample_insurance( + sample_code: str, + body: SampleHandleBody, + db: Session = Depends(get_db), + user: dict = Depends(_MENU), +): + try: + tenant_no = _get_tenant_no(db, user) + user_id = _get_current_user_id(user) + code = (sample_code or "").strip() + pid = (body.collection_protocol_id or "").strip() or _resolve_protocol_id( + db, tenant_no, code + ) + ht = parse_datetime(body.handle_time) if body.handle_time else None + policy_raw = body.policy_send_time + update_policy = policy_raw is not None + policy_t = ( + parse_datetime(policy_raw) if policy_raw and str(policy_raw).strip() else None + ) + row = insurance_crud.upsert_handle( + db, + tenant_no=tenant_no, + sample_code=code, + collection_protocol_id=pid, + user_id=user_id, + handle_time=ht, + handler_user_id=body.handler_user_id, + insurance_attachments=body.insurance_attachments or "", + handle_remark=body.handle_remark or body.remark or "", + policy_send_time=policy_t, + update_policy_send_time=update_policy, + ) + db.commit() + return success_response(insurance_crud.row_to_dict(row), message="保存成功") + except PermissionError as e: + db.rollback() + return error_response(403, str(e)) + except ValueError as e: + db.rollback() + return error_response(400, str(e)) + except Exception as e: + db.rollback() + return error_response(400, str(e)) diff --git a/backend/app/main.py b/backend/app/main.py index 9d1d22d5..013a7cec 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -13,7 +13,7 @@ from app.core.database import SessionLocal from app.core.api_request_log_middleware import ApiRequestLogMiddleware from app.core.api_crypto_middleware import ApiCryptoMiddleware from app.models import models # noqa: F401 -from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router, market_mobile_router, sample_storage_cert_qc_router, customer_service_dict_router +from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, protocol_sample_payment_router, protocol_refund_record_router, presign_order_router, sample_freight_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router, mini_menu_router, market_mobile_router, sample_storage_cert_qc_router, customer_service_dict_router, sample_cert_insurance_router @asynccontextmanager @@ -155,6 +155,7 @@ app.include_router(mini_menu_router, prefix=settings.API_V1_PREFIX) app.include_router(market_mobile_router, prefix=settings.API_V1_PREFIX) app.include_router(sample_storage_cert_qc_router, prefix=settings.API_V1_PREFIX) app.include_router(customer_service_dict_router, prefix=settings.API_V1_PREFIX) +app.include_router(sample_cert_insurance_router, prefix=settings.API_V1_PREFIX) def _mask_secret(value: str) -> str: diff --git a/web-admin/src/api/sample-info-handle.ts b/web-admin/src/api/sample-info-handle.ts index 7fd3861f..68f53daf 100644 --- a/web-admin/src/api/sample-info-handle.ts +++ b/web-admin/src/api/sample-info-handle.ts @@ -55,3 +55,45 @@ export function fetchUpdateFullmoonHandle( ) { return request.put({ url: `/web/admin/fullmoon-todos/${protocolId}/handle`, data }) } + +export function fetchSampleStorageCert(sampleCode: string) { + return request.get({ url: `/web/admin/sample-storage-certs/${encodeURIComponent(sampleCode)}` }) +} + +export function fetchUpdateSampleStorageCert( + sampleCode: string, + data: { + handleTime?: string | null + handlerUserId?: string | null + certImageUrls?: string + remark?: string + mailCertTime?: string | null + collectionProtocolId?: string + } +) { + return request.put({ + url: `/web/admin/sample-storage-certs/${encodeURIComponent(sampleCode)}`, + data + }) +} + +export function fetchSampleInsurance(sampleCode: string) { + return request.get({ url: `/web/admin/sample-insurances/${encodeURIComponent(sampleCode)}` }) +} + +export function fetchUpdateSampleInsurance( + sampleCode: string, + data: { + handleTime?: string | null + handlerUserId?: string | null + insuranceAttachments?: string + handleRemark?: string + policySendTime?: string | null + collectionProtocolId?: string + } +) { + return request.put({ + url: `/web/admin/sample-insurances/${encodeURIComponent(sampleCode)}`, + data + }) +} diff --git a/web-admin/src/components/business/protocol-handle-panel/index.vue b/web-admin/src/components/business/protocol-handle-panel/index.vue index 5df54d01..310f01c9 100644 --- a/web-admin/src/components/business/protocol-handle-panel/index.vue +++ b/web-admin/src/components/business/protocol-handle-panel/index.vue @@ -1,5 +1,6 @@