Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/jingchuan-brand-shipment-todo

This commit is contained in:
jingchuan 2026-08-11 14:54:22 +08:00
commit b2fb4165b5
4 changed files with 232 additions and 18 deletions

View File

@ -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,

View File

@ -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) {

View File

@ -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 {

View File

@ -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 = ?