task-l.1
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { mutationOptions } from '@tanstack/react-query';
|
||||
import { getQueryClient } from '@/lib/query-client';
|
||||
import {
|
||||
createCrmUserRoleAssignment,
|
||||
deleteCrmUserRoleAssignment,
|
||||
updateCrmUserRoleAssignment
|
||||
} from './service';
|
||||
import { crmUserRoleAssignmentQueryKeys } from './queries';
|
||||
|
||||
export const createCrmUserRoleAssignmentMutation = mutationOptions({
|
||||
mutationFn: createCrmUserRoleAssignment,
|
||||
onSettled: async () => {
|
||||
await getQueryClient().invalidateQueries({
|
||||
queryKey: crmUserRoleAssignmentQueryKeys.all
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const updateCrmUserRoleAssignmentMutation = mutationOptions({
|
||||
mutationFn: ({
|
||||
id,
|
||||
payload
|
||||
}: {
|
||||
id: string;
|
||||
payload: Parameters<typeof updateCrmUserRoleAssignment>[1];
|
||||
}) => updateCrmUserRoleAssignment(id, payload),
|
||||
onSettled: async () => {
|
||||
await getQueryClient().invalidateQueries({
|
||||
queryKey: crmUserRoleAssignmentQueryKeys.all
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteCrmUserRoleAssignmentMutation = mutationOptions({
|
||||
mutationFn: deleteCrmUserRoleAssignment,
|
||||
onSettled: async () => {
|
||||
await getQueryClient().invalidateQueries({
|
||||
queryKey: crmUserRoleAssignmentQueryKeys.all
|
||||
});
|
||||
}
|
||||
});
|
||||
13
src/features/foundation/crm-role-assignments/api/queries.ts
Normal file
13
src/features/foundation/crm-role-assignments/api/queries.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { getCrmUserRoleAssignments } from './service';
|
||||
|
||||
export const crmUserRoleAssignmentQueryKeys = {
|
||||
all: ['crm-user-role-assignments'] as const
|
||||
};
|
||||
|
||||
export function crmUserRoleAssignmentsQueryOptions() {
|
||||
return queryOptions({
|
||||
queryKey: crmUserRoleAssignmentQueryKeys.all,
|
||||
queryFn: getCrmUserRoleAssignments
|
||||
});
|
||||
}
|
||||
42
src/features/foundation/crm-role-assignments/api/service.ts
Normal file
42
src/features/foundation/crm-role-assignments/api/service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { apiClient } from '@/lib/api-client';
|
||||
import type {
|
||||
CrmUserRoleAssignmentMutationPayload,
|
||||
CrmUserRoleAssignmentsResponse,
|
||||
CrmUserRoleAssignmentRecord,
|
||||
UpdateCrmUserRoleAssignmentPayload
|
||||
} from './types';
|
||||
|
||||
export async function getCrmUserRoleAssignments() {
|
||||
return apiClient<CrmUserRoleAssignmentsResponse>('/crm/settings/user-role-assignments');
|
||||
}
|
||||
|
||||
export async function createCrmUserRoleAssignment(
|
||||
payload: CrmUserRoleAssignmentMutationPayload
|
||||
) {
|
||||
return apiClient<{ success: boolean; assignment: CrmUserRoleAssignmentRecord }>(
|
||||
'/crm/settings/user-role-assignments',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateCrmUserRoleAssignment(
|
||||
id: string,
|
||||
payload: UpdateCrmUserRoleAssignmentPayload
|
||||
) {
|
||||
return apiClient<{ success: boolean; assignment: CrmUserRoleAssignmentRecord }>(
|
||||
`/crm/settings/user-role-assignments/${id}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteCrmUserRoleAssignment(id: string) {
|
||||
return apiClient<{ success: boolean }>(`/crm/settings/user-role-assignments/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
63
src/features/foundation/crm-role-assignments/api/types.ts
Normal file
63
src/features/foundation/crm-role-assignments/api/types.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { CrmRoleAssignmentScopeMode } from '@/lib/auth/rbac';
|
||||
|
||||
export interface CrmUserRoleAssignmentRecord {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
roleProfileId: string;
|
||||
roleProfileCode: string;
|
||||
roleProfileName: string;
|
||||
branchScopeMode: CrmRoleAssignmentScopeMode;
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeMode: CrmRoleAssignmentScopeMode;
|
||||
productTypeScopeIds: string[];
|
||||
isPrimary: boolean;
|
||||
isActive: boolean;
|
||||
assignedBy: string;
|
||||
assignedAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
export interface CrmRoleAssignmentReferenceOption {
|
||||
id: string;
|
||||
code?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface CrmUserRoleAssignmentsResponse {
|
||||
success: boolean;
|
||||
time: string;
|
||||
message: string;
|
||||
assignments: CrmUserRoleAssignmentRecord[];
|
||||
scopeModes: CrmRoleAssignmentScopeMode[];
|
||||
roleProfiles: Array<{ id: string; code: string; name: string }>;
|
||||
users: Array<{ id: string; name: string; email: string }>;
|
||||
branches: Array<{ id: string; code: string; label: string }>;
|
||||
productTypes: Array<{ id: string; code: string; label: string }>;
|
||||
}
|
||||
|
||||
export interface CrmUserRoleAssignmentMutationPayload {
|
||||
userId: string;
|
||||
roleProfileId: string;
|
||||
branchScopeMode: CrmRoleAssignmentScopeMode;
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeMode: CrmRoleAssignmentScopeMode;
|
||||
productTypeScopeIds: string[];
|
||||
isPrimary: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateCrmUserRoleAssignmentPayload {
|
||||
branchScopeMode: CrmRoleAssignmentScopeMode;
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeMode: CrmRoleAssignmentScopeMode;
|
||||
productTypeScopeIds: string[];
|
||||
isPrimary: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
createCrmUserRoleAssignmentMutation,
|
||||
deleteCrmUserRoleAssignmentMutation,
|
||||
updateCrmUserRoleAssignmentMutation
|
||||
} from '../api/mutations';
|
||||
import { crmUserRoleAssignmentsQueryOptions } from '../api/queries';
|
||||
import type {
|
||||
CrmUserRoleAssignmentMutationPayload,
|
||||
CrmUserRoleAssignmentRecord,
|
||||
UpdateCrmUserRoleAssignmentPayload
|
||||
} from '../api/types';
|
||||
|
||||
type AssignmentEditorState = {
|
||||
branchScopeMode: CrmUserRoleAssignmentMutationPayload['branchScopeMode'];
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeMode: CrmUserRoleAssignmentMutationPayload['productTypeScopeMode'];
|
||||
productTypeScopeIds: string[];
|
||||
isPrimary: boolean;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
const emptyCreateState: CrmUserRoleAssignmentMutationPayload = {
|
||||
userId: '',
|
||||
roleProfileId: '',
|
||||
branchScopeMode: 'inherit',
|
||||
branchScopeIds: [],
|
||||
productTypeScopeMode: 'inherit',
|
||||
productTypeScopeIds: [],
|
||||
isPrimary: false,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
function ScopeSelector(props: {
|
||||
label: string;
|
||||
mode: CrmUserRoleAssignmentMutationPayload['branchScopeMode'];
|
||||
ids: string[];
|
||||
options: Array<{ id: string; label: string; code: string }>;
|
||||
onModeChange: (value: CrmUserRoleAssignmentMutationPayload['branchScopeMode']) => void;
|
||||
onIdsChange: (value: string[]) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<div className='space-y-2'>
|
||||
<Label>{props.label}</Label>
|
||||
<Select value={props.mode} onValueChange={props.onModeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='inherit'>Inherit role profile</SelectItem>
|
||||
<SelectItem value='all'>All</SelectItem>
|
||||
<SelectItem value='selected'>Selected</SelectItem>
|
||||
<SelectItem value='none'>None</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{props.mode === 'selected' ? (
|
||||
<div className='grid gap-2 rounded-md border p-3 md:grid-cols-2'>
|
||||
{props.options.map((option) => {
|
||||
const checked = props.ids.includes(option.id);
|
||||
|
||||
return (
|
||||
<label key={option.id} className='flex items-start gap-3 rounded-md border p-3'>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={(value) => {
|
||||
props.onIdsChange(
|
||||
value === true
|
||||
? [...new Set([...props.ids, option.id])]
|
||||
: props.ids.filter((item) => item !== option.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className='font-medium'>{option.label}</div>
|
||||
<div className='text-muted-foreground text-xs'>{option.code}</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserRoleAssignmentsPage() {
|
||||
const { data } = useSuspenseQuery(crmUserRoleAssignmentsQueryOptions());
|
||||
const [selectedId, setSelectedId] = useState<string | null>(data.assignments[0]?.id ?? null);
|
||||
const [createState, setCreateState] =
|
||||
useState<CrmUserRoleAssignmentMutationPayload>(emptyCreateState);
|
||||
const [editorState, setEditorState] = useState<AssignmentEditorState | null>(null);
|
||||
const selectedAssignment = useMemo(
|
||||
() => data.assignments.find((assignment) => assignment.id === selectedId) ?? null,
|
||||
[data.assignments, selectedId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAssignment) {
|
||||
setEditorState(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setEditorState({
|
||||
branchScopeMode: selectedAssignment.branchScopeMode,
|
||||
branchScopeIds: selectedAssignment.branchScopeIds,
|
||||
productTypeScopeMode: selectedAssignment.productTypeScopeMode,
|
||||
productTypeScopeIds: selectedAssignment.productTypeScopeIds,
|
||||
isPrimary: selectedAssignment.isPrimary,
|
||||
isActive: selectedAssignment.isActive
|
||||
});
|
||||
}, [selectedAssignment]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
...createCrmUserRoleAssignmentMutation,
|
||||
onSuccess: () => {
|
||||
toast.success('CRM role assignment created successfully');
|
||||
setCreateState(emptyCreateState);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : 'Unable to create CRM role assignment'
|
||||
)
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
...updateCrmUserRoleAssignmentMutation,
|
||||
onSuccess: () => toast.success('CRM role assignment updated successfully'),
|
||||
onError: (error) =>
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : 'Unable to update CRM role assignment'
|
||||
)
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
...deleteCrmUserRoleAssignmentMutation,
|
||||
onSuccess: () => {
|
||||
toast.success('CRM role assignment deleted successfully');
|
||||
setSelectedId(null);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : 'Unable to delete CRM role assignment'
|
||||
)
|
||||
});
|
||||
|
||||
async function handleCreate() {
|
||||
if (!createState.userId || !createState.roleProfileId) {
|
||||
toast.error('User and role profile are required');
|
||||
return;
|
||||
}
|
||||
|
||||
await createMutation.mutateAsync(createState);
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
if (!selectedAssignment || !editorState) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateMutation.mutateAsync({
|
||||
id: selectedAssignment.id,
|
||||
payload: editorState satisfies UpdateCrmUserRoleAssignmentPayload
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-6'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Assign CRM Role</CardTitle>
|
||||
<CardDescription>
|
||||
One user can hold multiple CRM roles in the same organization. Primary role is for
|
||||
display; effective access unions across active assignments.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<div className='space-y-2'>
|
||||
<Label>User</Label>
|
||||
<Select
|
||||
value={createState.userId}
|
||||
onValueChange={(value) => setCreateState({ ...createState, userId: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select user' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{data.users.map((user) => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.name} ({user.email})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Label>Role Profile</Label>
|
||||
<Select
|
||||
value={createState.roleProfileId}
|
||||
onValueChange={(value) =>
|
||||
setCreateState({ ...createState, roleProfileId: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='Select role profile' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{data.roleProfiles.map((role) => (
|
||||
<SelectItem key={role.id} value={role.id}>
|
||||
{role.name} ({role.code})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-2'>
|
||||
<ScopeSelector
|
||||
label='Branch Scope'
|
||||
mode={createState.branchScopeMode}
|
||||
ids={createState.branchScopeIds}
|
||||
options={data.branches}
|
||||
onModeChange={(value) =>
|
||||
setCreateState({
|
||||
...createState,
|
||||
branchScopeMode: value,
|
||||
branchScopeIds: value === 'selected' ? createState.branchScopeIds : []
|
||||
})
|
||||
}
|
||||
onIdsChange={(value) => setCreateState({ ...createState, branchScopeIds: value })}
|
||||
/>
|
||||
<ScopeSelector
|
||||
label='Product Type Scope'
|
||||
mode={createState.productTypeScopeMode}
|
||||
ids={createState.productTypeScopeIds}
|
||||
options={data.productTypes}
|
||||
onModeChange={(value) =>
|
||||
setCreateState({
|
||||
...createState,
|
||||
productTypeScopeMode: value,
|
||||
productTypeScopeIds: value === 'selected' ? createState.productTypeScopeIds : []
|
||||
})
|
||||
}
|
||||
onIdsChange={(value) =>
|
||||
setCreateState({ ...createState, productTypeScopeIds: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap items-center gap-6'>
|
||||
<label className='flex items-center gap-3'>
|
||||
<Checkbox
|
||||
checked={createState.isPrimary}
|
||||
onCheckedChange={(value) =>
|
||||
setCreateState({ ...createState, isPrimary: value === true })
|
||||
}
|
||||
/>
|
||||
<span className='text-sm font-medium'>Primary role</span>
|
||||
</label>
|
||||
<label className='flex items-center gap-3'>
|
||||
<Checkbox
|
||||
checked={createState.isActive}
|
||||
onCheckedChange={(value) =>
|
||||
setCreateState({ ...createState, isActive: value === true })
|
||||
}
|
||||
/>
|
||||
<span className='text-sm font-medium'>Active</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => void handleCreate()} isLoading={createMutation.isPending}>
|
||||
Assign Role
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Existing Assignments</CardTitle>
|
||||
<CardDescription>
|
||||
Active and historical CRM role assignments in this organization.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-3'>
|
||||
{data.assignments.map((assignment) => (
|
||||
<button
|
||||
key={assignment.id}
|
||||
type='button'
|
||||
onClick={() => setSelectedId(assignment.id)}
|
||||
className={`w-full rounded-lg border p-3 text-left transition ${
|
||||
assignment.id === selectedId
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'hover:bg-muted/40'
|
||||
}`}
|
||||
>
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<div className='font-medium'>{assignment.userName}</div>
|
||||
<div className='flex gap-2'>
|
||||
{assignment.isPrimary ? <Badge>Primary</Badge> : null}
|
||||
{!assignment.isActive ? <Badge variant='outline'>Inactive</Badge> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-muted-foreground mt-1 text-xs'>{assignment.userEmail}</div>
|
||||
<div className='mt-2 text-sm'>
|
||||
{assignment.roleProfileName} ({assignment.roleProfileCode})
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Assignment Detail</CardTitle>
|
||||
<CardDescription>
|
||||
Update scope, activation, and primary-role display for the selected CRM assignment.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
{!selectedAssignment || !editorState ? (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
|
||||
Select an assignment to edit.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<div className='rounded-lg border p-4'>
|
||||
<div className='text-muted-foreground text-xs'>User</div>
|
||||
<div className='font-medium'>{selectedAssignment.userName}</div>
|
||||
<div className='text-muted-foreground text-sm'>{selectedAssignment.userEmail}</div>
|
||||
</div>
|
||||
<div className='rounded-lg border p-4'>
|
||||
<div className='text-muted-foreground text-xs'>Role Profile</div>
|
||||
<div className='font-medium'>{selectedAssignment.roleProfileName}</div>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
{selectedAssignment.roleProfileCode}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-2'>
|
||||
<ScopeSelector
|
||||
label='Branch Scope'
|
||||
mode={editorState.branchScopeMode}
|
||||
ids={editorState.branchScopeIds}
|
||||
options={data.branches}
|
||||
onModeChange={(value) =>
|
||||
setEditorState({
|
||||
...editorState,
|
||||
branchScopeMode: value,
|
||||
branchScopeIds: value === 'selected' ? editorState.branchScopeIds : []
|
||||
})
|
||||
}
|
||||
onIdsChange={(value) =>
|
||||
setEditorState({ ...editorState, branchScopeIds: value })
|
||||
}
|
||||
/>
|
||||
<ScopeSelector
|
||||
label='Product Type Scope'
|
||||
mode={editorState.productTypeScopeMode}
|
||||
ids={editorState.productTypeScopeIds}
|
||||
options={data.productTypes}
|
||||
onModeChange={(value) =>
|
||||
setEditorState({
|
||||
...editorState,
|
||||
productTypeScopeMode: value,
|
||||
productTypeScopeIds:
|
||||
value === 'selected' ? editorState.productTypeScopeIds : []
|
||||
})
|
||||
}
|
||||
onIdsChange={(value) =>
|
||||
setEditorState({ ...editorState, productTypeScopeIds: value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap items-center gap-6'>
|
||||
<label className='flex items-center gap-3'>
|
||||
<Checkbox
|
||||
checked={editorState.isPrimary}
|
||||
onCheckedChange={(value) =>
|
||||
setEditorState({ ...editorState, isPrimary: value === true })
|
||||
}
|
||||
/>
|
||||
<span className='text-sm font-medium'>Primary role</span>
|
||||
</label>
|
||||
<label className='flex items-center gap-3'>
|
||||
<Checkbox
|
||||
checked={editorState.isActive}
|
||||
onCheckedChange={(value) =>
|
||||
setEditorState({ ...editorState, isActive: value === true })
|
||||
}
|
||||
/>
|
||||
<span className='text-sm font-medium'>Active</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap gap-3'>
|
||||
<Button onClick={() => void handleUpdate()} isLoading={updateMutation.isPending}>
|
||||
Save Assignment
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={() => void deleteMutation.mutateAsync(selectedAssignment.id)}
|
||||
isLoading={deleteMutation.isPending}
|
||||
>
|
||||
Remove Assignment
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
625
src/features/foundation/crm-role-assignments/server/service.ts
Normal file
625
src/features/foundation/crm-role-assignments/server/service.ts
Normal file
@@ -0,0 +1,625 @@
|
||||
import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
|
||||
import {
|
||||
crmRoleProfiles,
|
||||
crmUserRoleAssignments,
|
||||
memberships,
|
||||
msOptions,
|
||||
users
|
||||
} from '@/db/schema';
|
||||
import { auditAction } from '@/features/foundation/audit-log/service';
|
||||
import { getUserBranches } from '@/features/foundation/branch-scope/service';
|
||||
import { getActiveOptionsByCategory } from '@/features/foundation/master-options/service';
|
||||
import { ensureCrmRoleProfiles } from '@/features/foundation/role-management/server/service';
|
||||
import {
|
||||
ROLE_ASSIGNMENT_SCOPE_MODES,
|
||||
isRoleAssignmentScopeMode,
|
||||
type CrmRoleAssignmentScopeMode
|
||||
} from '@/lib/auth/rbac';
|
||||
import { db } from '@/lib/db';
|
||||
import { AuthError } from '@/lib/auth/session';
|
||||
|
||||
function normalizeStringArray(value: unknown) {
|
||||
return Array.isArray(value)
|
||||
? [
|
||||
...new Set(
|
||||
value.filter(
|
||||
(item): item is string => typeof item === 'string' && item.trim().length > 0
|
||||
)
|
||||
)
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
function isSelectedScopeInvalid(mode: CrmRoleAssignmentScopeMode, values: string[]) {
|
||||
return mode === 'selected' && values.length === 0;
|
||||
}
|
||||
|
||||
function mapAssignmentRow(
|
||||
row: typeof crmUserRoleAssignments.$inferSelect,
|
||||
options: {
|
||||
roleProfileCode: string;
|
||||
roleProfileName: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
}
|
||||
) {
|
||||
return {
|
||||
id: row.id,
|
||||
organizationId: row.organizationId,
|
||||
userId: row.userId,
|
||||
userName: options.userName,
|
||||
userEmail: options.userEmail,
|
||||
roleProfileId: row.roleProfileId,
|
||||
roleProfileCode: options.roleProfileCode,
|
||||
roleProfileName: options.roleProfileName,
|
||||
branchScopeMode: isRoleAssignmentScopeMode(row.branchScopeMode)
|
||||
? row.branchScopeMode
|
||||
: 'inherit',
|
||||
branchScopeIds: normalizeStringArray(row.branchScopeIds),
|
||||
productTypeScopeMode: isRoleAssignmentScopeMode(row.productTypeScopeMode)
|
||||
? row.productTypeScopeMode
|
||||
: 'inherit',
|
||||
productTypeScopeIds: normalizeStringArray(row.productTypeScopeIds),
|
||||
isPrimary: row.isPrimary,
|
||||
isActive: row.isActive,
|
||||
assignedBy: row.assignedBy,
|
||||
assignedAt: row.assignedAt.toISOString(),
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
deletedAt: row.deletedAt?.toISOString() ?? null
|
||||
};
|
||||
}
|
||||
|
||||
async function ensurePrimaryAssignmentForUser(organizationId: string, userId: string) {
|
||||
const activeAssignments = await db
|
||||
.select({
|
||||
id: crmUserRoleAssignments.id,
|
||||
isPrimary: crmUserRoleAssignments.isPrimary
|
||||
})
|
||||
.from(crmUserRoleAssignments)
|
||||
.where(
|
||||
and(
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId),
|
||||
eq(crmUserRoleAssignments.userId, userId),
|
||||
eq(crmUserRoleAssignments.isActive, true),
|
||||
isNull(crmUserRoleAssignments.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(crmUserRoleAssignments.assignedAt));
|
||||
|
||||
if (!activeAssignments.length || activeAssignments.some((item) => item.isPrimary)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
isPrimary: true,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(crmUserRoleAssignments.id, activeAssignments[0]!.id));
|
||||
}
|
||||
|
||||
async function setPrimaryAssignment(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
assignmentId: string,
|
||||
isPrimary: boolean
|
||||
) {
|
||||
if (!isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
isPrimary: false,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId),
|
||||
eq(crmUserRoleAssignments.userId, userId),
|
||||
isNull(crmUserRoleAssignments.deletedAt)
|
||||
)
|
||||
);
|
||||
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
isPrimary: true,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(crmUserRoleAssignments.id, assignmentId));
|
||||
}
|
||||
|
||||
export async function ensureCrmRoleAssignmentsBackfillForMembership(
|
||||
organizationId: string,
|
||||
membership: typeof memberships.$inferSelect,
|
||||
actorUserId: string
|
||||
) {
|
||||
await ensureCrmRoleProfiles(organizationId, actorUserId);
|
||||
|
||||
const [roleProfile] = await db
|
||||
.select({
|
||||
id: crmRoleProfiles.id
|
||||
})
|
||||
.from(crmRoleProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(crmRoleProfiles.organizationId, organizationId),
|
||||
eq(crmRoleProfiles.code, membership.businessRole),
|
||||
isNull(crmRoleProfiles.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!roleProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await db.query.crmUserRoleAssignments.findFirst({
|
||||
where: and(
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId),
|
||||
eq(crmUserRoleAssignments.userId, membership.userId),
|
||||
eq(crmUserRoleAssignments.roleProfileId, roleProfile.id)
|
||||
)
|
||||
});
|
||||
|
||||
const branchScopeIds = normalizeStringArray(membership.branchScopeIds);
|
||||
const productTypeScopeIds = normalizeStringArray(membership.productTypeScopeIds);
|
||||
const branchScopeMode: CrmRoleAssignmentScopeMode = branchScopeIds.length ? 'selected' : 'all';
|
||||
const productTypeScopeMode: CrmRoleAssignmentScopeMode = productTypeScopeIds.length
|
||||
? 'selected'
|
||||
: 'all';
|
||||
|
||||
if (existing) {
|
||||
if (existing.deletedAt || !existing.isActive) {
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
branchScopeMode,
|
||||
branchScopeIds,
|
||||
productTypeScopeMode,
|
||||
productTypeScopeIds,
|
||||
isPrimary: true,
|
||||
isActive: true,
|
||||
assignedBy: actorUserId,
|
||||
assignedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null
|
||||
})
|
||||
.where(eq(crmUserRoleAssignments.id, existing.id));
|
||||
}
|
||||
|
||||
await setPrimaryAssignment(organizationId, membership.userId, existing.id, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(crmUserRoleAssignments)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
organizationId,
|
||||
userId: membership.userId,
|
||||
roleProfileId: roleProfile.id,
|
||||
branchScopeMode,
|
||||
branchScopeIds,
|
||||
productTypeScopeMode,
|
||||
productTypeScopeIds,
|
||||
isPrimary: true,
|
||||
isActive: true,
|
||||
assignedBy: actorUserId
|
||||
})
|
||||
.returning();
|
||||
|
||||
await setPrimaryAssignment(organizationId, membership.userId, created.id, true);
|
||||
}
|
||||
|
||||
export async function ensureCrmRoleAssignmentsBackfillForOrganization(
|
||||
organizationId: string,
|
||||
actorUserId: string
|
||||
) {
|
||||
const membershipRows = await db.query.memberships.findMany({
|
||||
where: eq(memberships.organizationId, organizationId)
|
||||
});
|
||||
|
||||
for (const membership of membershipRows) {
|
||||
await ensureCrmRoleAssignmentsBackfillForMembership(organizationId, membership, actorUserId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listResolvedCrmRoleAssignments(
|
||||
organizationId: string,
|
||||
actorUserId: string
|
||||
) {
|
||||
await ensureCrmRoleAssignmentsBackfillForOrganization(organizationId, actorUserId);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
assignment: crmUserRoleAssignments,
|
||||
roleProfileCode: crmRoleProfiles.code,
|
||||
roleProfileName: crmRoleProfiles.name,
|
||||
userName: users.name,
|
||||
userEmail: users.email
|
||||
})
|
||||
.from(crmUserRoleAssignments)
|
||||
.innerJoin(crmRoleProfiles, eq(crmRoleProfiles.id, crmUserRoleAssignments.roleProfileId))
|
||||
.innerJoin(users, eq(users.id, crmUserRoleAssignments.userId))
|
||||
.where(
|
||||
and(
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId),
|
||||
isNull(crmUserRoleAssignments.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(users.name), asc(crmUserRoleAssignments.assignedAt));
|
||||
|
||||
const [branchOptions, productTypeOptions, roleProfiles, orgUsers] = await Promise.all([
|
||||
getUserBranches(),
|
||||
getActiveOptionsByCategory('crm_product_type', { organizationId }),
|
||||
db
|
||||
.select({
|
||||
id: crmRoleProfiles.id,
|
||||
code: crmRoleProfiles.code,
|
||||
name: crmRoleProfiles.name
|
||||
})
|
||||
.from(crmRoleProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(crmRoleProfiles.organizationId, organizationId),
|
||||
eq(crmRoleProfiles.isActive, true),
|
||||
isNull(crmRoleProfiles.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(crmRoleProfiles.name)),
|
||||
db
|
||||
.select({
|
||||
userId: users.id,
|
||||
name: users.name,
|
||||
email: users.email
|
||||
})
|
||||
.from(memberships)
|
||||
.innerJoin(users, eq(users.id, memberships.userId))
|
||||
.where(eq(memberships.organizationId, organizationId))
|
||||
.orderBy(asc(users.name))
|
||||
]);
|
||||
|
||||
return {
|
||||
assignments: rows.map((row) =>
|
||||
mapAssignmentRow(row.assignment, {
|
||||
roleProfileCode: row.roleProfileCode,
|
||||
roleProfileName: row.roleProfileName,
|
||||
userName: row.userName,
|
||||
userEmail: row.userEmail
|
||||
})
|
||||
),
|
||||
scopeModes: [...ROLE_ASSIGNMENT_SCOPE_MODES],
|
||||
roleProfiles,
|
||||
users: [...new Map(orgUsers.map((row) => [row.userId, row])).values()].map((row) => ({
|
||||
id: row.userId,
|
||||
name: row.name,
|
||||
email: row.email
|
||||
})),
|
||||
branches: branchOptions.map((branch) => ({
|
||||
id: branch.id,
|
||||
code: branch.code,
|
||||
label: branch.name
|
||||
})),
|
||||
productTypes: productTypeOptions.map((option) => ({
|
||||
id: option.id,
|
||||
code: option.code,
|
||||
label: option.label
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpsertCrmUserRoleAssignmentPayload {
|
||||
userId: string;
|
||||
roleProfileId: string;
|
||||
branchScopeMode: CrmRoleAssignmentScopeMode;
|
||||
branchScopeIds: string[];
|
||||
productTypeScopeMode: CrmRoleAssignmentScopeMode;
|
||||
productTypeScopeIds: string[];
|
||||
isPrimary: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
async function validateAssignmentPayload(
|
||||
organizationId: string,
|
||||
payload: UpsertCrmUserRoleAssignmentPayload
|
||||
) {
|
||||
const [userMembership, roleProfile] = await Promise.all([
|
||||
db.query.memberships.findFirst({
|
||||
where: and(
|
||||
eq(memberships.organizationId, organizationId),
|
||||
eq(memberships.userId, payload.userId)
|
||||
)
|
||||
}),
|
||||
db.query.crmRoleProfiles.findFirst({
|
||||
where: and(
|
||||
eq(crmRoleProfiles.organizationId, organizationId),
|
||||
eq(crmRoleProfiles.id, payload.roleProfileId),
|
||||
isNull(crmRoleProfiles.deletedAt)
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
if (!userMembership) {
|
||||
throw new AuthError('User does not belong to this organization', 404);
|
||||
}
|
||||
|
||||
if (!roleProfile) {
|
||||
throw new AuthError('CRM role profile not found', 404);
|
||||
}
|
||||
|
||||
if (isSelectedScopeInvalid(payload.branchScopeMode, payload.branchScopeIds)) {
|
||||
throw new AuthError('Select at least one branch when branch scope mode is selected', 400);
|
||||
}
|
||||
|
||||
if (isSelectedScopeInvalid(payload.productTypeScopeMode, payload.productTypeScopeIds)) {
|
||||
throw new AuthError(
|
||||
'Select at least one product type when product scope mode is selected',
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const branches = await getUserBranches();
|
||||
const validBranchIds = new Set(branches.map((branch) => branch.id));
|
||||
const productTypes = await getActiveOptionsByCategory('crm_product_type', { organizationId });
|
||||
const validProductTypeIds = new Set(productTypes.map((option) => option.id));
|
||||
|
||||
if (payload.branchScopeIds.some((id) => !validBranchIds.has(id))) {
|
||||
throw new AuthError('Invalid branch scope selection', 400);
|
||||
}
|
||||
|
||||
if (payload.productTypeScopeIds.some((id) => !validProductTypeIds.has(id))) {
|
||||
throw new AuthError('Invalid product type scope selection', 400);
|
||||
}
|
||||
|
||||
return {
|
||||
userMembership,
|
||||
roleProfile
|
||||
};
|
||||
}
|
||||
|
||||
export async function createCrmUserRoleAssignment(
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
payload: UpsertCrmUserRoleAssignmentPayload
|
||||
) {
|
||||
await validateAssignmentPayload(organizationId, payload);
|
||||
|
||||
const duplicate = await db.query.crmUserRoleAssignments.findFirst({
|
||||
where: and(
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId),
|
||||
eq(crmUserRoleAssignments.userId, payload.userId),
|
||||
eq(crmUserRoleAssignments.roleProfileId, payload.roleProfileId)
|
||||
)
|
||||
});
|
||||
|
||||
let assignmentId = duplicate?.id ?? null;
|
||||
|
||||
if (duplicate) {
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
branchScopeMode: payload.branchScopeMode,
|
||||
branchScopeIds: payload.branchScopeIds,
|
||||
productTypeScopeMode: payload.productTypeScopeMode,
|
||||
productTypeScopeIds: payload.productTypeScopeIds,
|
||||
isPrimary: payload.isPrimary,
|
||||
isActive: payload.isActive,
|
||||
assignedBy: actorUserId,
|
||||
assignedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null
|
||||
})
|
||||
.where(eq(crmUserRoleAssignments.id, duplicate.id));
|
||||
} else {
|
||||
const [created] = await db
|
||||
.insert(crmUserRoleAssignments)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
organizationId,
|
||||
userId: payload.userId,
|
||||
roleProfileId: payload.roleProfileId,
|
||||
branchScopeMode: payload.branchScopeMode,
|
||||
branchScopeIds: payload.branchScopeIds,
|
||||
productTypeScopeMode: payload.productTypeScopeMode,
|
||||
productTypeScopeIds: payload.productTypeScopeIds,
|
||||
isPrimary: payload.isPrimary,
|
||||
isActive: payload.isActive,
|
||||
assignedBy: actorUserId
|
||||
})
|
||||
.returning();
|
||||
|
||||
assignmentId = created.id;
|
||||
}
|
||||
|
||||
if (!assignmentId) {
|
||||
throw new AuthError('Unable to create CRM role assignment', 500);
|
||||
}
|
||||
|
||||
await setPrimaryAssignment(organizationId, payload.userId, assignmentId, payload.isPrimary);
|
||||
await ensurePrimaryAssignmentForUser(organizationId, payload.userId);
|
||||
|
||||
const refreshed = await listResolvedCrmRoleAssignments(organizationId, actorUserId);
|
||||
const assignment = refreshed.assignments.find((item) => item.id === assignmentId);
|
||||
|
||||
if (!assignment) {
|
||||
throw new AuthError('CRM role assignment not found after create', 500);
|
||||
}
|
||||
|
||||
await auditAction({
|
||||
organizationId,
|
||||
userId: actorUserId,
|
||||
entityType: 'crm_user_role_assignment',
|
||||
entityId: assignment.id,
|
||||
action: duplicate ? 'reactivate' : 'create',
|
||||
afterData: assignment
|
||||
});
|
||||
|
||||
return assignment;
|
||||
}
|
||||
|
||||
export async function updateCrmUserRoleAssignment(
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
assignmentId: string,
|
||||
payload: Omit<UpsertCrmUserRoleAssignmentPayload, 'userId' | 'roleProfileId'>
|
||||
) {
|
||||
const current = await db.query.crmUserRoleAssignments.findFirst({
|
||||
where: and(
|
||||
eq(crmUserRoleAssignments.id, assignmentId),
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId)
|
||||
)
|
||||
});
|
||||
|
||||
if (!current || current.deletedAt) {
|
||||
throw new AuthError('CRM role assignment not found', 404);
|
||||
}
|
||||
|
||||
await validateAssignmentPayload(organizationId, {
|
||||
userId: current.userId,
|
||||
roleProfileId: current.roleProfileId,
|
||||
...payload
|
||||
});
|
||||
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
branchScopeMode: payload.branchScopeMode,
|
||||
branchScopeIds: payload.branchScopeIds,
|
||||
productTypeScopeMode: payload.productTypeScopeMode,
|
||||
productTypeScopeIds: payload.productTypeScopeIds,
|
||||
isPrimary: payload.isPrimary,
|
||||
isActive: payload.isActive,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(crmUserRoleAssignments.id, assignmentId));
|
||||
|
||||
await setPrimaryAssignment(organizationId, current.userId, assignmentId, payload.isPrimary);
|
||||
await ensurePrimaryAssignmentForUser(organizationId, current.userId);
|
||||
|
||||
const refreshed = await listResolvedCrmRoleAssignments(organizationId, actorUserId);
|
||||
const assignment = refreshed.assignments.find((item) => item.id === assignmentId);
|
||||
|
||||
if (!assignment) {
|
||||
throw new AuthError('CRM role assignment not found after update', 500);
|
||||
}
|
||||
|
||||
await auditAction({
|
||||
organizationId,
|
||||
userId: actorUserId,
|
||||
entityType: 'crm_user_role_assignment',
|
||||
entityId: assignment.id,
|
||||
action: payload.isActive ? 'update' : 'deactivate',
|
||||
beforeData: current,
|
||||
afterData: assignment
|
||||
});
|
||||
|
||||
return assignment;
|
||||
}
|
||||
|
||||
export async function deleteCrmUserRoleAssignment(
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
assignmentId: string
|
||||
) {
|
||||
const current = await db.query.crmUserRoleAssignments.findFirst({
|
||||
where: and(
|
||||
eq(crmUserRoleAssignments.id, assignmentId),
|
||||
eq(crmUserRoleAssignments.organizationId, organizationId)
|
||||
)
|
||||
});
|
||||
|
||||
if (!current || current.deletedAt) {
|
||||
throw new AuthError('CRM role assignment not found', 404);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(crmUserRoleAssignments)
|
||||
.set({
|
||||
isActive: false,
|
||||
isPrimary: false,
|
||||
deletedAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(crmUserRoleAssignments.id, assignmentId));
|
||||
|
||||
await ensurePrimaryAssignmentForUser(organizationId, current.userId);
|
||||
|
||||
await auditAction({
|
||||
organizationId,
|
||||
userId: actorUserId,
|
||||
entityType: 'crm_user_role_assignment',
|
||||
entityId: assignmentId,
|
||||
action: 'delete',
|
||||
beforeData: current
|
||||
});
|
||||
}
|
||||
|
||||
export async function listCrmRoleAssignmentsForUser(
|
||||
organizationId: string,
|
||||
actorUserId: string,
|
||||
userId: string
|
||||
) {
|
||||
const data = await listResolvedCrmRoleAssignments(organizationId, actorUserId);
|
||||
|
||||
return data.assignments.filter((assignment) => assignment.userId === userId);
|
||||
}
|
||||
|
||||
export function parseRoleAssignmentScopeMode(value: string): CrmRoleAssignmentScopeMode {
|
||||
if (!isRoleAssignmentScopeMode(value)) {
|
||||
throw new AuthError('Invalid role assignment scope mode', 400);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function getRoleAssignmentReferenceSummaries(organizationId: string) {
|
||||
const [roleProfiles, usersInOrganization, branches, productTypes] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: crmRoleProfiles.id,
|
||||
code: crmRoleProfiles.code,
|
||||
name: crmRoleProfiles.name
|
||||
})
|
||||
.from(crmRoleProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(crmRoleProfiles.organizationId, organizationId),
|
||||
eq(crmRoleProfiles.isActive, true),
|
||||
isNull(crmRoleProfiles.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(crmRoleProfiles.name)),
|
||||
db
|
||||
.select({
|
||||
id: users.id,
|
||||
name: users.name,
|
||||
email: users.email
|
||||
})
|
||||
.from(memberships)
|
||||
.innerJoin(users, eq(users.id, memberships.userId))
|
||||
.where(eq(memberships.organizationId, organizationId))
|
||||
.orderBy(asc(users.name)),
|
||||
getUserBranches(),
|
||||
getActiveOptionsByCategory('crm_product_type', { organizationId })
|
||||
]);
|
||||
|
||||
return {
|
||||
roleProfiles,
|
||||
users: [...new Map(usersInOrganization.map((row) => [row.id, row])).values()],
|
||||
branches: branches.map((branch) => ({
|
||||
id: branch.id,
|
||||
code: branch.code,
|
||||
label: branch.name
|
||||
})),
|
||||
productTypes: productTypes.map((option) => ({
|
||||
id: option.id,
|
||||
code: option.code,
|
||||
label: option.label
|
||||
}))
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user