feat(supply-chain): add new product data views

This commit is contained in:
jingchuan 2026-08-15 17:27:50 +08:00
parent 88fbdbacd2
commit ceda595797
8 changed files with 121 additions and 17 deletions

View File

@ -65,7 +65,7 @@ type FilterOption = {
label: string
}
type OaBoardViewTab =
export type OaBoardViewTab =
| 'summary'
| 'daily'
| 'monthly'
@ -3310,12 +3310,16 @@ export default function OaBoardClient({
sampleProgressItems = [],
currentSampleStageOpeningCodes = [],
currentUserName = '',
embeddedTab,
embeddedTitle,
}: {
dashboard: OaBoardDashboardDTO
loadErrorMessage?: string | null
sampleProgressItems?: SampleProgressDTO[]
currentSampleStageOpeningCodes?: string[]
currentUserName?: string
embeddedTab?: 'detail'
embeddedTitle?: string
}) {
const { items: allItems, stageGraph, dailyReport, monthlyReport } = dashboard
const canViewDailyReport = Boolean(dailyReport)
@ -3330,7 +3334,7 @@ export default function OaBoardClient({
return params.toString()
}, [searchParamsKey])
const [isPending, startTransition] = useTransition()
const [showTimingColumns, setShowTimingColumns] = useState(false)
const [showTimingColumns, setShowTimingColumns] = useState(Boolean(embeddedTab === 'detail'))
const [currentPage, setCurrentPage] = useState(1)
const [previewImage, setPreviewImage] = useState<StyleImagePreview | null>(null)
const [timeoutFollowups, setTimeoutFollowups] = useState<OaBoardTimeoutFollowup[]>([])
@ -3377,6 +3381,7 @@ export default function OaBoardClient({
)
const [filters, setFilters] = useState<FilterState>(appliedFilters)
const [activeTab, setActiveTab] = useState<OaBoardViewTab>(() => {
if (embeddedTab) return embeddedTab
const requestedTab = parseOaBoardViewTab(searchParams.get('tab'))
if (requestedTab === 'daily' && !canViewDailyReport) return 'summary'
if (requestedTab === 'monthly' && !canViewMonthlyReport) return 'summary'
@ -3384,7 +3389,7 @@ export default function OaBoardClient({
})
const showingDailyReport = canViewDailyReport && activeTab === 'daily'
const showingMonthlyReport = canViewMonthlyReport && activeTab === 'monthly'
const activeTabLabel =
const activeTabLabel = embeddedTitle || (
activeTab === 'summary'
? '新品-进度汇总'
: showingDailyReport
@ -3401,7 +3406,7 @@ export default function OaBoardClient({
? '新品-超时分析'
: activeTab === 'shipment'
? '新品-出货进度(供应链中心)'
: '新品-上架数据'
: '新品-上架数据')
const sideNavSections = [
{
title: '产品开发',
@ -3966,12 +3971,14 @@ export default function OaBoardClient({
<>
<div
className={
activeTab === 'summary' || showingDailyReport || showingMonthlyReport
? 'min-h-full bg-[#f5f7f8]'
: 'flex h-[calc(100vh-72px)] min-h-0 flex-col overflow-hidden bg-[#f5f7f8]'
embeddedTab
? 'flex h-full min-h-0 flex-col overflow-hidden bg-[#f5f7f8]'
: activeTab === 'summary' || showingDailyReport || showingMonthlyReport
? 'min-h-full bg-[#f5f7f8]'
: 'flex h-[calc(100vh-72px)] min-h-0 flex-col overflow-hidden bg-[#f5f7f8]'
}
>
<div className="flex min-h-0 gap-4 px-4 py-4 xl:px-6 2xl:px-8">
<div className={`flex min-h-0 gap-4 px-4 py-4 xl:px-6 2xl:px-8 ${embeddedTab ? 'flex-1' : ''}`}>
<div className="flex min-w-0 flex-1 flex-col gap-3">
{loadErrorMessage ? (
<section className="rounded-[2rem] border border-amber-200 bg-amber-50/88 px-5 py-4 shadow-apple-sm">
@ -4512,7 +4519,7 @@ export default function OaBoardClient({
</section>
)}
</div>
<aside className="hidden w-[248px] shrink-0 lg:block">
{!embeddedTab ? <aside className="hidden w-[248px] shrink-0 lg:block">
<div className="flex h-full flex-col gap-3">
{sideNavSections.map((section) => (
<section key={section.title} className={WORKBENCH_SIDE_SECTION_CLASS}>
@ -4555,7 +4562,7 @@ export default function OaBoardClient({
</section>
))}
</div>
</aside>
</aside> : null}
</div>
</div>
{selectedTimeoutFollowupItem ? (

View File

@ -12,11 +12,20 @@ import { sampleProgressService } from '@/server/services/sample-progress.service
export const dynamic = 'force-dynamic'
export default async function OaBoardPage({
searchParams,
}: {
type OaBoardPageProps = {
searchParams?: Promise<Record<string, string | string[] | undefined>>
}) {
}
type OaBoardPageContentProps = OaBoardPageProps & {
embeddedTab?: 'detail'
embeddedTitle?: string
}
export async function OaBoardPageContent({
searchParams,
embeddedTab,
embeddedTitle,
}: OaBoardPageContentProps) {
const allowLocalPreview = process.env.OA_BOARD_LOCAL_PREVIEW === '1'
const session = allowLocalPreview ? null : await requireSession('/login')
const canViewDailyReport = canViewOaBoardDailyReport(session?.authUser)
@ -106,6 +115,7 @@ export default async function OaBoardPage({
: await resolveDashboard()
const pageStateKey = JSON.stringify({
embeddedTab,
keyword,
approvalNumber,
overallStatus,
@ -128,9 +138,13 @@ export default async function OaBoardPage({
const dashboard = canViewDailyReport
? result.dashboard
: { ...result.dashboard, dailyReport: undefined }
await sampleProgressService.syncFromOaDashboard()
const sampleProgressItems = await sampleProgressService.listStored()
const currentSampleStageOpeningCodes = await sampleProgressService.listCurrentSampleStageOpeningCodes()
if (!embeddedTab) {
await sampleProgressService.syncFromOaDashboard()
}
const sampleProgressItems = embeddedTab ? [] : await sampleProgressService.listStored()
const currentSampleStageOpeningCodes = embeddedTab
? []
: await sampleProgressService.listCurrentSampleStageOpeningCodes()
return (
<OaBoardClient
@ -140,6 +154,12 @@ export default async function OaBoardPage({
sampleProgressItems={sampleProgressItems}
currentSampleStageOpeningCodes={currentSampleStageOpeningCodes}
currentUserName={session?.user.name ?? ''}
embeddedTab={embeddedTab}
embeddedTitle={embeddedTitle}
/>
)
}
export default async function OaBoardPage(props: OaBoardPageProps) {
return OaBoardPageContent(props)
}

View File

@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation'
import {
LayoutDashboard,
Database,
PackagePlus,
ShoppingCart,
type LucideIcon,
} from 'lucide-react'
@ -13,6 +14,7 @@ import { SUPPLY_CHAIN_MENU, type SupplyChainMenuModule } from '../config/supply-
const ICON_MAP: Record<string, LucideIcon> = {
LayoutDashboard,
Database,
PackagePlus,
ShoppingCart,
}

View File

@ -84,6 +84,8 @@ export const SUPPLY_CHAIN_ROUTE_TITLES: Record<string, string> = {
'/supply-chain/database/product-archive': '产品全量档案',
'/supply-chain/database/category-config': '类目配置',
'/supply-chain/database/warehouse-config': '仓库配置',
'/supply-chain/new-products/unified-table': '新品统一宽表',
'/supply-chain/new-products/sample-progress': '新品打样明细宽表',
'/supply-chain/purchase/daily-affairs': '日常事项',
'/supply-chain/purchase/price-change': '产品变价',
}

View File

@ -1,6 +1,25 @@
import { flattenSupplyChainMenu } from './supply-chain-menu'
describe('supply chain menu', () => {
it('exposes the new product shared datasets', () => {
const items = flattenSupplyChainMenu()
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: '新品统一宽表',
module: '新品管理',
route: '/supply-chain/new-products/unified-table',
}),
expect.objectContaining({
label: '新品打样明细宽表',
module: '新品管理',
route: '/supply-chain/new-products/sample-progress',
}),
]),
)
})
it('does not expose the administrator operation log through menu search', () => {
expect(flattenSupplyChainMenu().map((item) => item.route)).not.toContain(
'/supply-chain/operation-logs',

View File

@ -45,6 +45,20 @@ export const SUPPLY_CHAIN_MENU: SupplyChainMenuModule[] = [
},
],
},
{
key: 'new-products',
label: '新品管理',
icon: 'PackagePlus',
sections: [
{
title: '公共数据源',
items: [
{ label: '新品统一宽表', route: '/supply-chain/new-products/unified-table' },
{ label: '新品打样明细宽表', route: '/supply-chain/new-products/sample-progress' },
],
},
],
},
{
key: 'purchase',
label: '采购管理',

View File

@ -0,0 +1,22 @@
import SampleProgressClient from '@/app/intake/sample-progress/SampleProgressClient'
import { requireSession } from '@/lib/server-session'
import { sampleProgressService } from '@/server/services/sample-progress.service'
export const dynamic = 'force-dynamic'
export const metadata = { title: '新品打样明细宽表 · 供应链中心' }
export default async function NewProductSampleProgressPage() {
const { user } = await requireSession('/login')
const items = await sampleProgressService.list()
const currentSampleStageOpeningCodes =
await sampleProgressService.listCurrentSampleStageOpeningCodes()
return (
<SampleProgressClient
initialItems={items}
currentUserName={user.name}
currentSampleStageOpeningCodes={currentSampleStageOpeningCodes}
embedded
/>
)
}

View File

@ -0,0 +1,18 @@
import { OaBoardPageContent } from '@/app/intake/oa-board/page'
export const dynamic = 'force-dynamic'
export const metadata = { title: '新品统一宽表 · 供应链中心' }
export default async function NewProductUnifiedTablePage({
searchParams,
}: {
searchParams?: Promise<Record<string, string | string[] | undefined>>
}) {
return (
<OaBoardPageContent
searchParams={searchParams}
embeddedTab="detail"
embeddedTitle="新品统一宽表"
/>
)
}