From cbcf04ff1db4d44b19bcf1b935cdc00f73c5ad90 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 16 Jun 2022 10:36:45 +0700 Subject: [PATCH] Leads import/upgrade (#2087) Signed-off-by: Andrey Sobolev --- .vscode/launch.json | 5 +- common/config/rush/pnpm-lock.yaml | 40 ++- common/scripts/build_docker.sh | 3 +- dev/docker-compose.yaml | 40 +-- dev/tool/package.json | 4 +- dev/tool/src/index.ts | 10 +- dev/tool/src/lead-importer.ts | 492 ++++++++++++++++++++++++++++++ models/view/src/index.ts | 8 +- 8 files changed, 569 insertions(+), 33 deletions(-) create mode 100644 dev/tool/src/lead-importer.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 192a8d6444..10c8eb81f3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -55,7 +55,7 @@ "name": "Debug tool", "type": "node", "request": "launch", - "args": ["src/index.ts", "backup-workspace", "ws1", "../../../dump/test/ws1"], + "args": ["src/index.ts", "import-lead-csv", "ws1", "../../../suho/COMPANIES_Agency_of_AlexeyS.csv"], "env": { "MINIO_ACCESS_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin", @@ -67,8 +67,7 @@ }, "runtimeArgs": ["--nolazy", "-r", "ts-node/register"], "sourceMaps": true, - "cwd": "${workspaceRoot}/dev/tool", - "protocol": "inspector" + "cwd": "${workspaceRoot}/dev/tool" } ] } diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 30f42369c2..b3d5bd2a81 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -4064,6 +4064,28 @@ packages: cssom: 0.3.8 dev: false + /csv-generate/4.1.0: + resolution: {integrity: sha512-Z17wI0xmDfpwzB7lShyK7INBt0YMyh5kV7svWTwsBSOa30T6Lq1fHHasmSCtf2rRTI7GnTk52HQRYBk0ToAXQQ==} + dev: false + + /csv-parse/5.1.0: + resolution: {integrity: sha512-JL+Q6YEikT2uoe57InjFFa6VejhSv0tDwOxeQ1bVQKeUC/NCnLAAZ8n3PzowPQQLuZ37fysDYZipB2UJkH9C6A==} + dev: false + + /csv-stringify/6.1.0: + resolution: {integrity: sha512-rdBqiy77TczvhGlpLHyoph2adMs6WMnmQY4PBRqeIWykI1FLnVCppnRdso8faGj2+eN7izk9YlbFyZkOrL9rAQ==} + dev: false + + /csv/6.1.0: + resolution: {integrity: sha512-kvd8YWk7emi/0fXhuv+IKaanNFhPi5Up1wT4ZGVUu39bd9lRWpxQu1F7eaRfxTEc9mGxeMWIzEilKKrL0MoEnQ==} + engines: {node: '>= 0.1.90'} + dependencies: + csv-generate: 4.1.0 + csv-parse: 5.1.0 + csv-stringify: 6.1.0 + stream-transform: 3.1.0 + dev: false + /data-urls/2.0.0: resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} engines: {node: '>=10'} @@ -8782,6 +8804,10 @@ packages: readable-stream: 3.6.0 dev: false + /stream-transform/3.1.0: + resolution: {integrity: sha512-ncT/rST/M2N2zGLacJmeB7TNq/XS8Ck0xqvhYX6bgLwSPTSZLVFCSvNnwTU+RYhk9fAfwzCkYH9I5kdgcDMvfw==} + dev: false + /streamsearch/1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -10318,7 +10344,7 @@ packages: dev: false file:projects/chunter-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c: - resolution: {integrity: sha512-hGY42Nm8WN8xitrfk9Z56o/r8a09cytzluQ8o4ifrZ63vDHfU4Jy6EP2fGUrdwMRr8jx1R9PFpAsjp8H5IUWzw==, tarball: file:projects/chunter-resources.tgz} + resolution: {integrity: sha512-FJpFm8VsdeaSW1Y1bRuwulXv3PVLIM/4nvTbIwPMoYUHQDt47UL27zIPbUAEUK0+3ar5arsiUO02jZJxlKA+xg==, tarball: file:projects/chunter-resources.tgz} id: file:projects/chunter-resources.tgz name: '@rush-temp/chunter-resources' version: 0.0.0 @@ -10496,7 +10522,7 @@ packages: dev: false file:projects/contact.tgz: - resolution: {integrity: sha512-2B/7NhxU+dIDhbVwlaOzJq+OwDzsf53p2DVvI+AbZa9Zj6gP5ajfKfkWqVT7pItU6Kt5he3t/5pg7KWvCyN6lw==, tarball: file:projects/contact.tgz} + resolution: {integrity: sha512-BngWK0q+CMT4P/1R02H53uqYvfhnSkx52G5hZ49BpcvdJAnrn+F8G7NiVSoUO2Ag2Cccundu89oVlUsnYXH4dw==, tarball: file:projects/contact.tgz} name: '@rush-temp/contact' version: 0.0.0 dependencies: @@ -11517,7 +11543,7 @@ packages: dev: false file:projects/model-inventory.tgz_typescript@4.7.2: - resolution: {integrity: sha512-PCAUuH2imVPnkolq5MCYxYnjoWvba3ctRhi6kDepGqgywCnyixHL5h7dYKf79X1fFoyTS3f0PZve5JSBPvIrYw==, tarball: file:projects/model-inventory.tgz} + resolution: {integrity: sha512-ZrDM3hzsPJZPI1EGZpP4sEkEnJmmfzW77c2g7JejOCROBy6gd7237XN/SPjPk85CcSg7iHyueJiG9QGgoRB5SQ==, tarball: file:projects/model-inventory.tgz} id: file:projects/model-inventory.tgz name: '@rush-temp/model-inventory' version: 0.0.0 @@ -12066,7 +12092,7 @@ packages: dev: false file:projects/model-task.tgz_typescript@4.7.2: - resolution: {integrity: sha512-fGY5a9pnS61IS1HaBJsxOnVBc/K+6A8S2YVvFp1zkU0Tqji7SoR34sLKFKz0IDQ4uYzaadkAs9cZmqs7NvfnDQ==, tarball: file:projects/model-task.tgz} + resolution: {integrity: sha512-rKwvH48V41O/F7EAUZoGCFkI1y2QpgBplsoZBNiZ+72Fy+zSrMtUWkTYNF1PCm+t+Bk4OkTOdQ84lLS6pllwtw==, tarball: file:projects/model-task.tgz} id: file:projects/model-task.tgz name: '@rush-temp/model-task' version: 0.0.0 @@ -12158,7 +12184,7 @@ packages: dev: false file:projects/model-tracker.tgz_typescript@4.7.2: - resolution: {integrity: sha512-MrEr3RwNVjkxXv3qaVSJu80bxkfUo7h+Q3BryQngr+CmPQeqnunU2qPOykoZvAVbmXKRO3w9qYz4DNsZmMBy2A==, tarball: file:projects/model-tracker.tgz} + resolution: {integrity: sha512-uRgEbWiEJ7vGg8Rwf72rMT+6dqDEiAR7Vn5UiH9j7geeOvz43eusMLcNJ/CAa9/b2301dHO3VKT5h/YnibM5GA==, tarball: file:projects/model-tracker.tgz} id: file:projects/model-tracker.tgz name: '@rush-temp/model-tracker' version: 0.0.0 @@ -14171,7 +14197,7 @@ packages: dev: false file:projects/tool.tgz: - resolution: {integrity: sha512-5wm5S9YigxgUWQofHRnls3wij0i4pohO/j/teRYNlR+VbEw1P82GIpZoSmOU1wL1VS+Kd2y29N8Gg8KwGsOA6w==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-YguV0HWnBdoHtfM//K6D5IJDro71EfyC7EbAsk7jPk1BR4NEq8fcPqUNO2ySQNOmCeRT8FlYIX5D8iiib+SQug==, tarball: file:projects/tool.tgz} name: '@rush-temp/tool' version: 0.0.0 dependencies: @@ -14188,6 +14214,8 @@ packages: '@typescript-eslint/parser': 5.27.0_eslint@7.32.0+typescript@4.7.2 commander: 8.3.0 cross-env: 7.0.3 + csv: 6.1.0 + csv-parse: 5.1.0 esbuild: 0.12.29 eslint: 7.32.0 eslint-config-standard-with-typescript: 21.0.1_99a5fe2f2ae1dc64d6b59974c931eb2a diff --git a/common/scripts/build_docker.sh b/common/scripts/build_docker.sh index b8440180cd..72bd750065 100755 --- a/common/scripts/build_docker.sh +++ b/common/scripts/build_docker.sh @@ -1,6 +1,7 @@ #!/bin/bash -roots='./server/server ./server/front ./pods/account ./pods/backup ./products/tracker' +roots='./server/server ./server/front ./pods/account ./pods/backup' +# ./products/tracker is temporary disabled for r in $roots do diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 659ff9b402..fa33a2c1dd 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -76,26 +76,26 @@ services: - MINIO_ENDPOINT=minio - MINIO_ACCESS_KEY=minioadmin - MINIO_SECRET_KEY=minioadmin - tracker-front: - image: hardcoreeng/tracker-front - links: - - mongodb - - minio - - elastic - - transactor - ports: - - 8084:8080 - environment: - - SERVER_PORT=8080 - - SERVER_SECRET=secret - - ACCOUNTS_URL=http://localhost:3000 - - FRONT_URL=http://localhost:8084 - - UPLOAD_URL=/files - - TRANSACTOR_URL=ws://localhost:3333 - - ELASTIC_URL=http://elastic:9200 - - MINIO_ENDPOINT=minio - - MINIO_ACCESS_KEY=minioadmin - - MINIO_SECRET_KEY=minioadmin + # tracker-front: + # image: hardcoreeng/tracker-front + # links: + # - mongodb + # - minio + # - elastic + # - transactor + # ports: + # - 8084:8080 + # environment: + # - SERVER_PORT=8080 + # - SERVER_SECRET=secret + # - ACCOUNTS_URL=http://localhost:3000 + # - FRONT_URL=http://localhost:8084 + # - UPLOAD_URL=/files + # - TRANSACTOR_URL=ws://localhost:3333 + # - ELASTIC_URL=http://elastic:9200 + # - MINIO_ENDPOINT=minio + # - MINIO_ACCESS_KEY=minioadmin + # - MINIO_SECRET_KEY=minioadmin transactor: image: hardcoreeng/transactor links: diff --git a/dev/tool/package.json b/dev/tool/package.json index 8fcec5813d..b345f80a0b 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -104,6 +104,8 @@ "@anticrm/rekoni": "~0.6.0", "got": "^11.8.3", "@anticrm/tags": "~0.6.2", - "@anticrm/server-backup": "~0.6.0" + "@anticrm/server-backup": "~0.6.0", + "csv-parse": "~5.1.0", + "@anticrm/lead": "~0.6.0" } } diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index d510aaca2b..1a484b81d7 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -36,6 +36,7 @@ import { Db, MongoClient } from 'mongodb' import { exit } from 'process' import { rebuildElastic } from './elastic' import { importXml } from './importer' +import { importLead } from './lead-importer' import { updateCandidates } from './recruit' import { clearTelegramHistory } from './telegram' import { diffWorkspace, dumpWorkspace, restoreWorkspace } from './workspace' @@ -302,11 +303,18 @@ program program .command('import-xml ') - .description('dump workspace transactions and minio resources') + .description('Import Talants.') .action(async (workspace, fileName, cmd) => { return await importXml(transactorUrl, workspace, minio, fileName, mongodbUri, elasticUrl) }) +program + .command('import-lead-csv ') + .description('Import LEAD csv customer organizations') + .action(async (workspace, fileName, cmd) => { + return await importLead(transactorUrl, workspace, fileName) + }) + program .command('generate-token ') .description('generate token') diff --git a/dev/tool/src/lead-importer.ts b/dev/tool/src/lead-importer.ts new file mode 100644 index 0000000000..575bf92f0e --- /dev/null +++ b/dev/tool/src/lead-importer.ts @@ -0,0 +1,492 @@ +// +// Copyright © 2020, 2021 Anticrm Platform Contributors. +// Copyright © 2021 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import contact, { Contact, EmployeeAccount, Organization } from '@anticrm/contact' +import core, { + AnyAttribute, + Class, + Data, + Doc, + DocumentUpdate, + Enum, + EnumOf, + MixinUpdate, + PropertyType, + Ref, + SortingOrder, + TxOperations, + Type +} from '@anticrm/core' +import lead, { Customer, Funnel, Lead } from '@anticrm/lead' +import { getEmbeddedLabel } from '@anticrm/platform' +import { connect } from '@anticrm/server-tool' +import task, { calcRank, DoneState, genRanks, State } from '@anticrm/task' +import { parse } from 'csv-parse' +import { readFile } from 'fs/promises' + +function filled (obj: any, uniqKeys: string[]): any { + const result: Record = {} + for (const [k, v] of Object.entries(obj)) { + if (typeof v === 'string' && v.trim().length === 0) { + continue + } + if (!uniqKeys.includes(k)) { + uniqKeys.push(k) + } + result[k] = v + } + return result +} + +const names = { + orgName: 'Название компании', + annualTurnover: 'Годовой оборот', + currency: 'Валюта', + comment: 'Комментарий', + created_by: 'Кем создана', + modified_by: 'Кем изменена', + source_ka: 'Источник (где связь) для КА', + corporate_site: 'Корпоративный сайт', + employees_count: 'Кол-во сотрудников', + activity_area: 'Сфера деятельности', + company_type: 'Тип компании', + processing_status: 'Статус обработки', + corporate_email: 'Корпоративный e-mail', + // Lead attrs + assignee: 'Ответственный', + startDate: 'Дата создания', + modifyDate: 'Дата изменения', + + kaStatus: 'Статус для КА', + responsible: 'Ответственный' +} + +interface CustomCustomer extends Customer { + annualTurnover?: string + currency?: string + comment?: string + source_ka: string + employees_count?: number + activity_area: string + company_type: string + processing_status: string + responsible: string +} + +const fieldMapping = { + 'Название компании': { name: 'name', type: core.class.TypeString, label: core.string.String }, + 'Годовой оборот': { name: 'annual.turnover', type: core.class.TypeNumber, label: core.string.Number }, + Валюта: { + name: 'currency', + type: core.class.EnumOf, + enumName: 'OrgCurrency', + label: core.string.Enum + }, + Комментарий: { name: 'comment', type: core.class.TypeMarkup, label: core.string.Markup }, + 'Кем создана': { name: 'created_by', type: undefined, label: core.string.String }, + 'Кем изменена': { name: 'modified_by', type: undefined, label: core.string.String }, + 'Источник (где связь) для КА': { name: 'source_ka', type: core.class.TypeString, label: core.string.String }, + 'Корпоративный сайт': { name: 'corporate_site', type: contact.channelProvider.Homepage, label: undefined }, + 'Кол-во сотрудников': { + name: 'employees_count', + type: core.class.EnumOf, + enumName: 'EmployeeCountType', + label: core.string.Enum + }, + 'Сфера деятельности': { + name: 'activity_area', + type: core.class.EnumOf, + enumName: 'ActivityArea', + label: core.string.Enum + }, + 'Тип компании': { + name: 'company_type', + type: core.class.EnumOf, + enumName: 'CompanyType', + label: core.string.Enum + }, + 'Статус обработки': { + name: 'processing_status', + type: core.class.EnumOf, + enumName: 'ProcessingStatus', + label: core.string.Enum + }, + 'Корпоративный e-mail': { + name: 'corporate_email', + type: contact.channelProvider.Email, + label: core.string.String + }, + Ответственный: { + name: 'responsible', + type: core.class.EnumOf, + enumName: 'ResponsiblePerson', + label: core.string.Enum + } +} +async function uodateClasses (client: TxOperations, records: any[]): Promise { + const allAttrs = client.getHierarchy().getAllAttributes(lead.mixin.Customer) + for (const [k, v] of Object.entries(fieldMapping)) { + let attr = allAttrs.get(v.name) + if (attr === undefined) { + try { + if (!client.getHierarchy().isDerived(v.type as Ref>, core.class.Type)) { + // Skip channels mapping + continue + } + } catch (any) { + continue + } + // Create attr + const data: Data = { + attributeOf: lead.mixin.Customer, + name: v.name, + label: getEmbeddedLabel(k), + isCustom: true, + type: { + _class: v.type as Ref>>, + label: v.label ?? core.string.String + } + } + if (client.getHierarchy().isDerived(v.type as Ref>, core.class.EnumOf)) { + ;(data.type as EnumOf).of = `lead:class:${(v as any).enumName as string}` as Ref + } + const attrId = (lead.mixin.Customer + '.' + v.name) as Ref + await client.createDoc(core.class.Attribute, core.space.Model, data, attrId) + attr = await client.findOne(core.class.Attribute, { _id: attrId }) + } + // Check update Enum/Values + if (client.getHierarchy().isDerived(v.type as Ref>, core.class.EnumOf)) { + const enumName = (v as any).enumName as string + const enumId = `lead:class:${enumName}` as Ref + let enumClass = await client.findOne(core.class.Enum, { _id: enumId }) + if (enumClass === undefined) { + await client.createDoc( + core.class.Enum, + core.space.Model, + { + name: enumName, + enumValues: [] + }, + enumId + ) + enumClass = client.getModel().getObject(enumId) + } + // Check values + const values = records.map((it) => it[k]).filter((it, idx, arr) => arr.indexOf(it) === idx) + for (const v of values) { + const vv = (v ?? '').trim().length === 0 ? 'не задано' : v + if (!enumClass.enumValues.includes(vv)) { + await client.update(enumClass, { + $push: { enumValues: vv } + }) + } + } + } + } +} +async function updateStates ( + client: TxOperations, + states: string[], + _class: Ref>, + importedFunnelId: Ref, + statesMap: Map +): Promise { + // Create update/states + const estates = await client.findAll(_class, { space: importedFunnelId }) + let i = 0 + const ranks = [...genRanks(states.length)] + for (const st of states) { + const ex = estates.find((it) => it.title === st) + if (ex === undefined) { + const id = await client.createDoc(_class, importedFunnelId, { + title: st, + color: i, + rank: ranks[i] + }) + statesMap.set(st, (await client.findOne(_class, { _id: id })) as T) + } else { + statesMap.set(st, ex) + } + i++ + } +} + +export async function importLead (transactorUrl: string, dbName: string, csvFile: string): Promise { + const connection = await connect(transactorUrl, dbName) + + try { + console.log('loading cvs document...') + + const csvData = await readFile(csvFile, 'utf-8') + const records: any[] = await parseCSV(csvData) + const uniqKeys: string[] = [] + const filledFields = records.map((it) => filled(it, uniqKeys)) + // console.log(filledFields) + + const client = new TxOperations(connection, 'core:account:lead-importer' as Ref) + + await uodateClasses(client, records) + await createCustomers(client, filledFields) + + const importedFunnelId = await createFunnel(records, client) + + // Check create/update states + const nonState = 'Не определено' + const states = [nonState, 'Отправлен запрос', 'Отправлен инмейл', 'Принят запрос', 'Заинтересован'] + const wonStates = ['Won'] + const lostStates = ['Отказ', 'Не трогать', 'когда-то работали'] + + const statesMap = new Map() + + // Create update/states + await updateStates(client, states, task.class.State, importedFunnelId, statesMap) + await updateStates(client, wonStates, task.class.WonState, importedFunnelId, statesMap) + await updateStates(client, lostStates, task.class.LostState, importedFunnelId, statesMap) + + if ((await client.findOne(task.class.Kanban, { attachedTo: importedFunnelId })) === undefined) { + await client.createDoc(task.class.Kanban, importedFunnelId, { + attachedTo: importedFunnelId + }) + } + + // Ok we have all states, let's create Leads. + const sequence = await client.findOne(task.class.Sequence, { attachedTo: lead.class.Lead }) + if (sequence === undefined) { + throw new Error('sequence object not found') + } + + for (const record of filledFields) { + const orgId = `imported-${record.ID as string}` as Ref + const orgLeadId = `imported-lead-${record.ID as string}` as Ref + const orgName = record[names.orgName] as string + + const orgLead = await client.findOne(lead.class.Lead, { _id: orgLeadId }) + console.log('processing', orgName) + + const state = statesMap.get(record[names.kaStatus]) + if (state === undefined) { + throw new Error('create lead: state not found') + } + + const leadState = + state._class === task.class.State + ? (state._id as Ref) + : (statesMap.get(nonState) as unknown as Ref) + const doneState = + state._class === task.class.WonState || state._class === task.class.LostState + ? (state._id as Ref) + : null + if (orgLead === undefined) { + const lastOne = await client.findOne( + lead.class.Lead, + { state: leadState }, + { sort: { rank: SortingOrder.Descending } } + ) + const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) + await client.addCollection( + lead.class.Lead, + importedFunnelId, + orgId, + lead.mixin.Customer, + 'leads', + { + title: orgName, + number: (incResult as any).object.sequence, + rank: calcRank(lastOne, undefined), + assignee: null, + startDate: null, + dueDate: null, + state: leadState, + doneState + }, + orgLeadId + ) + } else { + const upd: DocumentUpdate = {} + const newValues = { + state: leadState, + doneState + } + for (const [k, v] of Object.entries(newValues)) { + if ((orgLead as any)[k] !== v && v !== undefined) { + ;(upd as any)[k] = v + } + } + if (Object.keys(upd).length > 0) { + await client.update(orgLead, upd) + } + } + } + } catch (err: any) { + console.error(err) + } finally { + await connection.close() + } +} +async function createFunnel (records: any[], client: TxOperations): Promise> { + const statusKaValues = records.map((it) => it[names.kaStatus]).filter((it, idx, arr) => arr.indexOf(it) === idx) + console.log(statusKaValues) + + const importedFunnelId = 'imported-funnel' as Ref + let funnel = await client.findOne(lead.class.Funnel, { _id: importedFunnelId }) + if (funnel === undefined) { + // No funnel, let's create one. + await client.createDoc( + lead.class.Funnel, + core.space.Space, + { + name: 'Organizations', + archived: false, + members: [], + private: false, + description: '' + }, + importedFunnelId + ) + funnel = await client.findOne(lead.class.Funnel, { _id: importedFunnelId }) + } + return importedFunnelId +} + +async function createCustomers (client: TxOperations, filledFields: any[]): Promise { + for (const record of filledFields) { + const orgId = `imported-${record.ID as string}` as Ref + const orgName = record[names.orgName] + const org = await client.findOne(lead.mixin.Customer, { _id: orgId }) + console.log('processing', orgName) + if (org === undefined) { + await client.createDoc( + contact.class.Organization, + contact.space.Contacts, + { + name: orgName, + city: '', + members: 0 + }, + orgId as unknown as Ref + ) + await client.createMixin( + orgId, + contact.class.Organization, + contact.space.Contacts, + lead.mixin.Customer, + { + description: '', + annualTurnover: record[names.annualTurnover], + currency: record[names.currency], + comment: record[names.comment], + source_ka: record[names.source_ka], + employees_count: record[names.employees_count], + activity_area: record[names.activity_area], + company_type: record[names.company_type], + processing_status: record[names.processing_status], + responsible: record[names.responsible] + } + ) + } else { + const upd: MixinUpdate = {} + const newValues = { + annualTurnover: record[names.annualTurnover], + currency: record[names.currency], + comment: record[names.comment], + source_ka: record[names.source_ka], + employees_count: record[names.employees_count], + activity_area: record[names.activity_area], + company_type: record[names.company_type], + processing_status: record[names.processing_status], + responsible: record[names.responsible] + } + for (const [k, v] of Object.entries(newValues)) { + if ((org as any)[k] !== v && v !== undefined) { + ;(upd as any)[k] = v + } + } + if (Object.keys(upd).length > 0) { + await client.updateMixin( + orgId, + contact.class.Organization, + contact.space.Contacts, + lead.mixin.Customer, + upd + ) + } + } + + const email = record[names.corporate_email] + if (email !== undefined) { + const channels = await client.findAll(contact.class.Channel, { attachedTo: orgId }) + const emailPr = channels.find((it) => it.value === email) + if (emailPr === undefined) { + await client.addCollection( + contact.class.Channel, + contact.space.Contacts, + orgId, + contact.class.Organization, + 'channels', + { + value: email, + provider: contact.channelProvider.Email + } + ) + } + } + + const site = record[names.corporate_site] + if (site !== undefined) { + const channels = await client.findAll(contact.class.Channel, { attachedTo: orgId }) + const sitePr = channels.find((it) => it.value === site) + if (sitePr === undefined) { + await client.addCollection( + contact.class.Channel, + contact.space.Contacts, + orgId, + contact.class.Organization, + 'channels', + { + value: site, + provider: contact.channelProvider.Homepage + } + ) + } + } + } +} + +async function parseCSV (csvData: string): Promise { + return await new Promise((resolve, reject) => { + parse( + csvData, + { + delimiter: ';', + columns: true, + quote: '"', + bom: true, + cast: true, + autoParse: true, + castDate: false, + skipEmptyLines: true, + skipRecordsWithEmptyValues: true + }, + (err, records) => { + if (err !== undefined) { + console.error(err) + reject(err) + } + resolve(records) + } + ) + }) +} diff --git a/models/view/src/index.ts b/models/view/src/index.ts index 4fb2a38261..4da7a9679f 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -294,7 +294,13 @@ export function createModel (builder: Builder): void { ) classPresenter(builder, core.class.TypeIntlString, view.component.IntlStringPresenter) classPresenter(builder, core.class.TypeNumber, view.component.NumberPresenter, view.component.NumberEditor) - classPresenter(builder, core.class.TypeMarkup, view.component.HTMLPresenter) + classPresenter( + builder, + core.class.TypeMarkup, + view.component.HTMLPresenter, + view.component.StringEditor, + view.component.StringEditorPopup + ) classPresenter(builder, core.class.TypeBoolean, view.component.BooleanPresenter, view.component.BooleanEditor) classPresenter(builder, core.class.TypeTimestamp, view.component.TimestampPresenter) classPresenter(builder, core.class.TypeDate, view.component.DatePresenter, view.component.DateEditor)