Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-brand-shipment-todo
This commit is contained in:
commit
b2fb4165b5
@ -90,7 +90,9 @@ function applyTimeoutFollowupExportStyle(worksheet: ExcelJS.Worksheet) {
|
||||
'超时原因',
|
||||
'解决措施',
|
||||
'超时跟进人',
|
||||
'预计完成状态',
|
||||
'预计完成日期',
|
||||
'下次跟进日期',
|
||||
'最近跟进时间',
|
||||
])
|
||||
const followupColumns: number[] = []
|
||||
@ -241,6 +243,12 @@ function formatTimeoutFollowupStatus(status: OaBoardTimeoutFollowupDTO['status']
|
||||
return '待填写'
|
||||
}
|
||||
|
||||
function formatCompletionEstimateStatus(
|
||||
status: OaBoardTimeoutFollowupDTO['completionEstimateStatus'],
|
||||
) {
|
||||
return status === 'PENDING_CONFIRMATION' ? '暂无法预估,需每日跟进' : '已确认预计完成'
|
||||
}
|
||||
|
||||
function buildTimeoutFollowupExportValues(followups: OaBoardTimeoutFollowupDTO[]) {
|
||||
const records = [...followups].sort((left, right) => {
|
||||
const leftOrder = TIMEOUT_FOLLOWUP_STAGE_ORDER.get(left.stageKey) ?? Number.MAX_SAFE_INTEGER
|
||||
@ -260,7 +268,15 @@ function buildTimeoutFollowupExportValues(followups: OaBoardTimeoutFollowupDTO[]
|
||||
timeoutReason: joinByStage((record) => record.reasonDetail),
|
||||
timeoutActionPlan: joinByStage((record) => record.actionPlan),
|
||||
timeoutFollowUpOwner: joinByStage((record) => record.followUpOwner),
|
||||
timeoutExpectedResolveDate: joinByStage((record) => record.expectedResolveDate),
|
||||
timeoutCompletionEstimateStatus: records
|
||||
.map((record) => `${record.stageLabel}:${formatCompletionEstimateStatus(record.completionEstimateStatus)}`)
|
||||
.join('\n'),
|
||||
timeoutExpectedResolveDate: joinByStage((record) =>
|
||||
record.completionEstimateStatus === 'PENDING_CONFIRMATION'
|
||||
? '待明确完成时间'
|
||||
: record.expectedResolveDate,
|
||||
),
|
||||
timeoutNextFollowUpDate: joinByStage((record) => record.nextFollowUpDate ?? record.expectedResolveDate),
|
||||
timeoutLastFollowedAt: joinByStage((record) =>
|
||||
(record.lastFollowedAt ?? record.updatedAt)?.replace('T', ' ').slice(0, 16) ?? null,
|
||||
),
|
||||
@ -439,7 +455,9 @@ export async function GET(request: NextRequest) {
|
||||
{ header: '超时原因', key: 'timeoutReason', width: 32 },
|
||||
{ header: '解决措施', key: 'timeoutActionPlan', width: 36 },
|
||||
{ header: '超时跟进人', key: 'timeoutFollowUpOwner', width: 20 },
|
||||
{ header: '预计完成状态', key: 'timeoutCompletionEstimateStatus', width: 24 },
|
||||
{ header: '预计完成日期', key: 'timeoutExpectedResolveDate', width: 20 },
|
||||
{ header: '下次跟进日期', key: 'timeoutNextFollowUpDate', width: 20 },
|
||||
{ header: '最近跟进时间', key: 'timeoutLastFollowedAt', width: 22 },
|
||||
...TIMING_DETAIL_DEFINITIONS.map((field) => ({
|
||||
header: field.header,
|
||||
|
||||
@ -3,6 +3,7 @@ import { z } from 'zod'
|
||||
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
|
||||
import { ErrorCode } from '@/server/core/errors'
|
||||
import {
|
||||
OA_BOARD_COMPLETION_ESTIMATE_STATUSES,
|
||||
OA_BOARD_TIMEOUT_FOLLOWUP_STATUSES,
|
||||
oaBoardTimeoutFollowupService,
|
||||
} from '@/server/services/oa-board-timeout-followup.service'
|
||||
@ -21,7 +22,9 @@ const timeoutFollowupSchema = z.object({
|
||||
reasonDetail: z.string().trim().max(4000).optional().nullable(),
|
||||
actionPlan: z.string().trim().max(4000).optional().nullable(),
|
||||
followUpOwner: z.string().trim().max(255).optional().nullable(),
|
||||
completionEstimateStatus: z.enum(OA_BOARD_COMPLETION_ESTIMATE_STATUSES).optional().default('CONFIRMED'),
|
||||
expectedResolveDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
||||
nextFollowUpDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
||||
})
|
||||
|
||||
function readBoardKeys(searchParams: URLSearchParams) {
|
||||
|
||||
@ -114,6 +114,7 @@ type CurrentOwnerInsight = {
|
||||
}
|
||||
|
||||
type TimeoutFollowupStatus = 'PENDING' | 'IN_PROGRESS' | 'RESOLVED' | 'NOT_REQUIRED'
|
||||
type CompletionEstimateStatus = 'CONFIRMED' | 'PENDING_CONFIRMATION'
|
||||
|
||||
type TimeoutAnalysisView = 'current' | 'history'
|
||||
|
||||
@ -132,7 +133,9 @@ type OaBoardTimeoutFollowup = {
|
||||
reasonDetail: string | null
|
||||
actionPlan: string | null
|
||||
followUpOwner: string | null
|
||||
completionEstimateStatus: CompletionEstimateStatus
|
||||
expectedResolveDate: string | null
|
||||
nextFollowUpDate: string | null
|
||||
updatedBy: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
@ -142,7 +145,9 @@ type TimeoutFollowupDraft = {
|
||||
reasonDetail: string
|
||||
actionPlan: string
|
||||
followUpOwner: string
|
||||
completionEstimateStatus: CompletionEstimateStatus
|
||||
expectedResolveDate: string
|
||||
nextFollowUpDate: string
|
||||
}
|
||||
|
||||
type OvertimeStage = {
|
||||
@ -208,6 +213,10 @@ const TIMEOUT_FOLLOWUP_STATUS_OPTIONS: FilterOption[] = [
|
||||
{ value: 'RESOLVED', label: '已解决' },
|
||||
{ value: 'NOT_REQUIRED', label: '无需处理' },
|
||||
]
|
||||
const COMPLETION_ESTIMATE_STATUS_OPTIONS: FilterOption[] = [
|
||||
{ value: 'CONFIRMED', label: '已确认预计完成' },
|
||||
{ value: 'PENDING_CONFIRMATION', label: '暂无法预估,需每日跟进' },
|
||||
]
|
||||
const COLUMN_WIDTHS = {
|
||||
image: 110,
|
||||
openingCode: 160,
|
||||
@ -774,18 +783,22 @@ function getTimeoutFollowupStatusLabel(status: TimeoutFollowupStatus) {
|
||||
return TIMEOUT_FOLLOWUP_STATUS_OPTIONS.find((option) => option.value === status)?.label ?? '待填写'
|
||||
}
|
||||
|
||||
function isFollowupCompletionPending(followup: OaBoardTimeoutFollowup | null | undefined) {
|
||||
return followup?.completionEstimateStatus === 'PENDING_CONFIRMATION'
|
||||
}
|
||||
|
||||
function getFollowupDeadlineStatus(followup: OaBoardTimeoutFollowup | null | undefined) {
|
||||
if (!followup) return null
|
||||
|
||||
return getOaBoardFollowupDeadlineStatus(
|
||||
followup.expectedResolveDate,
|
||||
followup.nextFollowUpDate ?? followup.expectedResolveDate,
|
||||
followup.status === 'RESOLVED' || followup.status === 'NOT_REQUIRED',
|
||||
)
|
||||
}
|
||||
|
||||
function getFollowupDeadlineLabel(status: ReturnType<typeof getOaBoardFollowupDeadlineStatus>) {
|
||||
if (!status) return null
|
||||
return status.state === 'DUE_TODAY' ? '今日到期' : `已逾期 ${status.overdueDays} 天`
|
||||
return status.state === 'DUE_TODAY' ? '今日需跟进' : `已逾期 ${status.overdueDays} 天未跟进`
|
||||
}
|
||||
|
||||
function createTimeoutFollowupDraft(
|
||||
@ -797,7 +810,9 @@ function createTimeoutFollowupDraft(
|
||||
reasonDetail: followup?.reasonDetail ?? '',
|
||||
actionPlan: followup?.actionPlan ?? '',
|
||||
followUpOwner: followup?.followUpOwner ?? defaultOwner,
|
||||
completionEstimateStatus: followup?.completionEstimateStatus ?? 'CONFIRMED',
|
||||
expectedResolveDate: followup?.expectedResolveDate ?? '',
|
||||
nextFollowUpDate: followup?.nextFollowUpDate ?? followup?.expectedResolveDate ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
@ -1372,6 +1387,7 @@ function HistoricalTimeoutAnalysis({
|
||||
() => [
|
||||
{ value: '', label: '全部跟进状态' },
|
||||
{ value: 'UNRECORDED', label: '未填写跟进' },
|
||||
{ value: 'PENDING_CONFIRMATION', label: '待明确完成时间' },
|
||||
{ value: 'DEADLINE_DUE', label: '跟进已到期' },
|
||||
...TIMEOUT_FOLLOWUP_STATUS_OPTIONS,
|
||||
],
|
||||
@ -1382,10 +1398,15 @@ function HistoricalTimeoutAnalysis({
|
||||
historicalRecords.filter((record) => {
|
||||
const updatedDate = String(record.followup?.updatedAt ?? '').slice(0, 10)
|
||||
if (statusFilter === 'UNRECORDED' && record.followup) return false
|
||||
if (
|
||||
statusFilter === 'PENDING_CONFIRMATION' &&
|
||||
record.followup?.completionEstimateStatus !== 'PENDING_CONFIRMATION'
|
||||
) return false
|
||||
if (statusFilter === 'DEADLINE_DUE' && !getFollowupDeadlineStatus(record.followup)) return false
|
||||
if (
|
||||
statusFilter &&
|
||||
statusFilter !== 'UNRECORDED' &&
|
||||
statusFilter !== 'PENDING_CONFIRMATION' &&
|
||||
statusFilter !== 'DEADLINE_DUE' &&
|
||||
record.followup?.status !== statusFilter
|
||||
) return false
|
||||
@ -1408,12 +1429,19 @@ function HistoricalTimeoutAnalysis({
|
||||
const resolved = filteredRecords.filter(
|
||||
(record) => record.followup?.status === 'RESOLVED' || record.followup?.status === 'NOT_REQUIRED',
|
||||
).length
|
||||
const pendingCompletionConfirmation = filteredRecords.filter(
|
||||
(record) =>
|
||||
record.followup?.completionEstimateStatus === 'PENDING_CONFIRMATION' &&
|
||||
record.followup.status !== 'RESOLVED' &&
|
||||
record.followup.status !== 'NOT_REQUIRED',
|
||||
).length
|
||||
const due = filteredRecords.filter((record) => Boolean(getFollowupDeadlineStatus(record.followup))).length
|
||||
|
||||
return {
|
||||
total: filteredRecords.length,
|
||||
pending,
|
||||
resolved,
|
||||
pendingCompletionConfirmation,
|
||||
due,
|
||||
averageOvertimeDays:
|
||||
filteredRecords.length > 0
|
||||
@ -1434,7 +1462,7 @@ function HistoricalTimeoutAnalysis({
|
||||
<FilterInput label="更新结束日期" type="date" value={updatedDateEnd} placeholder="结束日期" onChange={setUpdatedDateEnd} />
|
||||
</div>
|
||||
</header>
|
||||
<div className="grid divide-y divide-apple-separator sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-5">
|
||||
<div className="grid divide-y divide-apple-separator sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-6">
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-[11px] text-apple-label-secondary">历史超时项</p>
|
||||
<p className="mt-1 text-2xl font-apple-semibold text-apple-label-primary">{historySummary.total}</p>
|
||||
@ -1447,6 +1475,10 @@ function HistoricalTimeoutAnalysis({
|
||||
<p className="text-[11px] text-apple-label-secondary">已完成</p>
|
||||
<p className="mt-1 text-2xl font-apple-semibold text-emerald-700">{historySummary.resolved}</p>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-[11px] text-apple-label-secondary">待明确完成时间</p>
|
||||
<p className="mt-1 text-2xl font-apple-semibold text-amber-700">{historySummary.pendingCompletionConfirmation}</p>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<p className="text-[11px] text-apple-label-secondary">跟进已到期</p>
|
||||
<p className="mt-1 text-2xl font-apple-semibold text-rose-700">{historySummary.due}</p>
|
||||
@ -1482,7 +1514,7 @@ function HistoricalTimeoutAnalysis({
|
||||
<th className="px-5 py-3 font-apple-semibold">标准时效</th>
|
||||
<th className="px-5 py-3 font-apple-semibold">实际用时</th>
|
||||
<th className="px-5 py-3 font-apple-semibold">超时用时</th>
|
||||
<th className="px-5 py-3 font-apple-semibold">预计完成</th>
|
||||
<th className="px-5 py-3 font-apple-semibold">预计完成 / 下次跟进</th>
|
||||
<th className="px-5 py-3 font-apple-semibold">最近更新</th>
|
||||
<th className="px-5 py-3 font-apple-semibold">查看</th>
|
||||
</tr>
|
||||
@ -1510,7 +1542,18 @@ function HistoricalTimeoutAnalysis({
|
||||
<td className="px-5 py-3 text-apple-label-primary">{record.stage.actualDays} 天</td>
|
||||
<td className="px-5 py-3 text-rose-700">{record.stage.overtimeDays} 天</td>
|
||||
<td className={`px-5 py-3 ${deadlineStatus ? 'font-apple-semibold text-rose-700' : 'text-apple-label-primary'}`}>
|
||||
{formatCellText(followup?.expectedResolveDate)}
|
||||
{followup ? (
|
||||
<div>
|
||||
<p>
|
||||
{isFollowupCompletionPending(followup)
|
||||
? '待明确完成时间'
|
||||
: `预计完成:${formatCellText(followup.expectedResolveDate)}`}
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] font-normal text-apple-label-secondary">
|
||||
下次跟进:{formatCellText(followup.nextFollowUpDate ?? followup.expectedResolveDate)}
|
||||
</p>
|
||||
</div>
|
||||
) : '-'}
|
||||
{deadlineStatus ? (
|
||||
<span className="mt-1 block text-[10px]">{getFollowupDeadlineLabel(deadlineStatus)}</span>
|
||||
) : null}
|
||||
@ -1617,6 +1660,7 @@ function TimeoutFollowupInspector({
|
||||
selectedFollowup,
|
||||
resolveTimeoutFollowupOwner(item, selectedStage.stageKey),
|
||||
)
|
||||
const isClosedFollowup = draft.status === 'RESOLVED' || draft.status === 'NOT_REQUIRED'
|
||||
const updateDraft = (update: (current: TimeoutFollowupDraft) => TimeoutFollowupDraft) => {
|
||||
setDraftsByStageKey((current) => ({
|
||||
...current,
|
||||
@ -1712,6 +1756,11 @@ function TimeoutFollowupInspector({
|
||||
? '未填写跟进'
|
||||
: '待跟进'}
|
||||
</span>
|
||||
{record && isFollowupCompletionPending(record) ? (
|
||||
<span className={`mt-1 text-[10px] ${active ? 'text-amber-200' : 'text-amber-700'}`}>
|
||||
待明确完成时间
|
||||
</span>
|
||||
) : null}
|
||||
{deadlineStatus ? (
|
||||
<span className={`mt-1 text-[10px] font-apple-semibold ${active ? 'text-rose-200' : 'text-rose-700'}`}>
|
||||
{getFollowupDeadlineLabel(deadlineStatus)}
|
||||
@ -1763,17 +1812,43 @@ function TimeoutFollowupInspector({
|
||||
placeholder="填写负责跟进的人员"
|
||||
onChange={(value) => updateDraft((current) => ({ ...current, followUpOwner: value }))}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="预计完成状态"
|
||||
value={draft.completionEstimateStatus}
|
||||
options={COMPLETION_ESTIMATE_STATUS_OPTIONS}
|
||||
onChange={(value) =>
|
||||
updateDraft((current) => ({
|
||||
...current,
|
||||
completionEstimateStatus: value as CompletionEstimateStatus,
|
||||
expectedResolveDate: value === 'PENDING_CONFIRMATION' ? '' : current.expectedResolveDate,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{draft.completionEstimateStatus === 'CONFIRMED' ? (
|
||||
<FilterInput
|
||||
label="预计完成日期"
|
||||
type="date"
|
||||
value={draft.expectedResolveDate}
|
||||
placeholder="预计完成日期"
|
||||
onChange={(value) => updateDraft((current) => ({ ...current, expectedResolveDate: value }))}
|
||||
/>
|
||||
) : null}
|
||||
<FilterInput
|
||||
label="预计完成日期"
|
||||
label="下次跟进日期"
|
||||
type="date"
|
||||
value={draft.expectedResolveDate}
|
||||
placeholder="预计完成日期"
|
||||
onChange={(value) => updateDraft((current) => ({ ...current, expectedResolveDate: value }))}
|
||||
value={draft.nextFollowUpDate}
|
||||
placeholder="下次跟进日期"
|
||||
onChange={(value) => updateDraft((current) => ({ ...current, nextFollowUpDate: value }))}
|
||||
/>
|
||||
</div>
|
||||
{draft.completionEstimateStatus === 'PENDING_CONFIRMATION' && !isClosedFollowup ? (
|
||||
<div className="mt-3 border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||
暂无法给出完成承诺时,请填写原因,并填写下一次跟进日期;该日期到期后会继续进入待跟进提醒。
|
||||
</div>
|
||||
) : null}
|
||||
{selectedFollowupDeadlineStatus ? (
|
||||
<div className="mt-3 border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-apple-semibold text-rose-700">
|
||||
{getFollowupDeadlineLabel(selectedFollowupDeadlineStatus)},请更新跟进状态或调整预计完成日期。
|
||||
{getFollowupDeadlineLabel(selectedFollowupDeadlineStatus)},请更新跟进状态或调整下次跟进日期。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -3688,13 +3763,38 @@ export default function OaBoardClient({
|
||||
if (!item || entries.length === 0) return false
|
||||
|
||||
const missingReason = entries.find(
|
||||
({ draft }) => draft.status !== 'PENDING' && !draft.reasonDetail.trim(),
|
||||
({ draft }) =>
|
||||
(draft.status !== 'PENDING' || draft.completionEstimateStatus === 'PENDING_CONFIRMATION') &&
|
||||
!draft.reasonDetail.trim(),
|
||||
)
|
||||
if (missingReason) {
|
||||
setTimeoutFollowupError(`${missingReason.stage.stageLabel}:请填写超时原因说明`)
|
||||
return false
|
||||
}
|
||||
|
||||
const missingNextFollowUpDate = entries.find(
|
||||
({ draft }) =>
|
||||
draft.status !== 'RESOLVED' &&
|
||||
draft.status !== 'NOT_REQUIRED' &&
|
||||
!draft.nextFollowUpDate.trim(),
|
||||
)
|
||||
if (missingNextFollowUpDate) {
|
||||
setTimeoutFollowupError(`${missingNextFollowUpDate.stage.stageLabel}:请填写下次跟进日期`)
|
||||
return false
|
||||
}
|
||||
|
||||
const missingExpectedResolveDate = entries.find(
|
||||
({ draft }) =>
|
||||
draft.status !== 'RESOLVED' &&
|
||||
draft.status !== 'NOT_REQUIRED' &&
|
||||
draft.completionEstimateStatus === 'CONFIRMED' &&
|
||||
!draft.expectedResolveDate.trim(),
|
||||
)
|
||||
if (missingExpectedResolveDate) {
|
||||
setTimeoutFollowupError(`${missingExpectedResolveDate.stage.stageLabel}:请填写预计完成日期,或选择暂无法预估`)
|
||||
return false
|
||||
}
|
||||
|
||||
setIsSavingTimeoutFollowup(true)
|
||||
setTimeoutFollowupError(null)
|
||||
|
||||
@ -3715,7 +3815,11 @@ export default function OaBoardClient({
|
||||
actualDays: stage.actualDays,
|
||||
overtimeDays: stage.overtimeDays,
|
||||
...draft,
|
||||
expectedResolveDate: draft.expectedResolveDate.trim() || null,
|
||||
expectedResolveDate:
|
||||
draft.completionEstimateStatus === 'CONFIRMED'
|
||||
? draft.expectedResolveDate.trim() || null
|
||||
: null,
|
||||
nextFollowUpDate: draft.nextFollowUpDate.trim() || null,
|
||||
}),
|
||||
})
|
||||
const payload = (await response.json()) as {
|
||||
|
||||
@ -37,7 +37,9 @@ CREATE TABLE IF NOT EXISTS ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE} (
|
||||
reason_detail TEXT NULL,
|
||||
action_plan TEXT NULL,
|
||||
follow_up_owner VARCHAR(255) NULL,
|
||||
completion_estimate_status VARCHAR(32) NOT NULL DEFAULT 'CONFIRMED',
|
||||
expected_resolve_date DATE NULL,
|
||||
next_follow_up_date DATE NULL,
|
||||
resolved_at DATETIME NULL,
|
||||
created_by VARCHAR(255) NULL,
|
||||
updated_by VARCHAR(255) NULL,
|
||||
@ -49,6 +51,7 @@ CREATE TABLE IF NOT EXISTS ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE} (
|
||||
KEY idx_oa_board_timeout_followup_opening_code (opening_code),
|
||||
KEY idx_oa_board_timeout_followup_spu (spu),
|
||||
KEY idx_oa_board_timeout_followup_status (status),
|
||||
KEY idx_oa_board_timeout_followup_next_follow_up_date (next_follow_up_date),
|
||||
KEY idx_oa_board_timeout_followup_updated_at (updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`
|
||||
@ -63,6 +66,14 @@ export const OA_BOARD_TIMEOUT_FOLLOWUP_STATUSES = [
|
||||
export type OaBoardTimeoutFollowupStatus =
|
||||
(typeof OA_BOARD_TIMEOUT_FOLLOWUP_STATUSES)[number]
|
||||
|
||||
export const OA_BOARD_COMPLETION_ESTIMATE_STATUSES = [
|
||||
'CONFIRMED',
|
||||
'PENDING_CONFIRMATION',
|
||||
] as const
|
||||
|
||||
export type OaBoardCompletionEstimateStatus =
|
||||
(typeof OA_BOARD_COMPLETION_ESTIMATE_STATUSES)[number]
|
||||
|
||||
export interface OaBoardTimeoutFollowupDTO {
|
||||
id: number
|
||||
boardKey: string
|
||||
@ -78,7 +89,9 @@ export interface OaBoardTimeoutFollowupDTO {
|
||||
reasonDetail: string | null
|
||||
actionPlan: string | null
|
||||
followUpOwner: string | null
|
||||
completionEstimateStatus: OaBoardCompletionEstimateStatus
|
||||
expectedResolveDate: string | null
|
||||
nextFollowUpDate: string | null
|
||||
resolvedAt: string | null
|
||||
createdBy: string | null
|
||||
updatedBy: string | null
|
||||
@ -101,7 +114,9 @@ export interface SaveOaBoardTimeoutFollowupInput {
|
||||
reasonDetail?: string | null
|
||||
actionPlan?: string | null
|
||||
followUpOwner?: string | null
|
||||
completionEstimateStatus: OaBoardCompletionEstimateStatus
|
||||
expectedResolveDate?: string | null
|
||||
nextFollowUpDate?: string | null
|
||||
updatedBy?: string | null
|
||||
}
|
||||
|
||||
@ -120,7 +135,9 @@ type TimeoutFollowupRow = RowDataPacket & {
|
||||
reason_detail: string | null
|
||||
action_plan: string | null
|
||||
follow_up_owner: string | null
|
||||
completion_estimate_status: string | null
|
||||
expected_resolve_date: string | null
|
||||
next_follow_up_date: string | null
|
||||
resolved_at: string | null
|
||||
created_by: string | null
|
||||
updated_by: string | null
|
||||
@ -309,12 +326,21 @@ function isFollowupStatus(value: string): value is OaBoardTimeoutFollowupStatus
|
||||
return (OA_BOARD_TIMEOUT_FOLLOWUP_STATUSES as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
function isCompletionEstimateStatus(value: string): value is OaBoardCompletionEstimateStatus {
|
||||
return (OA_BOARD_COMPLETION_ESTIMATE_STATUSES as readonly string[]).includes(value)
|
||||
}
|
||||
|
||||
function isLegacyNodeFollowupStageKey(value: string) {
|
||||
return value.includes(':')
|
||||
}
|
||||
|
||||
function toDto(row: TimeoutFollowupRow): OaBoardTimeoutFollowupDTO {
|
||||
const status = isFollowupStatus(row.status) ? row.status : 'PENDING'
|
||||
const completionEstimateStatus: OaBoardCompletionEstimateStatus = isCompletionEstimateStatus(
|
||||
row.completion_estimate_status ?? '',
|
||||
)
|
||||
? (row.completion_estimate_status as OaBoardCompletionEstimateStatus)
|
||||
: 'CONFIRMED'
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
@ -331,7 +357,9 @@ function toDto(row: TimeoutFollowupRow): OaBoardTimeoutFollowupDTO {
|
||||
reasonDetail: asNullableText(row.reason_detail),
|
||||
actionPlan: asNullableText(row.action_plan),
|
||||
followUpOwner: asNullableText(row.follow_up_owner),
|
||||
completionEstimateStatus,
|
||||
expectedResolveDate: toDateOnly(row.expected_resolve_date),
|
||||
nextFollowUpDate: toDateOnly(row.next_follow_up_date),
|
||||
resolvedAt: asNullableText(row.resolved_at),
|
||||
createdBy: asNullableText(row.created_by),
|
||||
updatedBy: asNullableText(row.updated_by),
|
||||
@ -362,9 +390,38 @@ function validateInput(input: SaveOaBoardTimeoutFollowupInput) {
|
||||
throw new Error('请填写超时原因说明')
|
||||
}
|
||||
|
||||
if (!isCompletionEstimateStatus(input.completionEstimateStatus)) {
|
||||
throw new Error('预计完成状态不正确')
|
||||
}
|
||||
|
||||
if (input.expectedResolveDate && !/^\d{4}-\d{2}-\d{2}$/.test(input.expectedResolveDate)) {
|
||||
throw new Error('预计完成日期格式不正确')
|
||||
}
|
||||
|
||||
if (input.nextFollowUpDate && !/^\d{4}-\d{2}-\d{2}$/.test(input.nextFollowUpDate)) {
|
||||
throw new Error('下次跟进日期格式不正确')
|
||||
}
|
||||
|
||||
const isClosed = input.status === 'RESOLVED' || input.status === 'NOT_REQUIRED'
|
||||
if (isClosed) return
|
||||
|
||||
if (!toDateOnly(input.nextFollowUpDate)) {
|
||||
throw new Error('请填写下次跟进日期')
|
||||
}
|
||||
|
||||
if (
|
||||
input.completionEstimateStatus === 'CONFIRMED' &&
|
||||
!toDateOnly(input.expectedResolveDate)
|
||||
) {
|
||||
throw new Error('请填写预计完成日期,或选择暂无法预估')
|
||||
}
|
||||
|
||||
if (
|
||||
input.completionEstimateStatus === 'PENDING_CONFIRMATION' &&
|
||||
!asNullableText(input.reasonDetail)
|
||||
) {
|
||||
throw new Error('暂无法预估时请填写原因说明')
|
||||
}
|
||||
}
|
||||
|
||||
function getPool() {
|
||||
@ -388,6 +445,26 @@ async function ensureTableSchema(connection: mysql.PoolConnection) {
|
||||
ADD KEY idx_oa_board_timeout_followup_spu (spu)
|
||||
`)
|
||||
}
|
||||
|
||||
const [estimateStatusColumns] = await connection.query<RowDataPacket[]>(
|
||||
`SHOW COLUMNS FROM ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE} LIKE 'completion_estimate_status'`,
|
||||
)
|
||||
if (estimateStatusColumns.length === 0) {
|
||||
await connection.query(`
|
||||
ALTER TABLE ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE}
|
||||
ADD COLUMN completion_estimate_status VARCHAR(32) NOT NULL DEFAULT 'CONFIRMED' AFTER follow_up_owner,
|
||||
ADD COLUMN next_follow_up_date DATE NULL AFTER expected_resolve_date,
|
||||
ADD KEY idx_oa_board_timeout_followup_next_follow_up_date (next_follow_up_date)
|
||||
`)
|
||||
}
|
||||
|
||||
await connection.query(`
|
||||
UPDATE ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE}
|
||||
SET next_follow_up_date = expected_resolve_date
|
||||
WHERE next_follow_up_date IS NULL
|
||||
AND expected_resolve_date IS NOT NULL
|
||||
AND status NOT IN ('RESOLVED', 'NOT_REQUIRED')
|
||||
`)
|
||||
}
|
||||
|
||||
async function ensureTable(connection: mysql.PoolConnection) {
|
||||
@ -493,7 +570,8 @@ export class OaBoardTimeoutFollowupService {
|
||||
`
|
||||
SELECT id, board_key, opening_code, spu, stage_key, stage_label, target_days, actual_days,
|
||||
overtime_days, status, reason_category, reason_detail, action_plan,
|
||||
follow_up_owner, expected_resolve_date, resolved_at, created_by, updated_by,
|
||||
follow_up_owner, completion_estimate_status, expected_resolve_date, next_follow_up_date,
|
||||
resolved_at, created_by, updated_by,
|
||||
last_followed_at, created_at, updated_at
|
||||
FROM ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE}
|
||||
WHERE board_key IN (${normalizedBoardKeys.map(() => '?').join(', ')})
|
||||
@ -534,7 +612,12 @@ export class OaBoardTimeoutFollowupService {
|
||||
reasonDetail: asNullableText(input.reasonDetail),
|
||||
actionPlan: asNullableText(input.actionPlan),
|
||||
followUpOwner: asNullableText(input.followUpOwner),
|
||||
expectedResolveDate: toDateOnly(input.expectedResolveDate),
|
||||
completionEstimateStatus: input.completionEstimateStatus,
|
||||
expectedResolveDate:
|
||||
input.completionEstimateStatus === 'CONFIRMED'
|
||||
? toDateOnly(input.expectedResolveDate)
|
||||
: null,
|
||||
nextFollowUpDate: toDateOnly(input.nextFollowUpDate),
|
||||
updatedBy: asNullableText(input.updatedBy),
|
||||
}
|
||||
|
||||
@ -543,9 +626,10 @@ export class OaBoardTimeoutFollowupService {
|
||||
INSERT INTO ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE} (
|
||||
board_key, opening_code, spu, stage_key, stage_label, target_days, actual_days,
|
||||
overtime_days, status, reason_category, reason_detail, action_plan,
|
||||
follow_up_owner, expected_resolve_date, resolved_at, created_by, updated_by,
|
||||
follow_up_owner, completion_estimate_status, expected_resolve_date, next_follow_up_date,
|
||||
resolved_at, created_by, updated_by,
|
||||
last_followed_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
CASE WHEN ? IN ('RESOLVED', 'NOT_REQUIRED') THEN NOW() ELSE NULL END,
|
||||
?, ?, NOW()
|
||||
)
|
||||
@ -561,7 +645,9 @@ export class OaBoardTimeoutFollowupService {
|
||||
reason_detail = VALUES(reason_detail),
|
||||
action_plan = VALUES(action_plan),
|
||||
follow_up_owner = VALUES(follow_up_owner),
|
||||
completion_estimate_status = VALUES(completion_estimate_status),
|
||||
expected_resolve_date = VALUES(expected_resolve_date),
|
||||
next_follow_up_date = VALUES(next_follow_up_date),
|
||||
resolved_at = CASE
|
||||
WHEN VALUES(status) IN ('RESOLVED', 'NOT_REQUIRED') THEN COALESCE(resolved_at, NOW())
|
||||
ELSE NULL
|
||||
@ -583,7 +669,9 @@ export class OaBoardTimeoutFollowupService {
|
||||
normalizedInput.reasonDetail,
|
||||
normalizedInput.actionPlan,
|
||||
normalizedInput.followUpOwner,
|
||||
normalizedInput.completionEstimateStatus,
|
||||
normalizedInput.expectedResolveDate,
|
||||
normalizedInput.nextFollowUpDate,
|
||||
normalizedInput.status,
|
||||
normalizedInput.updatedBy,
|
||||
normalizedInput.updatedBy,
|
||||
@ -594,7 +682,8 @@ export class OaBoardTimeoutFollowupService {
|
||||
`
|
||||
SELECT id, board_key, opening_code, spu, stage_key, stage_label, target_days, actual_days,
|
||||
overtime_days, status, reason_category, reason_detail, action_plan,
|
||||
follow_up_owner, expected_resolve_date, resolved_at, created_by, updated_by,
|
||||
follow_up_owner, completion_estimate_status, expected_resolve_date, next_follow_up_date,
|
||||
resolved_at, created_by, updated_by,
|
||||
last_followed_at, created_at, updated_at
|
||||
FROM ${OA_BOARD_TIMEOUT_FOLLOWUP_TABLE}
|
||||
WHERE board_key = ? AND stage_key = ?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user