feat: switch protocol view cert/insurance handle panels to sample level.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
8cebb8e0b8
commit
efbf8f6ac9
6 files changed
+353
-14
No files matched your search
@@ -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",
|
||||
]
|
||||
@@ -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))
|
||||
+2
-1
@@ -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:
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="protocol-handle-panel">
|
||||
<p v-if="sampleCode" class="handle-sample-hint">当前样本:{{ sampleCode }}</p>
|
||||
<el-form label-position="top" class="biz-dialog-form" @submit.prevent>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="办理时间" required>
|
||||
@@ -31,6 +32,26 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="mode === 'storage-cert'" label="邮寄储存证时间">
|
||||
<el-date-picker
|
||||
v-model="form.mailCertTime"
|
||||
type="datetime"
|
||||
placeholder="可选"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="mode === 'insurance'" label="保单发送时间">
|
||||
<el-date-picker
|
||||
v-model="form.policySendTime"
|
||||
type="datetime"
|
||||
placeholder="可选"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="attachmentLabel" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
@@ -78,7 +99,11 @@
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import {
|
||||
fetchSampleInsurance,
|
||||
fetchSampleStorageCert,
|
||||
fetchUpdateFullmoonHandle,
|
||||
fetchUpdateSampleInsurance,
|
||||
fetchUpdateSampleStorageCert,
|
||||
fetchUpdateStorageCertHandle
|
||||
} from '@/api/sample-info-handle'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
@@ -88,6 +113,8 @@
|
||||
const props = defineProps<{
|
||||
mode: 'storage-cert' | 'insurance'
|
||||
protocolId: string
|
||||
/** 有值时按样本级读写(客户服务管理入口) */
|
||||
sampleCode?: string
|
||||
initial?: Record<string, unknown> | null
|
||||
}>()
|
||||
|
||||
@@ -101,13 +128,17 @@
|
||||
const form = reactive({
|
||||
handleTime: '' as string,
|
||||
handlerUserId: '' as string,
|
||||
remark: ''
|
||||
remark: '',
|
||||
mailCertTime: '',
|
||||
policySendTime: ''
|
||||
})
|
||||
|
||||
const attachmentLabel = computed(() =>
|
||||
props.mode === 'storage-cert' ? '储存证附件' : '保险附件'
|
||||
)
|
||||
|
||||
const useSampleLevel = computed(() => Boolean((props.sampleCode || '').trim()))
|
||||
|
||||
function urlsToFiles(raw: string) {
|
||||
return String(raw || '')
|
||||
.split(',')
|
||||
@@ -120,32 +151,55 @@
|
||||
}))
|
||||
}
|
||||
|
||||
function applyInitial() {
|
||||
const init = props.initial || {}
|
||||
function applyInitial(init: Record<string, unknown>) {
|
||||
form.handleTime = String(init.handleTime || '')
|
||||
form.handlerUserId = String(init.handlerUserId || '')
|
||||
form.remark = String(
|
||||
props.mode === 'storage-cert' ? init.remark || '' : init.handleRemark || ''
|
||||
props.mode === 'storage-cert' ? init.remark || '' : init.handleRemark || init.remark || ''
|
||||
)
|
||||
form.mailCertTime = String(init.mailCertTime || '')
|
||||
form.policySendTime = String(init.policySendTime || '')
|
||||
const urlField = props.mode === 'storage-cert' ? 'certImageUrls' : 'insuranceAttachments'
|
||||
files.value = urlsToFiles(String(init[urlField] || ''))
|
||||
const uid = form.handlerUserId || String(userStore.info?.userId || '')
|
||||
const uid = form.handlerUserId || String((userStore.getUserInfo as any)?.id || '')
|
||||
if (!form.handlerUserId && !form.handleTime) {
|
||||
form.handlerUserId = uid
|
||||
}
|
||||
if (form.handlerUserId) {
|
||||
const name = String(init.handlerUserName || userStore.info?.userName || '')
|
||||
const name = String(
|
||||
init.handlerUserName ||
|
||||
(userStore.getUserInfo as any)?.realName ||
|
||||
(userStore.getUserInfo as any)?.userName ||
|
||||
''
|
||||
)
|
||||
if (name) {
|
||||
userOptions.value = [
|
||||
{ id: form.handlerUserId, nickName: name, userName: name }
|
||||
]
|
||||
userOptions.value = [{ id: form.handlerUserId, nickName: name, userName: name }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSampleInitial() {
|
||||
const code = (props.sampleCode || '').trim()
|
||||
if (!code) {
|
||||
applyInitial(props.initial || {})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res =
|
||||
props.mode === 'storage-cert'
|
||||
? ((await fetchSampleStorageCert(code)) as Record<string, unknown>)
|
||||
: ((await fetchSampleInsurance(code)) as Record<string, unknown>)
|
||||
applyInitial(res || {})
|
||||
} catch {
|
||||
applyInitial(props.initial || {})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.protocolId, props.initial],
|
||||
() => applyInitial(),
|
||||
() => [props.protocolId, props.sampleCode, props.initial, props.mode],
|
||||
() => {
|
||||
void loadSampleInitial()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
@@ -182,11 +236,33 @@
|
||||
ElMessage.warning('填写办理时间时必须指定办理人员')
|
||||
return
|
||||
}
|
||||
if (!props.protocolId) return
|
||||
const code = (props.sampleCode || '').trim()
|
||||
if (useSampleLevel.value && !code) return
|
||||
if (!useSampleLevel.value && !props.protocolId) return
|
||||
saving.value = true
|
||||
try {
|
||||
const urls = files.value.map((f) => f.fileUrl).filter(Boolean).join(',')
|
||||
if (props.mode === 'storage-cert') {
|
||||
if (useSampleLevel.value) {
|
||||
if (props.mode === 'storage-cert') {
|
||||
await fetchUpdateSampleStorageCert(code, {
|
||||
handleTime: form.handleTime || null,
|
||||
handlerUserId: form.handleTime ? form.handlerUserId : null,
|
||||
certImageUrls: urls,
|
||||
remark: form.remark,
|
||||
mailCertTime: form.mailCertTime || null,
|
||||
collectionProtocolId: props.protocolId
|
||||
})
|
||||
} else {
|
||||
await fetchUpdateSampleInsurance(code, {
|
||||
handleTime: form.handleTime || null,
|
||||
handlerUserId: form.handleTime ? form.handlerUserId : null,
|
||||
insuranceAttachments: urls,
|
||||
handleRemark: form.remark,
|
||||
policySendTime: form.policySendTime || null,
|
||||
collectionProtocolId: props.protocolId
|
||||
})
|
||||
}
|
||||
} else if (props.mode === 'storage-cert') {
|
||||
await fetchUpdateStorageCertHandle(props.protocolId, {
|
||||
handleTime: form.handleTime || null,
|
||||
handlerUserId: form.handleTime ? form.handlerUserId : null,
|
||||
@@ -210,6 +286,12 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.handle-sample-hint {
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.handle-actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
v-if="protocolId"
|
||||
mode="storage-cert"
|
||||
:protocol-id="protocolId"
|
||||
:sample-code="highlightSampleCode || undefined"
|
||||
:initial="storageCertInfo"
|
||||
@saved="loadDetail"
|
||||
/>
|
||||
@@ -270,6 +271,7 @@
|
||||
v-if="protocolId"
|
||||
mode="insurance"
|
||||
:protocol-id="protocolId"
|
||||
:sample-code="highlightSampleCode || undefined"
|
||||
:initial="fullmoonInsuranceInfo"
|
||||
@saved="loadDetail"
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user