feat(intake): allow designers to delete their own product records
product intake list delete button now also resolves for designers (DESIGN business role or design department paths), restricted to records they created; admins keep full delete scope; server DELETE route and client row button share the same record-level predicate; delete stays soft with operation logs.
This commit is contained in:
parent
74ac36f34d
commit
925868fb65
@ -1,5 +1,5 @@
|
||||
import {
|
||||
canDeleteNonIntegratedProductByIntakePermission,
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission,
|
||||
isIntakeNonIntegratedDesignDepartmentUser,
|
||||
type IntakeNonIntegratedPermissionUser,
|
||||
} from "@/lib/non-integrated-permissions";
|
||||
@ -8,8 +8,11 @@ type IntakeProductPermissionUser = IntakeNonIntegratedPermissionUser & {
|
||||
role: string;
|
||||
};
|
||||
|
||||
export function canDeleteIntakeProducts(role: string): boolean {
|
||||
return canDeleteNonIntegratedProductByIntakePermission({ role });
|
||||
export function canDeleteIntakeProductRecord(
|
||||
user: IntakeProductPermissionUser,
|
||||
product: { createdBy?: string | null },
|
||||
): boolean {
|
||||
return canDeleteNonIntegratedProductRecordByIntakePermission(user, product);
|
||||
}
|
||||
|
||||
export function canManageSpuRuleLibrary(role: string): boolean {
|
||||
|
||||
@ -133,7 +133,7 @@ export function buildIntakePrimaryDomainProps(params: {
|
||||
nonIntegratedProducts: NonIntegratedProductRecord[];
|
||||
productDevelopmentProgressProducts: NonIntegratedProductRecord[];
|
||||
initialNonIntegratedPageInfo: IntakeClientProps["initialNonIntegratedPageInfo"];
|
||||
canDeleteNonIntegratedProducts: boolean;
|
||||
canDeleteIntakeProductByRecord: (product: NonIntegratedProductRecord) => boolean;
|
||||
canCreateNonIntegratedProducts: boolean;
|
||||
canUseDesignControlledProductData: boolean;
|
||||
canViewProductDevelopmentList: boolean;
|
||||
@ -181,7 +181,7 @@ export function buildIntakePrimaryDomainProps(params: {
|
||||
productDevelopmentProgressProducts:
|
||||
params.productDevelopmentProgressProducts,
|
||||
initialNonIntegratedPageInfo: params.initialNonIntegratedPageInfo,
|
||||
canDeleteProducts: params.canDeleteNonIntegratedProducts,
|
||||
canDeleteProducts: params.canDeleteIntakeProductByRecord,
|
||||
canCreateProducts: params.canCreateNonIntegratedProducts,
|
||||
canUseDesignControlledProductData:
|
||||
params.canUseDesignControlledProductData,
|
||||
|
||||
@ -227,7 +227,7 @@ export function NonIntegratedPendingDetail({
|
||||
}: {
|
||||
initialProducts: NonIntegratedProductRecord[];
|
||||
initialPageInfo?: NonIntegratedProductPageInfo | null;
|
||||
canDeleteProducts: boolean;
|
||||
canDeleteProducts: (product: NonIntegratedProductRecord) => boolean;
|
||||
currentUser: NonIntegratedListPermissionUser;
|
||||
productDevelopmentKind?: ProductDevelopmentKind;
|
||||
fixedProductKind?: NonIntegratedProductKindFilter;
|
||||
@ -1702,7 +1702,7 @@ export function NonIntegratedPendingDetail({
|
||||
<span className="sr-only">复制</span>
|
||||
</Button>
|
||||
) : null}
|
||||
{canDeleteProducts && canEditProduct ? (
|
||||
{canDeleteProducts(product) && canEditProduct ? (
|
||||
<Button
|
||||
variant="plain"
|
||||
size="small"
|
||||
|
||||
@ -33,7 +33,7 @@ type ProductDevelopmentDomainProps = {
|
||||
stock?: NonIntegratedProductPageInfo;
|
||||
series?: NonIntegratedProductPageInfo;
|
||||
};
|
||||
canDeleteProducts: boolean;
|
||||
canDeleteProducts: (product: NonIntegratedProductRecord) => boolean;
|
||||
canCreateProducts: boolean;
|
||||
canUseDesignControlledProductData: boolean;
|
||||
canViewProductDevelopmentList: boolean;
|
||||
|
||||
@ -26,7 +26,7 @@ import { buildIntakeClientDerivedData } from "@/app/intake/intakeClientDerivedDa
|
||||
import { canCreateNonIntegratedProductFromIntake } from "@/lib/non-integrated-permissions";
|
||||
import {
|
||||
canAccessDesignControlledProductData,
|
||||
canDeleteIntakeProducts,
|
||||
canDeleteIntakeProductRecord,
|
||||
canManageSpuRuleLibrary,
|
||||
} from "@/app/intake/intakeClientPermissions";
|
||||
import {
|
||||
@ -39,9 +39,9 @@ import {
|
||||
export function useIntakeClientViewModel(
|
||||
props: IntakeClientProps,
|
||||
): IntakeClientWorkspaceViewProps {
|
||||
const canDeleteNonIntegratedProducts = canDeleteIntakeProducts(
|
||||
props.user.role,
|
||||
);
|
||||
const canDeleteIntakeProductByRecord = (
|
||||
product: { createdBy?: string | null },
|
||||
) => canDeleteIntakeProductRecord(props.user, product);
|
||||
const canUseDesignControlledProductData =
|
||||
canAccessDesignControlledProductData(props.user);
|
||||
const canCreateNonIntegratedProducts =
|
||||
@ -231,7 +231,7 @@ export function useIntakeClientViewModel(
|
||||
nonIntegratedProducts,
|
||||
productDevelopmentProgressProducts,
|
||||
initialNonIntegratedPageInfo: props.initialNonIntegratedPageInfo,
|
||||
canDeleteNonIntegratedProducts,
|
||||
canDeleteIntakeProductByRecord,
|
||||
canCreateNonIntegratedProducts,
|
||||
canUseDesignControlledProductData,
|
||||
canViewProductDevelopmentList,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
canCreateNonIntegratedProductFromIntake,
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission,
|
||||
canDownloadNonIntegratedWorkspace,
|
||||
canEditAnyNonIntegratedWorkspace,
|
||||
canEditNonIntegratedProductIdentity,
|
||||
@ -478,3 +479,74 @@ describe('non-integrated intake permissions', () => {
|
||||
expect(canEditNonIntegratedWorkspace(baseProduct, purchasingUser, '工厂质检')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('non-integrated intake product delete permission', () => {
|
||||
const designer = {
|
||||
id: 'designer-id',
|
||||
name: '希文',
|
||||
role: 'USER',
|
||||
businessRoles: [] as string[],
|
||||
departmentPaths: ['江苏简尚品牌管理有限公司 > 产品研发部 > 基本盘 > 桌类组'],
|
||||
}
|
||||
const ownProduct = { createdBy: 'designer-id' }
|
||||
const othersProduct = { createdBy: 'creator-id' }
|
||||
|
||||
it('keeps full delete access for admins', () => {
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(
|
||||
{ id: 'admin-1', name: 'AdminA', role: 'SUPER_ADMIN' },
|
||||
othersProduct,
|
||||
),
|
||||
).toBe(true)
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(
|
||||
{ id: 'admin-2', name: 'AdminB', role: 'ADMIN' },
|
||||
othersProduct,
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('lets designers delete only products they created', () => {
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(designer, ownProduct),
|
||||
).toBe(true)
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(designer, othersProduct),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts explicitly assigned DESIGN business role as designer', () => {
|
||||
const assignedDesigner = {
|
||||
id: 'designer-2',
|
||||
name: 'DesignerB',
|
||||
role: 'USER',
|
||||
businessRoles: ['DESIGN'],
|
||||
}
|
||||
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(assignedDesigner, {
|
||||
createdBy: 'designer-2',
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(assignedDesigner, {
|
||||
createdBy: 'someone-else',
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps other roles without delete access', () => {
|
||||
const purchasingUser = {
|
||||
id: 'purchase-role-id',
|
||||
name: '采购B',
|
||||
role: 'USER',
|
||||
businessRoles: ['PURCHASING'],
|
||||
}
|
||||
|
||||
expect(
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission(purchasingUser, {
|
||||
createdBy: 'purchase-role-id',
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -674,8 +674,17 @@ export function canPushNonIntegratedProductPricingToJushuitan(
|
||||
)
|
||||
}
|
||||
|
||||
export function canDeleteNonIntegratedProductByIntakePermission(
|
||||
export function canDeleteNonIntegratedProductRecordByIntakePermission(
|
||||
user: IntakeNonIntegratedPermissionUser,
|
||||
product: { createdBy?: string | null },
|
||||
) {
|
||||
return isIntakeNonIntegratedDeleteAdmin(user)
|
||||
if (isIntakeNonIntegratedDeleteAdmin(user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
isIntakeNonIntegratedDesignDepartmentUser(user) &&
|
||||
Boolean(user.id) &&
|
||||
product.createdBy === user.id
|
||||
)
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@ import type { AuthUser } from '@/server/core/types'
|
||||
import {
|
||||
canUploadNonIntegratedDetachedAsset,
|
||||
canCreateNonIntegratedProductFromIntake,
|
||||
canDeleteNonIntegratedProductByIntakePermission,
|
||||
canDeleteNonIntegratedProductRecordByIntakePermission,
|
||||
isIntakeNonIntegratedDesignDepartmentUser,
|
||||
canEditAnyNonIntegratedWorkspace,
|
||||
canEditNonIntegratedWorkspace,
|
||||
canDownloadNonIntegratedWorkspace,
|
||||
@ -686,11 +687,16 @@ class IntakeNonIntegratedScopeService {
|
||||
}
|
||||
|
||||
assertCanDeleteRecord(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): void {
|
||||
if (canDeleteNonIntegratedProductByIntakePermission(user)) {
|
||||
if (canDeleteNonIntegratedProductRecordByIntakePermission(user, record)) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限删除这条非集成产品记录')
|
||||
throw new AppError(
|
||||
ErrorCode.AUTH_PERMISSION_DENIED,
|
||||
isIntakeNonIntegratedDesignDepartmentUser(user)
|
||||
? '设计师仅可删除自己创建的产品'
|
||||
: '没有权限删除这条非集成产品记录',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user