fix(supply-chain): isolate permissions for every menu page
This commit is contained in:
parent
729eaf492d
commit
9b7ab9e532
@ -0,0 +1,85 @@
|
||||
BEGIN;
|
||||
|
||||
-- Use the same lock as permission configuration writes. Retain every old row.
|
||||
SELECT pg_advisory_xact_lock(2026081501);
|
||||
LOCK TABLE "SupplyChainPermissionOverride" IN SHARE ROW EXCLUSIVE MODE;
|
||||
|
||||
WITH mappings("source", "target") AS (
|
||||
VALUES
|
||||
('businessManagement', 'ownerConfig'),
|
||||
('businessManagement', 'limitManagement'),
|
||||
('businessManagement', 'productionStopDemands'),
|
||||
('businessManagement', 'managementBlankPage1'),
|
||||
('businessManagement', 'managementBlankPage2'),
|
||||
('newProductSampleProgress', 'finishedProductProgress')
|
||||
)
|
||||
INSERT INTO "SupplyChainPermissionOverride"
|
||||
("id", "targetKey", "subjectKey", "action", "enabled", "createdBy", "updatedBy", "createdAt", "updatedAt")
|
||||
SELECT
|
||||
'scp-' || md5(old."id" || ':' || mappings."target"),
|
||||
mappings."target", old."subjectKey", old."action", old."enabled",
|
||||
old."createdBy", old."updatedBy", old."createdAt", old."updatedAt"
|
||||
FROM "SupplyChainPermissionOverride" AS old
|
||||
JOIN mappings ON old."targetKey" = mappings."source"
|
||||
ON CONFLICT ("targetKey", "subjectKey", "action") DO NOTHING;
|
||||
|
||||
-- Logistics previously allowed exporting whenever effective view was true.
|
||||
-- Preserve that ability once, then enforce its independent download permission.
|
||||
-- Personal download=false used to suppress editing, not the view-based export.
|
||||
-- Leave inherited personal view/download unset in that case and copy edit=false.
|
||||
WITH legacy AS (
|
||||
SELECT
|
||||
"subjectKey",
|
||||
bool_or("enabled") FILTER (WHERE "action" = 'view') AS v,
|
||||
bool_or("enabled") FILTER (WHERE "action" = 'download') AS d,
|
||||
bool_or("enabled") FILTER (WHERE "action" = 'edit') AS e,
|
||||
min("id") AS "sourceId",
|
||||
max("createdBy") AS "createdBy", max("updatedBy") AS "updatedBy",
|
||||
min("createdAt") AS "createdAt", max("updatedAt") AS "updatedAt"
|
||||
FROM "SupplyChainPermissionOverride"
|
||||
WHERE "targetKey" = 'businessManagement'
|
||||
GROUP BY "subjectKey"
|
||||
), access AS (
|
||||
SELECT *,
|
||||
CASE
|
||||
WHEN "subjectKey" LIKE 'user@%' THEN
|
||||
CASE WHEN v IS NOT NULL THEN v
|
||||
WHEN d IS DISTINCT FROM FALSE AND (d IS TRUE OR e IS TRUE) THEN TRUE
|
||||
ELSE NULL END
|
||||
WHEN v IS FALSE THEN FALSE
|
||||
WHEN d IS FALSE THEN v IS TRUE
|
||||
ELSE v IS TRUE OR d IS TRUE OR e IS TRUE
|
||||
END AS "newView",
|
||||
CASE
|
||||
WHEN "subjectKey" LIKE 'user@%' THEN
|
||||
CASE WHEN v IS FALSE THEN FALSE
|
||||
WHEN v IS TRUE THEN TRUE
|
||||
WHEN d IS DISTINCT FROM FALSE AND (d IS TRUE OR e IS TRUE) THEN TRUE
|
||||
ELSE NULL END
|
||||
WHEN v IS FALSE THEN FALSE
|
||||
WHEN d IS FALSE THEN v IS TRUE
|
||||
ELSE v IS TRUE OR d IS TRUE OR e IS TRUE
|
||||
END AS "newDownload",
|
||||
CASE
|
||||
WHEN "subjectKey" LIKE 'user@%' THEN
|
||||
CASE WHEN v IS FALSE OR d IS FALSE THEN FALSE ELSE e END
|
||||
ELSE v IS DISTINCT FROM FALSE AND d IS DISTINCT FROM FALSE AND e IS TRUE
|
||||
END AS "newEdit"
|
||||
FROM legacy
|
||||
)
|
||||
INSERT INTO "SupplyChainPermissionOverride"
|
||||
("id", "targetKey", "subjectKey", "action", "enabled", "createdBy", "updatedBy", "createdAt", "updatedAt")
|
||||
SELECT
|
||||
'scp-' || md5(access."sourceId" || ':logisticsArrivalSigningDuration:' || actions."action"),
|
||||
'logisticsArrivalSigningDuration', access."subjectKey", actions."action", actions."enabled",
|
||||
access."createdBy", access."updatedBy", access."createdAt", access."updatedAt"
|
||||
FROM access
|
||||
CROSS JOIN LATERAL (VALUES
|
||||
('view', access."newView"),
|
||||
('download', access."newDownload"),
|
||||
('edit', access."newEdit")
|
||||
) AS actions("action", "enabled")
|
||||
WHERE actions."enabled" IS NOT NULL
|
||||
ON CONFLICT ("targetKey", "subjectKey", "action") DO NOTHING;
|
||||
|
||||
COMMIT;
|
||||
@ -34,10 +34,12 @@ export function SupplyChainLayout({
|
||||
children,
|
||||
visibleTargetKeys,
|
||||
canEditOwnerConfigLabels = false,
|
||||
editableTabRoutes,
|
||||
}: {
|
||||
children: ReactNode
|
||||
visibleTargetKeys: readonly SupplyChainPermissionTargetKey[]
|
||||
canEditOwnerConfigLabels?: boolean
|
||||
editableTabRoutes?: readonly string[]
|
||||
}) {
|
||||
const homePath = visibleTargetKeys
|
||||
.map((targetKey) => SUPPLY_CHAIN_PERMISSION_TARGET_ROUTES[targetKey])
|
||||
@ -77,7 +79,7 @@ export function SupplyChainLayout({
|
||||
/>
|
||||
) : null}
|
||||
<div className="sc-layout-main">
|
||||
<SupplyChainTabBar canEditOwnerConfigLabels={canEditOwnerConfigLabels} />
|
||||
<SupplyChainTabBar canEditOwnerConfigLabels={canEditOwnerConfigLabels} editableTabRoutes={editableTabRoutes} />
|
||||
<div className="sc-layout-content">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -66,7 +66,7 @@ describe('SupplyChainSidebar', () => {
|
||||
'/supply-chain/management/logistics-arrival-signing-duration',
|
||||
)
|
||||
|
||||
render(<SupplyChainSidebar visibleTargetKeys={['businessManagement']} />)
|
||||
render(<SupplyChainSidebar visibleTargetKeys={['logisticsArrivalSigningDuration']} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: '经营管理' })).toHaveAttribute(
|
||||
'aria-expanded',
|
||||
@ -82,7 +82,7 @@ describe('SupplyChainSidebar', () => {
|
||||
'/supply-chain/management/production-stop-demands',
|
||||
)
|
||||
|
||||
render(<SupplyChainSidebar visibleTargetKeys={['businessManagement']} />)
|
||||
render(<SupplyChainSidebar visibleTargetKeys={['productionStopDemands']} />)
|
||||
|
||||
expect(
|
||||
screen.getByRole('link', { name: '停产需求管理' }),
|
||||
|
||||
@ -31,6 +31,11 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('SupplyChainTabBar owner config labels', () => {
|
||||
it('does not grant editing of other tab labels through owner-config permission', () => {
|
||||
mockActivePath = '/supply-chain/management/custom-table'
|
||||
render(<SupplyChainTabBar editableTabRoutes={['/supply-chain/management/owner-config']} />)
|
||||
expect(screen.queryByRole('button', { name: '修改限价管理页签名称' })).not.toBeInTheDocument()
|
||||
})
|
||||
it('allows the development page title to be renamed and persisted', () => {
|
||||
render(<SupplyChainTabBar canEditOwnerConfigLabels />)
|
||||
|
||||
|
||||
@ -13,8 +13,10 @@ const CUSTOM_TABLE_PATH = '/supply-chain/management/custom-table'
|
||||
|
||||
export function SupplyChainTabBar({
|
||||
canEditOwnerConfigLabels = false,
|
||||
editableTabRoutes,
|
||||
}: {
|
||||
canEditOwnerConfigLabels?: boolean
|
||||
editableTabRoutes?: readonly string[]
|
||||
}) {
|
||||
const { tabs, activePath, clickTab, closeTab } = useSupplyChainTabs()
|
||||
const { labels, updateLabel } = useOwnerConfigLabels()
|
||||
@ -27,6 +29,7 @@ export function SupplyChainTabBar({
|
||||
<div className="sc-tabbar">
|
||||
{visibleTabs.map((tab) => {
|
||||
const active = tab.path === activePath
|
||||
const canEditLabel = editableTabRoutes?.includes(tab.path) ?? canEditOwnerConfigLabels
|
||||
const isOwnerConfig = tab.path === OWNER_CONFIG_PATH
|
||||
const isCustomTable = tab.path === CUSTOM_TABLE_PATH
|
||||
const blankPage = MANAGEMENT_BLANK_PAGES.find((page) => page.route === tab.path)
|
||||
@ -41,21 +44,21 @@ export function SupplyChainTabBar({
|
||||
<EditableSupplyChainLabel
|
||||
value={labels.pageTitle}
|
||||
editLabel="修改页签名称"
|
||||
allowEdit={active && canEditOwnerConfigLabels}
|
||||
allowEdit={active && canEditLabel}
|
||||
onSave={(value) => updateLabel('pageTitle', value)}
|
||||
/>
|
||||
) : isCustomTable ? (
|
||||
<EditableSupplyChainLabel
|
||||
value={customTableTitle}
|
||||
editLabel="修改限价管理页签名称"
|
||||
allowEdit={active && canEditOwnerConfigLabels}
|
||||
allowEdit={active && canEditLabel}
|
||||
onSave={updateCustomTableTitle}
|
||||
/>
|
||||
) : blankPage ? (
|
||||
<EditableSupplyChainLabel
|
||||
value={blankPageTitles[blankPage.key] ?? blankPage.label}
|
||||
editLabel={`修改${blankPageTitles[blankPage.key] ?? blankPage.label}页签名称`}
|
||||
allowEdit={active && canEditOwnerConfigLabels}
|
||||
allowEdit={active && canEditLabel}
|
||||
onSave={(value) => updateBlankPageTitle(blankPage.key, value)}
|
||||
/>
|
||||
) : tab.title}
|
||||
|
||||
@ -142,7 +142,7 @@ describe('supply chain menu', () => {
|
||||
})
|
||||
|
||||
it('places the logistics arrival signing duration dashboard under business management', () => {
|
||||
expect(flattenSupplyChainMenu(filterSupplyChainMenu(['businessManagement']))).toEqual(
|
||||
expect(flattenSupplyChainMenu(filterSupplyChainMenu(['logisticsArrivalSigningDuration']))).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({
|
||||
label: '物流到货签收时长看板',
|
||||
module: '经营管理',
|
||||
@ -152,7 +152,7 @@ describe('supply chain menu', () => {
|
||||
})
|
||||
|
||||
it('places production stop demands under business management', () => {
|
||||
expect(flattenSupplyChainMenu(filterSupplyChainMenu(['businessManagement']))).toEqual(
|
||||
expect(flattenSupplyChainMenu(filterSupplyChainMenu(['productionStopDemands']))).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({
|
||||
label: '停产需求管理',
|
||||
module: '经营管理',
|
||||
|
||||
@ -5,8 +5,10 @@ import type { SupplyChainPermissionTargetKey } from '@/lib/supply-chain-permissi
|
||||
import { MANAGEMENT_BLANK_PAGES } from '@/lib/supply-chain-management-blank-pages'
|
||||
|
||||
// 商品SABC打标(blank-page-3)的查看/导出走独立权限节点(默认全员开放,
|
||||
// 可在用户管理后台关闭);其余经营管理空白页仍跟随「经营管理」节点。
|
||||
// 可在用户管理后台关闭);每个空白页使用自己的权限节点。
|
||||
const BLANK_PAGE_TARGET_KEYS: Record<string, SupplyChainPermissionTargetKey> = {
|
||||
'blank-page-1': 'managementBlankPage1',
|
||||
'blank-page-2': 'managementBlankPage2',
|
||||
'blank-page-3': 'productSabc',
|
||||
}
|
||||
|
||||
@ -77,7 +79,7 @@ export const SUPPLY_CHAIN_MENU: SupplyChainMenuModule[] = [
|
||||
{
|
||||
items: [
|
||||
{ label: '打样进度明细', route: '/supply-chain/new-products/sample-progress', targetKey: 'newProductSampleProgress' },
|
||||
{ label: '成品进度明细', route: '/supply-chain/purchase/finished-product-progress', targetKey: 'newProductSampleProgress' },
|
||||
{ label: '成品进度明细', route: '/supply-chain/purchase/finished-product-progress', targetKey: 'finishedProductProgress' },
|
||||
{ label: '供应商新品推荐', route: '/supply-chain/purchase/supplier-recommended-products', targetKey: 'purchaseSupplierRecommendedProducts' },
|
||||
{ label: '供应商信息', route: '/supply-chain/purchase/supplier-information', targetKey: 'purchaseSupplierInformation' },
|
||||
{ label: '日常事项', route: '/supply-chain/purchase/daily-affairs', targetKey: 'purchaseDailyAffairs' },
|
||||
@ -96,28 +98,28 @@ export const SUPPLY_CHAIN_MENU: SupplyChainMenuModule[] = [
|
||||
{
|
||||
label: '人员信息',
|
||||
route: '/supply-chain/management/owner-config',
|
||||
targetKey: 'businessManagement',
|
||||
targetKey: 'ownerConfig',
|
||||
},
|
||||
{
|
||||
label: '限价管理',
|
||||
route: '/supply-chain/management/custom-table',
|
||||
targetKey: 'businessManagement',
|
||||
targetKey: 'limitManagement',
|
||||
},
|
||||
{
|
||||
label: '停产需求管理',
|
||||
route: '/supply-chain/management/production-stop-demands',
|
||||
targetKey: 'businessManagement',
|
||||
targetKey: 'productionStopDemands',
|
||||
},
|
||||
...MANAGEMENT_BLANK_PAGES.map((page) => ({
|
||||
label: page.label,
|
||||
route: page.route,
|
||||
targetKey: BLANK_PAGE_TARGET_KEYS[page.key] ?? 'businessManagement' as const,
|
||||
targetKey: BLANK_PAGE_TARGET_KEYS[page.key]!,
|
||||
})),
|
||||
{ label: '员工工时统计', route: '/supply-chain/management/employee-hours', targetKey: 'employeeHours' },
|
||||
{
|
||||
label: '物流到货签收时长看板',
|
||||
route: '/supply-chain/management/logistics-arrival-signing-duration',
|
||||
targetKey: 'businessManagement',
|
||||
targetKey: 'logisticsArrivalSigningDuration',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -5,6 +5,7 @@ import { includeSampleProgressTargetForBusinessEditor } from '@/lib/sample-progr
|
||||
import { requireSession } from '@/lib/server-session'
|
||||
import { supplyChainPermissionSettingsService } from '@/server/services/supply-chain-permission-settings.service'
|
||||
import { SupplyChainLayout } from './components/SupplyChainLayout'
|
||||
import { SUPPLY_CHAIN_PERMISSION_TARGET_ROUTES, type SupplyChainPermissionTargetKey } from '@/lib/supply-chain-permission-settings'
|
||||
|
||||
// 浏览器标签页标题统一为工作区名,子页面不再各自覆盖,内部切换标签页时标题保持不变
|
||||
export const metadata: Metadata = {
|
||||
@ -24,17 +25,18 @@ export default async function SupplyChainRootLayout({ children }: { children: Re
|
||||
authUser,
|
||||
await supplyChainPermissionSettingsService.getVisibleTargetKeys(authUser),
|
||||
)
|
||||
const businessManagementAccess = await supplyChainPermissionSettingsService.getUserAccess(
|
||||
authUser,
|
||||
'businessManagement',
|
||||
)
|
||||
const labelTargets: SupplyChainPermissionTargetKey[] = ['ownerConfig', 'limitManagement', 'managementBlankPage1', 'managementBlankPage2', 'productSabc']
|
||||
const editableTabRoutes = process.env.NODE_ENV === 'production' ? [] : (await Promise.all(
|
||||
labelTargets.map(async (target) => {
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(authUser, target)
|
||||
return access.edit ? SUPPLY_CHAIN_PERMISSION_TARGET_ROUTES[target] : undefined
|
||||
}),
|
||||
)).filter((route): route is string => Boolean(route))
|
||||
|
||||
return (
|
||||
<SupplyChainLayout
|
||||
visibleTargetKeys={visibleTargetKeys}
|
||||
canEditOwnerConfigLabels={
|
||||
process.env.NODE_ENV !== 'production' && businessManagementAccess.edit
|
||||
}
|
||||
editableTabRoutes={editableTabRoutes}
|
||||
>
|
||||
{children}
|
||||
</SupplyChainLayout>
|
||||
|
||||
@ -31,7 +31,10 @@ export default async function ManagementBlankPage({ params }: { params: Promise<
|
||||
)
|
||||
}
|
||||
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(authUser, 'businessManagement')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(
|
||||
authUser,
|
||||
definition.key === 'blank-page-1' ? 'managementBlankPage1' : 'managementBlankPage2',
|
||||
)
|
||||
if (!access.view) redirect('/supply-chain')
|
||||
|
||||
return (
|
||||
|
||||
@ -7,7 +7,7 @@ export default async function CustomTablePage() {
|
||||
const { authUser } = await requireSession('/login')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(
|
||||
authUser,
|
||||
'businessManagement',
|
||||
'limitManagement',
|
||||
)
|
||||
if (!access.view) redirect('/supply-chain')
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ export default async function LogisticsArrivalSigningDurationPage({
|
||||
const { authUser } = await requireSession('/login')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(
|
||||
authUser,
|
||||
'businessManagement',
|
||||
'logisticsArrivalSigningDuration',
|
||||
)
|
||||
if (!access.view) redirect('/supply-chain')
|
||||
|
||||
@ -56,7 +56,7 @@ export default async function LogisticsArrivalSigningDurationPage({
|
||||
initialData={initialData}
|
||||
defaultExcludeKeywords={defaultExcludeKeywords}
|
||||
canManageExcludeKeywords={canManageSupplyChainPermissions(authUser)}
|
||||
canDownload={access.view}
|
||||
canDownload={access.download}
|
||||
dateRangeCleared={dateRangeCleared}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -7,7 +7,7 @@ export default async function OwnerConfigPage() {
|
||||
const { authUser } = await requireSession('/login')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(
|
||||
authUser,
|
||||
'businessManagement',
|
||||
'ownerConfig',
|
||||
)
|
||||
if (!access.view) redirect('/supply-chain')
|
||||
const canEditDevelopmentData = process.env.NODE_ENV !== 'production' && access.edit
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { requireSession } from '@/lib/server-session'
|
||||
import { supplyChainPermissionSettingsService } from '@/server/services/supply-chain-permission-settings.service'
|
||||
import { filterSupplyChainMenu } from '../config/supply-chain-menu'
|
||||
|
||||
export default async function BusinessManagementPage() {
|
||||
const { authUser } = await requireSession('/login')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(
|
||||
authUser,
|
||||
'businessManagement',
|
||||
)
|
||||
if (!access.view) redirect('/supply-chain')
|
||||
redirect('/supply-chain/management/owner-config')
|
||||
const visible = await supplyChainPermissionSettingsService.getVisibleTargetKeys(authUser)
|
||||
const management = filterSupplyChainMenu(visible).find((module) => module.key === 'management')
|
||||
redirect(management?.sections?.[0]?.items[0]?.route ?? '/supply-chain')
|
||||
}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { SUPPLY_CHAIN_MENU, filterSupplyChainMenu, flattenSupplyChainMenu } from '@/app/supply-chain/config/supply-chain-menu'
|
||||
import { PERMISSION_MATRIX_SECTIONS } from '@/app/users/components/permissionMatrixGroups'
|
||||
import { getSupplyChainApiPermissionRequirement, getSupplyChainPermissionTargetForPath, resolveSupplyChainPermissionAccess } from './supply-chain-permission-settings'
|
||||
|
||||
describe('independent supply-chain page permissions', () => {
|
||||
const pages = SUPPLY_CHAIN_MENU.flatMap((module) => [
|
||||
...(module.route ? [{ label: module.label, route: module.route, targetKey: module.targetKey! }] : []),
|
||||
...(module.sections?.flatMap((section) => section.items) ?? []),
|
||||
])
|
||||
|
||||
it('gives every menu page its own target, route mapping and permission row', () => {
|
||||
expect(pages).toHaveLength(26)
|
||||
expect(new Set(pages.map((page) => page.targetKey)).size).toBe(pages.length)
|
||||
const matrixRows = PERMISSION_MATRIX_SECTIONS.find((section) => section.id === 'supply')!.groups.flatMap((group) => group.rows)
|
||||
for (const page of pages) {
|
||||
expect(getSupplyChainPermissionTargetForPath(page.route)).toBe(page.targetKey)
|
||||
expect(matrixRows.filter((row) => row.targetKey === page.targetKey)).toEqual([
|
||||
expect.objectContaining({ label: page.label }),
|
||||
])
|
||||
expect(flattenSupplyChainMenu(filterSupplyChainMenu([page.targetKey])).map((item) => item.route)).toEqual([page.route])
|
||||
}
|
||||
})
|
||||
|
||||
it('does not reuse old management or sample grants for the new nodes', () => {
|
||||
const overrides = { 'businessManagement:purchase:edit': true, 'newProductSampleProgress:purchase:edit': true }
|
||||
for (const target of ['ownerConfig', 'limitManagement', 'productionStopDemands', 'logisticsArrivalSigningDuration', 'managementBlankPage1', 'managementBlankPage2', 'finishedProductProgress'] as const) {
|
||||
expect(resolveSupplyChainPermissionAccess(overrides, target, ['purchase'])).toEqual({ view: false, download: false, edit: false })
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps logistics download and operation separate from visibility', () => {
|
||||
const overrides = { 'logisticsArrivalSigningDuration:purchase:view': true }
|
||||
expect(resolveSupplyChainPermissionAccess(overrides, 'logisticsArrivalSigningDuration', ['purchase'])).toEqual({ view: true, download: false, edit: false })
|
||||
expect(getSupplyChainApiPermissionRequirement('/api/supply-chain/management/logistics-arrival-signing-duration/export', 'POST')).toEqual({ targetKey: 'logisticsArrivalSigningDuration', action: 'download' })
|
||||
expect(getSupplyChainApiPermissionRequirement('/api/supply-chain/management/logistics-arrival-signing-duration/settings', 'PUT')).toEqual({ targetKey: 'logisticsArrivalSigningDuration', action: 'edit' })
|
||||
})
|
||||
|
||||
it('guards finished-product mutations using its own node', () => {
|
||||
expect(getSupplyChainApiPermissionRequirement('/api/supply-chain/finished-product-progress/bulk', 'PATCH')).toEqual({ targetKey: 'finishedProductProgress', action: 'edit' })
|
||||
})
|
||||
})
|
||||
168
furniture-3d-system/src/lib/supply-chain-permission-paths.ts
Normal file
168
furniture-3d-system/src/lib/supply-chain-permission-paths.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import type { SupplyChainPermissionTargetKey } from './supply-chain-permission-targets'
|
||||
|
||||
export function getSupplyChainPermissionTargetForPath(pathname: string): SupplyChainPermissionTargetKey | null {
|
||||
if (pathname === '/supply-chain/dashboard') return 'dashboard' as const
|
||||
if (pathname === '/supply-chain/management/owner-config') return 'ownerConfig' as const
|
||||
if (pathname === '/supply-chain/management/custom-table') return 'limitManagement' as const
|
||||
// 商品SABC打标:明细走独立节点(默认开放),「各部门数据分类」子页走对应模块节点
|
||||
if (pathname.startsWith('/supply-chain/management/blank-pages/blank-page-3/departments')) {
|
||||
return 'departmentDistribution' as const
|
||||
}
|
||||
if (pathname.startsWith('/supply-chain/management/blank-pages/blank-page-3')) {
|
||||
return 'productSabc' as const
|
||||
}
|
||||
if (pathname === '/supply-chain/management/blank-pages/blank-page-1') return 'managementBlankPage1' as const
|
||||
if (pathname === '/supply-chain/management/blank-pages/blank-page-2') return 'managementBlankPage2' as const
|
||||
if (pathname === '/supply-chain/management/employee-hours') return 'employeeHours' as const
|
||||
if (pathname === '/supply-chain/management/logistics-arrival-signing-duration') return 'logisticsArrivalSigningDuration' as const
|
||||
if (pathname === '/supply-chain/management/production-stop-demands') return 'productionStopDemands' as const
|
||||
if (pathname === '/supply-chain/database/product-archive') return 'productArchive' as const
|
||||
if (pathname === '/supply-chain/database/category-config') return 'categoryConfig' as const
|
||||
if (pathname === '/supply-chain/database/warehouse-config') return 'warehouseConfig' as const
|
||||
if (pathname === '/supply-chain/database/store-account-management') return 'storeAccountManagement' as const
|
||||
if (pathname === '/supply-chain/new-products/overview') return 'newProductOverview' as const
|
||||
if (pathname === '/supply-chain/new-products/1688-dropship-data') return 'newProduct1688DropshipData' as const
|
||||
if (pathname === '/supply-chain/new-products/termination-feedback') return 'newProductTerminationFeedback' as const
|
||||
if (pathname === '/supply-chain/new-products/unified-table') return 'newProductUnifiedTable' as const
|
||||
if (pathname === '/supply-chain/new-products/sample-progress') return 'newProductSampleProgress' as const
|
||||
if (pathname === '/supply-chain/purchase/finished-product-progress') return 'finishedProductProgress' as const
|
||||
if (pathname === '/supply-chain/purchase/supplier-information') return 'purchaseSupplierInformation' as const
|
||||
if (pathname === '/supply-chain/purchase/supplier-recommended-products') return 'purchaseSupplierRecommendedProducts' as const
|
||||
if (pathname === '/supply-chain/purchase/daily-affairs') return 'purchaseDailyAffairs' as const
|
||||
if (pathname === '/supply-chain/purchase/price-change') return 'purchasePriceChange' as const
|
||||
if (pathname === '/supply-chain/script-monitor') return 'scriptMonitor' as const
|
||||
if (pathname === '/supply-chain/new-products/dropship-issues') return 'newProductDropshipIssues' as const
|
||||
if (pathname === '/supply-chain/operation-logs') return 'operationLogs' as const
|
||||
return null
|
||||
}
|
||||
|
||||
export function getSupplyChainApiPermissionRequirement(pathname: string, method: string) {
|
||||
if (pathname === '/api/supply-chain/permission-settings') return null
|
||||
|
||||
const action = method.toUpperCase() === 'GET' ? 'view' : 'edit'
|
||||
if (pathname.startsWith('/api/supply-chain/management/logistics-arrival-signing-duration')) {
|
||||
if (pathname.endsWith('/export')) {
|
||||
// 导出独立校验下载权限。
|
||||
return { targetKey: 'logisticsArrivalSigningDuration' as const, action: 'download' as const }
|
||||
}
|
||||
return { targetKey: 'logisticsArrivalSigningDuration' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/management/employee-hours')) {
|
||||
return { targetKey: 'employeeHours' as const, action: 'view' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/management/production-stop-demands')) {
|
||||
// 指定确认人的确认接口与列表只读权限在服务层单独校验(确认人可能没有停产需求管理权限)。
|
||||
if (pathname === '/api/supply-chain/management/production-stop-demands/confirmations') {
|
||||
return null
|
||||
}
|
||||
// 需求状态流转:重新提交需停产编辑权限,强制确认/走流程需流程负责人权限,均在服务层校验。
|
||||
if (/^\/api\/supply-chain\/management\/production-stop-demands\/demands\/(status|[^/]+\/status)$/.test(pathname)) {
|
||||
return null
|
||||
}
|
||||
if (pathname === '/api/supply-chain/management/production-stop-demands' && method.toUpperCase() === 'GET') {
|
||||
return null
|
||||
}
|
||||
return { targetKey: 'productionStopDemands' as const, action: pathname.endsWith('/export') ? 'download' as const : action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/filter-options') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: method.toUpperCase() === 'POST' ? 'view' as const : action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/source-image') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/sourcing-list') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: 'view' as const }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/sourcing-export') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: 'download' as const }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/sourcing-import') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: 'edit' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/new-products/dropship-issues')) {
|
||||
return { targetKey: 'newProductDropshipIssues' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-archive/dashboard')) {
|
||||
return { targetKey: 'dashboard' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-archive')) {
|
||||
return {
|
||||
targetKey: 'productArchive' as const,
|
||||
action: pathname.endsWith('/image-search') ? 'view' as const : action,
|
||||
}
|
||||
}
|
||||
// 限价管理:数据复用产品全量档案,接口使用独立的限价查看权限。
|
||||
if (pathname.startsWith('/api/supply-chain/limit-management')) {
|
||||
return { targetKey: 'limitManagement' as const, action: 'view' as const }
|
||||
}
|
||||
// 商品SABC打标 · 商品明细:可见 / 下载(默认开放)
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/export')) {
|
||||
return { targetKey: 'productSabc' as const, action: 'download' as const }
|
||||
}
|
||||
// 商品SABC打标 · 各部门数据分类:可见 / 下载 走独立模块节点
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/departments/settings')) {
|
||||
return {
|
||||
targetKey: 'departmentDistributionConfig' as const,
|
||||
action: method.toUpperCase() === 'GET' ? 'view' as const : 'edit' as const,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/departments/export')) {
|
||||
return { targetKey: 'departmentDistribution' as const, action: 'download' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/departments')) {
|
||||
return {
|
||||
targetKey: 'departmentDistribution' as const,
|
||||
action: method.toUpperCase() === 'GET' ? 'view' as const : 'edit' as const,
|
||||
}
|
||||
}
|
||||
// 商品SABC打标:商品明细与执行核算分别使用对应的独立节点。
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc') && method.toUpperCase() !== 'GET') {
|
||||
return { targetKey: 'productSabcCalculation' as const, action: 'edit' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc')) {
|
||||
return { targetKey: 'productSabc' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/dingtalk-workflow-monitor')) {
|
||||
return { targetKey: 'ownerConfig' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/category-config')) {
|
||||
return {
|
||||
targetKey: 'categoryConfig' as const,
|
||||
action: pathname.endsWith('/export') ? 'download' as const : action,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/warehouse-config')) {
|
||||
return { targetKey: 'warehouseConfig' as const, action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/management/owner-config/export') {
|
||||
return { targetKey: 'ownerConfig' as const, action: 'download' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/management/owner-config')) {
|
||||
return { targetKey: 'ownerConfig' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/store-account-management')) {
|
||||
return { targetKey: 'storeAccountManagement' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/finished-product-progress')) {
|
||||
return {
|
||||
targetKey: 'finishedProductProgress' as const,
|
||||
action: pathname.endsWith('/template') ? 'download' as const : action,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/new-product-termination-feedback')) {
|
||||
return { targetKey: 'newProductTerminationFeedback' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/supplier-information')) {
|
||||
return { targetKey: 'purchaseSupplierInformation' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/supplier-recommended-products')) {
|
||||
return {
|
||||
targetKey: 'purchaseSupplierRecommendedProducts' as const,
|
||||
action: pathname.endsWith('/template') || pathname.endsWith('/export') ? 'download' as const : pathname.endsWith('/selection') || pathname.endsWith('/dashboard') ? 'view' as const : action,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/script-monitor')) {
|
||||
return { targetKey: 'scriptMonitor' as const, action }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@ -15,10 +15,10 @@ describe('supply chain permission settings', () => {
|
||||
it('requires business management view for owner queries and edit for mutations', () => {
|
||||
const path = '/api/supply-chain/management/owner-config'
|
||||
expect(getSupplyChainApiPermissionRequirement(path, 'GET'))
|
||||
.toEqual({ targetKey: 'businessManagement', action: 'view' })
|
||||
.toEqual({ targetKey: 'ownerConfig', action: 'view' })
|
||||
for (const method of ['POST', 'PATCH', 'DELETE']) {
|
||||
expect(getSupplyChainApiPermissionRequirement(`${path}/config-1`, method))
|
||||
.toEqual({ targetKey: 'businessManagement', action: 'edit' })
|
||||
.toEqual({ targetKey: 'ownerConfig', action: 'edit' })
|
||||
}
|
||||
})
|
||||
|
||||
@ -26,7 +26,7 @@ describe('supply chain permission settings', () => {
|
||||
expect(getSupplyChainApiPermissionRequirement(
|
||||
'/api/supply-chain/management/owner-config/export',
|
||||
'GET',
|
||||
)).toEqual({ targetKey: 'businessManagement', action: 'download' })
|
||||
)).toEqual({ targetKey: 'ownerConfig', action: 'download' })
|
||||
})
|
||||
|
||||
it('keeps ordinary business types closed until an administrator explicitly opens them', () => {
|
||||
@ -230,19 +230,19 @@ describe('supply chain permission settings', () => {
|
||||
).toBe('purchaseSupplierInformation')
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management'),
|
||||
).toBe('businessManagement')
|
||||
).toBeNull()
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management/owner-config'),
|
||||
).toBe('businessManagement')
|
||||
).toBe('ownerConfig')
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management/custom-table'),
|
||||
).toBe('businessManagement')
|
||||
).toBe('limitManagement')
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management/blank-pages/blank-page-1'),
|
||||
).toBe('businessManagement')
|
||||
).toBe('managementBlankPage1')
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management/blank-pages/future-page'),
|
||||
).toBe('businessManagement')
|
||||
).toBeNull()
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/script-monitor'),
|
||||
).toBe('scriptMonitor')
|
||||
@ -341,32 +341,32 @@ describe('supply chain permission settings', () => {
|
||||
it('maps the logistics arrival signing duration dashboard and its APIs to business management', () => {
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management/logistics-arrival-signing-duration'),
|
||||
).toBe('businessManagement')
|
||||
).toBe('logisticsArrivalSigningDuration')
|
||||
expect(
|
||||
getSupplyChainApiPermissionRequirement(
|
||||
'/api/supply-chain/management/logistics-arrival-signing-duration',
|
||||
'GET',
|
||||
),
|
||||
).toEqual({ targetKey: 'businessManagement', action: 'view' })
|
||||
).toEqual({ targetKey: 'logisticsArrivalSigningDuration', action: 'view' })
|
||||
// 导出按 view 放行(能否导出由页面自身控制),写入类接口按 edit 校验。
|
||||
expect(
|
||||
getSupplyChainApiPermissionRequirement(
|
||||
'/api/supply-chain/management/logistics-arrival-signing-duration/export',
|
||||
'POST',
|
||||
),
|
||||
).toEqual({ targetKey: 'businessManagement', action: 'view' })
|
||||
).toEqual({ targetKey: 'logisticsArrivalSigningDuration', action: 'download' })
|
||||
expect(
|
||||
getSupplyChainApiPermissionRequirement(
|
||||
'/api/supply-chain/management/logistics-arrival-signing-duration/settings',
|
||||
'PUT',
|
||||
),
|
||||
).toEqual({ targetKey: 'businessManagement', action: 'edit' })
|
||||
).toEqual({ targetKey: 'logisticsArrivalSigningDuration', action: 'edit' })
|
||||
})
|
||||
|
||||
it('maps production stop demand routes and API exemptions', () => {
|
||||
expect(
|
||||
getSupplyChainPermissionTargetForPath('/supply-chain/management/production-stop-demands'),
|
||||
).toBe('businessManagement')
|
||||
).toBe('productionStopDemands')
|
||||
expect(
|
||||
getSupplyChainApiPermissionRequirement(
|
||||
'/api/supply-chain/management/production-stop-demands',
|
||||
@ -378,7 +378,7 @@ describe('supply chain permission settings', () => {
|
||||
'/api/supply-chain/management/production-stop-demands',
|
||||
'POST',
|
||||
),
|
||||
).toEqual({ targetKey: 'businessManagement', action: 'edit' })
|
||||
).toEqual({ targetKey: 'productionStopDemands', action: 'edit' })
|
||||
expect(
|
||||
getSupplyChainApiPermissionRequirement(
|
||||
'/api/supply-chain/management/production-stop-demands/confirmations',
|
||||
|
||||
@ -8,7 +8,9 @@ import {
|
||||
import { isExternalIntakeUser } from '@/lib/non-integrated-external-entry-permissions'
|
||||
import { isAdmin, isSupervisor } from '@/lib/role-scope'
|
||||
import { isSupplierAccount } from '@/lib/supplier-account'
|
||||
import { isManagementBlankPagePath } from '@/lib/supply-chain-management-blank-pages'
|
||||
import { SUPPLY_CHAIN_DEFAULT_OPEN_TARGETS, SUPPLY_CHAIN_PERMISSION_TARGETS, type SupplyChainPermissionTargetKey } from './supply-chain-permission-targets'
|
||||
export * from './supply-chain-permission-targets'
|
||||
export { getSupplyChainPermissionTargetForPath, getSupplyChainApiPermissionRequirement } from './supply-chain-permission-paths'
|
||||
|
||||
export type SupplyChainPermissionAction = 'view' | 'download' | 'edit'
|
||||
export type SupplyChainPermissionSubjectKey = Exclude<
|
||||
@ -18,34 +20,6 @@ export type SupplyChainPermissionSubjectKey = Exclude<
|
||||
export type SupplyChainPermissionRoleKey =
|
||||
| SupplyChainPermissionSubjectKey
|
||||
| IntakeNodePermissionDynamicRoleKey
|
||||
export type SupplyChainPermissionTargetKey =
|
||||
| 'workspace'
|
||||
| 'dashboard'
|
||||
| 'businessManagement'
|
||||
| 'employeeHours'
|
||||
| 'productArchive'
|
||||
| 'categoryConfig'
|
||||
| 'warehouseConfig'
|
||||
| 'storeAccountManagement'
|
||||
| 'newProductOverview'
|
||||
| 'newProduct1688DropshipData'
|
||||
| 'newProductTerminationFeedback'
|
||||
| 'newProductUnifiedTable'
|
||||
| 'newProductSampleProgress'
|
||||
| 'purchaseSupplierInformation'
|
||||
| 'purchaseSupplierRecommendedProducts'
|
||||
| 'purchaseDailyAffairs'
|
||||
| 'purchasePriceChange'
|
||||
| 'scriptMonitor'
|
||||
| 'newProductDropshipIssues'
|
||||
| 'operationLogs'
|
||||
// 页面内的动作权限:商品SABC打标「执行核算」(只走权限配置,不占侧边栏菜单)
|
||||
| 'productSabcCalculation'
|
||||
| 'productSabc'
|
||||
// 模块权限:商品SABC打标「各部门数据分类」(可见/下载分开控制)
|
||||
| 'departmentDistribution'
|
||||
// 商品SABC打标「各部门数据分类」的配置(品牌范围/兜底规则/目标参数)
|
||||
| 'departmentDistributionConfig'
|
||||
|
||||
export type SupplyChainPermissionSubject = Omit<
|
||||
IntakeNodePermissionSubject,
|
||||
@ -54,12 +28,6 @@ export type SupplyChainPermissionSubject = Omit<
|
||||
key: SupplyChainPermissionSubjectKey
|
||||
}
|
||||
|
||||
export type SupplyChainPermissionTarget = {
|
||||
key: SupplyChainPermissionTargetKey
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export type SupplyChainPermissionOverrideMap = Record<string, boolean>
|
||||
export type SupplyChainPermissionPrincipalKey =
|
||||
| SupplyChainPermissionRoleKey
|
||||
@ -84,49 +52,6 @@ export const SUPPLY_CHAIN_PERMISSION_ACTION_LABELS: Record<
|
||||
edit: '操作',
|
||||
}
|
||||
|
||||
export const SUPPLY_CHAIN_PERMISSION_TARGETS: SupplyChainPermissionTarget[] = [
|
||||
{ key: 'dashboard', label: '工作台', description: '供应链工作台总览' },
|
||||
{ key: 'businessManagement', label: '经营管理', description: '后续经营类功能统一入口' },
|
||||
{ key: 'employeeHours', label: '员工工时统计', description: '员工本人、部门或全公司数据范围单独设置' },
|
||||
{ key: 'productArchive', label: '产品全量档案', description: '商品、供应链和运营档案' },
|
||||
{ key: 'categoryConfig', label: '类目配置', description: '产品全量档案类目维护' },
|
||||
{ key: 'warehouseConfig', label: '仓库配置', description: '出入库仓库维护' },
|
||||
{ key: 'storeAccountManagement', label: '店铺账号管理', description: '店铺账号人工维护与权限控制' },
|
||||
{ key: 'newProductOverview', label: '新品经营总览', description: '新品经营指标和阶段时效分析' },
|
||||
{ key: 'newProduct1688DropshipData', label: '1688代发数据明细', description: '1688 导单店铺销售、履约和退款明细' },
|
||||
{ key: 'newProductTerminationFeedback', label: '新品终止任务反馈', description: '维护已撤销、已拒绝和已终止流程的原因' },
|
||||
{ key: 'newProductUnifiedTable', label: '新品开发进度总表', description: '新品流程公共数据源' },
|
||||
{ key: 'newProductSampleProgress', label: '打样进度明细', description: '新品打样进度公共数据源' },
|
||||
{ key: 'purchaseSupplierInformation', label: '供应商信息', description: '采购供应商信息汇总' },
|
||||
{ key: 'purchaseSupplierRecommendedProducts', label: '供应商新品推荐', description: '供应商推荐新品维护' },
|
||||
{ key: 'purchaseDailyAffairs', label: '日常事项', description: '采购日常协作' },
|
||||
{ key: 'purchasePriceChange', label: '产品变价', description: '采购价格变更协作' },
|
||||
{ key: 'scriptMonitor', label: '脚本监控', description: '跨机器脚本任务、状态和运行结果' },
|
||||
{ key: 'newProductDropshipIssues', label: '1688代发异常事项', description: '1688代发异常事项' },
|
||||
{ key: 'operationLogs', label: '操作日志', description: '供应链操作审计' },
|
||||
{ key: 'productSabcCalculation', label: '商品SABC打标 · 执行核算', description: '每月核算与重新生成商品SABC打标数据(页面内按钮)' },
|
||||
{ key: 'departmentDistribution', label: '商品SABC打标 · 各部门数据分类', description: '按利润中心汇总的部门数据(可见、下载分别控制)' },
|
||||
{ key: 'productSabc', label: '商品SABC打标 · 商品明细', description: '商品SABC打标明细页与明细导出(默认全员开放,可在用户管理后台关闭)' },
|
||||
{ key: 'departmentDistributionConfig', label: '商品SABC打标 · 部门数据配置', description: '维护品牌范围、负责人兜底与目标参数(操作列控制)' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 只用于权限配置、不在侧边栏展示的额外权限节点。
|
||||
* 按菜单模块归组,权限页会把它挂到对应模块下面。
|
||||
*/
|
||||
export const SUPPLY_CHAIN_PERMISSION_EXTRA_TARGETS: Record<string, SupplyChainPermissionTargetKey[]> = {
|
||||
management: ['productSabcCalculation', 'departmentDistribution', 'departmentDistributionConfig'],
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认开放的权限节点:不做任何配置时所有人都可以查看/导出;
|
||||
* 只有管理员在「用户管理后台」里显式把「可见/下载」关掉,才会受限。
|
||||
*/
|
||||
export const SUPPLY_CHAIN_DEFAULT_OPEN_TARGETS = new Set<SupplyChainPermissionTargetKey>([
|
||||
'productSabc',
|
||||
'departmentDistribution',
|
||||
])
|
||||
|
||||
/**
|
||||
* 按角色的默认权限(只针对少数节点):
|
||||
* 商品企划(企划部/商品计划部)默认可以维护「部门数据配置」;
|
||||
@ -144,34 +69,6 @@ export function getSupplyChainTargetRoleDefaultAccess(
|
||||
return null
|
||||
}
|
||||
|
||||
export const SUPPLY_CHAIN_PERMISSION_TARGET_KEYS = SUPPLY_CHAIN_PERMISSION_TARGETS.map(
|
||||
(target) => target.key,
|
||||
) as SupplyChainPermissionTargetKey[]
|
||||
|
||||
export const SUPPLY_CHAIN_PERMISSION_TARGET_ROUTES: Partial<
|
||||
Record<SupplyChainPermissionTargetKey, string>
|
||||
> = {
|
||||
dashboard: '/supply-chain/dashboard',
|
||||
businessManagement: '/supply-chain/management/owner-config',
|
||||
employeeHours: '/supply-chain/management/employee-hours',
|
||||
productArchive: '/supply-chain/database/product-archive',
|
||||
categoryConfig: '/supply-chain/database/category-config',
|
||||
warehouseConfig: '/supply-chain/database/warehouse-config',
|
||||
storeAccountManagement: '/supply-chain/database/store-account-management',
|
||||
newProductOverview: '/supply-chain/new-products/overview',
|
||||
newProduct1688DropshipData: '/supply-chain/new-products/1688-dropship-data',
|
||||
newProductTerminationFeedback: '/supply-chain/new-products/termination-feedback',
|
||||
newProductUnifiedTable: '/supply-chain/new-products/unified-table',
|
||||
newProductSampleProgress: '/supply-chain/new-products/sample-progress',
|
||||
purchaseSupplierInformation: '/supply-chain/purchase/supplier-information',
|
||||
purchaseSupplierRecommendedProducts: '/supply-chain/purchase/supplier-recommended-products',
|
||||
purchaseDailyAffairs: '/supply-chain/purchase/daily-affairs',
|
||||
purchasePriceChange: '/supply-chain/purchase/price-change',
|
||||
scriptMonitor: '/supply-chain/script-monitor',
|
||||
newProductDropshipIssues: '/supply-chain/new-products/dropship-issues',
|
||||
operationLogs: '/supply-chain/operation-logs',
|
||||
}
|
||||
|
||||
const SUPPLY_CHAIN_PERMISSION_SUBJECT_KEYS = new Set<SupplyChainPermissionSubjectKey>([
|
||||
'customerService',
|
||||
'design',
|
||||
@ -190,7 +87,7 @@ const SUPPLY_CHAIN_PERMISSION_SUBJECT_KEYS = new Set<SupplyChainPermissionSubjec
|
||||
])
|
||||
|
||||
const SUPPLY_CHAIN_PERMISSION_TARGET_KEY_SET = new Set<SupplyChainPermissionTargetKey>(
|
||||
['workspace', ...SUPPLY_CHAIN_PERMISSION_TARGETS.map((target) => target.key)],
|
||||
['workspace', 'businessManagement', ...SUPPLY_CHAIN_PERMISSION_TARGETS.map((target) => target.key)],
|
||||
)
|
||||
const SUPPLY_CHAIN_PERMISSION_ACTION_SET = new Set<SupplyChainPermissionAction>([
|
||||
'view',
|
||||
@ -384,170 +281,3 @@ export function resolveSupplyChainPermissionAccess(
|
||||
|
||||
return { view, download, edit }
|
||||
}
|
||||
|
||||
export function getSupplyChainPermissionTargetForPath(pathname: string) {
|
||||
if (pathname === '/supply-chain/dashboard') return 'dashboard' as const
|
||||
if (pathname === '/supply-chain/management') return 'businessManagement' as const
|
||||
if (pathname === '/supply-chain/management/owner-config') return 'businessManagement' as const
|
||||
if (pathname === '/supply-chain/management/custom-table') return 'businessManagement' as const
|
||||
// 商品SABC打标:明细走独立节点(默认开放),「各部门数据分类」子页走对应模块节点
|
||||
if (pathname.startsWith('/supply-chain/management/blank-pages/blank-page-3/departments')) {
|
||||
return 'departmentDistribution' as const
|
||||
}
|
||||
if (pathname.startsWith('/supply-chain/management/blank-pages/blank-page-3')) {
|
||||
return 'productSabc' as const
|
||||
}
|
||||
if (isManagementBlankPagePath(pathname)) return 'businessManagement' as const
|
||||
if (pathname === '/supply-chain/management/employee-hours') return 'employeeHours' as const
|
||||
if (pathname === '/supply-chain/management/logistics-arrival-signing-duration') return 'businessManagement' as const
|
||||
if (pathname === '/supply-chain/management/production-stop-demands') return 'businessManagement' as const
|
||||
if (pathname === '/supply-chain/database/product-archive') return 'productArchive' as const
|
||||
if (pathname === '/supply-chain/database/category-config') return 'categoryConfig' as const
|
||||
if (pathname === '/supply-chain/database/warehouse-config') return 'warehouseConfig' as const
|
||||
if (pathname === '/supply-chain/database/store-account-management') return 'storeAccountManagement' as const
|
||||
if (pathname === '/supply-chain/new-products/overview') return 'newProductOverview' as const
|
||||
if (pathname === '/supply-chain/new-products/1688-dropship-data') return 'newProduct1688DropshipData' as const
|
||||
if (pathname === '/supply-chain/new-products/termination-feedback') return 'newProductTerminationFeedback' as const
|
||||
if (pathname === '/supply-chain/new-products/unified-table') return 'newProductUnifiedTable' as const
|
||||
if (pathname === '/supply-chain/new-products/sample-progress') return 'newProductSampleProgress' as const
|
||||
if (pathname === '/supply-chain/purchase/finished-product-progress') return 'newProductSampleProgress' as const
|
||||
if (pathname === '/supply-chain/purchase/supplier-information') return 'purchaseSupplierInformation' as const
|
||||
if (pathname === '/supply-chain/purchase/supplier-recommended-products') return 'purchaseSupplierRecommendedProducts' as const
|
||||
if (pathname === '/supply-chain/purchase/daily-affairs') return 'purchaseDailyAffairs' as const
|
||||
if (pathname === '/supply-chain/purchase/price-change') return 'purchasePriceChange' as const
|
||||
if (pathname === '/supply-chain/script-monitor') return 'scriptMonitor' as const
|
||||
if (pathname === '/supply-chain/new-products/dropship-issues') return 'newProductDropshipIssues' as const
|
||||
if (pathname === '/supply-chain/operation-logs') return 'operationLogs' as const
|
||||
return null
|
||||
}
|
||||
|
||||
export function getSupplyChainApiPermissionRequirement(pathname: string, method: string) {
|
||||
if (pathname === '/api/supply-chain/permission-settings') return null
|
||||
|
||||
const action = method.toUpperCase() === 'GET' ? 'view' : 'edit'
|
||||
if (pathname.startsWith('/api/supply-chain/management/logistics-arrival-signing-duration')) {
|
||||
if (pathname.endsWith('/export')) {
|
||||
// 该看板允许所有能看的人导出当前筛选明细。
|
||||
return { targetKey: 'businessManagement' as const, action: 'view' as const }
|
||||
}
|
||||
return { targetKey: 'businessManagement' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/management/employee-hours')) {
|
||||
return { targetKey: 'employeeHours' as const, action: 'view' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/management/production-stop-demands')) {
|
||||
// 指定确认人的确认接口与列表只读权限在服务层单独校验(确认人可能没有经营管理权限)。
|
||||
if (pathname === '/api/supply-chain/management/production-stop-demands/confirmations') {
|
||||
return null
|
||||
}
|
||||
// 需求状态流转:重新提交需经营管理编辑权限,强制确认/走流程需流程负责人权限,均在服务层校验。
|
||||
if (/^\/api\/supply-chain\/management\/production-stop-demands\/demands\/(status|[^/]+\/status)$/.test(pathname)) {
|
||||
return null
|
||||
}
|
||||
if (pathname === '/api/supply-chain/management/production-stop-demands' && method.toUpperCase() === 'GET') {
|
||||
return null
|
||||
}
|
||||
return { targetKey: 'businessManagement' as const, action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/filter-options') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: method.toUpperCase() === 'POST' ? 'view' as const : action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/source-image') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/sourcing-list') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: 'view' as const }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/sourcing-export') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: 'download' as const }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/1688-dropship-data/sourcing-import') {
|
||||
return { targetKey: 'newProduct1688DropshipData' as const, action: 'edit' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/new-products/dropship-issues')) {
|
||||
return { targetKey: 'newProductDropshipIssues' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-archive/dashboard')) {
|
||||
return { targetKey: 'dashboard' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-archive')) {
|
||||
return {
|
||||
targetKey: 'productArchive' as const,
|
||||
action: pathname.endsWith('/image-search') ? 'view' as const : action,
|
||||
}
|
||||
}
|
||||
// 限价管理(经营管理):数据复用产品全量档案,权限跟随页面入口;接口均为只读查询。
|
||||
if (pathname.startsWith('/api/supply-chain/limit-management')) {
|
||||
return { targetKey: 'businessManagement' as const, action: 'view' as const }
|
||||
}
|
||||
// 商品SABC打标 · 商品明细:可见 / 下载(默认开放)
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/export')) {
|
||||
return { targetKey: 'productSabc' as const, action: 'download' as const }
|
||||
}
|
||||
// 商品SABC打标 · 各部门数据分类:可见 / 下载 走独立模块节点
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/departments/settings')) {
|
||||
return {
|
||||
targetKey: 'departmentDistributionConfig' as const,
|
||||
action: method.toUpperCase() === 'GET' ? 'view' as const : 'edit' as const,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/departments/export')) {
|
||||
return { targetKey: 'departmentDistribution' as const, action: 'download' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc/departments')) {
|
||||
return {
|
||||
targetKey: 'departmentDistribution' as const,
|
||||
action: method.toUpperCase() === 'GET' ? 'view' as const : 'edit' as const,
|
||||
}
|
||||
}
|
||||
// 商品SABC打标:读接口按「经营管理」可见,写接口(执行核算)按独立权限节点校验
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc') && method.toUpperCase() !== 'GET') {
|
||||
return { targetKey: 'productSabcCalculation' as const, action: 'edit' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/product-sabc')) {
|
||||
return { targetKey: 'productSabc' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/dingtalk-workflow-monitor')) {
|
||||
return { targetKey: 'businessManagement' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/category-config')) {
|
||||
return {
|
||||
targetKey: 'categoryConfig' as const,
|
||||
action: pathname.endsWith('/export') ? 'download' as const : action,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/warehouse-config')) {
|
||||
return { targetKey: 'warehouseConfig' as const, action }
|
||||
}
|
||||
if (pathname === '/api/supply-chain/management/owner-config/export') {
|
||||
return { targetKey: 'businessManagement' as const, action: 'download' as const }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/management/owner-config')) {
|
||||
return { targetKey: 'businessManagement' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/store-account-management')) {
|
||||
return { targetKey: 'storeAccountManagement' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/finished-product-progress')) {
|
||||
return {
|
||||
targetKey: 'newProductSampleProgress' as const,
|
||||
action: pathname.endsWith('/template') ? 'download' as const : action,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/new-product-termination-feedback')) {
|
||||
return { targetKey: 'newProductTerminationFeedback' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/supplier-information')) {
|
||||
return { targetKey: 'purchaseSupplierInformation' as const, action }
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/supplier-recommended-products')) {
|
||||
return {
|
||||
targetKey: 'purchaseSupplierRecommendedProducts' as const,
|
||||
action: pathname.endsWith('/template') || pathname.endsWith('/export') ? 'download' as const : pathname.endsWith('/selection') || pathname.endsWith('/dashboard') ? 'view' as const : action,
|
||||
}
|
||||
}
|
||||
if (pathname.startsWith('/api/supply-chain/script-monitor')) {
|
||||
return { targetKey: 'scriptMonitor' as const, action }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
132
furniture-3d-system/src/lib/supply-chain-permission-targets.ts
Normal file
132
furniture-3d-system/src/lib/supply-chain-permission-targets.ts
Normal file
@ -0,0 +1,132 @@
|
||||
export type SupplyChainPermissionTargetKey =
|
||||
| 'workspace'
|
||||
| 'dashboard'
|
||||
// Historical key is accepted only to retain existing permission records.
|
||||
| 'businessManagement'
|
||||
| 'ownerConfig'
|
||||
| 'limitManagement'
|
||||
| 'productionStopDemands'
|
||||
| 'logisticsArrivalSigningDuration'
|
||||
| 'managementBlankPage1'
|
||||
| 'managementBlankPage2'
|
||||
| 'finishedProductProgress'
|
||||
| 'employeeHours'
|
||||
| 'productArchive'
|
||||
| 'categoryConfig'
|
||||
| 'warehouseConfig'
|
||||
| 'storeAccountManagement'
|
||||
| 'newProductOverview'
|
||||
| 'newProduct1688DropshipData'
|
||||
| 'newProductTerminationFeedback'
|
||||
| 'newProductUnifiedTable'
|
||||
| 'newProductSampleProgress'
|
||||
| 'purchaseSupplierInformation'
|
||||
| 'purchaseSupplierRecommendedProducts'
|
||||
| 'purchaseDailyAffairs'
|
||||
| 'purchasePriceChange'
|
||||
| 'scriptMonitor'
|
||||
| 'newProductDropshipIssues'
|
||||
| 'operationLogs'
|
||||
// 页面内的动作权限:商品SABC打标「执行核算」(只走权限配置,不占侧边栏菜单)
|
||||
| 'productSabcCalculation'
|
||||
| 'productSabc'
|
||||
// 模块权限:商品SABC打标「各部门数据分类」(可见/下载分开控制)
|
||||
| 'departmentDistribution'
|
||||
// 商品SABC打标「各部门数据分类」的配置(品牌范围/兜底规则/目标参数)
|
||||
| 'departmentDistributionConfig'
|
||||
|
||||
|
||||
export type SupplyChainPermissionTarget = {
|
||||
key: SupplyChainPermissionTargetKey
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
|
||||
export const SUPPLY_CHAIN_PERMISSION_TARGETS: SupplyChainPermissionTarget[] = [
|
||||
{ key: 'dashboard', label: '工作台', description: '供应链工作台总览' },
|
||||
{ key: 'ownerConfig', label: '人员信息', description: '人员目录查看与导出' },
|
||||
{ key: 'limitManagement', label: '限价管理', description: '限价查询与维护' },
|
||||
{ key: 'productionStopDemands', label: '停产需求管理', description: '停产需求提单与处理' },
|
||||
{ key: 'logisticsArrivalSigningDuration', label: '物流到货签收时长看板', description: '物流签收查询与明细导出' },
|
||||
{ key: 'managementBlankPage1', label: '空白页 1', description: '经营管理空白页 1' },
|
||||
{ key: 'managementBlankPage2', label: '空白页 2', description: '经营管理空白页 2' },
|
||||
{ key: 'finishedProductProgress', label: '成品进度明细', description: '成品进度查看、导出与维护' },
|
||||
{ key: 'employeeHours', label: '员工工时统计', description: '员工本人、部门或全公司数据范围单独设置' },
|
||||
{ key: 'productArchive', label: '产品全量档案', description: '商品、供应链和运营档案' },
|
||||
{ key: 'categoryConfig', label: '类目配置', description: '产品全量档案类目维护' },
|
||||
{ key: 'warehouseConfig', label: '仓库配置', description: '出入库仓库维护' },
|
||||
{ key: 'storeAccountManagement', label: '店铺账号管理', description: '店铺账号人工维护与权限控制' },
|
||||
{ key: 'newProductOverview', label: '新品经营总览', description: '新品经营指标和阶段时效分析' },
|
||||
{ key: 'newProduct1688DropshipData', label: '1688代发数据明细', description: '1688 导单店铺销售、履约和退款明细' },
|
||||
{ key: 'newProductTerminationFeedback', label: '新品终止任务反馈', description: '维护已撤销、已拒绝和已终止流程的原因' },
|
||||
{ key: 'newProductUnifiedTable', label: '新品开发进度总表', description: '新品流程公共数据源' },
|
||||
{ key: 'newProductSampleProgress', label: '打样进度明细', description: '新品打样进度公共数据源' },
|
||||
{ key: 'purchaseSupplierInformation', label: '供应商信息', description: '采购供应商信息汇总' },
|
||||
{ key: 'purchaseSupplierRecommendedProducts', label: '供应商新品推荐', description: '供应商推荐新品维护' },
|
||||
{ key: 'purchaseDailyAffairs', label: '日常事项', description: '采购日常协作' },
|
||||
{ key: 'purchasePriceChange', label: '产品变价', description: '采购价格变更协作' },
|
||||
{ key: 'scriptMonitor', label: '脚本监控', description: '跨机器脚本任务、状态和运行结果' },
|
||||
{ key: 'newProductDropshipIssues', label: '1688代发异常事项', description: '1688代发异常事项' },
|
||||
{ key: 'operationLogs', label: '操作日志', description: '供应链操作审计' },
|
||||
{ key: 'productSabcCalculation', label: '商品SABC打标 · 执行核算', description: '每月核算与重新生成商品SABC打标数据(页面内按钮)' },
|
||||
{ key: 'departmentDistribution', label: '商品SABC打标 · 各部门数据分类', description: '按利润中心汇总的部门数据(可见、下载分别控制)' },
|
||||
{ key: 'productSabc', label: '商品SABC打标 · 商品明细', description: '商品SABC打标明细页与明细导出(默认全员开放,可在用户管理后台关闭)' },
|
||||
{ key: 'departmentDistributionConfig', label: '商品SABC打标 · 部门数据配置', description: '维护品牌范围、负责人兜底与目标参数(操作列控制)' },
|
||||
]
|
||||
|
||||
|
||||
/**
|
||||
* 只用于权限配置、不在侧边栏展示的额外权限节点。
|
||||
* 按菜单模块归组,权限页会把它挂到对应模块下面。
|
||||
*/
|
||||
export const SUPPLY_CHAIN_PERMISSION_EXTRA_TARGETS: Record<string, SupplyChainPermissionTargetKey[]> = {
|
||||
management: ['productSabcCalculation', 'departmentDistribution', 'departmentDistributionConfig'],
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 默认开放的权限节点:不做任何配置时所有人都可以查看/导出;
|
||||
* 只有管理员在「用户管理后台」里显式把「可见/下载」关掉,才会受限。
|
||||
*/
|
||||
export const SUPPLY_CHAIN_DEFAULT_OPEN_TARGETS = new Set<SupplyChainPermissionTargetKey>([
|
||||
'productSabc',
|
||||
'departmentDistribution',
|
||||
])
|
||||
|
||||
|
||||
export const SUPPLY_CHAIN_PERMISSION_TARGET_KEYS = SUPPLY_CHAIN_PERMISSION_TARGETS.map(
|
||||
(target) => target.key,
|
||||
) as SupplyChainPermissionTargetKey[]
|
||||
|
||||
|
||||
export const SUPPLY_CHAIN_PERMISSION_TARGET_ROUTES: Partial<
|
||||
Record<SupplyChainPermissionTargetKey, string>
|
||||
> = {
|
||||
productSabc: '/supply-chain/management/blank-pages/blank-page-3',
|
||||
dashboard: '/supply-chain/dashboard',
|
||||
ownerConfig: '/supply-chain/management/owner-config',
|
||||
limitManagement: '/supply-chain/management/custom-table',
|
||||
productionStopDemands: '/supply-chain/management/production-stop-demands',
|
||||
logisticsArrivalSigningDuration: '/supply-chain/management/logistics-arrival-signing-duration',
|
||||
managementBlankPage1: '/supply-chain/management/blank-pages/blank-page-1',
|
||||
managementBlankPage2: '/supply-chain/management/blank-pages/blank-page-2',
|
||||
finishedProductProgress: '/supply-chain/purchase/finished-product-progress',
|
||||
employeeHours: '/supply-chain/management/employee-hours',
|
||||
productArchive: '/supply-chain/database/product-archive',
|
||||
categoryConfig: '/supply-chain/database/category-config',
|
||||
warehouseConfig: '/supply-chain/database/warehouse-config',
|
||||
storeAccountManagement: '/supply-chain/database/store-account-management',
|
||||
newProductOverview: '/supply-chain/new-products/overview',
|
||||
newProduct1688DropshipData: '/supply-chain/new-products/1688-dropship-data',
|
||||
newProductTerminationFeedback: '/supply-chain/new-products/termination-feedback',
|
||||
newProductUnifiedTable: '/supply-chain/new-products/unified-table',
|
||||
newProductSampleProgress: '/supply-chain/new-products/sample-progress',
|
||||
purchaseSupplierInformation: '/supply-chain/purchase/supplier-information',
|
||||
purchaseSupplierRecommendedProducts: '/supply-chain/purchase/supplier-recommended-products',
|
||||
purchaseDailyAffairs: '/supply-chain/purchase/daily-affairs',
|
||||
purchasePriceChange: '/supply-chain/purchase/price-change',
|
||||
scriptMonitor: '/supply-chain/script-monitor',
|
||||
newProductDropshipIssues: '/supply-chain/new-products/dropship-issues',
|
||||
operationLogs: '/supply-chain/operation-logs',
|
||||
}
|
||||
@ -10,7 +10,7 @@ export class FinishedProductBulkUpdateService {
|
||||
constructor(private readonly repository = finishedProductBulkUpdateRepository) {}
|
||||
|
||||
async update(input: unknown, user: AuthUser) {
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(user, 'newProductSampleProgress')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(user, 'finishedProductProgress')
|
||||
if (!access.edit) throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '当前账号没有成品进度明细编辑权限')
|
||||
const parsed = finishedProductBulkUpdateSchema.safeParse(input)
|
||||
if (!parsed.success) throw new AppError(ErrorCode.VALIDATION_FAILED, parsed.error.issues[0]?.message ?? '批量修改参数不正确')
|
||||
|
||||
@ -8,7 +8,7 @@ import { supplyChainPermissionSettingsService } from './supply-chain-permission-
|
||||
|
||||
export const finishedProductDirectoryService = {
|
||||
async listOptions(search: string, user: AuthUser, kind?: 'supplier' | 'brand'): Promise<FinishedProductEditOptions> {
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(user, 'newProductSampleProgress')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(user, 'finishedProductProgress')
|
||||
if (!access.edit) throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '当前账号没有成品进度明细编辑权限')
|
||||
const directory = kind === 'brand' ? { suppliers: [] } : await jstPlanningDirectoryService.listOptions()
|
||||
const keyword = search.trim().toLocaleLowerCase()
|
||||
|
||||
@ -99,7 +99,7 @@ export class FinishedProductProgressService {
|
||||
constructor(private readonly repository: FinishedProductProgressRepository = finishedProductProgressRepository) {}
|
||||
|
||||
async getAccess(user: AuthUser) {
|
||||
return supplyChainPermissionSettingsService.getUserAccess(user, 'newProductSampleProgress')
|
||||
return supplyChainPermissionSettingsService.getUserAccess(user, 'finishedProductProgress')
|
||||
}
|
||||
|
||||
private async requireAccess(user: AuthUser, action: 'view' | 'download' | 'edit') {
|
||||
|
||||
@ -126,7 +126,7 @@ class ProductionStopDemandsService {
|
||||
user: AuthUser,
|
||||
): Promise<{ canView: boolean; canEdit: boolean; isApprover: boolean }> {
|
||||
const [access, approver] = await Promise.all([
|
||||
supplyChainPermissionSettingsService.getUserAccess(user, 'businessManagement'),
|
||||
supplyChainPermissionSettingsService.getUserAccess(user, 'productionStopDemands'),
|
||||
productionStopDemandRepository.findApproverByUserId(user.id),
|
||||
])
|
||||
return { canView: access.view, canEdit: access.edit, isApprover: Boolean(approver?.isActive) }
|
||||
@ -627,7 +627,7 @@ class ProductionStopDemandsService {
|
||||
|
||||
/** 需求驳回后二次修改并重新提交确认:清空本轮确认票,重新计算截止时间并推送。 */
|
||||
async resubmitDemand(demandId: string, user: AuthUser): Promise<ProductionStopConfirmationInfo> {
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(user, 'businessManagement')
|
||||
const access = await supplyChainPermissionSettingsService.getUserAccess(user, 'productionStopDemands')
|
||||
if (!access.edit) {
|
||||
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '当前账号没有该供应链功能的权限')
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
resolveSupplyChainPermissionAccess,
|
||||
resolveSupplyChainPermissionSubjectKeys,
|
||||
sanitizeSupplyChainPermissionOverrides,
|
||||
SUPPLY_CHAIN_PERMISSION_TARGET_KEYS,
|
||||
type SupplyChainPermissionAction,
|
||||
type SupplyChainPermissionOverrideMap,
|
||||
type SupplyChainPermissionPrincipalKey,
|
||||
@ -82,7 +83,12 @@ class SupplyChainPermissionSettingsService {
|
||||
targetKey: SupplyChainPermissionTargetKey,
|
||||
) {
|
||||
const roleDefaultAccess = getSupplyChainRoleDefaultAccess(user)
|
||||
if (roleDefaultAccess) return roleDefaultAccess
|
||||
if (roleDefaultAccess) {
|
||||
// Supervisors could already export this dashboard through its old view guard.
|
||||
return targetKey === 'logisticsArrivalSigningDuration'
|
||||
? { ...roleDefaultAccess, download: roleDefaultAccess.view }
|
||||
: roleDefaultAccess
|
||||
}
|
||||
|
||||
// 少数节点按角色默认放行(如「部门数据配置」默认给商品企划)
|
||||
const targetRoleDefaultAccess = getSupplyChainTargetRoleDefaultAccess(user, targetKey)
|
||||
@ -120,61 +126,13 @@ class SupplyChainPermissionSettingsService {
|
||||
|
||||
async getVisibleTargetKeys(user: SupplyChainPermissionUser) {
|
||||
if (getSupplyChainRoleDefaultAccess(user)?.view) {
|
||||
return [
|
||||
'dashboard',
|
||||
'businessManagement',
|
||||
'employeeHours',
|
||||
'productArchive',
|
||||
'categoryConfig',
|
||||
'warehouseConfig',
|
||||
'storeAccountManagement',
|
||||
'newProductOverview',
|
||||
'newProduct1688DropshipData',
|
||||
'newProductTerminationFeedback',
|
||||
'newProductUnifiedTable',
|
||||
'newProductSampleProgress',
|
||||
'purchaseSupplierInformation',
|
||||
'purchaseSupplierRecommendedProducts',
|
||||
'purchaseDailyAffairs',
|
||||
'purchasePriceChange',
|
||||
'scriptMonitor',
|
||||
'newProductDropshipIssues',
|
||||
'operationLogs',
|
||||
'productSabc',
|
||||
'productSabcCalculation',
|
||||
'departmentDistribution',
|
||||
'departmentDistributionConfig',
|
||||
] satisfies SupplyChainPermissionTargetKey[]
|
||||
return [...SUPPLY_CHAIN_PERMISSION_TARGET_KEYS]
|
||||
}
|
||||
|
||||
try {
|
||||
const overrides = await this.readOverrides()
|
||||
const subjectKeys = resolveSupplyChainPermissionSubjectKeys(user)
|
||||
const targetKeys: SupplyChainPermissionTargetKey[] = [
|
||||
'dashboard',
|
||||
'businessManagement',
|
||||
'employeeHours',
|
||||
'productArchive',
|
||||
'categoryConfig',
|
||||
'warehouseConfig',
|
||||
'storeAccountManagement',
|
||||
'newProductOverview',
|
||||
'newProduct1688DropshipData',
|
||||
'newProductTerminationFeedback',
|
||||
'newProductUnifiedTable',
|
||||
'newProductSampleProgress',
|
||||
'purchaseSupplierInformation',
|
||||
'purchaseSupplierRecommendedProducts',
|
||||
'purchaseDailyAffairs',
|
||||
'purchasePriceChange',
|
||||
'scriptMonitor',
|
||||
'newProductDropshipIssues',
|
||||
'operationLogs',
|
||||
'productSabc',
|
||||
'productSabcCalculation',
|
||||
'departmentDistribution',
|
||||
'departmentDistributionConfig',
|
||||
]
|
||||
const targetKeys = SUPPLY_CHAIN_PERMISSION_TARGET_KEYS
|
||||
return targetKeys.filter((targetKey) =>
|
||||
resolveSupplyChainPermissionAccess(
|
||||
overrides,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user