From 35ecf78d76f663535659fad6dadc2f942afff892 Mon Sep 17 00:00:00 2001 From: Alllex202 <56914444+Alllex202@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:59:41 +0500 Subject: [PATCH] feat(condo): DOMA-8408 ticket auto assignment by classifiers (#4435) * feat(condo): DOMA-8408 added aut assingned * test(condo): DOMA-8404 added tests * feat(condo): DOMA-8404 updated gql. Added translations * refactor(condo): DOMA-8404 refactored naming * feat(condo): DOMA-8404 updated AutoAssigner * chore(condo): DOMA-8404 updated schema * feat(condo): DOMA-8404 added migrations * feat(condo): DOMA-8404 added page for configure of ticket auto assignment * fix(condo): DOMA-8404 fixed refetch of useAllObjects * feat(condo): DOMA-8404 updated direct access config (add schema TicketAutoAssignment) * feat(condo): DOMA-8404 updated migrations * feat(condo): DOMA-8404 updated schema * feat(condo): DOMA-8404 updated migrations after rebase * feat(condo): DOMA-8404 added translations * fix(condo): DOMA-8404 fixed code-style * refactor(condo): DOMA-8404 refactored code * chore(condo): DOMA-8404 uodated docs * refactor(condo): DOMA-8404 refactored code * fix(condo): DOMA-8408 fixed translations * fix(condo): DOMA-8408 fixed AutoAssigner logic * chore(condo): DOMA-8408 changed columns order * feat(condo): DOMA-8408 updated page for managing of TicketAutoAssignment * chore(condo): DOMA-8404 updated migrations * fix(condo): DOMA-8408 fixed translations * fix(condo): DOMA-8637 changed fetchPolicy when get TicketAutoAssignment * fix(condo): DOMA-8637 changed fetchPolicy when get TicketAutoAssignment * fix(condo): DOMA-8636 fixed table with TicketAutoAssignment if no data * chore(condo): DOMA-8408 updated migrations after rebase --- .../utils/clientSchema/search.tsx | 4 +- .../ticket/access/TicketAutoAssignment.js | 68 ++ .../BaseTicketForm/AutoAssigner.tsx | 85 ++- .../BaseTicketForm/TicketAssignments.tsx | 7 +- apps/condo/domains/ticket/gql.js | 6 +- .../ticket/schema/TicketAutoAssignment.js | 109 ++++ .../schema/TicketAutoAssignment.test.js | 309 +++++++++ apps/condo/domains/ticket/schema/index.js | 4 +- .../clientSchema/TicketAutoAssignment.ts | 35 + .../ticket/utils/clientSchema/index.ts | 1 + .../ticket/utils/serverSchema/index.js | 3 + .../domains/ticket/utils/testSchema/index.js | 37 ++ .../domains/user/utils/directAccess/config.js | 1 + apps/condo/lang/en/en.json | 18 + apps/condo/lang/ru/ru.json | 18 + ...ketautoassignmenthistoryrecord_and_more.js | 92 +++ .../pages/ticket/autoAssignment/index.tsx | 614 ++++++++++++++++++ apps/condo/schema.graphql | 492 ++++++++++++++ apps/condo/schema.ts | 561 ++++++++++++++++ packages/codegen/generate.hooks.ts | 14 +- 20 files changed, 2455 insertions(+), 23 deletions(-) create mode 100644 apps/condo/domains/ticket/access/TicketAutoAssignment.js create mode 100644 apps/condo/domains/ticket/schema/TicketAutoAssignment.js create mode 100644 apps/condo/domains/ticket/schema/TicketAutoAssignment.test.js create mode 100644 apps/condo/domains/ticket/utils/clientSchema/TicketAutoAssignment.ts create mode 100644 apps/condo/migrations/20240314185127-0373_ticketautoassignmenthistoryrecord_and_more.js create mode 100644 apps/condo/pages/ticket/autoAssignment/index.tsx diff --git a/apps/condo/domains/organization/utils/clientSchema/search.tsx b/apps/condo/domains/organization/utils/clientSchema/search.tsx index 1a35bf9f054..0d0345c8d1b 100644 --- a/apps/condo/domains/organization/utils/clientSchema/search.tsx +++ b/apps/condo/domains/organization/utils/clientSchema/search.tsx @@ -69,7 +69,7 @@ export function searchEmployeeWithSpecializations (intl, organizationId, filter) } } -export function searchEmployeeUserWithSpecializations (intl, organizationId, filter) { +export function searchEmployeeUser (intl, organizationId, filter) { if (!organizationId) return return async function (client, value, query = {}, first, skip) { @@ -91,4 +91,4 @@ export function searchEmployeeUserWithSpecializations (intl, organizationId, fil employee, })) } -} \ No newline at end of file +} diff --git a/apps/condo/domains/ticket/access/TicketAutoAssignment.js b/apps/condo/domains/ticket/access/TicketAutoAssignment.js new file mode 100644 index 00000000000..00d229b5f08 --- /dev/null +++ b/apps/condo/domains/ticket/access/TicketAutoAssignment.js @@ -0,0 +1,68 @@ +/** + * Generated by `createschema ticket.TicketAutoAssignment 'assignee:Relationship:OrganizationEmployee:SET_NULL;executor:Relationship:OrganizationEmployee:SET_NULL;classifier:Relationship:TicketClassifier:CASCADE;'` + */ + +const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter') + +const { + queryOrganizationEmployeeFor, + queryOrganizationEmployeeFromRelatedOrganizationFor, +} = require('@condo/domains/organization/utils/accessSchema') +const { STAFF } = require('@condo/domains/user/constants/common') +const { canDirectlyReadSchemaObjects, canDirectlyManageSchemaObjects } = require('@condo/domains/user/utils/directAccess') + + +/** + * TicketAutoAssignment can be read by + * 1. Admin + * 2. Support + * 3. Users with direct access + * 4. Employee who can manage tickets + */ +async function canReadTicketAutoAssignments ({ authentication: { item: user }, listKey }) { + if (!user) return throwAuthenticationError() + if (user.deletedAt) return false + + if (user.isAdmin || user.isSupport) return {} + + const hasDirectAccess = await canDirectlyReadSchemaObjects(user, listKey) + if (hasDirectAccess) return {} + + if (user.type === STAFF) { + return { + organization: { + OR: [ + queryOrganizationEmployeeFor(user.id, 'canManageTickets'), + queryOrganizationEmployeeFromRelatedOrganizationFor(user.id, 'canManageTickets'), + ], + deletedAt: null, + }, + } + } + + return false +} + +/** + * TicketAutoAssignment can be manage by + * 1. Admin + * 2. Support + * 3. Users with direct access + */ +async function canManageTicketAutoAssignments ({ authentication: { item: user }, originalInput, operation, itemId, listKey }) { + if (!user) return throwAuthenticationError() + if (user.deletedAt) return false + + if (user.isAdmin || user.isSupport) return true + + return await canDirectlyManageSchemaObjects(user, listKey, originalInput, operation) +} + +/* + Rules are logical functions that used for list access, and may return a boolean (meaning + all or no items are available) or a set of filters that limit the available items. +*/ +module.exports = { + canReadTicketAutoAssignments, + canManageTicketAutoAssignments, +} diff --git a/apps/condo/domains/ticket/components/BaseTicketForm/AutoAssigner.tsx b/apps/condo/domains/ticket/components/BaseTicketForm/AutoAssigner.tsx index cbb4b21a3e1..8fc988f0721 100644 --- a/apps/condo/domains/ticket/components/BaseTicketForm/AutoAssigner.tsx +++ b/apps/condo/domains/ticket/components/BaseTicketForm/AutoAssigner.tsx @@ -10,10 +10,34 @@ import { getEmployeesSortedByTicketVisibilityType, getPropertyScopeNameByEmployee, isEmployeeSpecializationAndPropertyMatchesToScope, } from '@condo/domains/scope/utils/clientSchema/utils' +import { TicketAutoAssignment } from '@condo/domains/ticket/utils/clientSchema' + + +const selectUserByAutoAssignmentRule = (rule, employees, key: 'assignee' | 'executor') => { + if (!rule) return null + + const desiredEmployee = get(rule, key) + if (desiredEmployee === null) return null + + const desiredEmployeeId = get(desiredEmployee, 'id') + if (!desiredEmployeeId) return null + + const employee = employees.find(employee => employee.id === desiredEmployeeId && !employee.isBlocked) + const employeeUserId = get(employee, 'user.id') + if (!employeeUserId) return null + + return employeeUserId +} /** * Sets the employee user in the assignee and executor fields after selecting ticket category classifier. - If an employee has a SpecializationScope with a specialization that matches the categoryClassifier + + 1) If the organization has configured rules for auto-substitution of “assignee” and “executor” + (the “TicketAutoAssignment” scheme), then we try to find a rule that matches the properties of a ticket + and set a corresponding employee users in the assignee and executor fields (there may be empty values). + If we couldn’t find the rules (not configured), then go to step 2. + + 2) If an employee has a SpecializationScope with a specialization that matches the categoryClassifier and the employee is in a PropertyScope that has a ticket property, then the employee's user is set to the assignee and executor fields. If there was no such employee, then the author of the ticket is set in the assignee field. @@ -26,6 +50,7 @@ export const AutoAssigner = ({ propertyScopeEmployees, propertyScopes, organizationEmployeeSpecializations, + organizationId, }) => { const intl = useIntl() const AutoAssignAlertTitle = intl.formatMessage({ id: 'pages.condo.ticket.autoAssignAlert.title' }) @@ -37,8 +62,39 @@ export const AutoAssigner = ({ const [autoAssigneePropertyScopeName, setAutoAssigneePropertyScopeName] = useState() + const classifierId = form.getFieldValue('classifier') + + const { loading, obj: rule } = TicketAutoAssignment.useObject({ + where: { + organization: { id: organizationId }, + classifier: { id: classifierId }, + }, + }, { + skip: !organizationId || !classifierId, + fetchPolicy: 'cache-and-network', + nextFetchPolicy: 'cache-and-network', + }) + + const loadedRule = !!rule || !loading + const allLoaded = allDataLoaded && loadedRule + useDeepCompareEffect(() => { - if (allDataLoaded) { + if (allLoaded) { + + // 1 - try set assignee and executor by TicketAutoAssignment + if (rule) { + const autoSelectedAssigneeUserId = selectUserByAutoAssignmentRule(rule, employees, 'assignee') + const autoSelectedExecutorUserId = selectUserByAutoAssignmentRule(rule, employees, 'executor') + + form.setFieldsValue({ + assignee: autoSelectedAssigneeUserId, + executor: autoSelectedExecutorUserId, + }) + setAutoAssigneePropertyScopeName(null) + return + } + + // 2 - try set assignee and executor by specialization and ticket visibility const employeesWithMatchesPropertyAndSpecializationScope = employees.filter( isEmployeeSpecializationAndPropertyMatchesToScope( { @@ -56,28 +112,29 @@ export const AutoAssigner = ({ organizationEmployeeSpecializations, categoryClassifierId, ) + const firstEmployee = sortedEmployees.find(employee => !employee.isBlocked) const firstEmployeeUserId = get(firstEmployee, 'user.id') + const propertyScopeName = getPropertyScopeNameByEmployee(firstEmployee, propertyScopes, propertyScopeEmployees) form.setFieldsValue({ assignee: firstEmployeeUserId, executor: firstEmployeeUserId, }) - - const propertyScopeName = getPropertyScopeNameByEmployee(firstEmployee, propertyScopes, propertyScopeEmployees) setAutoAssigneePropertyScopeName(propertyScopeName) - } else { - form.setFieldsValue({ - assignee: currentUserCanBeAssignee ? currentUserId : null, - executor: null, - }) - - setAutoAssigneePropertyScopeName(null) + return } + + // 3 - set current user as assignee or null and executor to null + form.setFieldsValue({ + assignee: currentUserCanBeAssignee ? currentUserId : null, + executor: null, + }) + setAutoAssigneePropertyScopeName(null) } }, [ - allDataLoaded, categoryClassifierId, employees, form, organizationEmployeeSpecializations, - propertyScopeEmployees, propertyScopes, currentUserId, currentUserCanBeAssignee, + categoryClassifierId, employees, form, organizationEmployeeSpecializations, + propertyScopeEmployees, propertyScopes, currentUserId, currentUserCanBeAssignee, allLoaded, rule, ]) return autoAssigneePropertyScopeName ? ( @@ -90,4 +147,4 @@ export const AutoAssigner = ({ /> ) : null -} \ No newline at end of file +} diff --git a/apps/condo/domains/ticket/components/BaseTicketForm/TicketAssignments.tsx b/apps/condo/domains/ticket/components/BaseTicketForm/TicketAssignments.tsx index 833c4813cec..4bf7c56b561 100644 --- a/apps/condo/domains/ticket/components/BaseTicketForm/TicketAssignments.tsx +++ b/apps/condo/domains/ticket/components/BaseTicketForm/TicketAssignments.tsx @@ -14,7 +14,7 @@ import { OrganizationEmployee, OrganizationEmployeeSpecialization, } from '@condo/domains/organization/utils/clientSchema' -import { searchEmployeeUserWithSpecializations } from '@condo/domains/organization/utils/clientSchema/search' +import { searchEmployeeUser } from '@condo/domains/organization/utils/clientSchema/search' import { PropertyScope, PropertyScopeOrganizationEmployee, @@ -202,7 +202,7 @@ const TicketAssignments = ({ filteredPropertyScopeEmployees, intl, DeletedEmployeeMessage, EmployeesOnPropertyMessage, OtherMessage, ]) - const search = useMemo(() => searchEmployeeUserWithSpecializations(intl, organizationId, null), + const search = useMemo(() => searchEmployeeUser(intl, organizationId, null), [intl, organizationId]) const loading = propertiesLoading || scopesLoading || employeesLoading || specializationsLoading @@ -216,7 +216,7 @@ const TicketAssignments = ({ { - autoAssign && !loading && propertyId && categoryClassifier && ( + autoAssign && !loading && propertyId && categoryClassifier && organizationId && ( ) } diff --git a/apps/condo/domains/ticket/gql.js b/apps/condo/domains/ticket/gql.js index 6da4865fc7b..52339e0407c 100644 --- a/apps/condo/domains/ticket/gql.js +++ b/apps/condo/domains/ticket/gql.js @@ -260,6 +260,9 @@ const TICKET_MULTIPLE_UPDATE_MUTATION = gql` } ` +const TICKET_AUTO_ASSIGNMENT_FIELDS = `{ organization { id name } assignee { id name } executor { id name } classifier { id place { id name } category { id name } problem { id name } } ${COMMON_FIELDS} }` +const TicketAutoAssignment = generateGqlQueries('TicketAutoAssignment', TICKET_AUTO_ASSIGNMENT_FIELDS) + /* AUTOGENERATE MARKER */ module.exports = { Ticket, @@ -297,5 +300,6 @@ module.exports = { CallRecord, CallRecordFragment, TICKET_MULTIPLE_UPDATE_MUTATION, - /* AUTOGENERATE MARKER */ + TicketAutoAssignment, +/* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/ticket/schema/TicketAutoAssignment.js b/apps/condo/domains/ticket/schema/TicketAutoAssignment.js new file mode 100644 index 00000000000..8df61a39261 --- /dev/null +++ b/apps/condo/domains/ticket/schema/TicketAutoAssignment.js @@ -0,0 +1,109 @@ +/** + * Generated by `createschema ticket.TicketAutoAssignment 'assignee:Relationship:OrganizationEmployee:SET_NULL;executor:Relationship:OrganizationEmployee:SET_NULL;classifier:Relationship:TicketClassifier:CASCADE;'` + */ + +const { GQLError, GQLErrorCode: { BAD_USER_INPUT } } = require('@open-condo/keystone/errors') +const { historical, versioned, uuided, tracked, softDeleted, dvAndSender } = require('@open-condo/keystone/plugins') +const { GQLListSchema, getByCondition } = require('@open-condo/keystone/schema') + +const { ORGANIZATION_OWNED_FIELD } = require('@condo/domains/organization/schema/fields') +const access = require('@condo/domains/ticket/access/TicketAutoAssignment') + +const ERRORS = { + ASSIGNEE_NOT_FOUND: { + code: BAD_USER_INPUT, + type: 'ASSIGNEE_NOT_FOUND', + messageForUser: 'api.ticket.TicketAutoAssignment.error.ASSIGNEE_NOT_FOUND', + message: 'There is no such employee (assignee) in the organization', + }, + EXECUTOR_NOT_FOUND: { + code: BAD_USER_INPUT, + type: 'EXECUTOR_NOT_FOUND', + messageForUser: 'api.ticket.TicketAutoAssignment.error.EXECUTOR_NOT_FOUND', + message: 'There is no such employee (executor) in the organization', + }, +} + +const TicketAutoAssignment = new GQLListSchema('TicketAutoAssignment', { + schemaDoc: 'This schema helps decides who should be assigned to ticket as executor and assignee', + fields: { + + organization: ORGANIZATION_OWNED_FIELD, + + assignee: { + schemaDoc: 'An employee who will be appointed as assignee of ticket. ' + + 'If "null", then the field will remain empty and dispatcher will have to fill it in independently in UI', + type: 'Relationship', + ref: 'OrganizationEmployee', + isRequired: false, + knexOptions: { isNotNullable: false }, // Required relationship only! + kmigratorOptions: { null: true, on_delete: 'models.SET_NULL' }, + }, + + executor: { + schemaDoc: 'An employee who will be appointed as executor of ticket' + + 'If "null", then the field will remain empty and dispatcher will have to fill it in independently in UI', + type: 'Relationship', + ref: 'OrganizationEmployee', + isRequired: false, + knexOptions: { isNotNullable: false }, // Required relationship only! + kmigratorOptions: { null: true, on_delete: 'models.SET_NULL' }, + }, + + classifier: { + schemaDoc: 'Ticket classifier', + type: 'Relationship', + ref: 'TicketClassifier', + isRequired: true, + knexOptions: { isNotNullable: true }, // Required relationship only! + kmigratorOptions: { null: false, on_delete: 'models.CASCADE' }, + }, + + }, + hooks: { + validateInput: async ({ resolvedData, context, existingItem }) => { + const newItem = { ...existingItem, ...resolvedData } + + if (newItem.assignee) { + const assignee = await getByCondition('OrganizationEmployee', { + id: newItem.assignee, + deletedAt: null, + organization: { id: newItem.organization, deletedAt: null }, + }) + if (!assignee) throw new GQLError(ERRORS.ASSIGNEE_NOT_FOUND, context) + } + + if (newItem.executor) { + const executor = await getByCondition('OrganizationEmployee', { + id: newItem.executor, + deletedAt: null, + organization: { id: newItem.organization, deletedAt: null }, + }) + if (!executor) throw new GQLError(ERRORS.EXECUTOR_NOT_FOUND, context) + } + }, + }, + plugins: [uuided(), versioned(), tracked(), softDeleted(), dvAndSender(), historical()], + kmigratorOptions: { + constraints: [ + { + type: 'models.UniqueConstraint', + fields: ['organization', 'classifier'], + condition: 'Q(deletedAt__isnull=True)', + name: 'ticket_auto_assignment_unique_organization_classifier', + }, + ], + }, + access: { + read: access.canReadTicketAutoAssignments, + create: access.canManageTicketAutoAssignments, + update: access.canManageTicketAutoAssignments, + delete: false, + auth: true, + }, +}) + +module.exports = { + TicketAutoAssignment, + ERRORS, +} diff --git a/apps/condo/domains/ticket/schema/TicketAutoAssignment.test.js b/apps/condo/domains/ticket/schema/TicketAutoAssignment.test.js new file mode 100644 index 00000000000..22860c6f689 --- /dev/null +++ b/apps/condo/domains/ticket/schema/TicketAutoAssignment.test.js @@ -0,0 +1,309 @@ +/** + * Generated by `createschema ticket.TicketAutoAssignment 'assignee:Relationship:OrganizationEmployee:SET_NULL;executor:Relationship:OrganizationEmployee:SET_NULL;classifier:Relationship:TicketClassifier:CASCADE;'` + */ + +const { + makeLoggedInAdminClient, + makeClient, + expectToThrowGQLError, + expectToThrowUniqueConstraintViolationError, +} = require('@open-condo/keystone/test.utils') +const { + expectToThrowAuthenticationErrorToObj, + expectToThrowAuthenticationErrorToObjects, + expectToThrowAccessDeniedErrorToObj, +} = require('@open-condo/keystone/test.utils') + +const { + makeAdminClientWithRegisteredOrganizationWithRoleWithEmployee, + createTestOrganizationEmployeeRole, + createTestOrganizationEmployee, + OrganizationEmployee, +} = require('@condo/domains/organization/utils/testSchema') +const { + TicketAutoAssignment, + createTestTicketAutoAssignment, + updateTestTicketAutoAssignment, + createTestTicketClassifier, + TicketClassifier, + TicketCategoryClassifier, + TicketPlaceClassifier, + TicketProblemClassifier, +} = require('@condo/domains/ticket/utils/testSchema') +const { + makeClientWithNewRegisteredAndLoggedInUser, + makeClientWithSupportUser, + registerNewUser, +} = require('@condo/domains/user/utils/testSchema') + +const { ERRORS } = require('./TicketAutoAssignment') + + +describe('TicketAutoAssignment', () => { + + let admin, support, employeeWithPermission, employeeWithoutPermission, user, anonymous, + organization, classifier, assignee, executor, otherClassifier, + ticketAutoAssignment + const classifiersToDelete = [] + + beforeAll(async () => { + admin = await makeLoggedInAdminClient() + support = await makeClientWithSupportUser() + user = await makeClientWithNewRegisteredAndLoggedInUser() + anonymous = await makeClient() + + const { organization: createdOrganization, userClient, role } = await makeAdminClientWithRegisteredOrganizationWithRoleWithEmployee({ canManageTickets: true }) + employeeWithPermission = userClient + organization = createdOrganization + + const [roleWithoutPermissions] = await createTestOrganizationEmployeeRole(admin, organization, { canManageTickets: false }) + employeeWithoutPermission = await makeClientWithNewRegisteredAndLoggedInUser() + await createTestOrganizationEmployee(admin, organization, employeeWithoutPermission.user, roleWithoutPermissions) + + const [assigneeUser] = await registerNewUser(await makeClient()); + [assignee] = await createTestOrganizationEmployee(admin, organization, assigneeUser, role) + const [executorUser] = await registerNewUser(await makeClient()); + [executor] = await createTestOrganizationEmployee(admin, organization, executorUser, role); + + [classifier] = await createTestTicketClassifier(admin); + [otherClassifier] = await createTestTicketClassifier(admin) + classifiersToDelete.push(classifier, otherClassifier) + }) + + beforeEach(async () => { + [ticketAutoAssignment] = await createTestTicketAutoAssignment(admin, organization, assignee, executor, classifier) + }) + + afterEach(async () => { + const items = await TicketAutoAssignment.getAll(admin, { organization: { id: organization.id }, deletedAt: null }) + for (const item of items) { + await TicketAutoAssignment.softDelete(admin, item.id) + } + }) + + afterAll(async () => { + for (const classifier of classifiersToDelete) { + await TicketPlaceClassifier.delete(admin, classifier.place.id) + await TicketCategoryClassifier.delete(admin, classifier.category.id) + await TicketProblemClassifier.delete(admin, classifier.problem.id) + await TicketClassifier.delete(admin, classifier.id) + } + }) + + describe('Accesses', () => { + describe('Admin', () => { + test('can create', async () => { + const [ticketAutoAssignment] = await createTestTicketAutoAssignment(admin, organization, assignee, executor, otherClassifier) + expect(ticketAutoAssignment).toBeDefined() + expect(ticketAutoAssignment).toHaveProperty('organization.id', organization.id) + expect(ticketAutoAssignment).toHaveProperty('assignee.id', assignee.id) + expect(ticketAutoAssignment).toHaveProperty('executor.id', executor.id) + expect(ticketAutoAssignment).toHaveProperty('classifier.id', otherClassifier.id) + }) + + test('can read', async () => { + const item = await TicketAutoAssignment.getOne(admin, { id: ticketAutoAssignment.id }) + expect(item).toHaveProperty('id', ticketAutoAssignment.id) + }) + + test('can update', async () => { + const [updatedItem] = await updateTestTicketAutoAssignment(admin, ticketAutoAssignment.id) + expect(updatedItem).toHaveProperty('id', ticketAutoAssignment.id) + expect(updatedItem).toHaveProperty('sender.fingerprint', expect.not.stringMatching(ticketAutoAssignment.sender.fingerprint)) + }) + + test('can soft-delete', async () => { + const [updatedItem] = await TicketAutoAssignment.softDelete(admin, ticketAutoAssignment.id) + expect(updatedItem).toHaveProperty('id', ticketAutoAssignment.id) + expect(updatedItem.deletedAt).not.toBeNull() + }) + + test('cannot hard-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.delete(admin, ticketAutoAssignment.id) + }) + }) + }) + + describe('Support', () => { + test('can create', async () => { + const [ticketAutoAssignment] = await createTestTicketAutoAssignment(support, organization, assignee, executor, otherClassifier) + expect(ticketAutoAssignment).toBeDefined() + }) + + test('can read', async () => { + const item = await TicketAutoAssignment.getOne(support, { id: ticketAutoAssignment.id }) + expect(item).toHaveProperty('id', ticketAutoAssignment.id) + }) + + test('can update', async () => { + const [updatedItem] = await updateTestTicketAutoAssignment(support, ticketAutoAssignment.id) + expect(updatedItem).toHaveProperty('id', ticketAutoAssignment.id) + expect(updatedItem).toHaveProperty('sender.fingerprint', expect.not.stringMatching(ticketAutoAssignment.sender.fingerprint)) + }) + + test('can soft-delete', async () => { + const [updatedItem] = await TicketAutoAssignment.softDelete(support, ticketAutoAssignment.id) + expect(updatedItem).toHaveProperty('id', ticketAutoAssignment.id) + expect(updatedItem.deletedAt).not.toBeNull() + }) + + test('cannot hard-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.delete(support, ticketAutoAssignment.id) + }) + }) + }) + + describe('Employee', () => { + test('cannot create', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await createTestTicketAutoAssignment(employeeWithPermission, organization, assignee, executor, otherClassifier) + }) + }) + + describe('read', () => { + test('can with permission canManageTickets', async () => { + const item = await TicketAutoAssignment.getOne(employeeWithPermission, { id: ticketAutoAssignment.id }) + expect(item).toBeDefined() + }) + + test('can not without permission canManageTickets', async () => { + const item = await TicketAutoAssignment.getOne(employeeWithoutPermission, { id: ticketAutoAssignment.id }) + expect(item).toBeUndefined() + }) + }) + + test('cannot update', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await updateTestTicketAutoAssignment(employeeWithPermission, ticketAutoAssignment.id) + }) + }) + + test('cannot soft-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.softDelete(employeeWithPermission, ticketAutoAssignment.id) + }) + }) + + test('cannot hard-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.delete(employeeWithPermission, ticketAutoAssignment.id) + }) + }) + }) + + describe('User', () => { + test('cannot create', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await createTestTicketAutoAssignment(user, organization, assignee, executor, otherClassifier) + }) + }) + + test('cannot read', async () => { + const item = await TicketAutoAssignment.getOne(user, { id: ticketAutoAssignment.id }) + expect(item).toBeUndefined() + }) + + test('cannot update', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await updateTestTicketAutoAssignment(user, ticketAutoAssignment.id) + }) + }) + + test('cannot soft-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.softDelete(user, ticketAutoAssignment.id) + }) + }) + + test('cannot hard-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.delete(user, ticketAutoAssignment.id) + }) + }) + }) + + describe('Anonymous', () => { + test('cannot create', async () => { + await expectToThrowAuthenticationErrorToObj(async () => { + await createTestTicketAutoAssignment(anonymous, organization, assignee, executor, otherClassifier) + }) + }) + + test('cannot read', async () => { + await expectToThrowAuthenticationErrorToObjects(async () => { + await TicketAutoAssignment.getOne(anonymous, { id: ticketAutoAssignment.id }) + }) + }) + + test('cannot update', async () => { + await expectToThrowAuthenticationErrorToObj(async () => { + await updateTestTicketAutoAssignment(anonymous, ticketAutoAssignment.id) + }) + }) + + test('cannot soft-delete', async () => { + await expectToThrowAuthenticationErrorToObj(async () => { + await TicketAutoAssignment.softDelete(anonymous, ticketAutoAssignment.id) + }) + }) + + test('cannot hard-delete', async () => { + await expectToThrowAccessDeniedErrorToObj(async () => { + await TicketAutoAssignment.delete(anonymous, ticketAutoAssignment.id) + }) + }) + }) + }) + + describe('Validations', () => { + test('should be throw error if assignee are not from organization', async () => { + const { employee: employeeFromOtherOrganization } = await makeAdminClientWithRegisteredOrganizationWithRoleWithEmployee() + await expectToThrowGQLError(async () => { + await createTestTicketAutoAssignment(admin, organization, employeeFromOtherOrganization, null, otherClassifier) + }, ERRORS.ASSIGNEE_NOT_FOUND) + await expectToThrowGQLError(async () => { + await updateTestTicketAutoAssignment(admin, ticketAutoAssignment.id, { + assignee: { connect: { id: employeeFromOtherOrganization.id } }, + }) + }, ERRORS.ASSIGNEE_NOT_FOUND) + }) + + test('should be throw error if executor are not from organization', async () => { + const { employee: employeeFromOtherOrganization } = await makeAdminClientWithRegisteredOrganizationWithRoleWithEmployee() + await expectToThrowGQLError(async () => { + await createTestTicketAutoAssignment(admin, organization, null, employeeFromOtherOrganization, otherClassifier) + }, ERRORS.EXECUTOR_NOT_FOUND) + await expectToThrowGQLError(async () => { + await updateTestTicketAutoAssignment(admin, ticketAutoAssignment.id, { + executor: { connect: { id: employeeFromOtherOrganization.id } }, + }) + }, ERRORS.EXECUTOR_NOT_FOUND) + }) + + test('should be create/update record if assignee and executor are from organization', async () => { + const [item] = await createTestTicketAutoAssignment(admin, organization, assignee, executor, otherClassifier) + expect(item).toBeDefined() + + const employeeFromSameOrganization = await OrganizationEmployee.getOne(admin, { + organization: { id: organization.id }, + user: { id: employeeWithPermission.user.id }, + }) + const [updatedItem] = await updateTestTicketAutoAssignment(admin, item.id, { + assignee: { connect: { id: employeeFromSameOrganization.id } }, + executor: { connect: { id: employeeFromSameOrganization.id } }, + }) + expect(updatedItem).toHaveProperty('id', item.id) + expect(updatedItem).toHaveProperty('assignee.id', employeeFromSameOrganization.id) + expect(updatedItem).toHaveProperty('executor.id', employeeFromSameOrganization.id) + }) + + test('should be throw error if classifier is duplicating in organization', async () => { + await createTestTicketAutoAssignment(admin, organization, assignee, executor, otherClassifier) + await expectToThrowUniqueConstraintViolationError(async () => { + await createTestTicketAutoAssignment(admin, organization, assignee, executor, otherClassifier) + }, 'ticket_auto_assignment_unique_organization_classifier') + }) + }) +}) diff --git a/apps/condo/domains/ticket/schema/index.js b/apps/condo/domains/ticket/schema/index.js index e156b5039f6..32d7cacc0e5 100644 --- a/apps/condo/domains/ticket/schema/index.js +++ b/apps/condo/domains/ticket/schema/index.js @@ -15,6 +15,7 @@ const { IncidentProperty } = require('./IncidentProperty') const { PredictTicketClassificationService } = require('./PredictTicketClassificationService') const { ShareTicketService } = require('./ShareTicketService') const { Ticket } = require('./Ticket') +const { TicketAutoAssignment } = require('./TicketAutoAssignment') const { TicketCategoryClassifier } = require('./TicketCategoryClassifier') const { TicketChange } = require('./TicketChange') const { TicketClassifier } = require('./TicketClassifier') @@ -67,5 +68,6 @@ module.exports = { CallRecord, CallRecordFragment, TicketMultipleUpdateService, - /* AUTOGENERATE MARKER */ + TicketAutoAssignment, +/* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/ticket/utils/clientSchema/TicketAutoAssignment.ts b/apps/condo/domains/ticket/utils/clientSchema/TicketAutoAssignment.ts new file mode 100644 index 00000000000..54f4c21b8a3 --- /dev/null +++ b/apps/condo/domains/ticket/utils/clientSchema/TicketAutoAssignment.ts @@ -0,0 +1,35 @@ +/** + * Generated by `createschema ticket.TicketAutoAssignment 'assignee:Relationship:OrganizationEmployee:SET_NULL;executor:Relationship:OrganizationEmployee:SET_NULL;classifier:Relationship:TicketClassifier:CASCADE;'` + */ + +import { + TicketAutoAssignment, + TicketAutoAssignmentCreateInput, + TicketAutoAssignmentUpdateInput, + QueryAllTicketAutoAssignmentsArgs, +} from '@app/condo/schema' + +import { generateReactHooks } from '@open-condo/codegen/generate.hooks' + +import { TicketAutoAssignment as TicketAutoAssignmentGQL } from '@condo/domains/ticket/gql' + + +const { + useObject, + useObjects, + useCreate, + useUpdate, + useSoftDelete, + useAllObjects, + useSoftDeleteMany, +} = generateReactHooks(TicketAutoAssignmentGQL) + +export { + useObject, + useObjects, + useCreate, + useUpdate, + useSoftDelete, + useAllObjects, + useSoftDeleteMany, +} diff --git a/apps/condo/domains/ticket/utils/clientSchema/index.ts b/apps/condo/domains/ticket/utils/clientSchema/index.ts index 783d479a8c7..1148d4c138f 100644 --- a/apps/condo/domains/ticket/utils/clientSchema/index.ts +++ b/apps/condo/domains/ticket/utils/clientSchema/index.ts @@ -30,4 +30,5 @@ export * as UserFavoriteTicket from './UserFavoriteTicket' export * as IncidentExportTask from './IncidentExportTask' export * as CallRecord from './CallRecord' export * as CallRecordFragment from './CallRecordFragment' +export * as TicketAutoAssignment from './TicketAutoAssignment' /* AUTOGENERATE MARKER */ diff --git a/apps/condo/domains/ticket/utils/serverSchema/index.js b/apps/condo/domains/ticket/utils/serverSchema/index.js index d558a81caa6..460a2140867 100644 --- a/apps/condo/domains/ticket/utils/serverSchema/index.js +++ b/apps/condo/domains/ticket/utils/serverSchema/index.js @@ -39,6 +39,7 @@ const { IncidentExportTask: IncidentExportTaskGQL } = require('@condo/domains/ti const { CallRecord: CallRecordGQL } = require('@condo/domains/ticket/gql') const { CallRecordFragment: CallRecordFragmentGQL } = require('@condo/domains/ticket/gql') const { TICKET_MULTIPLE_UPDATE_MUTATION } = require('@condo/domains/ticket/gql') +const { TicketAutoAssignment: TicketAutoAssignmentGQL } = require('@condo/domains/ticket/gql') /* AUTOGENERATE MARKER */ const Ticket = generateServerUtils(TicketGQL) @@ -101,6 +102,7 @@ async function ticketMultipleUpdate (context, data) { }) } +const TicketAutoAssignment = generateServerUtils(TicketAutoAssignmentGQL) /* AUTOGENERATE MARKER */ /** @@ -328,5 +330,6 @@ module.exports = { CallRecord, CallRecordFragment, ticketMultipleUpdate, + TicketAutoAssignment, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/ticket/utils/testSchema/index.js b/apps/condo/domains/ticket/utils/testSchema/index.js index ac09174e800..95b249502e1 100644 --- a/apps/condo/domains/ticket/utils/testSchema/index.js +++ b/apps/condo/domains/ticket/utils/testSchema/index.js @@ -46,6 +46,7 @@ const { CallRecord: CallRecordGQL } = require('@condo/domains/ticket/gql') const { CallRecordFragment: CallRecordFragmentGQL } = require('@condo/domains/ticket/gql') const { createTestPhone } = require('@condo/domains/user/utils/testSchema') const { TICKET_MULTIPLE_UPDATE_MUTATION } = require('@condo/domains/ticket/gql') +const { TicketAutoAssignment: TicketAutoAssignmentGQL } = require('@condo/domains/ticket/gql') /* AUTOGENERATE MARKER */ const TICKET_OPEN_STATUS_ID ='6ef3abc4-022f-481b-90fb-8430345ebfc2' @@ -79,6 +80,7 @@ const UserFavoriteTicket = generateGQLTestUtils(UserFavoriteTicketGQL) const IncidentExportTask = generateGQLTestUtils(IncidentExportTaskGQL) const CallRecord = generateGQLTestUtils(CallRecordGQL) const CallRecordFragment = generateGQLTestUtils(CallRecordFragmentGQL) +const TicketAutoAssignment = generateGQLTestUtils(TicketAutoAssignmentGQL) /* AUTOGENERATE MARKER */ async function createTestTicket (client, organization, property, extraAttrs = {}) { @@ -967,6 +969,40 @@ async function ticketMultipleUpdateByTestClient(client, extraAttrs = {}) { throwIfError(data, errors) return [data.result, attrs] } +async function createTestTicketAutoAssignment (client, organization, assignee, executor, classifier, extraAttrs = {}) { + if (!client) throw new Error('no client') + if (assignee && !assignee.id) throw new Error('no assignee.id') + if (executor && !executor.id) throw new Error('no executor.id') + if (!classifier || !classifier.id) throw new Error('no classifier.id') + const sender = { dv: 1, fingerprint: faker.random.alphaNumeric(8) } + + const attrs = { + dv: 1, + sender, + organization: { connect: { id: organization.id } }, + classifier: { connect: { id: classifier.id } }, + ...(assignee ? { assignee: { connect: { id: assignee.id } }} : undefined), + ...(executor ? { executor: { connect: { id: executor.id } }} : undefined), + ...extraAttrs, + } + const obj = await TicketAutoAssignment.create(client, attrs) + return [obj, attrs] +} + +async function updateTestTicketAutoAssignment (client, id, extraAttrs = {}) { + if (!client) throw new Error('no client') + if (!id) throw new Error('no id') + const sender = { dv: 1, fingerprint: faker.random.alphaNumeric(8) } + + const attrs = { + dv: 1, + sender, + ...extraAttrs, + } + const obj = await TicketAutoAssignment.update(client, id, attrs) + return [obj, attrs] +} + /* AUTOGENERATE MARKER */ async function makeClientWithTicket () { @@ -1021,5 +1057,6 @@ module.exports = { CallRecord, createTestCallRecord, updateTestCallRecord, CallRecordFragment, createTestCallRecordFragment, updateTestCallRecordFragment, ticketMultipleUpdateByTestClient, + TicketAutoAssignment, createTestTicketAutoAssignment, updateTestTicketAutoAssignment, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/user/utils/directAccess/config.js b/apps/condo/domains/user/utils/directAccess/config.js index 5df57ed2fd2..6a470f46895 100644 --- a/apps/condo/domains/user/utils/directAccess/config.js +++ b/apps/condo/domains/user/utils/directAccess/config.js @@ -42,6 +42,7 @@ const DIRECT_ACCESS_AVAILABLE_SCHEMAS = { // Ticket domain 'Ticket', + 'TicketAutoAssignment', // User domain 'OidcClient', diff --git a/apps/condo/lang/en/en.json b/apps/condo/lang/en/en.json index 5b5f98180aa..a5b35c2f934 100644 --- a/apps/condo/lang/en/en.json +++ b/apps/condo/lang/en/en.json @@ -880,6 +880,22 @@ "pages.condo.ticket.PaymentsAvailable": "Payments are available in the resident's mobile app", "pages.condo.ticket.PaymentsNotAvailable": "Payments are not available in the resident's mobile app", "pages.condo.ticket.WithoutDeadline": "This type of ticket does not have a deadline.", + "pages.ticket.autoAssignment.deleteConfirm.message": "Sure to delete?", + "pages.ticket.autoAssignment.cancelConfirm.message": "Sure to cancel?", + "pages.ticket.autoAssignment.columns.classifier.title": "Classifier", + "pages.ticket.autoAssignment.columns.assignee.title": "Assignee", + "pages.ticket.autoAssignment.columns.executor.title": "Executor", + "pages.ticket.autoAssignment.columns.operations.title": "Operations", + "pages.ticket.autoAssignment.button.create.label": "Create", + "pages.ticket.autoAssignment.button.cancel.label": "Cancel", + "pages.ticket.autoAssignment.button.refresh.label": "Refresh", + "pages.ticket.autoAssignment.button.save.label": "Save", + "pages.ticket.autoAssignment.button.edit.label": "Edit", + "pages.ticket.autoAssignment.button.delete.label": "Delete", + "pages.ticket.autoAssignment.button.deleteEverything.label": "Delete everything", + "pages.ticket.autoAssignment.notifications.save.message": "Data saved", + "pages.ticket.autoAssignment.notifications.delete.message": "Data deleted", + "pages.ticket.autoAssignment.notifications.error.message": "Failed to load data", "pages.condo.meter.warning.modal.HelpMessage": "New counters and readings will not be saved if you leave the page.", "pages.condo.meter.form.ConfirmDeleteTitle": "Remove meter?", "pages.condo.meter.form.ConfirmDeleteMessage": "All data, related to this property, will be lost", @@ -2135,6 +2151,8 @@ "api.ticket.TICKET_FOR_PHONE_DAY_LIMIT_REACHED": "Please try again tomorrow. You are not able to send more then {ticketLimit} tickets per day.", "api.ticket.SAME_TICKET_FOR_PHONE_DAY_LIMIT_REACHED": "You already sent this ticket.", "api.ticket.FEEDBACK_VALUE_MUST_BE_SPECIFIED": "When specifying a comment or additional evaluation parameters, you must specify the evaluation itself", + "api.ticket.TicketAutoAssignment.error.ASSIGNEE_NOT_FOUND": "There is no such employee (assignee) in the organization", + "api.ticket.TicketAutoAssignment.error.EXECUTOR_NOT_FOUND": "There is no such employee (executor) in the organization", "api.property.exportPropertiesToExcel.NOTHING_TO_EXPORT": "No properties are found to export for current organization", "api.property.create.sameAddressError": "Property with the same address already exists in selected organization", "api.meter.exportMeterReadings.NOTHING_TO_EXPORT": "No meter readings to export for specified organization", diff --git a/apps/condo/lang/ru/ru.json b/apps/condo/lang/ru/ru.json index e50ca18b6e9..98543899a53 100644 --- a/apps/condo/lang/ru/ru.json +++ b/apps/condo/lang/ru/ru.json @@ -880,6 +880,22 @@ "pages.condo.ticket.PaymentsAvailable": "Оплаты доступны в моб. приложении жителя", "pages.condo.ticket.PaymentsNotAvailable": "Оплаты не доступны в моб. приложении жителя", "pages.condo.ticket.WithoutDeadline": "У данного типа заявки отсутствует срок выполнения", + "pages.ticket.autoAssignment.deleteConfirm.message": "Удалить данные?", + "pages.ticket.autoAssignment.cancelConfirm.message": "Отменить изменения?", + "pages.ticket.autoAssignment.columns.classifier.title": "Классификатор", + "pages.ticket.autoAssignment.columns.assignee.title": "Ответственный", + "pages.ticket.autoAssignment.columns.executor.title": "Исполнитель", + "pages.ticket.autoAssignment.columns.operations.title": "Действия", + "pages.ticket.autoAssignment.button.create.label": "Создать", + "pages.ticket.autoAssignment.button.cancel.label": "Отменить", + "pages.ticket.autoAssignment.button.refresh.label": "Обновить", + "pages.ticket.autoAssignment.button.save.label": "Сохранить", + "pages.ticket.autoAssignment.button.edit.label": "Редактировать", + "pages.ticket.autoAssignment.button.delete.label": "Удалить", + "pages.ticket.autoAssignment.button.deleteEverything.label": "Удалить все", + "pages.ticket.autoAssignment.notifications.save.message": "Данные сохранены", + "pages.ticket.autoAssignment.notifications.delete.message": "Данные удалены", + "pages.ticket.autoAssignment.notifications.error.message": "Что-то пошло не так", "pages.condo.meter.warning.modal.HelpMessage": "Новые счетчики и показания не сохранятся, если вы уйдете со страницы.", "pages.condo.meter.form.ConfirmDeleteTitle": "Удалить счётчик?", "pages.condo.meter.form.ConfirmDeleteMessage": "Восстановить счетчик будет невозможно", @@ -2135,6 +2151,8 @@ "api.ticket.TICKET_FOR_PHONE_DAY_LIMIT_REACHED": "Попробуйте завтра. Сегодня управляющая компания уже получила {ticketLimit} заявок от вас. На главной странице раздела «Заявки» можно уточнить статус по каждой из них.", "api.ticket.SAME_TICKET_FOR_PHONE_DAY_LIMIT_REACHED": "Вы уже отправляли эту заявку. Управляющая компания уже занимается вашей проблемой. Уточнить статус можно на главной странице раздела «Заявки».", "api.ticket.FEEDBACK_VALUE_MUST_BE_SPECIFIED": "При указании комментария или дополнительных параметров оценки нужно указать саму оценку", + "api.ticket.TicketAutoAssignment.error.ASSIGNEE_NOT_FOUND": "Такого сотрудника нет в организации (ответственный)", + "api.ticket.TicketAutoAssignment.error.EXECUTOR_NOT_FOUND": "Такого сотрудника нет в организации (исполнитель)", "api.property.exportPropertiesToExcel.NOTHING_TO_EXPORT": "Нет домов для экспорта, которые связаны с данной организацией", "api.property.create.sameAddressError": "Дом с таким адресом уже создан в выбранной организации", "api.meter.exportMeterReadings.NOTHING_TO_EXPORT": "Нет показаний ИПУ для экспорта, которые связаны с указанной организацией", diff --git a/apps/condo/migrations/20240314185127-0373_ticketautoassignmenthistoryrecord_and_more.js b/apps/condo/migrations/20240314185127-0373_ticketautoassignmenthistoryrecord_and_more.js new file mode 100644 index 00000000000..f4ef1082c7f --- /dev/null +++ b/apps/condo/migrations/20240314185127-0373_ticketautoassignmenthistoryrecord_and_more.js @@ -0,0 +1,92 @@ +// auto generated by kmigrator +// KMIGRATOR:0373_ticketautoassignmenthistoryrecord_and_more:IyBHZW5lcmF0ZWQgYnkgRGphbmdvIDQuMC41IG9uIDIwMjQtMDMtMTQgMTM6NTIKCmZyb20gZGphbmdvLmRiIGltcG9ydCBtaWdyYXRpb25zLCBtb2RlbHMKaW1wb3J0IGRqYW5nby5kYi5tb2RlbHMuZGVsZXRpb24KCgpjbGFzcyBNaWdyYXRpb24obWlncmF0aW9ucy5NaWdyYXRpb24pOgoKICAgIGRlcGVuZGVuY2llcyA9IFsKICAgICAgICAoJ19kamFuZ29fc2NoZW1hJywgJzAzNzJfYXV0b18yMDI0MDMwN18xMTUzJyksCiAgICBdCgogICAgb3BlcmF0aW9ucyA9IFsKICAgICAgICBtaWdyYXRpb25zLkNyZWF0ZU1vZGVsKAogICAgICAgICAgICBuYW1lPSd0aWNrZXRhdXRvYXNzaWdubWVudGhpc3RvcnlyZWNvcmQnLAogICAgICAgICAgICBmaWVsZHM9WwogICAgICAgICAgICAgICAgKCdvcmdhbml6YXRpb24nLCBtb2RlbHMuVVVJREZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSkpLAogICAgICAgICAgICAgICAgKCdhc3NpZ25lZScsIG1vZGVscy5VVUlERmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSksCiAgICAgICAgICAgICAgICAoJ2V4ZWN1dG9yJywgbW9kZWxzLlVVSURGaWVsZChibGFuaz1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgnY2xhc3NpZmllcicsIG1vZGVscy5VVUlERmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSksCiAgICAgICAgICAgICAgICAoJ2lkJywgbW9kZWxzLlVVSURGaWVsZChwcmltYXJ5X2tleT1UcnVlLCBzZXJpYWxpemU9RmFsc2UpKSwKICAgICAgICAgICAgICAgICgndicsIG1vZGVscy5JbnRlZ2VyRmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSksCiAgICAgICAgICAgICAgICAoJ2NyZWF0ZWRBdCcsIG1vZGVscy5EYXRlVGltZUZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSkpLAogICAgICAgICAgICAgICAgKCd1cGRhdGVkQXQnLCBtb2RlbHMuRGF0ZVRpbWVGaWVsZChibGFuaz1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgnY3JlYXRlZEJ5JywgbW9kZWxzLlVVSURGaWVsZChibGFuaz1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgndXBkYXRlZEJ5JywgbW9kZWxzLlVVSURGaWVsZChibGFuaz1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgnZGVsZXRlZEF0JywgbW9kZWxzLkRhdGVUaW1lRmllbGQoYmxhbms9VHJ1ZSwgbnVsbD1UcnVlKSksCiAgICAgICAgICAgICAgICAoJ25ld0lkJywgbW9kZWxzLkpTT05GaWVsZChibGFuaz1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgnZHYnLCBtb2RlbHMuSW50ZWdlckZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSkpLAogICAgICAgICAgICAgICAgKCdzZW5kZXInLCBtb2RlbHMuSlNPTkZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSkpLAogICAgICAgICAgICAgICAgKCdoaXN0b3J5X2RhdGUnLCBtb2RlbHMuRGF0ZVRpbWVGaWVsZCgpKSwKICAgICAgICAgICAgICAgICgnaGlzdG9yeV9hY3Rpb24nLCBtb2RlbHMuQ2hhckZpZWxkKGNob2ljZXM9WygnYycsICdjJyksICgndScsICd1JyksICgnZCcsICdkJyldLCBtYXhfbGVuZ3RoPTUwKSksCiAgICAgICAgICAgICAgICAoJ2hpc3RvcnlfaWQnLCBtb2RlbHMuVVVJREZpZWxkKGRiX2luZGV4PVRydWUpKSwKICAgICAgICAgICAgXSwKICAgICAgICAgICAgb3B0aW9ucz17CiAgICAgICAgICAgICAgICAnZGJfdGFibGUnOiAnVGlja2V0QXV0b0Fzc2lnbm1lbnRIaXN0b3J5UmVjb3JkJywKICAgICAgICAgICAgfSwKICAgICAgICApLAogICAgICAgIG1pZ3JhdGlvbnMuQWRkRmllbGQoCiAgICAgICAgICAgIG1vZGVsX25hbWU9J3VzZXJyaWdodHNzZXQnLAogICAgICAgICAgICBuYW1lPSdjYW5NYW5hZ2VUaWNrZXRBdXRvQXNzaWdubWVudHMnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGRlZmF1bHQ9RmFsc2UpLAogICAgICAgICAgICBwcmVzZXJ2ZV9kZWZhdWx0PUZhbHNlLAogICAgICAgICksCiAgICAgICAgbWlncmF0aW9ucy5BZGRGaWVsZCgKICAgICAgICAgICAgbW9kZWxfbmFtZT0ndXNlcnJpZ2h0c3NldCcsCiAgICAgICAgICAgIG5hbWU9J2NhblJlYWRUaWNrZXRBdXRvQXNzaWdubWVudHMnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGRlZmF1bHQ9RmFsc2UpLAogICAgICAgICAgICBwcmVzZXJ2ZV9kZWZhdWx0PUZhbHNlLAogICAgICAgICksCiAgICAgICAgbWlncmF0aW9ucy5BZGRGaWVsZCgKICAgICAgICAgICAgbW9kZWxfbmFtZT0ndXNlcnJpZ2h0c3NldGhpc3RvcnlyZWNvcmQnLAogICAgICAgICAgICBuYW1lPSdjYW5NYW5hZ2VUaWNrZXRBdXRvQXNzaWdubWVudHMnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSksCiAgICAgICAgKSwKICAgICAgICBtaWdyYXRpb25zLkFkZEZpZWxkKAogICAgICAgICAgICBtb2RlbF9uYW1lPSd1c2VycmlnaHRzc2V0aGlzdG9yeXJlY29yZCcsCiAgICAgICAgICAgIG5hbWU9J2NhblJlYWRUaWNrZXRBdXRvQXNzaWdubWVudHMnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSksCiAgICAgICAgKSwKICAgICAgICBtaWdyYXRpb25zLkNyZWF0ZU1vZGVsKAogICAgICAgICAgICBuYW1lPSd0aWNrZXRhdXRvYXNzaWdubWVudCcsCiAgICAgICAgICAgIGZpZWxkcz1bCiAgICAgICAgICAgICAgICAoJ2lkJywgbW9kZWxzLlVVSURGaWVsZChwcmltYXJ5X2tleT1UcnVlLCBzZXJpYWxpemU9RmFsc2UpKSwKICAgICAgICAgICAgICAgICgndicsIG1vZGVscy5JbnRlZ2VyRmllbGQoZGVmYXVsdD0xKSksCiAgICAgICAgICAgICAgICAoJ2NyZWF0ZWRBdCcsIG1vZGVscy5EYXRlVGltZUZpZWxkKGJsYW5rPVRydWUsIGRiX2luZGV4PVRydWUsIG51bGw9VHJ1ZSkpLAogICAgICAgICAgICAgICAgKCd1cGRhdGVkQXQnLCBtb2RlbHMuRGF0ZVRpbWVGaWVsZChibGFuaz1UcnVlLCBkYl9pbmRleD1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgnZGVsZXRlZEF0JywgbW9kZWxzLkRhdGVUaW1lRmllbGQoYmxhbms9VHJ1ZSwgZGJfaW5kZXg9VHJ1ZSwgbnVsbD1UcnVlKSksCiAgICAgICAgICAgICAgICAoJ25ld0lkJywgbW9kZWxzLlVVSURGaWVsZChibGFuaz1UcnVlLCBudWxsPVRydWUpKSwKICAgICAgICAgICAgICAgICgnZHYnLCBtb2RlbHMuSW50ZWdlckZpZWxkKCkpLAogICAgICAgICAgICAgICAgKCdzZW5kZXInLCBtb2RlbHMuSlNPTkZpZWxkKCkpLAogICAgICAgICAgICAgICAgKCdhc3NpZ25lZScsIG1vZGVscy5Gb3JlaWduS2V5KGJsYW5rPVRydWUsIGRiX2NvbHVtbj0nYXNzaWduZWUnLCBudWxsPVRydWUsIG9uX2RlbGV0ZT1kamFuZ28uZGIubW9kZWxzLmRlbGV0aW9uLlNFVF9OVUxMLCByZWxhdGVkX25hbWU9JysnLCB0bz0nX2RqYW5nb19zY2hlbWEub3JnYW5pemF0aW9uZW1wbG95ZWUnKSksCiAgICAgICAgICAgICAgICAoJ2NsYXNzaWZpZXInLCBtb2RlbHMuRm9yZWlnbktleShkYl9jb2x1bW49J2NsYXNzaWZpZXInLCBvbl9kZWxldGU9ZGphbmdvLmRiLm1vZGVscy5kZWxldGlvbi5DQVNDQURFLCByZWxhdGVkX25hbWU9JysnLCB0bz0nX2RqYW5nb19zY2hlbWEudGlja2V0Y2xhc3NpZmllcicpKSwKICAgICAgICAgICAgICAgICgnY3JlYXRlZEJ5JywgbW9kZWxzLkZvcmVpZ25LZXkoYmxhbms9VHJ1ZSwgZGJfY29sdW1uPSdjcmVhdGVkQnknLCBudWxsPVRydWUsIG9uX2RlbGV0ZT1kamFuZ28uZGIubW9kZWxzLmRlbGV0aW9uLlNFVF9OVUxMLCByZWxhdGVkX25hbWU9JysnLCB0bz0nX2RqYW5nb19zY2hlbWEudXNlcicpKSwKICAgICAgICAgICAgICAgICgnZXhlY3V0b3InLCBtb2RlbHMuRm9yZWlnbktleShibGFuaz1UcnVlLCBkYl9jb2x1bW49J2V4ZWN1dG9yJywgbnVsbD1UcnVlLCBvbl9kZWxldGU9ZGphbmdvLmRiLm1vZGVscy5kZWxldGlvbi5TRVRfTlVMTCwgcmVsYXRlZF9uYW1lPScrJywgdG89J19kamFuZ29fc2NoZW1hLm9yZ2FuaXphdGlvbmVtcGxveWVlJykpLAogICAgICAgICAgICAgICAgKCdvcmdhbml6YXRpb24nLCBtb2RlbHMuRm9yZWlnbktleShkYl9jb2x1bW49J29yZ2FuaXphdGlvbicsIG9uX2RlbGV0ZT1kamFuZ28uZGIubW9kZWxzLmRlbGV0aW9uLkNBU0NBREUsIHJlbGF0ZWRfbmFtZT0nKycsIHRvPSdfZGphbmdvX3NjaGVtYS5vcmdhbml6YXRpb24nKSksCiAgICAgICAgICAgICAgICAoJ3VwZGF0ZWRCeScsIG1vZGVscy5Gb3JlaWduS2V5KGJsYW5rPVRydWUsIGRiX2NvbHVtbj0ndXBkYXRlZEJ5JywgbnVsbD1UcnVlLCBvbl9kZWxldGU9ZGphbmdvLmRiLm1vZGVscy5kZWxldGlvbi5TRVRfTlVMTCwgcmVsYXRlZF9uYW1lPScrJywgdG89J19kamFuZ29fc2NoZW1hLnVzZXInKSksCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgIG9wdGlvbnM9ewogICAgICAgICAgICAgICAgJ2RiX3RhYmxlJzogJ1RpY2tldEF1dG9Bc3NpZ25tZW50JywKICAgICAgICAgICAgfSwKICAgICAgICApLAogICAgICAgIG1pZ3JhdGlvbnMuQWRkQ29uc3RyYWludCgKICAgICAgICAgICAgbW9kZWxfbmFtZT0ndGlja2V0YXV0b2Fzc2lnbm1lbnQnLAogICAgICAgICAgICBjb25zdHJhaW50PW1vZGVscy5VbmlxdWVDb25zdHJhaW50KGNvbmRpdGlvbj1tb2RlbHMuUSgoJ2RlbGV0ZWRBdF9faXNudWxsJywgVHJ1ZSkpLCBmaWVsZHM9KCdvcmdhbml6YXRpb24nLCAnY2xhc3NpZmllcicpLCBuYW1lPSd0aWNrZXRfYXV0b19hc3NpZ25tZW50X3VuaXF1ZV9vcmdhbml6YXRpb25fY2xhc3NpZmllcicpLAogICAgICAgICksCiAgICBdCg== + +exports.up = async (knex) => { + await knex.raw(` + BEGIN; +-- +-- Create model ticketautoassignmenthistoryrecord +-- +CREATE TABLE "TicketAutoAssignmentHistoryRecord" ("organization" uuid NULL, "assignee" uuid NULL, "executor" uuid NULL, "classifier" uuid NULL, "id" uuid NOT NULL PRIMARY KEY, "v" integer NULL, "createdAt" timestamp with time zone NULL, "updatedAt" timestamp with time zone NULL, "createdBy" uuid NULL, "updatedBy" uuid NULL, "deletedAt" timestamp with time zone NULL, "newId" jsonb NULL, "dv" integer NULL, "sender" jsonb NULL, "history_date" timestamp with time zone NOT NULL, "history_action" varchar(50) NOT NULL, "history_id" uuid NOT NULL); +-- +-- Add field canManageTicketAutoAssignments to userrightsset +-- +ALTER TABLE "UserRightsSet" ADD COLUMN "canManageTicketAutoAssignments" boolean DEFAULT false NOT NULL; +ALTER TABLE "UserRightsSet" ALTER COLUMN "canManageTicketAutoAssignments" DROP DEFAULT; +-- +-- Add field canReadTicketAutoAssignments to userrightsset +-- +ALTER TABLE "UserRightsSet" ADD COLUMN "canReadTicketAutoAssignments" boolean DEFAULT false NOT NULL; +ALTER TABLE "UserRightsSet" ALTER COLUMN "canReadTicketAutoAssignments" DROP DEFAULT; +-- +-- Add field canManageTicketAutoAssignments to userrightssethistoryrecord +-- +ALTER TABLE "UserRightsSetHistoryRecord" ADD COLUMN "canManageTicketAutoAssignments" boolean NULL; +-- +-- Add field canReadTicketAutoAssignments to userrightssethistoryrecord +-- +ALTER TABLE "UserRightsSetHistoryRecord" ADD COLUMN "canReadTicketAutoAssignments" boolean NULL; +-- +-- Create model ticketautoassignment +-- +CREATE TABLE "TicketAutoAssignment" ("id" uuid NOT NULL PRIMARY KEY, "v" integer NOT NULL, "createdAt" timestamp with time zone NULL, "updatedAt" timestamp with time zone NULL, "deletedAt" timestamp with time zone NULL, "newId" uuid NULL, "dv" integer NOT NULL, "sender" jsonb NOT NULL, "assignee" uuid NULL, "classifier" uuid NOT NULL, "createdBy" uuid NULL, "executor" uuid NULL, "organization" uuid NOT NULL, "updatedBy" uuid NULL); +-- +-- Create constraint ticket_auto_assignment_unique_organization_classifier on model ticketautoassignment +-- +CREATE UNIQUE INDEX "ticket_auto_assignment_unique_organization_classifier" ON "TicketAutoAssignment" ("organization", "classifier") WHERE "deletedAt" IS NULL; +CREATE INDEX "TicketAutoAssignmentHistoryRecord_history_id_3f61146e" ON "TicketAutoAssignmentHistoryRecord" ("history_id"); +ALTER TABLE "TicketAutoAssignment" ADD CONSTRAINT "TicketAutoAssignment_assignee_0bd6c3a4_fk_Organizat" FOREIGN KEY ("assignee") REFERENCES "OrganizationEmployee" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "TicketAutoAssignment" ADD CONSTRAINT "TicketAutoAssignment_classifier_6d6a10fb_fk_TicketClassifier_id" FOREIGN KEY ("classifier") REFERENCES "TicketClassifier" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "TicketAutoAssignment" ADD CONSTRAINT "TicketAutoAssignment_createdBy_e171c8ac_fk_User_id" FOREIGN KEY ("createdBy") REFERENCES "User" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "TicketAutoAssignment" ADD CONSTRAINT "TicketAutoAssignment_executor_0fb1e6f5_fk_Organizat" FOREIGN KEY ("executor") REFERENCES "OrganizationEmployee" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "TicketAutoAssignment" ADD CONSTRAINT "TicketAutoAssignment_organization_abfc2267_fk_Organization_id" FOREIGN KEY ("organization") REFERENCES "Organization" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "TicketAutoAssignment" ADD CONSTRAINT "TicketAutoAssignment_updatedBy_97aaff52_fk_User_id" FOREIGN KEY ("updatedBy") REFERENCES "User" ("id") DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX "TicketAutoAssignment_createdAt_1b0fc93d" ON "TicketAutoAssignment" ("createdAt"); +CREATE INDEX "TicketAutoAssignment_updatedAt_fd5ea320" ON "TicketAutoAssignment" ("updatedAt"); +CREATE INDEX "TicketAutoAssignment_deletedAt_aeaca327" ON "TicketAutoAssignment" ("deletedAt"); +CREATE INDEX "TicketAutoAssignment_assignee_0bd6c3a4" ON "TicketAutoAssignment" ("assignee"); +CREATE INDEX "TicketAutoAssignment_classifier_6d6a10fb" ON "TicketAutoAssignment" ("classifier"); +CREATE INDEX "TicketAutoAssignment_createdBy_e171c8ac" ON "TicketAutoAssignment" ("createdBy"); +CREATE INDEX "TicketAutoAssignment_executor_0fb1e6f5" ON "TicketAutoAssignment" ("executor"); +CREATE INDEX "TicketAutoAssignment_organization_abfc2267" ON "TicketAutoAssignment" ("organization"); +CREATE INDEX "TicketAutoAssignment_updatedBy_97aaff52" ON "TicketAutoAssignment" ("updatedBy"); +COMMIT; + + `) +} + +exports.down = async (knex) => { + await knex.raw(` + BEGIN; +-- +-- Create constraint ticket_auto_assignment_unique_organization_classifier on model ticketautoassignment +-- +DROP INDEX IF EXISTS "ticket_auto_assignment_unique_organization_classifier"; +-- +-- Create model ticketautoassignment +-- +DROP TABLE "TicketAutoAssignment" CASCADE; +-- +-- Add field canReadTicketAutoAssignments to userrightssethistoryrecord +-- +ALTER TABLE "UserRightsSetHistoryRecord" DROP COLUMN "canReadTicketAutoAssignments" CASCADE; +-- +-- Add field canManageTicketAutoAssignments to userrightssethistoryrecord +-- +ALTER TABLE "UserRightsSetHistoryRecord" DROP COLUMN "canManageTicketAutoAssignments" CASCADE; +-- +-- Add field canReadTicketAutoAssignments to userrightsset +-- +ALTER TABLE "UserRightsSet" DROP COLUMN "canReadTicketAutoAssignments" CASCADE; +-- +-- Add field canManageTicketAutoAssignments to userrightsset +-- +ALTER TABLE "UserRightsSet" DROP COLUMN "canManageTicketAutoAssignments" CASCADE; +-- +-- Create model ticketautoassignmenthistoryrecord +-- +DROP TABLE "TicketAutoAssignmentHistoryRecord" CASCADE; +COMMIT; + + `) +} diff --git a/apps/condo/pages/ticket/autoAssignment/index.tsx b/apps/condo/pages/ticket/autoAssignment/index.tsx new file mode 100644 index 00000000000..9487dd9f2b0 --- /dev/null +++ b/apps/condo/pages/ticket/autoAssignment/index.tsx @@ -0,0 +1,614 @@ +/** + * This page is not visible to users! + * This is a service page! + * This page is available only with direct access (canManageTicketAutoAssignments and canReadTicketAutoAssignments)! + */ + +import { + SortTicketAutoAssignmentsBy, + TicketAutoAssignment as ITicketAutoAssignment, + TicketAutoAssignmentCreateInput as ITicketAutoAssignmentCreateInput, + TicketAutoAssignmentUpdateInput as ITicketAutoAssignmentUpdateInput, + TicketClassifier as ITicketClassifier, +} from '@app/condo/schema' +import { Button, Col, Form, Popconfirm, Row, Select, Table, Typography, notification } from 'antd' +import { FormItemProps } from 'antd/lib/form/FormItem' +import { gql } from 'graphql-tag' +import chunk from 'lodash/chunk' +import get from 'lodash/get' +import isArray from 'lodash/isArray' +import isUndefined from 'lodash/isUndefined' +import React, { useCallback, useEffect, useMemo, useState } from 'react' + +import { useQuery, useApolloClient } from '@open-condo/next/apollo' +import { useAuth } from '@open-condo/next/auth' +import { useIntl } from '@open-condo/next/intl' +import { useOrganization } from '@open-condo/next/organization' + +import { AccessDeniedPage } from '@condo/domains/common/components/containers/AccessDeniedPage' +import LoadingOrErrorPage from '@condo/domains/common/components/containers/LoadingOrErrorPage' +import { PermissionsRequired } from '@condo/domains/organization/components/OrganizationRequired' +import { OrganizationEmployee } from '@condo/domains/organization/utils/clientSchema' +import { TicketAutoAssignment } from '@condo/domains/ticket/utils/clientSchema' +import { ClassifiersQueryLocal } from '@condo/domains/ticket/utils/clientSchema/classifierSearch' + + +interface ITicketAutoAssignmentPage extends React.FC { + headerAction?: JSX.Element + requiredAccess?: React.FC +} + +interface Item { + key?: string + id?: string + organization?: string + classifier?: string + assignee?: string + executor?: string +} + +interface EditableCellProps { + editing?: boolean + children?: React.ReactNode + inputNode?: React.ReactNode + cellProps?: React.HTMLAttributes + formItemProps?: FormItemProps, +} + +const RELATIONS = ['assignee', 'organization', 'executor', 'classifier'] +const DISCONNECT_ON_NULL = ['executor', 'assignee'] + +type MutationType = ITicketAutoAssignmentCreateInput | ITicketAutoAssignmentUpdateInput +function formValuesProcessor (formValues: Item): MutationType { + const result: MutationType = {} + for (const key of Object.keys(formValues)) { + const isRelation = RELATIONS.includes(key) + if (isRelation) { + if (DISCONNECT_ON_NULL.includes(key) && formValues[key] === null) { + result[key] = { disconnectAll: true } + } else if (formValues[key]) { + result[key] = { connect: { id: formValues[key] } } + } + } else if (!isUndefined(formValues[key])) { + result[key] = formValues[key] + } + } + + return result +} + +const EditableCell: React.FC = ({ + editing, + children, + inputNode, + formItemProps, + ...cellProps +}) => { + return ( + + {editing ? ( + + {inputNode} + + ) : ( + children + )} + + ) +} + +const getClassifierName = (classifier) => { + const place = get(classifier, 'place.name', '').trim() + const category = get(classifier, 'category.name', '').trim() + const problem = get(classifier, 'problem.name', '').trim() + return [place, category, problem].filter(Boolean).join(' » ') +} + +const EMPTY_TABLE_DATA = [{}] + +const TicketAutoAssignmentPage: ITicketAutoAssignmentPage = () => { + const intl = useIntl() + const NoMessage = intl.formatMessage({ id: 'No' }) + const YesMessage = intl.formatMessage({ id: 'Yes' }) + const DeleteConfirmMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.deleteConfirm.message' }) + const CancelConfirmMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.cancelConfirm.message' }) + const ClassifierMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.columns.classifier.title' }) + const AssigneeMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.columns.assignee.title' }) + const ExecutorMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.columns.executor.title' }) + const OperationsMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.columns.operations.title' }) + const CreateMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.create.label' }) + const CancelMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.cancel.label' }) + const RefreshMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.refresh.label' }) + const SaveMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.save.label' }) + const EditMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.edit.label' }) + const DeleteMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.delete.label' }) + const DeleteEverythingMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.button.deleteEverything.label' }) + const SaveNotificationMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.notifications.save.message' }) + const DeleteNotificationMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.notifications.delete.message' }) + const ErrorNotificationMessage = intl.formatMessage({ id: 'pages.ticket.autoAssignment.notifications.error.message' }) + + const { organization } = useOrganization() + const organizationId = useMemo(() => get(organization, 'id', null), [organization]) + + const client = useApolloClient() + const ClassifierLoader = useMemo(() => new ClassifiersQueryLocal(client), [client]) + const [classifierLoading, setClassifierLoading] = useState(true) + const [classifiers, setClassifiers] = useState>([]) + + const [api, contextHolder] = notification.useNotification() + + const [form] = Form.useForm() + + const updateTicketAutoAssignment = TicketAutoAssignment.useUpdate({}) + const createTicketAutoAssignment = TicketAutoAssignment.useCreate({}) + const deleteTicketAutoAssignment = TicketAutoAssignment.useSoftDelete() + const deleteManyTicketAutoAssignment = TicketAutoAssignment.useSoftDeleteMany() + const { objs: rules, refetch: refetchRoles, allDataLoaded: rulesLoaded, error: rolesError, loading: rolesLoading } = TicketAutoAssignment.useAllObjects({ + where: { + organization: { id: organizationId }, + }, + sortBy: [SortTicketAutoAssignmentsBy.CreatedAtDesc, SortTicketAutoAssignmentsBy.IdDesc], + }, { + fetchPolicy: 'network-only', + }) + const { loading: employeesLoading, objs: employees, error: employeesError } = OrganizationEmployee.useObjects({ + where: { + organization: { id: organizationId }, + }, + }) + + const [editingKey, setEditingKey] = useState('') + const [creating, setCreating] = useState(false) + const isEditing = useCallback((record: ITicketAutoAssignment) => record.id === editingKey, [editingKey]) + const [saving, setSaving] = useState(false) + + const loading = classifierLoading || !rulesLoaded || employeesLoading || rolesLoading + const error = rolesError || employeesError + + const edit = useCallback((record: Partial) => { + form.setFieldsValue({ + classifier: get(record, 'classifier.id', null), + assignee: get(record, 'assignee.id', null), + executor: get(record, 'executor.id', null), + }) + setEditingKey(record.id) + }, [form]) + + const cancel = useCallback(() => { + setEditingKey('') + setCreating(false) + form.resetFields() + }, [form]) + + const save = useCallback(async (prevRecord: ITicketAutoAssignment) => { + try { + if (saving) return + setSaving(true) + const prevClassifierId = get(prevRecord, 'classifier.id', null) + const prevExecutorId = get(prevRecord, 'executor.id', null) + const prevAssigneeId = get(prevRecord, 'assignee.id', null) + const id = get(prevRecord, 'id') + const row = (await form.validateFields()) as Item + const classifier = get(row, 'classifier', null) + const executor = get(row, 'executor', null) + const assignee = get(row, 'assignee', null) + const newData = { classifier, executor, assignee } + + const hasChanges = id && (prevClassifierId !== classifier || prevExecutorId !== executor || prevAssigneeId !== assignee) + + if (creating) { + await createTicketAutoAssignment(formValuesProcessor({ ...newData, organization: organizationId })) + } else if (hasChanges) { + await updateTicketAutoAssignment(formValuesProcessor(newData), { id }) + } + if (creating || hasChanges) { + await refetchRoles() + } + api.success({ + message: SaveNotificationMessage, + }) + } catch (error) { + api.error({ + message: ErrorNotificationMessage, + description: error.message, + }) + console.error('Failed to save data:', error) + } finally { + cancel() + setSaving(false) + } + }, [saving, form, creating, api, SaveNotificationMessage, organizationId, ErrorNotificationMessage, cancel]) + + const deleteItem = useCallback(async (item) => { + try { + if (saving) return + setSaving(true) + await deleteTicketAutoAssignment({ id: item.id }) + await refetchRoles() + api.success({ + message: DeleteNotificationMessage, + }) + cancel() + } catch (error) { + api.error({ + message: ErrorNotificationMessage, + description: error.message, + }) + console.error('Failed to delete data:', error) + } finally { + setSaving(false) + } + }, [DeleteNotificationMessage, ErrorNotificationMessage, api, cancel, saving]) + + const deleteAll = useCallback(async () => { + try { + if (saving) return + setSaving(true) + const chunks = chunk(rules, 50) + for (const chunk of chunks) { + await deleteManyTicketAutoAssignment(chunk) + } + api.success({ + message: DeleteNotificationMessage, + }) + } catch (error) { + api.error({ + message: ErrorNotificationMessage, + description: error.message, + }) + console.error('Failed to delete all data:', error) + } finally { + setSaving(false) + } + }, [DeleteNotificationMessage, ErrorNotificationMessage, api, rules, saving]) + + const handleSaveClick = useCallback((record) => () => save(record), [save]) + + const employeeOptions = useMemo(() => employees.map(item => ({ + value: item.id, + label: item.name, + })), [employees]) + + const classifierOptions = useMemo(() => classifiers.map(item => ({ + value: item.id, + label: getClassifierName(item), + })), [classifiers]) + + const classifierFilters = useMemo( + () => classifierOptions.map(item => ({ ...item, text: item.label })).sort((a, b) => a.label.localeCompare(b.label)), + [classifierOptions] + ) + + const getVisibleClassifiers = useCallback((record: ITicketAutoAssignment) => { + return classifierOptions.filter((option) => { + const isCurrentClassifier = option.value === get(record, 'classifier.id') + const notUsedClassifier = !rules.find(rule => rule.classifier.id === option.value) + return isCurrentClassifier || notUsedClassifier + }) + }, [rules, classifierOptions]) + + const renderClassifierInput = useCallback((record: ITicketAutoAssignment) => ( + a.label.localeCompare(b.label)} + /> + ), [employeeOptions]) + + const columns = useMemo(() => [ + { + title: ClassifierMessage, + dataIndex: ['classifier', 'id'], + key: 'classifier', + width: 300, + editable: true, + render: (_, item: ITicketAutoAssignment) => getClassifierName(item.classifier), + inputNode: renderClassifierInput, + formItemProps: { + name: 'classifier', + rules: [{ + required: true, + message: 'Required', + }], + }, + sorter: (a, b) => { + return getClassifierName(a.classifier).localeCompare(getClassifierName(b.classifier)) + }, + defaultSortOrder: 'ascend', + filters: classifierFilters, + filterSearch: true, + onFilter: (value: string, record: ITicketAutoAssignment) => { + return get(record, 'classifier.id') === value + }, + }, + { + title: ExecutorMessage, + dataIndex: ['executor', 'name'], + key: 'executor', + inputNode: renderEmployeeInput, + formItemProps: { + name: 'executor', + }, + width: 250, + editable: true, + sorter: (a, b) => { + const aName = get(a, 'executor.name') + const bName = get(b, 'executor.name') + if (!aName || !bName) return !aName ? 1 : -1 + return aName.localeCompare(bName) + }, + filterSearch: true, + filters: employeeOptions.map(item => ({ ...item, text: item.label })).sort((a, b) => a.label.localeCompare(b.label)), + onFilter: (value: string, record) => { + return get(record, 'executor.id') === value + }, + }, + { + title: AssigneeMessage, + dataIndex: ['assignee', 'name'], + key: 'assignee', + width: 250, + editable: true, + inputNode: renderEmployeeInput, + formItemProps: { + name: 'assignee', + }, + filterSearch: true, + filters: employeeOptions.map(item => ({ ...item, text: item.label })).sort((a, b) => a.label.localeCompare(b.label)), + onFilter: (value: string, record) => { + return get(record, 'assignee.id') === value + }, + sorter: (a, b) => { + const aName = get(a, 'assignee.name') + const bName = get(b, 'assignee.name') + if (!aName || !bName) return !aName ? 1 : -1 + return aName.localeCompare(bName) + }, + }, + { + title: OperationsMessage, + dataIndex: 'operation', + key: 'operation', + width: 100, + render: (_, record: ITicketAutoAssignment) => { + const editable = isEditing(record) + const disabled = saving + return editable ? ( + + save(record)} style={{ marginRight: 8 }}> + {SaveMessage} + + + {CancelMessage} + + + ) : ( + + edit(record)} style={{ marginRight: 8 }}> + {EditMessage} + + deleteItem(record)} + cancelText={NoMessage} + okText={YesMessage} + > + {DeleteMessage} + + + ) + }, + }, + ], [AssigneeMessage, CancelConfirmMessage, CancelMessage, ClassifierMessage, DeleteConfirmMessage, DeleteMessage, EditMessage, ExecutorMessage, NoMessage, OperationsMessage, SaveMessage, YesMessage, cancel, classifierFilters, deleteItem, edit, employeeOptions, isEditing, renderClassifierInput, renderEmployeeInput, save, saving]) + + const columnsForData = useMemo(() => columns.map((col) => { + if (!col.editable) return col + + return { + ...col, + onCell: (record: ITicketAutoAssignment) => { + return { + formItemProps: col.formItemProps, + editing: isEditing(record), + inputNode: col.inputNode(record), + } + }, + } + }), [columns, isEditing]) + + const columnsForCreating = useMemo(() => columns.map((col) => { + if (!col.editable && col.key !== 'operation') return col + + if (col.key === 'operation') { + return { + ...col, + render: (_, record: ITicketAutoAssignment) => { + const disabled = saving + return ( + + + {SaveMessage} + + + {CancelMessage} + + + ) + }, + } + } + + return { + ...col, + onCell: (record: ITicketAutoAssignment) => { + return { + formItemProps: col.formItemProps, + editing: true, + inputNode: col.inputNode(record), + } + }, + } + }), [CancelConfirmMessage, CancelMessage, SaveMessage, cancel, columns, handleSaveClick, saving]) + + useEffect(() => { + setClassifierLoading(true) + ClassifierLoader.init().then(async () => { + const values = await ClassifierLoader.search('', 'rules', null, 500) + setClassifiers(values) + setClassifierLoading(false) + }) + return () => { + ClassifierLoader.clear() + } + }, []) + + if (error) { + return + } + + return ( + <> + {contextHolder} +
+ + + + + + + + + + { + creating && ( + + + + ) + } + +
+ + + + + ) +} + +const USER_WITH_RIGHTS_SET = gql` + query GetUserWithUserRightsSet ($userId: ID!) { + objs: allUsers(where: {id: $userId}) { + id + rightsSet { canManageTicketAutoAssignments canReadTicketAutoAssignments } + } + } +` + +const TicketAutoAssignmentPermissionRequired: React.FC = ({ children }) => { + const { user } = useAuth() + const userId = get(user, 'id', null) + + const { loading, error, data } = useQuery(USER_WITH_RIGHTS_SET, { + variables: { + userId: userId, + }, + }) + const userWithRightSets = useMemo(() => { + if (!data && (loading || error)) return null + const objs = get(data, 'objs') + if (!isArray(objs) || objs.length !== 1) { + console.warn('Should be 1 item userWithRightSets') + return null + } + return objs[0] + }, [data, error, loading]) + + const canOpenPage = useMemo( + () => get(userWithRightSets, 'rightsSet.canManageTicketAutoAssignments') + && get(userWithRightSets, 'rightsSet.canReadTicketAutoAssignments'), + [userWithRightSets] + ) + + if (loading || error) { + return + } + + if (!canOpenPage) { + return + } + + return ( + + ) +} + +TicketAutoAssignmentPage.requiredAccess = TicketAutoAssignmentPermissionRequired + +export default TicketAutoAssignmentPage diff --git a/apps/condo/schema.graphql b/apps/condo/schema.graphql index b6bc1b03485..b97ae0b17f4 100644 --- a/apps/condo/schema.graphql +++ b/apps/condo/schema.graphql @@ -3214,6 +3214,8 @@ type UserRightsSetHistoryRecord { canManageOrganizations: Boolean canReadTickets: Boolean canManageTickets: Boolean + canReadTicketAutoAssignments: Boolean + canManageTicketAutoAssignments: Boolean canReadOidcClients: Boolean canManageOidcClients: Boolean canReadUsers: Boolean @@ -3310,6 +3312,10 @@ input UserRightsSetHistoryRecordWhereInput { canReadTickets_not: Boolean canManageTickets: Boolean canManageTickets_not: Boolean + canReadTicketAutoAssignments: Boolean + canReadTicketAutoAssignments_not: Boolean + canManageTicketAutoAssignments: Boolean + canManageTicketAutoAssignments_not: Boolean canReadOidcClients: Boolean canReadOidcClients_not: Boolean canManageOidcClients: Boolean @@ -3463,6 +3469,10 @@ enum SortUserRightsSetHistoryRecordsBy { canReadTickets_DESC canManageTickets_ASC canManageTickets_DESC + canReadTicketAutoAssignments_ASC + canReadTicketAutoAssignments_DESC + canManageTicketAutoAssignments_ASC + canManageTicketAutoAssignments_DESC canReadOidcClients_ASC canReadOidcClients_DESC canManageOidcClients_ASC @@ -3525,6 +3535,8 @@ input UserRightsSetHistoryRecordUpdateInput { canManageOrganizations: Boolean canReadTickets: Boolean canManageTickets: Boolean + canReadTicketAutoAssignments: Boolean + canManageTicketAutoAssignments: Boolean canReadOidcClients: Boolean canManageOidcClients: Boolean canReadUsers: Boolean @@ -3580,6 +3592,8 @@ input UserRightsSetHistoryRecordCreateInput { canManageOrganizations: Boolean canReadTickets: Boolean canManageTickets: Boolean + canReadTicketAutoAssignments: Boolean + canManageTicketAutoAssignments: Boolean canReadOidcClients: Boolean canManageOidcClients: Boolean canReadUsers: Boolean @@ -3726,6 +3740,14 @@ type UserRightsSet { """ canManageTickets: Boolean + """ Enables a user with the given UserRightsSet to view all entities of model "TicketAutoAssignment" as support / admin users do + """ + canReadTicketAutoAssignments: Boolean + + """ Enables a user with the given UserRightsSet to create, update or soft-delete entities of model "TicketAutoAssignment" similar to support users + """ + canManageTicketAutoAssignments: Boolean + """ Enables a user with the given UserRightsSet to view all entities of model "OidcClient" as support / admin users do """ canReadOidcClients: Boolean @@ -3853,6 +3875,10 @@ input UserRightsSetWhereInput { canReadTickets_not: Boolean canManageTickets: Boolean canManageTickets_not: Boolean + canReadTicketAutoAssignments: Boolean + canReadTicketAutoAssignments_not: Boolean + canManageTicketAutoAssignments: Boolean + canManageTicketAutoAssignments_not: Boolean canReadOidcClients: Boolean canReadOidcClients_not: Boolean canManageOidcClients: Boolean @@ -3986,6 +4012,10 @@ enum SortUserRightsSetsBy { canReadTickets_DESC canManageTickets_ASC canManageTickets_DESC + canReadTicketAutoAssignments_ASC + canReadTicketAutoAssignments_DESC + canManageTicketAutoAssignments_ASC + canManageTicketAutoAssignments_DESC canReadOidcClients_ASC canReadOidcClients_DESC canManageOidcClients_ASC @@ -4048,6 +4078,8 @@ input UserRightsSetUpdateInput { canManageOrganizations: Boolean canReadTickets: Boolean canManageTickets: Boolean + canReadTicketAutoAssignments: Boolean + canManageTicketAutoAssignments: Boolean canReadOidcClients: Boolean canManageOidcClients: Boolean canReadUsers: Boolean @@ -4100,6 +4132,8 @@ input UserRightsSetCreateInput { canManageOrganizations: Boolean canReadTickets: Boolean canManageTickets: Boolean + canReadTicketAutoAssignments: Boolean + canManageTicketAutoAssignments: Boolean canReadOidcClients: Boolean canManageOidcClients: Boolean canReadUsers: Boolean @@ -36646,6 +36680,398 @@ input CallRecordFragmentsCreateInput { data: CallRecordFragmentCreateInput } +enum TicketAutoAssignmentHistoryRecordHistoryActionType { + c + u + d +} + +""" A keystone list """ +type TicketAutoAssignmentHistoryRecord { + """ + This virtual field will be resolved in one of the following ways (in this order): + 1. Execution of 'labelResolver' set on the TicketAutoAssignmentHistoryRecord List config, or + 2. As an alias to the field set on 'labelField' in the TicketAutoAssignmentHistoryRecord List config, or + 3. As an alias to a 'name' field on the TicketAutoAssignmentHistoryRecord List (if one exists), or + 4. As an alias to the 'id' field on the TicketAutoAssignmentHistoryRecord List. + """ + _label_: String + organization: String + assignee: String + executor: String + classifier: String + id: ID! + v: Int + createdAt: String + updatedAt: String + createdBy: String + updatedBy: String + deletedAt: String + newId: JSON + dv: Int + sender: JSON + history_date: String + history_action: TicketAutoAssignmentHistoryRecordHistoryActionType + history_id: String +} + +input TicketAutoAssignmentHistoryRecordWhereInput { + AND: [TicketAutoAssignmentHistoryRecordWhereInput] + OR: [TicketAutoAssignmentHistoryRecordWhereInput] + organization: String + organization_not: String + organization_in: [String] + organization_not_in: [String] + assignee: String + assignee_not: String + assignee_in: [String] + assignee_not_in: [String] + executor: String + executor_not: String + executor_in: [String] + executor_not_in: [String] + classifier: String + classifier_not: String + classifier_in: [String] + classifier_not_in: [String] + id: ID + id_not: ID + id_in: [ID] + id_not_in: [ID] + v: Int + v_not: Int + v_lt: Int + v_lte: Int + v_gt: Int + v_gte: Int + v_in: [Int] + v_not_in: [Int] + createdAt: String + createdAt_not: String + createdAt_lt: String + createdAt_lte: String + createdAt_gt: String + createdAt_gte: String + createdAt_in: [String] + createdAt_not_in: [String] + updatedAt: String + updatedAt_not: String + updatedAt_lt: String + updatedAt_lte: String + updatedAt_gt: String + updatedAt_gte: String + updatedAt_in: [String] + updatedAt_not_in: [String] + createdBy: String + createdBy_not: String + createdBy_in: [String] + createdBy_not_in: [String] + updatedBy: String + updatedBy_not: String + updatedBy_in: [String] + updatedBy_not_in: [String] + deletedAt: String + deletedAt_not: String + deletedAt_lt: String + deletedAt_lte: String + deletedAt_gt: String + deletedAt_gte: String + deletedAt_in: [String] + deletedAt_not_in: [String] + newId: JSON + newId_not: JSON + newId_in: [JSON] + newId_not_in: [JSON] + dv: Int + dv_not: Int + dv_lt: Int + dv_lte: Int + dv_gt: Int + dv_gte: Int + dv_in: [Int] + dv_not_in: [Int] + sender: JSON + sender_not: JSON + sender_in: [JSON] + sender_not_in: [JSON] + history_date: String + history_date_not: String + history_date_lt: String + history_date_lte: String + history_date_gt: String + history_date_gte: String + history_date_in: [String] + history_date_not_in: [String] + history_action: TicketAutoAssignmentHistoryRecordHistoryActionType + history_action_not: TicketAutoAssignmentHistoryRecordHistoryActionType + history_action_in: [TicketAutoAssignmentHistoryRecordHistoryActionType] + history_action_not_in: [TicketAutoAssignmentHistoryRecordHistoryActionType] + history_id: String + history_id_not: String + history_id_in: [String] + history_id_not_in: [String] +} + +input TicketAutoAssignmentHistoryRecordWhereUniqueInput { + id: ID! +} + +enum SortTicketAutoAssignmentHistoryRecordsBy { + id_ASC + id_DESC + v_ASC + v_DESC + createdAt_ASC + createdAt_DESC + updatedAt_ASC + updatedAt_DESC + deletedAt_ASC + deletedAt_DESC + dv_ASC + dv_DESC + history_date_ASC + history_date_DESC + history_action_ASC + history_action_DESC +} + +input TicketAutoAssignmentHistoryRecordUpdateInput { + organization: String + assignee: String + executor: String + classifier: String + v: Int + createdAt: String + updatedAt: String + createdBy: String + updatedBy: String + deletedAt: String + newId: JSON + dv: Int + sender: JSON + history_date: String + history_action: TicketAutoAssignmentHistoryRecordHistoryActionType + history_id: String +} + +input TicketAutoAssignmentHistoryRecordsUpdateInput { + id: ID! + data: TicketAutoAssignmentHistoryRecordUpdateInput +} + +input TicketAutoAssignmentHistoryRecordCreateInput { + organization: String + assignee: String + executor: String + classifier: String + v: Int + createdAt: String + updatedAt: String + createdBy: String + updatedBy: String + deletedAt: String + newId: JSON + dv: Int + sender: JSON + history_date: String + history_action: TicketAutoAssignmentHistoryRecordHistoryActionType + history_id: String +} + +input TicketAutoAssignmentHistoryRecordsCreateInput { + data: TicketAutoAssignmentHistoryRecordCreateInput +} + +""" This schema helps decides who should be assigned to ticket as executor and assignee +""" +type TicketAutoAssignment { + """ + This virtual field will be resolved in one of the following ways (in this order): + 1. Execution of 'labelResolver' set on the TicketAutoAssignment List config, or + 2. As an alias to the field set on 'labelField' in the TicketAutoAssignment List config, or + 3. As an alias to a 'name' field on the TicketAutoAssignment List (if one exists), or + 4. As an alias to the 'id' field on the TicketAutoAssignment List. + """ + _label_: String + + """ Ref to the organization. The object will be deleted if the organization ceases to exist + """ + organization: Organization + + """ An employee who will be appointed as assignee of ticket. If "null", then the field will remain empty and dispatcher will have to fill it in independently in UI + """ + assignee: OrganizationEmployee + + """ An employee who will be appointed as executor of ticketIf "null", then the field will remain empty and dispatcher will have to fill it in independently in UI + """ + executor: OrganizationEmployee + + """ Ticket classifier """ + classifier: TicketClassifier + id: ID! + v: Int + createdAt: String + updatedAt: String + + """ Identifies a user, which has created this record. It is a technical connection, that can represent real users, as well as automated systems (bots, scripts). This field should not participate in business logic. + """ + createdBy: User + + """ Identifies a user, which has updated this record. It is a technical connection, that can represent real users, as well as automated systems (bots, scripts). This field should not participate in business logic. + """ + updatedBy: User + deletedAt: String + newId: String + + """ Data structure Version """ + dv: Int + + """ Client-side device identification used for the anti-fraud detection. Example `{ "dv":1, "fingerprint":"VaxSw2aXZa"}`. Where the `fingerprint` should be the same for the same devices and it's not linked to the user ID. It's the device ID like browser / mobile application / remote system + """ + sender: SenderField +} + +input TicketAutoAssignmentWhereInput { + AND: [TicketAutoAssignmentWhereInput] + OR: [TicketAutoAssignmentWhereInput] + organization: OrganizationWhereInput + organization_is_null: Boolean + assignee: OrganizationEmployeeWhereInput + assignee_is_null: Boolean + executor: OrganizationEmployeeWhereInput + executor_is_null: Boolean + classifier: TicketClassifierWhereInput + classifier_is_null: Boolean + id: ID + id_not: ID + id_in: [ID] + id_not_in: [ID] + v: Int + v_not: Int + v_lt: Int + v_lte: Int + v_gt: Int + v_gte: Int + v_in: [Int] + v_not_in: [Int] + createdAt: String + createdAt_not: String + createdAt_lt: String + createdAt_lte: String + createdAt_gt: String + createdAt_gte: String + createdAt_in: [String] + createdAt_not_in: [String] + updatedAt: String + updatedAt_not: String + updatedAt_lt: String + updatedAt_lte: String + updatedAt_gt: String + updatedAt_gte: String + updatedAt_in: [String] + updatedAt_not_in: [String] + createdBy: UserWhereInput + createdBy_is_null: Boolean + updatedBy: UserWhereInput + updatedBy_is_null: Boolean + deletedAt: String + deletedAt_not: String + deletedAt_lt: String + deletedAt_lte: String + deletedAt_gt: String + deletedAt_gte: String + deletedAt_in: [String] + deletedAt_not_in: [String] + newId: String + newId_not: String + newId_in: [String] + newId_not_in: [String] + dv: Int + dv_not: Int + dv_lt: Int + dv_lte: Int + dv_gt: Int + dv_gte: Int + dv_in: [Int] + dv_not_in: [Int] + sender: SenderFieldInput + sender_not: SenderFieldInput + sender_in: [SenderFieldInput] + sender_not_in: [SenderFieldInput] +} + +input TicketAutoAssignmentWhereUniqueInput { + id: ID! +} + +enum SortTicketAutoAssignmentsBy { + organization_ASC + organization_DESC + assignee_ASC + assignee_DESC + executor_ASC + executor_DESC + classifier_ASC + classifier_DESC + id_ASC + id_DESC + v_ASC + v_DESC + createdAt_ASC + createdAt_DESC + updatedAt_ASC + updatedAt_DESC + createdBy_ASC + createdBy_DESC + updatedBy_ASC + updatedBy_DESC + deletedAt_ASC + deletedAt_DESC + dv_ASC + dv_DESC +} + +input TicketAutoAssignmentUpdateInput { + organization: OrganizationRelateToOneInput + assignee: OrganizationEmployeeRelateToOneInput + executor: OrganizationEmployeeRelateToOneInput + classifier: TicketClassifierRelateToOneInput + v: Int + createdAt: String + updatedAt: String + createdBy: UserRelateToOneInput + updatedBy: UserRelateToOneInput + deletedAt: String + newId: String + dv: Int + sender: SenderFieldInput +} + +input TicketAutoAssignmentsUpdateInput { + id: ID! + data: TicketAutoAssignmentUpdateInput +} + +input TicketAutoAssignmentCreateInput { + organization: OrganizationRelateToOneInput + assignee: OrganizationEmployeeRelateToOneInput + executor: OrganizationEmployeeRelateToOneInput + classifier: TicketClassifierRelateToOneInput + v: Int + createdAt: String + updatedAt: String + createdBy: UserRelateToOneInput + updatedBy: UserRelateToOneInput + deletedAt: String + newId: String + dv: Int + sender: SenderFieldInput +} + +input TicketAutoAssignmentsCreateInput { + data: TicketAutoAssignmentCreateInput +} + enum MessageHistoryRecordHistoryActionType { c u @@ -75511,6 +75937,36 @@ type Query { """ Retrieve the meta-data for the CallRecordFragment list. """ _CallRecordFragmentsMeta: _ListMeta + """ Search for all TicketAutoAssignmentHistoryRecord items which match the where clause. + """ + allTicketAutoAssignmentHistoryRecords(where: TicketAutoAssignmentHistoryRecordWhereInput, search: String, sortBy: [SortTicketAutoAssignmentHistoryRecordsBy!], orderBy: String, first: Int, skip: Int): [TicketAutoAssignmentHistoryRecord] + + """ Search for the TicketAutoAssignmentHistoryRecord item with the matching ID. + """ + TicketAutoAssignmentHistoryRecord(where: TicketAutoAssignmentHistoryRecordWhereUniqueInput!): TicketAutoAssignmentHistoryRecord + + """ Perform a meta-query on all TicketAutoAssignmentHistoryRecord items which match the where clause. + """ + _allTicketAutoAssignmentHistoryRecordsMeta(where: TicketAutoAssignmentHistoryRecordWhereInput, search: String, sortBy: [SortTicketAutoAssignmentHistoryRecordsBy!], orderBy: String, first: Int, skip: Int): _QueryMeta + + """ Retrieve the meta-data for the TicketAutoAssignmentHistoryRecord list. + """ + _TicketAutoAssignmentHistoryRecordsMeta: _ListMeta + + """ Search for all TicketAutoAssignment items which match the where clause. + """ + allTicketAutoAssignments(where: TicketAutoAssignmentWhereInput, search: String, sortBy: [SortTicketAutoAssignmentsBy!], orderBy: String, first: Int, skip: Int): [TicketAutoAssignment] + + """ Search for the TicketAutoAssignment item with the matching ID. """ + TicketAutoAssignment(where: TicketAutoAssignmentWhereUniqueInput!): TicketAutoAssignment + + """ Perform a meta-query on all TicketAutoAssignment items which match the where clause. + """ + _allTicketAutoAssignmentsMeta(where: TicketAutoAssignmentWhereInput, search: String, sortBy: [SortTicketAutoAssignmentsBy!], orderBy: String, first: Int, skip: Int): _QueryMeta + + """ Retrieve the meta-data for the TicketAutoAssignment list. """ + _TicketAutoAssignmentsMeta: _ListMeta + """ Search for all MessageHistoryRecord items which match the where clause. """ allMessageHistoryRecords(where: MessageHistoryRecordWhereInput, search: String, sortBy: [SortMessageHistoryRecordsBy!], orderBy: String, first: Int, skip: Int): [MessageHistoryRecord] @@ -79758,6 +80214,42 @@ type Mutation { """ Delete multiple CallRecordFragment items by ID. """ deleteCallRecordFragments(ids: [ID!]): [CallRecordFragment] + """ Create a single TicketAutoAssignmentHistoryRecord item. """ + createTicketAutoAssignmentHistoryRecord(data: TicketAutoAssignmentHistoryRecordCreateInput): TicketAutoAssignmentHistoryRecord + + """ Create multiple TicketAutoAssignmentHistoryRecord items. """ + createTicketAutoAssignmentHistoryRecords(data: [TicketAutoAssignmentHistoryRecordsCreateInput]): [TicketAutoAssignmentHistoryRecord] + + """ Update a single TicketAutoAssignmentHistoryRecord item by ID. """ + updateTicketAutoAssignmentHistoryRecord(id: ID!, data: TicketAutoAssignmentHistoryRecordUpdateInput): TicketAutoAssignmentHistoryRecord + + """ Update multiple TicketAutoAssignmentHistoryRecord items by ID. """ + updateTicketAutoAssignmentHistoryRecords(data: [TicketAutoAssignmentHistoryRecordsUpdateInput]): [TicketAutoAssignmentHistoryRecord] + + """ Delete a single TicketAutoAssignmentHistoryRecord item by ID. """ + deleteTicketAutoAssignmentHistoryRecord(id: ID!): TicketAutoAssignmentHistoryRecord + + """ Delete multiple TicketAutoAssignmentHistoryRecord items by ID. """ + deleteTicketAutoAssignmentHistoryRecords(ids: [ID!]): [TicketAutoAssignmentHistoryRecord] + + """ Create a single TicketAutoAssignment item. """ + createTicketAutoAssignment(data: TicketAutoAssignmentCreateInput): TicketAutoAssignment + + """ Create multiple TicketAutoAssignment items. """ + createTicketAutoAssignments(data: [TicketAutoAssignmentsCreateInput]): [TicketAutoAssignment] + + """ Update a single TicketAutoAssignment item by ID. """ + updateTicketAutoAssignment(id: ID!, data: TicketAutoAssignmentUpdateInput): TicketAutoAssignment + + """ Update multiple TicketAutoAssignment items by ID. """ + updateTicketAutoAssignments(data: [TicketAutoAssignmentsUpdateInput]): [TicketAutoAssignment] + + """ Delete a single TicketAutoAssignment item by ID. """ + deleteTicketAutoAssignment(id: ID!): TicketAutoAssignment + + """ Delete multiple TicketAutoAssignment items by ID. """ + deleteTicketAutoAssignments(ids: [ID!]): [TicketAutoAssignment] + """ Create a single MessageHistoryRecord item. """ createMessageHistoryRecord(data: MessageHistoryRecordCreateInput): MessageHistoryRecord diff --git a/apps/condo/schema.ts b/apps/condo/schema.ts index c30694e8fdc..60f67ebec20 100644 --- a/apps/condo/schema.ts +++ b/apps/condo/schema.ts @@ -36454,6 +36454,30 @@ export type Mutation = { deleteCallRecordFragment?: Maybe; /** Delete multiple CallRecordFragment items by ID. */ deleteCallRecordFragments?: Maybe>>; + /** Create a single TicketAutoAssignmentHistoryRecord item. */ + createTicketAutoAssignmentHistoryRecord?: Maybe; + /** Create multiple TicketAutoAssignmentHistoryRecord items. */ + createTicketAutoAssignmentHistoryRecords?: Maybe>>; + /** Update a single TicketAutoAssignmentHistoryRecord item by ID. */ + updateTicketAutoAssignmentHistoryRecord?: Maybe; + /** Update multiple TicketAutoAssignmentHistoryRecord items by ID. */ + updateTicketAutoAssignmentHistoryRecords?: Maybe>>; + /** Delete a single TicketAutoAssignmentHistoryRecord item by ID. */ + deleteTicketAutoAssignmentHistoryRecord?: Maybe; + /** Delete multiple TicketAutoAssignmentHistoryRecord items by ID. */ + deleteTicketAutoAssignmentHistoryRecords?: Maybe>>; + /** Create a single TicketAutoAssignment item. */ + createTicketAutoAssignment?: Maybe; + /** Create multiple TicketAutoAssignment items. */ + createTicketAutoAssignments?: Maybe>>; + /** Update a single TicketAutoAssignment item by ID. */ + updateTicketAutoAssignment?: Maybe; + /** Update multiple TicketAutoAssignment items by ID. */ + updateTicketAutoAssignments?: Maybe>>; + /** Delete a single TicketAutoAssignment item by ID. */ + deleteTicketAutoAssignment?: Maybe; + /** Delete multiple TicketAutoAssignment items by ID. */ + deleteTicketAutoAssignments?: Maybe>>; /** Create a single MessageHistoryRecord item. */ createMessageHistoryRecord?: Maybe; /** Create multiple MessageHistoryRecord items. */ @@ -44636,6 +44660,68 @@ export type MutationDeleteCallRecordFragmentsArgs = { }; +export type MutationCreateTicketAutoAssignmentHistoryRecordArgs = { + data?: Maybe; +}; + + +export type MutationCreateTicketAutoAssignmentHistoryRecordsArgs = { + data?: Maybe>>; +}; + + +export type MutationUpdateTicketAutoAssignmentHistoryRecordArgs = { + id: Scalars['ID']; + data?: Maybe; +}; + + +export type MutationUpdateTicketAutoAssignmentHistoryRecordsArgs = { + data?: Maybe>>; +}; + + +export type MutationDeleteTicketAutoAssignmentHistoryRecordArgs = { + id: Scalars['ID']; +}; + + +export type MutationDeleteTicketAutoAssignmentHistoryRecordsArgs = { + ids?: Maybe>; +}; + + +export type MutationCreateTicketAutoAssignmentArgs = { + data?: Maybe; +}; + + +export type MutationCreateTicketAutoAssignmentsArgs = { + data?: Maybe>>; +}; + + +export type MutationUpdateTicketAutoAssignmentArgs = { + id: Scalars['ID']; + data?: Maybe; +}; + + +export type MutationUpdateTicketAutoAssignmentsArgs = { + data?: Maybe>>; +}; + + +export type MutationDeleteTicketAutoAssignmentArgs = { + id: Scalars['ID']; +}; + + +export type MutationDeleteTicketAutoAssignmentsArgs = { + ids?: Maybe>; +}; + + export type MutationCreateMessageHistoryRecordArgs = { data?: Maybe; }; @@ -61751,6 +61837,22 @@ export type Query = { _allCallRecordFragmentsMeta?: Maybe<_QueryMeta>; /** Retrieve the meta-data for the CallRecordFragment list. */ _CallRecordFragmentsMeta?: Maybe<_ListMeta>; + /** Search for all TicketAutoAssignmentHistoryRecord items which match the where clause. */ + allTicketAutoAssignmentHistoryRecords?: Maybe>>; + /** Search for the TicketAutoAssignmentHistoryRecord item with the matching ID. */ + TicketAutoAssignmentHistoryRecord?: Maybe; + /** Perform a meta-query on all TicketAutoAssignmentHistoryRecord items which match the where clause. */ + _allTicketAutoAssignmentHistoryRecordsMeta?: Maybe<_QueryMeta>; + /** Retrieve the meta-data for the TicketAutoAssignmentHistoryRecord list. */ + _TicketAutoAssignmentHistoryRecordsMeta?: Maybe<_ListMeta>; + /** Search for all TicketAutoAssignment items which match the where clause. */ + allTicketAutoAssignments?: Maybe>>; + /** Search for the TicketAutoAssignment item with the matching ID. */ + TicketAutoAssignment?: Maybe; + /** Perform a meta-query on all TicketAutoAssignment items which match the where clause. */ + _allTicketAutoAssignmentsMeta?: Maybe<_QueryMeta>; + /** Retrieve the meta-data for the TicketAutoAssignment list. */ + _TicketAutoAssignmentsMeta?: Maybe<_ListMeta>; /** Search for all MessageHistoryRecord items which match the where clause. */ allMessageHistoryRecords?: Maybe>>; /** Search for the MessageHistoryRecord item with the matching ID. */ @@ -65996,6 +66098,56 @@ export type Query_AllCallRecordFragmentsMetaArgs = { }; +export type QueryAllTicketAutoAssignmentHistoryRecordsArgs = { + where?: Maybe; + search?: Maybe; + sortBy?: Maybe>; + orderBy?: Maybe; + first?: Maybe; + skip?: Maybe; +}; + + +export type QueryTicketAutoAssignmentHistoryRecordArgs = { + where: TicketAutoAssignmentHistoryRecordWhereUniqueInput; +}; + + +export type Query_AllTicketAutoAssignmentHistoryRecordsMetaArgs = { + where?: Maybe; + search?: Maybe; + sortBy?: Maybe>; + orderBy?: Maybe; + first?: Maybe; + skip?: Maybe; +}; + + +export type QueryAllTicketAutoAssignmentsArgs = { + where?: Maybe; + search?: Maybe; + sortBy?: Maybe>; + orderBy?: Maybe; + first?: Maybe; + skip?: Maybe; +}; + + +export type QueryTicketAutoAssignmentArgs = { + where: TicketAutoAssignmentWhereUniqueInput; +}; + + +export type Query_AllTicketAutoAssignmentsMetaArgs = { + where?: Maybe; + search?: Maybe; + sortBy?: Maybe>; + orderBy?: Maybe; + first?: Maybe; + skip?: Maybe; +}; + + export type QueryAllMessageHistoryRecordsArgs = { where?: Maybe; search?: Maybe; @@ -79442,6 +79594,52 @@ export enum SortTelegramUserChatsBy { DvDesc = 'dv_DESC' } +export enum SortTicketAutoAssignmentHistoryRecordsBy { + IdAsc = 'id_ASC', + IdDesc = 'id_DESC', + VAsc = 'v_ASC', + VDesc = 'v_DESC', + CreatedAtAsc = 'createdAt_ASC', + CreatedAtDesc = 'createdAt_DESC', + UpdatedAtAsc = 'updatedAt_ASC', + UpdatedAtDesc = 'updatedAt_DESC', + DeletedAtAsc = 'deletedAt_ASC', + DeletedAtDesc = 'deletedAt_DESC', + DvAsc = 'dv_ASC', + DvDesc = 'dv_DESC', + HistoryDateAsc = 'history_date_ASC', + HistoryDateDesc = 'history_date_DESC', + HistoryActionAsc = 'history_action_ASC', + HistoryActionDesc = 'history_action_DESC' +} + +export enum SortTicketAutoAssignmentsBy { + OrganizationAsc = 'organization_ASC', + OrganizationDesc = 'organization_DESC', + AssigneeAsc = 'assignee_ASC', + AssigneeDesc = 'assignee_DESC', + ExecutorAsc = 'executor_ASC', + ExecutorDesc = 'executor_DESC', + ClassifierAsc = 'classifier_ASC', + ClassifierDesc = 'classifier_DESC', + IdAsc = 'id_ASC', + IdDesc = 'id_DESC', + VAsc = 'v_ASC', + VDesc = 'v_DESC', + CreatedAtAsc = 'createdAt_ASC', + CreatedAtDesc = 'createdAt_DESC', + UpdatedAtAsc = 'updatedAt_ASC', + UpdatedAtDesc = 'updatedAt_DESC', + CreatedByAsc = 'createdBy_ASC', + CreatedByDesc = 'createdBy_DESC', + UpdatedByAsc = 'updatedBy_ASC', + UpdatedByDesc = 'updatedBy_DESC', + DeletedAtAsc = 'deletedAt_ASC', + DeletedAtDesc = 'deletedAt_DESC', + DvAsc = 'dv_ASC', + DvDesc = 'dv_DESC' +} + export enum SortTicketCategoryClassifierHistoryRecordsBy { NameAsc = 'name_ASC', NameDesc = 'name_DESC', @@ -80799,6 +80997,10 @@ export enum SortUserRightsSetHistoryRecordsBy { CanReadTicketsDesc = 'canReadTickets_DESC', CanManageTicketsAsc = 'canManageTickets_ASC', CanManageTicketsDesc = 'canManageTickets_DESC', + CanReadTicketAutoAssignmentsAsc = 'canReadTicketAutoAssignments_ASC', + CanReadTicketAutoAssignmentsDesc = 'canReadTicketAutoAssignments_DESC', + CanManageTicketAutoAssignmentsAsc = 'canManageTicketAutoAssignments_ASC', + CanManageTicketAutoAssignmentsDesc = 'canManageTicketAutoAssignments_DESC', CanReadOidcClientsAsc = 'canReadOidcClients_ASC', CanReadOidcClientsDesc = 'canReadOidcClients_DESC', CanManageOidcClientsAsc = 'canManageOidcClients_ASC', @@ -80888,6 +81090,10 @@ export enum SortUserRightsSetsBy { CanReadTicketsDesc = 'canReadTickets_DESC', CanManageTicketsAsc = 'canManageTickets_ASC', CanManageTicketsDesc = 'canManageTickets_DESC', + CanReadTicketAutoAssignmentsAsc = 'canReadTicketAutoAssignments_ASC', + CanReadTicketAutoAssignmentsDesc = 'canReadTicketAutoAssignments_DESC', + CanManageTicketAutoAssignmentsAsc = 'canManageTicketAutoAssignments_ASC', + CanManageTicketAutoAssignmentsDesc = 'canManageTicketAutoAssignments_DESC', CanReadOidcClientsAsc = 'canReadOidcClients_ASC', CanReadOidcClientsDesc = 'canReadOidcClients_DESC', CanManageOidcClientsAsc = 'canManageOidcClients_ASC', @@ -81681,6 +81887,339 @@ export type TicketAnalyticsReportOutput = { ticketLabels?: Maybe>>; }; +/** This schema helps decides who should be assigned to ticket as executor and assignee */ +export type TicketAutoAssignment = { + __typename?: 'TicketAutoAssignment'; + /** + * This virtual field will be resolved in one of the following ways (in this order): + * 1. Execution of 'labelResolver' set on the TicketAutoAssignment List config, or + * 2. As an alias to the field set on 'labelField' in the TicketAutoAssignment List config, or + * 3. As an alias to a 'name' field on the TicketAutoAssignment List (if one exists), or + * 4. As an alias to the 'id' field on the TicketAutoAssignment List. + */ + _label_?: Maybe; + /** Ref to the organization. The object will be deleted if the organization ceases to exist */ + organization?: Maybe; + /** An employee who will be appointed as assignee of ticket. If "null", then the field will remain empty and dispatcher will have to fill it in independently in UI */ + assignee?: Maybe; + /** An employee who will be appointed as executor of ticketIf "null", then the field will remain empty and dispatcher will have to fill it in independently in UI */ + executor?: Maybe; + /** Ticket classifier */ + classifier?: Maybe; + id: Scalars['ID']; + v?: Maybe; + createdAt?: Maybe; + updatedAt?: Maybe; + /** Identifies a user, which has created this record. It is a technical connection, that can represent real users, as well as automated systems (bots, scripts). This field should not participate in business logic. */ + createdBy?: Maybe; + /** Identifies a user, which has updated this record. It is a technical connection, that can represent real users, as well as automated systems (bots, scripts). This field should not participate in business logic. */ + updatedBy?: Maybe; + deletedAt?: Maybe; + newId?: Maybe; + /** Data structure Version */ + dv?: Maybe; + /** Client-side device identification used for the anti-fraud detection. Example `{ "dv":1, "fingerprint":"VaxSw2aXZa"}`. Where the `fingerprint` should be the same for the same devices and it's not linked to the user ID. It's the device ID like browser / mobile application / remote system */ + sender?: Maybe; +}; + +export type TicketAutoAssignmentCreateInput = { + organization?: Maybe; + assignee?: Maybe; + executor?: Maybe; + classifier?: Maybe; + v?: Maybe; + createdAt?: Maybe; + updatedAt?: Maybe; + createdBy?: Maybe; + updatedBy?: Maybe; + deletedAt?: Maybe; + newId?: Maybe; + dv?: Maybe; + sender?: Maybe; +}; + +/** A keystone list */ +export type TicketAutoAssignmentHistoryRecord = { + __typename?: 'TicketAutoAssignmentHistoryRecord'; + /** + * This virtual field will be resolved in one of the following ways (in this order): + * 1. Execution of 'labelResolver' set on the TicketAutoAssignmentHistoryRecord List config, or + * 2. As an alias to the field set on 'labelField' in the TicketAutoAssignmentHistoryRecord List config, or + * 3. As an alias to a 'name' field on the TicketAutoAssignmentHistoryRecord List (if one exists), or + * 4. As an alias to the 'id' field on the TicketAutoAssignmentHistoryRecord List. + */ + _label_?: Maybe; + organization?: Maybe; + assignee?: Maybe; + executor?: Maybe; + classifier?: Maybe; + id: Scalars['ID']; + v?: Maybe; + createdAt?: Maybe; + updatedAt?: Maybe; + createdBy?: Maybe; + updatedBy?: Maybe; + deletedAt?: Maybe; + newId?: Maybe; + dv?: Maybe; + sender?: Maybe; + history_date?: Maybe; + history_action?: Maybe; + history_id?: Maybe; +}; + +export type TicketAutoAssignmentHistoryRecordCreateInput = { + organization?: Maybe; + assignee?: Maybe; + executor?: Maybe; + classifier?: Maybe; + v?: Maybe; + createdAt?: Maybe; + updatedAt?: Maybe; + createdBy?: Maybe; + updatedBy?: Maybe; + deletedAt?: Maybe; + newId?: Maybe; + dv?: Maybe; + sender?: Maybe; + history_date?: Maybe; + history_action?: Maybe; + history_id?: Maybe; +}; + +export enum TicketAutoAssignmentHistoryRecordHistoryActionType { + C = 'c', + U = 'u', + D = 'd' +} + +export type TicketAutoAssignmentHistoryRecordUpdateInput = { + organization?: Maybe; + assignee?: Maybe; + executor?: Maybe; + classifier?: Maybe; + v?: Maybe; + createdAt?: Maybe; + updatedAt?: Maybe; + createdBy?: Maybe; + updatedBy?: Maybe; + deletedAt?: Maybe; + newId?: Maybe; + dv?: Maybe; + sender?: Maybe; + history_date?: Maybe; + history_action?: Maybe; + history_id?: Maybe; +}; + +export type TicketAutoAssignmentHistoryRecordWhereInput = { + AND?: Maybe>>; + OR?: Maybe>>; + organization?: Maybe; + organization_not?: Maybe; + organization_in?: Maybe>>; + organization_not_in?: Maybe>>; + assignee?: Maybe; + assignee_not?: Maybe; + assignee_in?: Maybe>>; + assignee_not_in?: Maybe>>; + executor?: Maybe; + executor_not?: Maybe; + executor_in?: Maybe>>; + executor_not_in?: Maybe>>; + classifier?: Maybe; + classifier_not?: Maybe; + classifier_in?: Maybe>>; + classifier_not_in?: Maybe>>; + id?: Maybe; + id_not?: Maybe; + id_in?: Maybe>>; + id_not_in?: Maybe>>; + v?: Maybe; + v_not?: Maybe; + v_lt?: Maybe; + v_lte?: Maybe; + v_gt?: Maybe; + v_gte?: Maybe; + v_in?: Maybe>>; + v_not_in?: Maybe>>; + createdAt?: Maybe; + createdAt_not?: Maybe; + createdAt_lt?: Maybe; + createdAt_lte?: Maybe; + createdAt_gt?: Maybe; + createdAt_gte?: Maybe; + createdAt_in?: Maybe>>; + createdAt_not_in?: Maybe>>; + updatedAt?: Maybe; + updatedAt_not?: Maybe; + updatedAt_lt?: Maybe; + updatedAt_lte?: Maybe; + updatedAt_gt?: Maybe; + updatedAt_gte?: Maybe; + updatedAt_in?: Maybe>>; + updatedAt_not_in?: Maybe>>; + createdBy?: Maybe; + createdBy_not?: Maybe; + createdBy_in?: Maybe>>; + createdBy_not_in?: Maybe>>; + updatedBy?: Maybe; + updatedBy_not?: Maybe; + updatedBy_in?: Maybe>>; + updatedBy_not_in?: Maybe>>; + deletedAt?: Maybe; + deletedAt_not?: Maybe; + deletedAt_lt?: Maybe; + deletedAt_lte?: Maybe; + deletedAt_gt?: Maybe; + deletedAt_gte?: Maybe; + deletedAt_in?: Maybe>>; + deletedAt_not_in?: Maybe>>; + newId?: Maybe; + newId_not?: Maybe; + newId_in?: Maybe>>; + newId_not_in?: Maybe>>; + dv?: Maybe; + dv_not?: Maybe; + dv_lt?: Maybe; + dv_lte?: Maybe; + dv_gt?: Maybe; + dv_gte?: Maybe; + dv_in?: Maybe>>; + dv_not_in?: Maybe>>; + sender?: Maybe; + sender_not?: Maybe; + sender_in?: Maybe>>; + sender_not_in?: Maybe>>; + history_date?: Maybe; + history_date_not?: Maybe; + history_date_lt?: Maybe; + history_date_lte?: Maybe; + history_date_gt?: Maybe; + history_date_gte?: Maybe; + history_date_in?: Maybe>>; + history_date_not_in?: Maybe>>; + history_action?: Maybe; + history_action_not?: Maybe; + history_action_in?: Maybe>>; + history_action_not_in?: Maybe>>; + history_id?: Maybe; + history_id_not?: Maybe; + history_id_in?: Maybe>>; + history_id_not_in?: Maybe>>; +}; + +export type TicketAutoAssignmentHistoryRecordWhereUniqueInput = { + id: Scalars['ID']; +}; + +export type TicketAutoAssignmentHistoryRecordsCreateInput = { + data?: Maybe; +}; + +export type TicketAutoAssignmentHistoryRecordsUpdateInput = { + id: Scalars['ID']; + data?: Maybe; +}; + +export type TicketAutoAssignmentUpdateInput = { + organization?: Maybe; + assignee?: Maybe; + executor?: Maybe; + classifier?: Maybe; + v?: Maybe; + createdAt?: Maybe; + updatedAt?: Maybe; + createdBy?: Maybe; + updatedBy?: Maybe; + deletedAt?: Maybe; + newId?: Maybe; + dv?: Maybe; + sender?: Maybe; +}; + +export type TicketAutoAssignmentWhereInput = { + AND?: Maybe>>; + OR?: Maybe>>; + organization?: Maybe; + organization_is_null?: Maybe; + assignee?: Maybe; + assignee_is_null?: Maybe; + executor?: Maybe; + executor_is_null?: Maybe; + classifier?: Maybe; + classifier_is_null?: Maybe; + id?: Maybe; + id_not?: Maybe; + id_in?: Maybe>>; + id_not_in?: Maybe>>; + v?: Maybe; + v_not?: Maybe; + v_lt?: Maybe; + v_lte?: Maybe; + v_gt?: Maybe; + v_gte?: Maybe; + v_in?: Maybe>>; + v_not_in?: Maybe>>; + createdAt?: Maybe; + createdAt_not?: Maybe; + createdAt_lt?: Maybe; + createdAt_lte?: Maybe; + createdAt_gt?: Maybe; + createdAt_gte?: Maybe; + createdAt_in?: Maybe>>; + createdAt_not_in?: Maybe>>; + updatedAt?: Maybe; + updatedAt_not?: Maybe; + updatedAt_lt?: Maybe; + updatedAt_lte?: Maybe; + updatedAt_gt?: Maybe; + updatedAt_gte?: Maybe; + updatedAt_in?: Maybe>>; + updatedAt_not_in?: Maybe>>; + createdBy?: Maybe; + createdBy_is_null?: Maybe; + updatedBy?: Maybe; + updatedBy_is_null?: Maybe; + deletedAt?: Maybe; + deletedAt_not?: Maybe; + deletedAt_lt?: Maybe; + deletedAt_lte?: Maybe; + deletedAt_gt?: Maybe; + deletedAt_gte?: Maybe; + deletedAt_in?: Maybe>>; + deletedAt_not_in?: Maybe>>; + newId?: Maybe; + newId_not?: Maybe; + newId_in?: Maybe>>; + newId_not_in?: Maybe>>; + dv?: Maybe; + dv_not?: Maybe; + dv_lt?: Maybe; + dv_lte?: Maybe; + dv_gt?: Maybe; + dv_gte?: Maybe; + dv_in?: Maybe>>; + dv_not_in?: Maybe>>; + sender?: Maybe; + sender_not?: Maybe; + sender_in?: Maybe>>; + sender_not_in?: Maybe>>; +}; + +export type TicketAutoAssignmentWhereUniqueInput = { + id: Scalars['ID']; +}; + +export type TicketAutoAssignmentsCreateInput = { + data?: Maybe; +}; + +export type TicketAutoAssignmentsUpdateInput = { + id: Scalars['ID']; + data?: Maybe; +}; + /** Describes what type of work needs to be done to fix incident */ export type TicketCategoryClassifier = { __typename?: 'TicketCategoryClassifier'; @@ -92026,6 +92565,10 @@ export type UserRightsSet = { canReadTickets?: Maybe; /** Enables a user with the given UserRightsSet to create, update or soft-delete entities of model "Ticket" similar to support users */ canManageTickets?: Maybe; + /** Enables a user with the given UserRightsSet to view all entities of model "TicketAutoAssignment" as support / admin users do */ + canReadTicketAutoAssignments?: Maybe; + /** Enables a user with the given UserRightsSet to create, update or soft-delete entities of model "TicketAutoAssignment" similar to support users */ + canManageTicketAutoAssignments?: Maybe; /** Enables a user with the given UserRightsSet to view all entities of model "OidcClient" as support / admin users do */ canReadOidcClients?: Maybe; /** Enables a user with the given UserRightsSet to create, update or soft-delete entities of model "OidcClient" similar to support users */ @@ -92086,6 +92629,8 @@ export type UserRightsSetCreateInput = { canManageOrganizations?: Maybe; canReadTickets?: Maybe; canManageTickets?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments?: Maybe; canReadOidcClients?: Maybe; canManageOidcClients?: Maybe; canReadUsers?: Maybe; @@ -92143,6 +92688,8 @@ export type UserRightsSetHistoryRecord = { canManageOrganizations?: Maybe; canReadTickets?: Maybe; canManageTickets?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments?: Maybe; canReadOidcClients?: Maybe; canManageOidcClients?: Maybe; canReadUsers?: Maybe; @@ -92194,6 +92741,8 @@ export type UserRightsSetHistoryRecordCreateInput = { canManageOrganizations?: Maybe; canReadTickets?: Maybe; canManageTickets?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments?: Maybe; canReadOidcClients?: Maybe; canManageOidcClients?: Maybe; canReadUsers?: Maybe; @@ -92250,6 +92799,8 @@ export type UserRightsSetHistoryRecordUpdateInput = { canManageOrganizations?: Maybe; canReadTickets?: Maybe; canManageTickets?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments?: Maybe; canReadOidcClients?: Maybe; canManageOidcClients?: Maybe; canReadUsers?: Maybe; @@ -92345,6 +92896,10 @@ export type UserRightsSetHistoryRecordWhereInput = { canReadTickets_not?: Maybe; canManageTickets?: Maybe; canManageTickets_not?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canReadTicketAutoAssignments_not?: Maybe; + canManageTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments_not?: Maybe; canReadOidcClients?: Maybe; canReadOidcClients_not?: Maybe; canManageOidcClients?: Maybe; @@ -92487,6 +93042,8 @@ export type UserRightsSetUpdateInput = { canManageOrganizations?: Maybe; canReadTickets?: Maybe; canManageTickets?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments?: Maybe; canReadOidcClients?: Maybe; canManageOidcClients?: Maybe; canReadUsers?: Maybe; @@ -92579,6 +93136,10 @@ export type UserRightsSetWhereInput = { canReadTickets_not?: Maybe; canManageTickets?: Maybe; canManageTickets_not?: Maybe; + canReadTicketAutoAssignments?: Maybe; + canReadTicketAutoAssignments_not?: Maybe; + canManageTicketAutoAssignments?: Maybe; + canManageTicketAutoAssignments_not?: Maybe; canReadOidcClients?: Maybe; canReadOidcClients_not?: Maybe; canManageOidcClients?: Maybe; diff --git a/packages/codegen/generate.hooks.ts b/packages/codegen/generate.hooks.ts index c09259740c2..262764a22bd 100644 --- a/packages/codegen/generate.hooks.ts +++ b/packages/codegen/generate.hooks.ts @@ -5,7 +5,7 @@ import { TypedDocumentNode } from '@graphql-typed-document-node/core' import dayjs from 'dayjs' import { DocumentNode } from 'graphql' import isFunction from 'lodash/isFunction' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { getClientSideSenderInfo } from '@open-condo/codegen/utils/userId' import { useMutation, useQuery } from '@open-condo/next/apollo' @@ -369,9 +369,19 @@ export function generateReactHooks< const { objs, count, error, loading, refetch: _refetch, fetchMore, stopPolling } = useObjects(variables, options) const [data, setData] = useState(objs) const [fetchMoreError, setFetchMoreError] = useState() + const innerLoadingRef = useRef(false) + useEffect(() => { + innerLoadingRef.current = loading + }, [loading]) + + // NOTE: returns only the first part of the data const refetch: IRefetchType = useCallback((...args) => { setData([]) + // NOTE: refetch from Apollo does not immediately update loading status. + // Because of this, the resulting array contains outdated data that has already been loaded + // That's why we make our own loading indicator to prevent this from happening. + innerLoadingRef.current = true return _refetch(...args) }, [_refetch]) @@ -381,7 +391,7 @@ export function generateReactHooks< useEffect(() => { const isAllDataLoaded = objs.length === count || data.length === count - if (isAllDataLoaded || loading || error || fetchMoreError) { + if (isAllDataLoaded || loading || error || fetchMoreError || innerLoadingRef.current) { return }