fix(sample-progress): align board with current OA stage

This commit is contained in:
jingchuan 2026-08-13 10:57:13 +08:00
parent 1433428cf7
commit 5318a30346
6 changed files with 26 additions and 20 deletions

View File

@ -3308,13 +3308,13 @@ export default function OaBoardClient({
dashboard,
loadErrorMessage,
sampleProgressItems = [],
currentSampleStageCount = 0,
currentSampleStageOpeningCodes = [],
currentUserName = '',
}: {
dashboard: OaBoardDashboardDTO
loadErrorMessage?: string | null
sampleProgressItems?: SampleProgressDTO[]
currentSampleStageCount?: number
currentSampleStageOpeningCodes?: string[]
currentUserName?: string
}) {
const { items: allItems, stageGraph, dailyReport, monthlyReport } = dashboard
@ -4475,7 +4475,7 @@ export default function OaBoardClient({
) : activeTab === 'brand-planning' ? (
<BrandPlanningBoardPanel />
) : activeTab === 'sample-progress' ? (
<SampleProgressClient initialItems={sampleProgressItems} currentUserName={currentUserName} currentSampleStageCount={currentSampleStageCount} embedded />
<SampleProgressClient initialItems={sampleProgressItems} currentUserName={currentUserName} currentSampleStageOpeningCodes={currentSampleStageOpeningCodes} embedded />
) : activeTab === 'timeout-analysis' ? (
<TimeoutAnalysis
items={items}

View File

@ -130,7 +130,7 @@ export default async function OaBoardPage({
: { ...result.dashboard, dailyReport: undefined }
await sampleProgressService.syncFromOaDashboard()
const sampleProgressItems = await sampleProgressService.listStored()
const currentSampleStageCount = await sampleProgressService.countCurrentSampleStage()
const currentSampleStageOpeningCodes = await sampleProgressService.listCurrentSampleStageOpeningCodes()
return (
<OaBoardClient
@ -138,7 +138,7 @@ export default async function OaBoardPage({
dashboard={dashboard}
loadErrorMessage={result.loadErrorMessage}
sampleProgressItems={sampleProgressItems}
currentSampleStageCount={currentSampleStageCount}
currentSampleStageOpeningCodes={currentSampleStageOpeningCodes}
currentUserName={session?.user.name ?? ''}
/>
)

View File

@ -17,10 +17,10 @@ const inputDate = (value: string | Date) => {
return Number.isNaN(date.getTime()) ? '' : date.toISOString().slice(0, 10)
}
export default function SampleProgressClient({ initialItems, currentUserName, currentSampleStageCount = 0, embedded = false }: { initialItems: SampleProgressDTO[]; currentUserName: string; currentSampleStageCount?: number; embedded?: boolean }) {
export default function SampleProgressClient({ initialItems, currentUserName, currentSampleStageOpeningCodes = [], embedded = false }: { initialItems: SampleProgressDTO[]; currentUserName: string; currentSampleStageOpeningCodes?: string[]; embedded?: boolean }) {
const [items, setItems] = useState(initialItems)
const [keyword, setKeyword] = useState('')
const [status, setStatus] = useState('ACTIVE')
const [status, setStatus] = useState('CURRENT_STAGE')
const [page, setPage] = useState(1)
const [editing, setEditing] = useState<SampleProgressDTO | null | 'new'>(null)
const [saving, setSaving] = useState(false)
@ -29,15 +29,17 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu
const [previewImage, setPreviewImage] = useState<SampleProgressDTO | null>(null)
const canEdit = (item: SampleProgressDTO) => canEditSampleProgress(currentUserName, item)
const currentStageCodes = useMemo(() => new Set(currentSampleStageOpeningCodes.map((code) => code.trim().toUpperCase())), [currentSampleStageOpeningCodes])
const currentStageItems = useMemo(() => items.filter((item) => currentStageCodes.has(item.openingCode.trim().toUpperCase())), [currentStageCodes, items])
const filtered = useMemo(() => items.filter((item) => {
const haystack = [item.openingCode, item.brand, item.supplier, item.designer, item.purchaseOwner, item.color, item.material].join(' ').toLowerCase()
const statusMatches = status === 'ALL' || (status === 'ACTIVE' ? !hiddenTerminalStatuses.has(item.status) : item.status === status)
const statusMatches = status === 'ALL' || (status === 'CURRENT_STAGE' ? currentStageCodes.has(item.openingCode.trim().toUpperCase()) : item.status === status)
return (!keyword || haystack.includes(keyword.toLowerCase())) && statusMatches
}), [items, keyword, status])
}), [currentStageCodes, items, keyword, status])
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
const rows = filtered.slice((Math.min(page, totalPages) - 1) * PAGE_SIZE, Math.min(page, totalPages) * PAGE_SIZE)
const overdueCount = items.filter(isOverdue).length
const statusCounts = SAMPLE_PROGRESS_STATUSES.map((value) => ({ value, count: items.filter((item) => item.status === value).length }))
const overdueCount = currentStageItems.filter(isOverdue).length
const statusCounts = SAMPLE_PROGRESS_STATUSES.map((value) => ({ value, count: currentStageItems.filter((item) => item.status === value).length }))
async function remove(item: SampleProgressDTO) {
if (!window.confirm(`确认删除 ${item.openingCode} 的打样记录?`)) return
@ -53,12 +55,13 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu
</div>
</header>
<section className="grid border-b border-black/[0.06] bg-white sm:grid-cols-2">
<Metric icon={<CalendarClock size={18}/>} label="当前打样节点" value={`${currentSampleStageCount} 款`} tone="plain" />
<section className="grid border-b border-black/[0.06] bg-white sm:grid-cols-3">
<Metric icon={<CalendarClock size={18}/>} label="当前打样节点" value={`${currentStageItems.length} 款`} tone="plain" />
<Metric icon={<AlertTriangle size={18}/>} label="交期已超期" value={`${overdueCount} 款`} tone={overdueCount ? 'danger' : 'plain'} />
<Metric icon={<Link2 size={18}/>} label="OA 台账匹配" value={`${currentStageItems.length} / ${currentStageCodes.size}`} tone={currentStageItems.length === currentStageCodes.size ? 'success' : 'danger'} />
</section>
<section className="grid border-b border-black/[0.06] bg-white sm:grid-cols-2 lg:grid-cols-4 2xl:grid-cols-7">
<section className="grid grid-cols-3 border-b border-black/[0.06] bg-white md:grid-cols-5 xl:grid-cols-9">
{statusCounts.map(({ value, count }) => (
<button key={value} type="button" onClick={() => { setStatus(value); setPage(1) }} className={`h-[68px] border-b border-r border-black/[0.05] px-4 text-left transition hover:bg-[#f7f9f8] ${status === value ? 'bg-[#edf4f1]' : ''}`}>
<div className="text-xs text-[#788083]">{SAMPLE_PROGRESS_STATUS_LABELS[value]}</div>
@ -70,7 +73,7 @@ export default function SampleProgressClient({ initialItems, currentUserName, cu
<section className="p-4 sm:p-6">
<div className="mb-3 flex flex-col gap-2 md:flex-row">
<label className="relative min-w-0 flex-1 md:max-w-[420px]"><Search className="absolute left-3 top-2.5 text-[#8c9496]" size={16}/><input value={keyword} onChange={(e) => { setKeyword(e.target.value); setPage(1) }} placeholder="搜索开款编号、品牌、供应商、设计师或采购负责人" className="h-10 w-full rounded-[6px] border border-black/[0.1] bg-white pl-9 pr-3 text-sm outline-none focus:border-[#26756b]"/></label>
<select aria-label="打样状态" value={status} onChange={(e) => { setStatus(e.target.value); setPage(1) }} className="h-10 rounded-[6px] border border-black/[0.1] bg-white px-3 text-sm"><option value="ACTIVE">打样状态</option><option value="ALL">全部打样状态</option>{SAMPLE_PROGRESS_STATUSES.map((value) => <option key={value} value={value}>{SAMPLE_PROGRESS_STATUS_LABELS[value]}</option>)}</select>
<select aria-label="打样状态" value={status} onChange={(e) => { setStatus(e.target.value); setPage(1) }} className="h-10 rounded-[6px] border border-black/[0.1] bg-white px-3 text-sm"><option value="CURRENT_STAGE">当前打样节点</option><option value="ALL">全部打样台账({items.length})</option>{SAMPLE_PROGRESS_STATUSES.map((value) => <option key={value} value={value}>{SAMPLE_PROGRESS_STATUS_LABELS[value]}</option>)}</select>
</div>
<div className="overflow-hidden rounded-[6px] border border-black/[0.08] bg-white shadow-[0_8px_24px_rgba(21,31,33,0.05)]">

View File

@ -6,6 +6,6 @@ export const dynamic = 'force-dynamic'
export default async function SampleProgressPage() {
const { user } = await requireSession('/login')
const items = await sampleProgressService.list()
const currentSampleStageCount = await sampleProgressService.countCurrentSampleStage()
return <SampleProgressClient initialItems={items} currentUserName={user.name} currentSampleStageCount={currentSampleStageCount} />
const currentSampleStageOpeningCodes = await sampleProgressService.listCurrentSampleStageOpeningCodes()
return <SampleProgressClient initialItems={items} currentUserName={user.name} currentSampleStageOpeningCodes={currentSampleStageOpeningCodes} />
}

View File

@ -8,4 +8,4 @@ const samples: SampleProgressDTO[] = [
{ id: 'preview-3', openingCode: 'XP202607220009', productDevelopmentFlowUrl: 'https://oa.example.com/product-development/process/XP202607220009', orderDate: '2026-07-22T00:00:00.000Z', sampleDueDate: '2026-08-06T00:00:00.000Z', oaReachedAt: '2026-07-23T01:00:00.000Z', anomaly: 'NONE', imageUrl: null, productSize: '1800 x 400 x 450 mm', color: '深灰', material: '实木多层板 + 岩板', sampleQuantity: 1, brand: '索菲亚', supplier: '嘉兴木业', finalSampleFactory: '嘉兴木业', status: 'COMPLETED', designer: '张凯', purchaseOwner: '孙丽', merchandiserOwner: '方敏', progressDescription: '实物审样通过,打样已完结', requestedCost: 2200, actualCost: 2180, followUpOwner: '孙丽', createdBy: null, updatedBy: null, createdAt: now, updatedAt: now },
]
export default function SampleProgressPreviewPage() { return <SampleProgressClient initialItems={samples} currentUserName="荆川" currentSampleStageCount={2} /> }
export default function SampleProgressPreviewPage() { return <SampleProgressClient initialItems={samples} currentUserName="荆川" currentSampleStageOpeningCodes={samples.slice(0, 2).map((item) => item.openingCode)} /> }

View File

@ -213,11 +213,14 @@ export const sampleProgressService = {
})
await syncFromOaItems(dashboard.items)
},
async countCurrentSampleStage() {
async listCurrentSampleStageOpeningCodes() {
const dashboard = await oaBoardService.getDashboard({
overallStatus: SAMPLE_PROGRESS_SYNC_FLOW_STATUSES,
})
return dashboard.items.filter(isCurrentSampleStage).length
return dashboard.items
.filter(isCurrentSampleStage)
.map((item) => (item.formSummary.openingCode || item.openingCode || '').trim().toUpperCase())
.filter(Boolean)
},
async listStored() { return (await sampleProgressRepository.list()).map(serialize) },
async list() {