diff --git a/furniture-3d-system/src/app/admin/AdminPortalClient.tsx b/furniture-3d-system/src/app/admin/AdminPortalClient.tsx index 6451d74d..dbf504d1 100644 --- a/furniture-3d-system/src/app/admin/AdminPortalClient.tsx +++ b/furniture-3d-system/src/app/admin/AdminPortalClient.tsx @@ -2,6 +2,7 @@ import Link from 'next/link' import { + GitBranch, Grid, Palette, ShieldCheck, @@ -54,6 +55,13 @@ const adminModules = [ icon: Grid, metric: '排序', }, + { + title: '流程单总表', + subtitle: '流程', + href: '/admin/approval-workflows', + icon: GitBranch, + metric: '审批 / 链接', + }, ] export function AdminPortalClient({ role }: AdminPortalClientProps) { diff --git a/furniture-3d-system/src/app/admin/approval-workflows/AdminApprovalWorkflowClient.tsx b/furniture-3d-system/src/app/admin/approval-workflows/AdminApprovalWorkflowClient.tsx new file mode 100644 index 00000000..4f1b43c2 --- /dev/null +++ b/furniture-3d-system/src/app/admin/approval-workflows/AdminApprovalWorkflowClient.tsx @@ -0,0 +1,191 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import Link from 'next/link' +import type { ApprovalWorkflowStatus } from '@prisma/client' +import { Copy, ExternalLink, Search } from 'lucide-react' +import { + buildApprovalWorkflowUrl, + listApprovalWorkflows, + type ApprovalWorkflowListItem, +} from '@/app/approval-workflows/approvalWorkflowApi' +import { BUSINESS_ACCOUNT_TYPE_LABELS } from '@/lib/business-account-types' + +const STATUS_LABELS: Record = { + '': '全部状态', + DRAFT: '草稿', + PENDING: '待处理', + IN_PROGRESS: '进行中', + COMPLETED: '已完成', + REJECTED: '已驳回', + ARCHIVED: '已归档', +} + +export function AdminApprovalWorkflowClient() { + const [keyword, setKeyword] = useState('') + const [status, setStatus] = useState('') + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [copiedKey, setCopiedKey] = useState(null) + const [workflows, setWorkflows] = useState([]) + + const load = async () => { + try { + setLoading(true) + setError(null) + const items = await listApprovalWorkflows({ + keyword: keyword.trim() || undefined, + status, + }) + setWorkflows(items) + } catch (loadError) { + setError(loadError instanceof Error ? loadError.message : '流程记录加载失败') + } finally { + setLoading(false) + } + } + + useEffect(() => { + void load() + }, []) + + const sortedWorkflows = useMemo( + () => [...workflows].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()), + [workflows], + ) + + const handleCopy = async (accessKey: string) => { + try { + await navigator.clipboard.writeText(buildApprovalWorkflowUrl(accessKey)) + setCopiedKey(accessKey) + window.setTimeout(() => setCopiedKey((current) => (current === accessKey ? null : current)), 1500) + } catch { + setError('复制链接失败,请检查浏览器剪贴板权限') + } + } + + return ( +
+
+
+
+
+

后台流程记录

+

流程单总表

+

+ 这里负责找旧流程、按审批编号排查、复制链接补发,不替代开品页里的主发起入口。 +

+
+ + 返回后台 + +
+ +
+ + + +
+ + {error ? ( +
+ {error} +
+ ) : null} +
+ +
+ {loading ? ( +
正在加载流程记录...
+ ) : sortedWorkflows.length === 0 ? ( +
当前没有符合条件的流程记录。
+ ) : ( +
+ {sortedWorkflows.map((workflow) => ( +
+
+
+
+

{workflow.title}

+ + {STATUS_LABELS[workflow.status]} + +
+

+ 审批编号:{workflow.approvalNumber} · 产品:{workflow.product.modelNumber} / {workflow.product.name} +

+

+ 参与业务:{workflow.businessRoles.map((role) => BUSINESS_ACCOUNT_TYPE_LABELS[role]).join(' / ') || '未配置'} +

+

+ 最近更新:{new Date(workflow.updatedAt).toLocaleString('zh-CN')} +

+
+
+ + + + 打开流程页 + + + 查看需求 + +
+
+
+ ))} +
+ )} +
+
+
+ ) +} diff --git a/furniture-3d-system/src/app/admin/approval-workflows/page.tsx b/furniture-3d-system/src/app/admin/approval-workflows/page.tsx new file mode 100644 index 00000000..8a3e8237 --- /dev/null +++ b/furniture-3d-system/src/app/admin/approval-workflows/page.tsx @@ -0,0 +1,12 @@ +import { redirect } from 'next/navigation' +import { requireSession } from '@/lib/server-session' +import { AdminApprovalWorkflowClient } from './AdminApprovalWorkflowClient' + +export default async function AdminApprovalWorkflowsPage() { + const { authUser } = await requireSession('/login') + if (!['SUPER_ADMIN', 'ADMIN'].includes(authUser.role)) { + redirect('/?error=unauthorized') + } + + return +} diff --git a/furniture-3d-system/src/app/api/approval-workflows/route.ts b/furniture-3d-system/src/app/api/approval-workflows/route.ts index 693d64f7..4726607e 100644 --- a/furniture-3d-system/src/app/api/approval-workflows/route.ts +++ b/furniture-3d-system/src/app/api/approval-workflows/route.ts @@ -1,8 +1,9 @@ import { NextRequest } from 'next/server' import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response' -import { AppError } from '@/server/core/errors' +import { AppError, ErrorCode } from '@/server/core/errors' import type { ApprovalWorkflowStatus, BusinessAccountType } from '@prisma/client' import { approvalWorkflowService } from '@/server/services/approval-workflow.service' +import { intakeNonIntegratedProductService } from '@/server/services' export async function GET(request: NextRequest) { return handleRequest( @@ -11,11 +12,19 @@ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const productId = searchParams.get('productId') || undefined const approvalNumber = searchParams.get('approvalNumber') || undefined + const keyword = searchParams.get('keyword') || undefined const status = (searchParams.get('status') as ApprovalWorkflowStatus | null) || undefined - const workflows = await approvalWorkflowService.list({ productId, approvalNumber, status }) + + if (productId) { + await intakeNonIntegratedProductService.getById(productId, ctx.user) + } else if (!['SUPER_ADMIN', 'ADMIN'].includes(ctx.user.role)) { + return jsonError(ErrorCode.AUTH_PERMISSION_DENIED, '只有后台管理员可以查看全量流程记录', 403, ctx.requestId) + } + + const workflows = await approvalWorkflowService.list({ productId, approvalNumber, status, keyword }) return jsonSuccess({ workflows }, 200, ctx.requestId) }, - { allowedRoles: ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM'] } + { requireAuth: true } ) } @@ -25,6 +34,7 @@ export async function POST(request: NextRequest) { async (ctx) => { const body = await request.json() try { + await intakeNonIntegratedProductService.getById(body.productId, ctx.user) const workflow = await approvalWorkflowService.create( { approvalNumber: body.approvalNumber, @@ -51,6 +61,6 @@ export async function POST(request: NextRequest) { throw error } }, - { allowedRoles: ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT'] } + { requireAuth: true } ) } diff --git a/furniture-3d-system/src/app/approval-workflows/[accessKey]/ApprovalWorkflowPageClient.tsx b/furniture-3d-system/src/app/approval-workflows/[accessKey]/ApprovalWorkflowPageClient.tsx index 48a6ba46..29c2b646 100644 --- a/furniture-3d-system/src/app/approval-workflows/[accessKey]/ApprovalWorkflowPageClient.tsx +++ b/furniture-3d-system/src/app/approval-workflows/[accessKey]/ApprovalWorkflowPageClient.tsx @@ -10,6 +10,9 @@ type FieldMatrixItem = { key: string label: string businessRole: BusinessAccountType + section?: string + helperText?: string + placeholder?: string required?: boolean allowSecondEdit?: boolean } @@ -118,6 +121,36 @@ function ApprovalWorkflowPageBody({ accessKey }: { accessKey: string }) { return groups }, [workflow]) + const fieldLabelMap = useMemo(() => { + const map = new Map() + for (const item of workflow?.fieldMatrix || []) { + map.set(item.key, item.label) + } + return map + }, [workflow]) + + const groupedFieldSections = useMemo(() => { + const sections = new Map>() + for (const role of ['PLANNING', 'PURCHASING', 'WAREHOUSE', 'SUPPLIER'] as BusinessAccountType[]) { + const roleFields = groupedFields.get(role) || [] + const bySection = new Map() + for (const field of roleFields) { + const sectionTitle = field.section || '未分组' + const current = bySection.get(sectionTitle) || [] + current.push(field) + bySection.set(sectionTitle, current) + } + sections.set( + role, + Array.from(bySection.entries()).map(([title, fields]) => ({ + title, + fields, + })), + ) + } + return sections + }, [groupedFields]) + const myBusinessRoles = user?.businessRoles || [] const handleSaveField = async (field: FieldMatrixItem) => { @@ -208,9 +241,46 @@ function ApprovalWorkflowPageBody({ accessKey }: { accessKey: string }) { +
+
+
+

这张流程单要补什么

+

+ 下面这份清单按正式产品和仓库管理当前真实流程整理,先把该填的内容列完整。 +

+
+ + 共 {(workflow.fieldMatrix || []).length} 项 + +
+
+ {(['PLANNING', 'PURCHASING', 'WAREHOUSE', 'SUPPLIER'] as BusinessAccountType[]).map((role) => { + const sections = groupedFieldSections.get(role) || [] + if (sections.length === 0) return null + return ( +
+

{BUSINESS_ACCOUNT_TYPE_LABELS[role]}

+
+ {sections.map((section) => ( +
+

{section.title}

+
    + {section.fields.map((field) => ( +
  • {field.label}
  • + ))} +
+
+ ))} +
+
+ ) + })} +
+
+ {(['PLANNING', 'PURCHASING', 'WAREHOUSE', 'SUPPLIER'] as BusinessAccountType[]).map((role) => { - const fields = groupedFields.get(role) || [] - if (fields.length === 0) return null + const sections = groupedFieldSections.get(role) || [] + if (sections.length === 0) return null const mine = myBusinessRoles.includes(role) return (
-
- {fields.map((field) => { - const readOnly = !mine && user?.role !== 'SUPER_ADMIN' && user?.role !== 'ADMIN' - return ( -
-
- - - {field.required ? '必填' : '选填'} - -
-