task-l
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
import { db } from '@/lib/db';
|
||||
import { PERMISSIONS } from '@/lib/auth/rbac';
|
||||
import { getUserBranches } from '@/features/foundation/branch-scope/service';
|
||||
import { hasBranchScopeAccess, hasProductScopeAccess } from '@/lib/auth/crm-access';
|
||||
import { listApprovalRequests } from '@/features/foundation/approval/server/service';
|
||||
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||
import {
|
||||
@@ -57,6 +58,11 @@ type DashboardContext = {
|
||||
membershipRole: string;
|
||||
businessRole: string;
|
||||
permissions: string[];
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeIds: string[];
|
||||
ownershipScope: string;
|
||||
branchScopeMode: string;
|
||||
productScopeMode: string;
|
||||
};
|
||||
|
||||
type NormalizedFilters = {
|
||||
@@ -73,15 +79,6 @@ type StatusMaps = {
|
||||
quotationStatusById: Map<string, string>;
|
||||
};
|
||||
|
||||
const DASHBOARD_FULL_ACCESS_ROLES = new Set([
|
||||
'marketing',
|
||||
'sales_manager',
|
||||
'department_manager',
|
||||
'top_manager'
|
||||
]);
|
||||
|
||||
const DASHBOARD_OWNED_SCOPE_ROLES = new Set(['sales', 'sales_support']);
|
||||
|
||||
function normalizeDashboardFilters(filters: CrmDashboardFilters): NormalizedFilters {
|
||||
const now = new Date();
|
||||
const monthStart = startOfDay(new Date(now.getFullYear(), now.getMonth(), 1));
|
||||
@@ -128,20 +125,26 @@ function canViewApprovalData(context: DashboardContext) {
|
||||
}
|
||||
|
||||
function hasDashboardFullScope(context: DashboardContext) {
|
||||
return (
|
||||
context.membershipRole === 'admin' || DASHBOARD_FULL_ACCESS_ROLES.has(context.businessRole)
|
||||
);
|
||||
return context.membershipRole === 'admin' || context.ownershipScope !== 'own';
|
||||
}
|
||||
|
||||
function canAccessDashboardEnquiry(
|
||||
row: typeof crmEnquiries.$inferSelect,
|
||||
context: DashboardContext
|
||||
) {
|
||||
if (!hasBranchScopeAccess(context, row.branchId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasProductScopeAccess(context, row.productType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDashboardFullScope(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DASHBOARD_OWNED_SCOPE_ROLES.has(context.businessRole)) {
|
||||
if (context.ownershipScope === 'own') {
|
||||
return row.createdBy === context.userId || row.assignedToUserId === context.userId;
|
||||
}
|
||||
|
||||
@@ -152,11 +155,19 @@ function canAccessDashboardQuotation(
|
||||
row: typeof crmQuotations.$inferSelect,
|
||||
context: DashboardContext
|
||||
) {
|
||||
if (!hasBranchScopeAccess(context, row.branchId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasProductScopeAccess(context, row.quotationType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasDashboardFullScope(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DASHBOARD_OWNED_SCOPE_ROLES.has(context.businessRole)) {
|
||||
if (context.ownershipScope === 'own') {
|
||||
return row.salesmanId === context.userId;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/db/schema';
|
||||
import { db } from '@/lib/db';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
import { hasBranchScopeAccess, hasProductScopeAccess } from '@/lib/auth/crm-access';
|
||||
import { listAuditLogs } from '@/features/foundation/audit-log/service';
|
||||
import { getUserBranches, validateBranchAccess } from '@/features/foundation/branch-scope/service';
|
||||
import { generateNextDocumentCode } from '@/features/foundation/document-sequence/service';
|
||||
@@ -50,13 +51,6 @@ const ENQUIRY_OPTION_CATEGORIES = {
|
||||
} as const;
|
||||
|
||||
const ASSIGNABLE_BUSINESS_ROLES = new Set(['sales_manager', 'sales', 'sales_support']);
|
||||
const SALES_OWNED_SCOPE_ROLES = new Set(['sales', 'sales_support']);
|
||||
const ENQUIRY_FULL_ACCESS_ROLES = new Set([
|
||||
'marketing',
|
||||
'sales_manager',
|
||||
'department_manager',
|
||||
'top_manager'
|
||||
]);
|
||||
const SALES_CREATED_ENQUIRY_ROLES = new Set([
|
||||
'sales',
|
||||
'sales_support',
|
||||
@@ -73,6 +67,11 @@ export interface EnquiryAccessContext {
|
||||
userId: string;
|
||||
membershipRole: string;
|
||||
businessRole: string;
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeIds: string[];
|
||||
ownershipScope: string;
|
||||
branchScopeMode: string;
|
||||
productScopeMode: string;
|
||||
}
|
||||
|
||||
function mapOption(
|
||||
@@ -141,20 +140,26 @@ function mapEnquiryRecord(
|
||||
}
|
||||
|
||||
function hasEnquiryFullAccess(context: EnquiryAccessContext) {
|
||||
return (
|
||||
context.membershipRole === 'admin' || ENQUIRY_FULL_ACCESS_ROLES.has(context.businessRole)
|
||||
);
|
||||
return context.membershipRole === 'admin' || context.ownershipScope !== 'own';
|
||||
}
|
||||
|
||||
function canAccessEnquiryRow(
|
||||
row: typeof crmEnquiries.$inferSelect,
|
||||
context: EnquiryAccessContext
|
||||
) {
|
||||
if (!hasBranchScopeAccess(context, row.branchId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasProductScopeAccess(context, row.productType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasEnquiryFullAccess(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SALES_OWNED_SCOPE_ROLES.has(context.businessRole)) {
|
||||
if (context.ownershipScope === 'own') {
|
||||
return row.createdBy === context.userId || row.assignedToUserId === context.userId;
|
||||
}
|
||||
|
||||
@@ -162,17 +167,23 @@ function canAccessEnquiryRow(
|
||||
}
|
||||
|
||||
function buildAccessScopedFilters(context: EnquiryAccessContext): SQL[] {
|
||||
if (hasEnquiryFullAccess(context)) {
|
||||
return [];
|
||||
const filters: SQL[] = [];
|
||||
|
||||
if (context.branchScopeMode === 'assigned' && context.branchScopeIds.length > 0) {
|
||||
filters.push(inArray(crmEnquiries.branchId, context.branchScopeIds));
|
||||
}
|
||||
|
||||
if (SALES_OWNED_SCOPE_ROLES.has(context.businessRole)) {
|
||||
return [
|
||||
if (context.productScopeMode === 'assigned' && context.productTypeScopeIds.length > 0) {
|
||||
filters.push(inArray(crmEnquiries.productType, context.productTypeScopeIds));
|
||||
}
|
||||
|
||||
if (!hasEnquiryFullAccess(context) && context.ownershipScope === 'own') {
|
||||
filters.push(
|
||||
or(eq(crmEnquiries.createdBy, context.userId), eq(crmEnquiries.assignedToUserId, context.userId))!
|
||||
];
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
return filters;
|
||||
}
|
||||
|
||||
function mapFollowupRecord(row: typeof crmEnquiryFollowups.$inferSelect): EnquiryFollowupRecord {
|
||||
@@ -437,7 +448,11 @@ async function assertFollowupBelongsToEnquiry(
|
||||
return followup;
|
||||
}
|
||||
|
||||
async function validateEnquiryPayload(organizationId: string, payload: EnquiryMutationPayload) {
|
||||
async function validateEnquiryPayload(
|
||||
organizationId: string,
|
||||
payload: EnquiryMutationPayload,
|
||||
accessContext?: EnquiryAccessContext
|
||||
) {
|
||||
await assertCustomerBelongsToOrganization(payload.customerId, organizationId);
|
||||
|
||||
if (payload.contactId) {
|
||||
@@ -465,6 +480,14 @@ async function validateEnquiryPayload(organizationId: string, payload: EnquiryMu
|
||||
await validateBranchAccess(payload.branchId);
|
||||
}
|
||||
|
||||
if (accessContext && !hasBranchScopeAccess(accessContext, payload.branchId)) {
|
||||
throw new AuthError('Forbidden branch scope', 403);
|
||||
}
|
||||
|
||||
if (accessContext && !hasProductScopeAccess(accessContext, payload.productType)) {
|
||||
throw new AuthError('Forbidden product scope', 403);
|
||||
}
|
||||
|
||||
for (const projectParty of payload.projectParties ?? []) {
|
||||
await assertCustomerBelongsToOrganization(projectParty.customerId, organizationId);
|
||||
await assertMasterOptionValue(
|
||||
@@ -996,13 +1019,13 @@ export async function getEnquiryActivity(
|
||||
export async function createEnquiry(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
businessRole: string,
|
||||
accessContext: EnquiryAccessContext,
|
||||
payload: EnquiryMutationPayload
|
||||
) {
|
||||
await validateEnquiryPayload(organizationId, payload);
|
||||
await validateEnquiryPayload(organizationId, payload, accessContext);
|
||||
const pipelineStage = await resolvePipelineStageForCreate(
|
||||
organizationId,
|
||||
businessRole,
|
||||
accessContext.businessRole,
|
||||
payload.status
|
||||
);
|
||||
|
||||
@@ -1064,7 +1087,7 @@ export async function updateEnquiry(
|
||||
payload: EnquiryMutationPayload,
|
||||
accessContext?: EnquiryAccessContext
|
||||
) {
|
||||
await validateEnquiryPayload(organizationId, payload);
|
||||
await validateEnquiryPayload(organizationId, payload, accessContext);
|
||||
const current = await assertEnquiryBelongsToOrganization(id, organizationId, accessContext);
|
||||
const pipelineStage = await resolvePipelineStageForUpdate(organizationId, current, payload);
|
||||
|
||||
@@ -1167,9 +1190,10 @@ async function updateEnquiryAssignment(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
payload: EnquiryAssignmentMutationPayload,
|
||||
mode: 'assign' | 'reassign'
|
||||
mode: 'assign' | 'reassign',
|
||||
accessContext?: EnquiryAccessContext
|
||||
) {
|
||||
const enquiry = await assertEnquiryBelongsToOrganization(id, organizationId);
|
||||
const enquiry = await assertEnquiryBelongsToOrganization(id, organizationId, accessContext);
|
||||
|
||||
if (mode === 'assign' && enquiry.assignedToUserId) {
|
||||
throw new AuthError('Enquiry is already assigned. Use reassign instead.', 400);
|
||||
@@ -1202,18 +1226,20 @@ export async function assignEnquiryToUser(
|
||||
id: string,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
payload: EnquiryAssignmentMutationPayload
|
||||
payload: EnquiryAssignmentMutationPayload,
|
||||
accessContext?: EnquiryAccessContext
|
||||
) {
|
||||
return updateEnquiryAssignment(id, organizationId, userId, payload, 'assign');
|
||||
return updateEnquiryAssignment(id, organizationId, userId, payload, 'assign', accessContext);
|
||||
}
|
||||
|
||||
export async function reassignEnquiryToUser(
|
||||
id: string,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
payload: EnquiryAssignmentMutationPayload
|
||||
payload: EnquiryAssignmentMutationPayload,
|
||||
accessContext?: EnquiryAccessContext
|
||||
) {
|
||||
return updateEnquiryAssignment(id, organizationId, userId, payload, 'reassign');
|
||||
return updateEnquiryAssignment(id, organizationId, userId, payload, 'reassign', accessContext);
|
||||
}
|
||||
|
||||
export async function listEnquiryFollowups(
|
||||
|
||||
Reference in New Issue
Block a user