Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(condo): DOMA-10516 reassign employee ticket #5563

Merged
merged 26 commits into from
Dec 16, 2024

Conversation

tolmachev21
Copy link
Contributor

Screenshot 2024-12-02 at 21 06 23

@@ -259,8 +259,31 @@ query getContactTickets ($contactId: ID!) {
}
}

query getOrganizationEmployeeTickets($where: TicketWhereInput!, $first: Int, $skip: Int) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have named it getTicketIdsByEmployeeId, seems more true to its nature

@@ -12,6 +12,7 @@ const {
expectToThrowAuthenticationErrorToObjects,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should add more tests, right now added tests do not cover added bulk operations

Really you should just take all tests for CRUD operations and duplicate it for bulk cases.

For example:

  1. I as employee try to update tickets from my org
  2. I as employee try to update tickets from my org, but I do not have the role (i'm not Administrator and do not have canManageTickets permission)
  3. I as employee try to update tickets from other org ...
  4. I as support try to update multiple tickets
    And so on

Do not forget about complex cases, like this:

  1. I as hacker try to update multiple tickets: 2 tickets are from my organization, and 1 ticket is from other organization
  2. I try to update stragne random fields

@@ -702,7 +702,7 @@
"employee.AllSpecializations": "all specializations",
"employee.BlockUser": "Block user",
"employee.CanNotBlockYourself": "(you can't block yourself)",
"employee.ConfirmDeleteMessage": "All data about him will be erased forever",
"employee.ConfirmDeleteMessage": "The data will not be recoverable and the employee will not be able to log in to the Doma.ai platform.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you should not mention Doma.ai in open source translations.

@@ -719,6 +719,15 @@
"employee.emptyList.description": "Your employer has blocked you from the organization",
"employee.emptyList.title": "You are blocked",
"employee.isBlocked": "blocked",
"employee.reassignTickets.title": "Who should I assign employee requests to?",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tickets

@@ -702,7 +702,7 @@
"employee.AllSpecializations": "Все типы работ",
"employee.BlockUser": "Заблокировать пользователя",
"employee.CanNotBlockYourself": "(вы не можете заблокировать себя)",
"employee.ConfirmDeleteMessage": "Все данные о нём сотрутся навсегда",
"employee.ConfirmDeleteMessage": "Данные нельзя будет восстановить, а сотрудник не сможет войти на платформу Doma.ai",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...

updateEmployeeAction={updateEmployeeAction}
activeTicketsOrganizationEmployeeCount={get(activeTicketsOrganizationEmployeeCount, 'meta.count', 0)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why 0 is default here? Default means something went wrong with the query, is it not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that there should be no default value or should there be another value? Or something else?

if (!itemId) return false
const ticket = await getById('Ticket', itemId)
organizationId = get(ticket, 'organization', null)
if (isBulkRequest) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any other checks for singular update case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These cases are below. There are cases when updating and creating one ticket - the property must belong to the same organization for which the ticket is created

Comment on lines 136 to 137
message: notificationTitle('Ошибка'),
description: notificationMessage('Повторите попытку'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^

Comment on lines 151 to 152
console.log(e)
console.error(e.friendlyDescription)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^


const [notificationApi, contextHolder] = notification.useNotification()

const filterTicket = `{"search":"${employeeName}", "status":["new_or_reopened","processing","completed","deferred"]}`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search filter includes a search across multiple fields (e.g clientName, details). If they contain the substring ${employeeName}, then the ticket will also be displayed.
Mb add new custom filter in useTicketTableFilters which filters by the id of the executor or assignee?
Or mb remove this logic?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And you can use enum from '@app/condo/schema' for status types

@@ -259,8 +259,31 @@ query getContactTickets ($contactId: ID!) {
}
}

query getOrganizationEmployeeTickets($where: TicketWhereInput!, $first: Int, $skip: Int) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a more specific name for your use, for example getOrganizationEmployeeTicketsForReassign. And specific arguments instead where: ($userId: ID!, !organizationId: ID!, $first ... )

Comment on lines 67 to 79
organization: { id: organizationId },
OR: [
{ assignee: { id: employeeUserId } },
{ executor: { id: employeeUserId } },
],
status: {
type_in: [
TicketStatusTypeType.NewOrReopened,
TicketStatusTypeType.Processing,
TicketStatusTypeType.Completed,
TicketStatusTypeType.Deferred,
],
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can pass userId and organizationId as query arguments and describe this where in graphql query

},
fetchPolicy: 'no-cache',
})
const [updateTicketsWithReassignOrganizationEmployee] = useUpdateTicketsMutation()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [updateTicketsWithReassignOrganizationEmployee] = useUpdateTicketsMutation()
const [updateTickets] = useUpdateTicketsMutation()

Comment on lines 136 to 137
message: notificationTitle('Ошибка'),
description: notificationMessage('Повторите попытку'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use intl

id_in: itemIds || [itemId],
deletedAt: null,
})
organizationId = uniq(tickets.map(ticket => get(ticket, 'organization', null)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it is organizationIds array

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like changes that are not related to this pr. This and icons below

apps/condo/domains/ticket/access/Ticket.js Show resolved Hide resolved
Comment on lines 158 to 163
setIsDeleting(false)
notificationApi.success({
message: notificationTitle(NotificationSuccessLabel),
description: notificationMessage(NotificationTitleLabel, updatedTicketsCount),
duration: 0,
key: 'reassignTicket',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runMutation has onCompleted and OnCompletedMsg args, look at them

Copy link
Contributor

@Alllex202 Alllex202 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use lodash.get in typescript (except when it's really necessary). It outputs bad types and doesn't show errors during development. You'll only learn about errors in production from real users.

And since we use typescript, please add good types. It's not that hard :)

@@ -278,13 +296,14 @@ export const EmployeePageContent = ({
)
}

export const EmployeeInfoPage: PageComponentType = () => {
export const EmployeeInfoPage = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bring the types back

const intl = useIntl()
const UpdateEmployeeMessage = intl.formatMessage({ id: 'employee.UpdateTitle' })
const ErrorMessage = intl.formatMessage({ id: 'errors.LoadingError' })

const organizationId = get(organization, 'id', null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not use lodash.get, you can write the same thing using optional chains

const { objs: organizationEmployeeSpecializations } = OrganizationEmployeeSpecialization.useObjects({
where: {
employee: { id: employeeId },
},
})

const employeeUserId = get(employee, 'user.id', null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't use get in typescript

Comment on lines 75 to 76
employeeUserId,
organizationId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?
There is no need to pass this data from the parent element. The employee is already passed.
The organization can also be get on the spot from an employee or hook (you can only delete an employee from active organization)

Comment on lines 324 to 337
const { data: activeTicketsOrganizationEmployeeCount } = useGetTicketsCountQuery({
variables: {
where: {
organization: { id: organizationId },
OR: [
{ assignee: { id: employeeUserId } },
{ executor: { id: employeeUserId } },
],
status: {
type_in: [
TicketStatusTypeType.NewOrReopened,
TicketStatusTypeType.Processing,
TicketStatusTypeType.Completed,
TicketStatusTypeType.Deferred,
],
},
},
},
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better make a separate request for this in .graphql file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like getOrganizationEmployeeTicketsForReassign, but for count

}

const tickets = await find('Ticket', {
id_in: itemIds || [itemId],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
id_in: itemIds || [itemId],
id_in: isBulkRequest ? itemIds : [itemId],

Comment on lines 153 to 156
originalInput.forEach((updatedTicket) => {
const propertyId = get(updatedTicket, ['property', 'connect', 'id'])
if (propertyId) propertyIds.push(itemId)
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just use map?

Suggested change
originalInput.forEach((updatedTicket) => {
const propertyId = get(updatedTicket, ['property', 'connect', 'id'])
if (propertyId) propertyIds.push(itemId)
})
propertyIds = originalInput.map(ticket => get(ticket, ['property', 'connect', 'id'], null)).filter(Boolean)

const propertyId = get(updatedTicket, ['property', 'connect', 'id'])
if (propertyId) propertyIds.push(itemId)
})
if (!isEmpty(propertyIds) && propertyIds.length !== originalInput.length) return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this definitely the correct condition?
You can pass data without a property

Suggested change
if (!isEmpty(propertyIds) && propertyIds.length !== originalInput.length) return false
if (!isEmpty(propertyIds) return false


return organizationId === get(property, 'organization')
return isEqual(organizationIds, propertyOrganizationIds)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition will not work correctly if the order of the elements is different.
You should fix this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to add tests that check different cases. There aren't any now :(

Comment on lines 107 to 106
const TICKET_OPEN_STATUS_ID = '6ef3abc4-022f-481b-90fb-8430345ebfc2'
const TICKET_OTHER_SOURCE_ID = '7da1e3be-06ba-4c9e-bba6-f97f278ac6e4'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have STATUS_IDS const, use it

@@ -111,6 +115,30 @@ async function canManageTickets (args) {
return ticket.client === user.id
}
} else {
if (isBulkRequest && operation === 'create') return false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a TODO here with task DOMA-10832

@@ -91,6 +91,24 @@ query getTicketsCount ($where: TicketWhereInput!) {
}
}

query getOrganizationEmployeeTicketsCountForReassign ($userId: ID!, $organizationId: ID!) {
Copy link
Contributor Author

@tolmachev21 tolmachev21 Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I need to change to?

Suggested change
query getOrganizationEmployeeTicketsCountForReassign ($userId: ID!, $organizationId: ID!) {
query getOrganizationEmployeeTicketsCountForReassignment ($userId: ID!, $organizationId: ID!) {

@@ -1448,6 +1464,528 @@ describe('Ticket', () => {
})
})

describe('Bulk-operation', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's normal that I put the logic in a separate describe?

Copy link
Contributor Author

@tolmachev21 tolmachev21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll also add a commit with the changes to callcenter here

@tolmachev21 tolmachev21 added the ✋🙂 Review please Comments are resolved, take a look, please label Dec 11, 2024
@@ -111,6 +115,30 @@ async function canManageTickets (args) {
return ticket.client === user.id
}
} else {
if (isBulkRequest && operation === 'create') return false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you already check it in line 73

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right. I thought about leaving the check for better understanding and easier scaling of the code in the future. But if it's unnecessary, I'll remove it. Let me know

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to remove it and leave in one place

Comment on lines 124 to 129
if (!originalInput.every((updateItem) => {
for (const key in updateItem.data) {
if (!BULK_UPDATE_ALLOWED_FIELDS.includes(key)) return false
}
return true
})) return false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this?

Copy link
Contributor Author

@tolmachev21 tolmachev21 Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, with a single update, we have a check of the address for its compliance with the organization. With a bulk update, we must also support this logic. However, the task of updating and maintaining all possible scenarios of bulk operations is a different task. Therefore, we decided to temporarily limit the update to only two fields - executor and assignee

Comment on lines 327 to 335
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false
return
}
if (!loading && !employee) {
push('/employee/')
}
}, [employee, loading, push])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is necessary that we do not have pages like these
Screenshot 2024-12-12 at 13 25 55
This employee was deleted

Copy link
Contributor

@Alllex202 Alllex202 Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something strange, better tell the user that the employee was not found.
This will be much clearer than a redirect

I haven't seen similar logic with redirect

}
return true
})) return false

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have crooked bulk updates of employees. This is true. I want to roll out this feature and then take on the task of updating accesses. However, if this is really bad, then I can make a crutch. We will check that only one user is updated. And only he will be in the check for compliance with the employee's organization - the ticket organization

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know what is the best way to proceed

return resultObj
}

const notificationTitle = useCallback((notificationStatus: React.ReactNode) => <Typography.Text strong>{notificationStatus}</Typography.Text>, [])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is a function, its name should start from verb, like getNotificationTitle

If this is a component – just make a component, maybe in this file

@@ -91,6 +91,24 @@ query getTicketsCount ($where: TicketWhereInput!) {
}
}

query getOrganizationEmployeeTicketsCountForReassign ($userId: ID!, $organizationId: ID!) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
query getOrganizationEmployeeTicketsCountForReassign ($userId: ID!, $organizationId: ID!) {
query getOrganizationEmployeeTicketsCountByUserAndOrgId ($userId: ID!, $organizationId: ID!) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this name is ok for me

Comment on lines 83 to 84
const notificationTitle = useCallback((notificationStatus: React.ReactNode) => <Typography.Text strong>{notificationStatus}</Typography.Text>, [])
const notificationMessage = useCallback((notificationTitleLabel: React.ReactNode, updatedTicketsCount = null) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is a function (which useCallback is) then it should be named as a function – getNotificationMessage / getNotificationTitle

Otherwise you should use useMemo

runMutation(
{
action: softDeleteAction,
onError: (e) => { throw e },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if error happens, whole page will fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, then it will work OnErrorMsg

})

updatedTicketsCount += reassignedTickets?.tickets?.length
await sleep(1000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this to const so this is more readable

@@ -55,6 +55,25 @@ const MAX_COMMENT_LENGTH = 700

const DEFAULT_DEFERRED_DAYS = 30

const TICKET_OTHER_SOURCE_ID = '7da1e3be-06ba-4c9e-bba6-f97f278ac6e4'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this const?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tests, I need to check operations for bulk ticket creation. There is no function for creating bulk tickets in test utilities. And I did not write it, because we decided not to support bulk ticket creation. And I need this constant only for creating a ticket that cannot be created

})

// Should an admin have access to bulk create tickets?
test.skip('admin: cannot bulk create Tickets', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skip here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it so?

"employee.reassignTickets.title": "Who should I assign employee requests to tickets?",
"employee.reassignTickets.buttonContent.deleteUser": "Remove employee without reassignment",
"employee.reassignTickets.buttonContent.reassignTickets": "Delete employee and reassign requests",
"employee.reassignTickets.alert.title": "{employeeName} is assigned to multiple tickets ({activeTicketsOrganizationEmployeeCount}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(

"employee.reassignTickets.title": "На кого назначить заявки сотрудника?",
"employee.reassignTickets.buttonContent.deleteUser": "Удалить сотрудника без переназначения",
"employee.reassignTickets.buttonContent.reassignTickets": "Удалить сотрудника и переназначить заявки",
"employee.reassignTickets.alert.title": "{employeeName} назначен на несколько заявок ({activeTicketsOrganizationEmployeeCount}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(

const { objs: organizationEmployeeSpecializations } = OrganizationEmployeeSpecialization.useObjects({
where: {
employee: { id: employeeId },
},
})

const { data: activeTicketsOrganizationEmployeeCount } = useGetOrganizationEmployeeTicketsCountForReassignQuery({
variables: {
userId: employee?.user?.id,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means that if userId is undefined for some reason, we will fire a gql request and will get 400 bad request error

@@ -28,6 +28,7 @@ const NEWS_SHARING_TEMPLATES = 'news-sharing-templates'
const SERVICE_PROBLEMS_ALERT = 'service-problems-alert'
const TICKET_AUTO_ASSIGNMENT_MANAGEMENT = 'ticket-auto-assignment-management'
const POLL_TICKET_COMMENTS = 'poll-ticket-comments'
const REASSIGN_EMPLOYEE_TICKETS = 'reassign-employee-tickets'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a question, why do we want to use feature flags for this feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they start dudosing us with this feature, we can turn it off

buttonContent: string
softDeleteAction: IUseSoftDeleteActionType<OrganizationEmployee>
disabled?: boolean
employee: OrganizationEmployee
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
employee: OrganizationEmployee
employee: Pick<OrganizationEmployee, 'name'> & { organization?: Pick<OrganizationEmployee['organization'], 'id'>, user?: Pick<OrganizationEmployee['organization'], 'id'> }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using new hooks this type will be automatically generated

import { searchEmployeeUser } from '@condo/domains/ticket/utils/clientSchema/search'


interface IDeleteEmployeeButtonWithReassignmentModel {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface IDeleteEmployeeButtonWithReassignmentModel {
type DeleteEmployeeButtonWithReassignmentModelProps = {


const [notificationApi, contextHolder] = notification.useNotification()

const employeeUserId = employee?.user?.id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const employeeUserId = employee?.user?.id
const employeeUserId = employee?.user?.id || null

const [notificationApi, contextHolder] = notification.useNotification()

const employeeUserId = employee?.user?.id
const employeeOrganizationId = employee?.organization?.id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const employeeOrganizationId = employee?.organization?.id
const employeeOrganizationId = employee?.organization?.id || null

const { data: activeTicketsOrganizationEmployeeCount } = useGetOrganizationEmployeeTicketsCountForReassignQuery({
variables: {
userId: employee?.user?.id,
organizationId: employee?.organization?.id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
organizationId: employee?.organization?.id,
organizationId: employee?.organization?.id || null,

Comment on lines 331 to 333
const { data: activeTicketsOrganizationEmployeeCount } = useGetOrganizationEmployeeTicketsCountForReassignQuery({
variables: {
userId: employee?.user?.id,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add skip if userId or organizationId is empty

const { objs: organizationEmployeeSpecializations } = OrganizationEmployeeSpecialization.useObjects({
where: {
employee: { id: employeeId },
},
})

const { data: activeTicketsOrganizationEmployeeCount } = useGetOrganizationEmployeeTicketsCountForReassignQuery({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about waiting for the data to load?

Comment on lines 346 to 349
if (error || loading || (!loading && !employee)) {
const errorToPrint = error ? ErrorMessage : (!loading && !employee) ? NotFoundMsg : ''
return <LoadingOrErrorPage title={loading ? LoadingInProgressMessage : errorToPrint} loading={loading} error={errorToPrint}/>
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (error || loading || (!loading && !employee)) {
const errorToPrint = error ? ErrorMessage : (!loading && !employee) ? NotFoundMsg : ''
return <LoadingOrErrorPage title={loading ? LoadingInProgressMessage : errorToPrint} loading={loading} error={errorToPrint}/>
}
if (error || loading) {
const errorToPrint = error ? ErrorMessage : ''
return <LoadingOrErrorPage title={loading ? LoadingInProgressMessage : errorToPrint} loading={loading} error={errorToPrint}/>
}
if (!employee) {
return <LoadingOrErrorPage title={errorToPrint} loading={false} error={NotFoundMsg}/>
}

It's a little easier to read this way

}

return (
<EmployeePageContent
employee={employeeWithSpecializations}
updateEmployeeAction={updateEmployeeAction}
activeTicketsOrganizationEmployeeCount={activeTicketsOrganizationEmployeeCount?.meta?.count}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
activeTicketsOrganizationEmployeeCount={activeTicketsOrganizationEmployeeCount?.meta?.count}
activeTicketsOrganizationEmployeeCount={activeTicketsOrganizationEmployeeCount?.meta?.count || 0}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to wait for the data to load also

apps/callcenter Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we provide the entire list of organizations for a call center request?

const { objs: organizationEmployeeSpecializations } = OrganizationEmployeeSpecialization.useObjects({
where: {
employee: { id: employeeId },
},
})

const { data: activeTicketsOrganizationEmployeeCount } = useGetOrganizationEmployeeTicketsCountForReassignQuery({
Copy link
Contributor

@Alllex202 Alllex202 Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your request is saved in the cache, so you should wait for the cache to load from local storage.
Let me remind you that the request to get the meta is cached for a minute.

wait persisor

const { persistor } = useCachePersistor()

...
skip: !persistor || ...,

@@ -961,8 +964,10 @@ const Ticket = new GQLListSchema('Ticket', {
]
)(...args)

/* NOTE: this sends different kinds of notifications on ticket create/update */
await sendTicketChangedNotifications.delay({ ticketId: updatedItem.id, existingItem, operation })
/* NOTE: this sends different kinds of notifications on ticket create/update except bulk update operation */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add please same comment on page

@@ -55,6 +55,25 @@ const MAX_COMMENT_LENGTH = 700

const DEFAULT_DEFERRED_DAYS = 30

const TICKET_OTHER_SOURCE_ID = '7da1e3be-06ba-4c9e-bba6-f97f278ac6e4'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to leave it in the test file, or just generate a random id.

* updateTicketsForReassignmentEmployee - Query name in query/Ticket.graphql. Usage in DeleteEmployeeButtonWithReassignmentModal.jsx
*/
const DISABLE_PUSH_NOTIFICATION_FOR_OPERATIONS = [
'updateTickets',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have this query

Suggested change
'updateTickets',

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need this query for tests. Frontend queries cannot be used in tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need this query for tests. Frontend queries cannot be used in tests

You can write the query you need for tests. You should to make sure that "updateTicketsForReassignmentEmployee" works correctly

@tolmachev21 tolmachev21 force-pushed the feat/condo/DOMA-10516/reassign-employee-ticket branch from 132b856 to ec520ce Compare December 14, 2024 07:52
const employeeUserId = employee?.user?.id || null
const employeeOrganizationId = employee?.organization?.id || null

const getTicketReassignData = (ticket: Ticket) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter is not of type Ticket. Use the correct generated type for your request (from @app/condo/gql)

return resultObj
}

const getNotificationInfo = useCallback((notificationType: string, updatedTicketsCount = null) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const getNotificationInfo = useCallback((notificationType: string, updatedTicketsCount = null) => {
const getNotificationInfo = useCallback((notificationType: 'error' | 'warning' | 'success', updatedTicketsCount = null) => {

It would be better to specify specific values in the type

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then you won't need to put these values into constants.
(but if you want, you can leave them)

}

const getNotificationInfo = useCallback((notificationType: string, updatedTicketsCount = null) => {
if (notificationType === ERROR_NOTIFICATION_TYPE) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use switch/case

}
}

if (activeTicketsOrganizationEmployeeCount === updatedTicketsCount) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this condition? If the number of tickets for an employee has changed up or down (not because of our feature), then an error will return, do we need this behavior?

* updateTicketsForReassignmentEmployee - Query name in query/Ticket.graphql. Usage in DeleteEmployeeButtonWithReassignmentModal.jsx
*/
const DISABLE_PUSH_NOTIFICATION_FOR_OPERATIONS = [
'updateTickets',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need this query for tests. Frontend queries cannot be used in tests

You can write the query you need for tests. You should to make sure that "updateTicketsForReassignmentEmployee" works correctly

apps/callcenter Outdated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check comments in cc pr

}
const errorToPrint = error ? ErrorMessage : ''

if (error || loading || loadingTicketsOrganizationEmployeeCount) return <LoadingOrErrorPage title={loading ? LoadingInProgressMessage : errorToPrint} loading={loading} error={errorToPrint}/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait persistor?

@tolmachev21 tolmachev21 merged commit a5979e6 into main Dec 16, 2024
27 checks passed
@tolmachev21 tolmachev21 deleted the feat/condo/DOMA-10516/reassign-employee-ticket branch December 16, 2024 07:59
})

// TODO: DOMA-10832 add check employee organization in Ticket access
test.skip('admin: cannot bulk update tickets executor field with user from different organization ', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it so?

})
})

test('admin: cannot bulk delete Tickets', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

})
})

test('employee with role "canManageTickets": can bulk update Tickets', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bulk create test for employee with role "canManageTickets"

expect(get(updatedTicket2, ['assignee', 'id'], null)).toEqual(client.user.id)
})

test('employee without role "canManageTickets": cannot bulk update Tickets', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bulk create test for employee without role "canManageTickets"

})
})

test('organization "from" employee: can bulk update organization "to" tickets', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bulk create for organization "from" employee?

/* NOTE: this sends different kinds of notifications on ticket create/update */
await sendTicketChangedNotifications.delay({ ticketId: updatedItem.id, existingItem, operation })
/* NOTE: this sends different kinds of notifications on ticket create/update except bulk update operation */
if (!DISABLE_PUSH_NOTIFICATION_FOR_OPERATIONS.includes(get(context, ['req', 'body', 'operationName'], null))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is impossible to assume that the logic of notifications will be tied to the name of the GQL mutation. This solution seems non-obvious and requires separate discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✋🙂 Review please Comments are resolved, take a look, please
Development

Successfully merging this pull request may close these issues.

5 participants