mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
Allow to create organizations from CSV (#2260)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
1ad15b904f
commit
849f94ef90
@ -98,7 +98,7 @@ const fieldMapping: Record<string, FieldType> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateStates<T extends State | DoneState> (
|
export async function updateStates<T extends State | DoneState> (
|
||||||
client: TxOperations,
|
client: TxOperations,
|
||||||
states: string[],
|
states: string[],
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
|
551
dev/tool/src/csv/org-importer.ts
Normal file
551
dev/tool/src/csv/org-importer.ts
Normal file
@ -0,0 +1,551 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2022 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, {
|
||||||
|
BackupClient,
|
||||||
|
BlobData,
|
||||||
|
Client,
|
||||||
|
DOMAIN_BLOB,
|
||||||
|
generateId,
|
||||||
|
MixinUpdate,
|
||||||
|
Ref,
|
||||||
|
SortingOrder,
|
||||||
|
TxOperations,
|
||||||
|
WithLookup
|
||||||
|
} from '@anticrm/core'
|
||||||
|
import lead, { Customer, Funnel, Lead } from '@anticrm/lead'
|
||||||
|
import { connect } from '@anticrm/server-tool'
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
import { updateClasses } from './classes'
|
||||||
|
import { CustomCustomer, FieldType } from './types'
|
||||||
|
import { filled, getValid, updateChannel } from './utils'
|
||||||
|
|
||||||
|
import task, { calcRank, DoneState, Sequence, State } from '@anticrm/task'
|
||||||
|
import { parse } from 'csv-parse'
|
||||||
|
import { updateStates } from './lead-importer'
|
||||||
|
import got from 'got'
|
||||||
|
import mimetypes from 'mime-types'
|
||||||
|
import attachment from '@anticrm/model-attachment'
|
||||||
|
|
||||||
|
const names = {
|
||||||
|
companyName: 'Company Name',
|
||||||
|
|
||||||
|
companyType: 'Company Type',
|
||||||
|
processingStatus: 'Статус обработки',
|
||||||
|
|
||||||
|
employees: 'Employees',
|
||||||
|
corporate_website: 'Corporate Website',
|
||||||
|
corporate_email: 'Корпоративный e-mail',
|
||||||
|
workEmail: 'Work E-mail',
|
||||||
|
|
||||||
|
responsible: 'Responsible',
|
||||||
|
industry: 'Industry',
|
||||||
|
currency: 'Currency',
|
||||||
|
comment: 'Comment',
|
||||||
|
createdBy: 'Created by',
|
||||||
|
created: 'Created',
|
||||||
|
modifiedBy: 'Modified by',
|
||||||
|
modified: 'Modified',
|
||||||
|
trelloLink: 'Ссылка на Trello',
|
||||||
|
research: 'Ресеч',
|
||||||
|
startCollaboration: 'Начало сотрудничества',
|
||||||
|
geography: 'География',
|
||||||
|
interestRate: 'Ставка вознаграждения',
|
||||||
|
agreement: 'Договор',
|
||||||
|
technologyStack: 'Стек технологий заказчика',
|
||||||
|
fieldOfActivity: 'Сфера деятельности (Другое)',
|
||||||
|
hrLanguage: 'Язык общения с заказчиком',
|
||||||
|
selectionFeatures: 'Особенности подбора',
|
||||||
|
source: 'Источник',
|
||||||
|
clientActivityStatus: 'Статус активности клиента',
|
||||||
|
valCompany: 'ВАЛ от компании (евро)'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMapping: Record<string, FieldType> = {
|
||||||
|
Status: {
|
||||||
|
name: 'status',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyStatus',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.companyType
|
||||||
|
},
|
||||||
|
[names.employees]: {
|
||||||
|
name: 'employees',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyEmployees',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.employees
|
||||||
|
},
|
||||||
|
[names.responsible]: {
|
||||||
|
name: 'responsible',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyResponsible',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.responsible
|
||||||
|
},
|
||||||
|
[names.industry]: {
|
||||||
|
name: 'industry',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyIndustry',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.industry
|
||||||
|
},
|
||||||
|
[names.currency]: {
|
||||||
|
name: 'currency',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyCurrency',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.currency
|
||||||
|
},
|
||||||
|
[names.research]: {
|
||||||
|
name: 'research',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyResearch',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.research
|
||||||
|
},
|
||||||
|
[names.geography]: {
|
||||||
|
name: 'geography',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyGeography',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.geography
|
||||||
|
},
|
||||||
|
[names.interestRate]: {
|
||||||
|
name: 'interestRate',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyInterestRate',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.interestRate
|
||||||
|
},
|
||||||
|
[names.agreement]: {
|
||||||
|
name: 'agreement',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
fName: names.agreement
|
||||||
|
},
|
||||||
|
[names.comment]: {
|
||||||
|
name: 'mainComment',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
fName: names.comment
|
||||||
|
},
|
||||||
|
[names.trelloLink]: {
|
||||||
|
name: 'trelloLink',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
fName: names.trelloLink
|
||||||
|
},
|
||||||
|
[names.technologyStack]: {
|
||||||
|
name: 'technologyStack',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
fName: names.technologyStack
|
||||||
|
},
|
||||||
|
[names.fieldOfActivity]: {
|
||||||
|
name: 'fieldOfActivity',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
fName: names.fieldOfActivity
|
||||||
|
},
|
||||||
|
[names.hrLanguage]: {
|
||||||
|
name: 'hrLanguage',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyHRLanguage',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.interestRate
|
||||||
|
},
|
||||||
|
[names.selectionFeatures]: {
|
||||||
|
name: 'selectionFeatures',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyInterestRate',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.interestRate
|
||||||
|
},
|
||||||
|
[names.source]: {
|
||||||
|
name: 'source',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanySource',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.source
|
||||||
|
},
|
||||||
|
[names.valCompany]: {
|
||||||
|
name: 'interestRate',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.String,
|
||||||
|
fName: names.valCompany
|
||||||
|
},
|
||||||
|
[names.startCollaboration]: {
|
||||||
|
name: 'startCollaboration',
|
||||||
|
type: core.class.TypeString,
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.startCollaboration
|
||||||
|
},
|
||||||
|
[names.clientActivityStatus]: {
|
||||||
|
name: 'clientActivityStatus',
|
||||||
|
type: core.class.EnumOf,
|
||||||
|
enumName: 'CompanyClientActivity',
|
||||||
|
label: core.string.Enum,
|
||||||
|
fName: names.clientActivityStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseCSV (csvData: string): Promise<any[]> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importOrgs (transactorUrl: string, dbName: string, csvFile: string): Promise<void> {
|
||||||
|
const connection = (await connect(transactorUrl, dbName, undefined, {
|
||||||
|
mode: 'backup'
|
||||||
|
})) as unknown as Client & BackupClient
|
||||||
|
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<EmployeeAccount>)
|
||||||
|
|
||||||
|
await updateClasses(client, records, fieldMapping)
|
||||||
|
|
||||||
|
const funnelId = await createFunnel(records, client)
|
||||||
|
|
||||||
|
const statusKaValues = records
|
||||||
|
.map((it) => it[names.processingStatus])
|
||||||
|
.filter((it, idx, arr) => arr.indexOf(it) === idx)
|
||||||
|
console.log(statusKaValues)
|
||||||
|
|
||||||
|
const wonStateFrom = 'Договор на рассмотрении'
|
||||||
|
|
||||||
|
const states = [
|
||||||
|
'Переговоры / назначен звонок',
|
||||||
|
'Обсуждение условий в переписке',
|
||||||
|
wonStateFrom,
|
||||||
|
'Направлено первичное письмо по e-mail',
|
||||||
|
'Направлен запрос в LinkedIn / другом канале'
|
||||||
|
]
|
||||||
|
const wonStates = ['Договор заключен']
|
||||||
|
const lostStates = ['Получен отказ', 'Перестал отвечать']
|
||||||
|
|
||||||
|
const statesMap = new Map<string, State | DoneState>()
|
||||||
|
|
||||||
|
// Create update/states
|
||||||
|
await updateStates(client, states, task.class.State, funnelId, statesMap)
|
||||||
|
await updateStates(client, wonStates, task.class.WonState, funnelId, statesMap)
|
||||||
|
await updateStates(client, lostStates, task.class.LostState, funnelId, statesMap)
|
||||||
|
|
||||||
|
if ((await client.findOne(task.class.Kanban, { attachedTo: funnelId })) === undefined) {
|
||||||
|
await client.createDoc(task.class.Kanban, funnelId, {
|
||||||
|
attachedTo: funnelId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
}
|
||||||
|
|
||||||
|
await createOrganizations(client, filledFields, connection, funnelId, statesMap, wonStateFrom, sequence)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
await connection.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFunnel (records: any[], client: TxOperations): Promise<Ref<Funnel>> {
|
||||||
|
const importedFunnelId = 'imported-funnel' as Ref<Funnel>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomOrg extends Customer {
|
||||||
|
status: any
|
||||||
|
clientActivityStatus: any
|
||||||
|
|
||||||
|
employees: any
|
||||||
|
|
||||||
|
responsible: any
|
||||||
|
industry: any
|
||||||
|
currency: any
|
||||||
|
trelloLink: string
|
||||||
|
research: any
|
||||||
|
startCollaboration: string
|
||||||
|
geography: any
|
||||||
|
interestRate: any
|
||||||
|
agreement: string
|
||||||
|
technologyStack: string
|
||||||
|
fieldOfActivity: string
|
||||||
|
hrLanguage: any
|
||||||
|
selectionFeatures: any
|
||||||
|
source: any
|
||||||
|
valCompany: any
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrganizations (
|
||||||
|
client: TxOperations,
|
||||||
|
filledFields: any[],
|
||||||
|
connection: Client & BackupClient,
|
||||||
|
funnelId: Ref<Funnel>,
|
||||||
|
statesMap: Map<string, State | DoneState>,
|
||||||
|
wonStateFrom: string,
|
||||||
|
sequence: WithLookup<Sequence>
|
||||||
|
): Promise<void> {
|
||||||
|
for (const record of filledFields) {
|
||||||
|
let orgId: Ref<Organization> = generateId()
|
||||||
|
const orgName = record[names.companyName]
|
||||||
|
if ((orgName?.toString() ?? '').trim().length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const org = await client.findOne(lead.mixin.Customer, { name: orgName })
|
||||||
|
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<Organization>
|
||||||
|
)
|
||||||
|
await client.createMixin<Contact, CustomOrg>(
|
||||||
|
orgId,
|
||||||
|
contact.class.Organization,
|
||||||
|
contact.space.Contacts,
|
||||||
|
lead.mixin.Customer,
|
||||||
|
{
|
||||||
|
description: record[names.comment],
|
||||||
|
status: record[names.companyType],
|
||||||
|
clientActivityStatus: record[names.clientActivityStatus],
|
||||||
|
|
||||||
|
employees: record[names.employees],
|
||||||
|
|
||||||
|
responsible: record[names.responsible],
|
||||||
|
industry: record[names.industry],
|
||||||
|
currency: record[names.currency],
|
||||||
|
trelloLink: record[names.trelloLink],
|
||||||
|
research: record[names.research],
|
||||||
|
startCollaboration: record[names.startCollaboration],
|
||||||
|
geography: record[names.geography],
|
||||||
|
interestRate: record[names.interestRate],
|
||||||
|
agreement: record[names.agreement],
|
||||||
|
technologyStack: record[names.technologyStack],
|
||||||
|
fieldOfActivity: record[names.fieldOfActivity],
|
||||||
|
hrLanguage: record[names.hrLanguage],
|
||||||
|
selectionFeatures: record[names.selectionFeatures],
|
||||||
|
source: record[names.source],
|
||||||
|
valCompany: record[names.valCompany]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
orgId = org._id as unknown as Ref<Organization>
|
||||||
|
const upd: MixinUpdate<Contact, CustomCustomer> = {}
|
||||||
|
const newValues = {
|
||||||
|
description: record[names.comment],
|
||||||
|
status: record[names.companyType],
|
||||||
|
clientActivityStatus: record[names.clientActivityStatus],
|
||||||
|
|
||||||
|
employees: record[names.employees],
|
||||||
|
|
||||||
|
responsible: record[names.responsible],
|
||||||
|
industry: record[names.industry],
|
||||||
|
currency: record[names.currency],
|
||||||
|
trelloLink: record[names.trelloLink],
|
||||||
|
research: record[names.research],
|
||||||
|
startCollaboration: record[names.startCollaboration],
|
||||||
|
geography: record[names.geography],
|
||||||
|
interestRate: record[names.interestRate],
|
||||||
|
agreement: record[names.agreement],
|
||||||
|
technologyStack: record[names.technologyStack],
|
||||||
|
fieldOfActivity: record[names.fieldOfActivity],
|
||||||
|
hrLanguage: record[names.hrLanguage],
|
||||||
|
selectionFeatures: record[names.selectionFeatures],
|
||||||
|
source: record[names.source],
|
||||||
|
valCompany: record[names.valCompany]
|
||||||
|
}
|
||||||
|
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<Contact, CustomCustomer>(
|
||||||
|
orgId,
|
||||||
|
contact.class.Organization,
|
||||||
|
contact.space.Contacts,
|
||||||
|
lead.mixin.Customer,
|
||||||
|
upd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateChannel(
|
||||||
|
client,
|
||||||
|
orgId,
|
||||||
|
getValid(record, names.corporate_website),
|
||||||
|
contact.channelProvider.Homepage,
|
||||||
|
contact.class.Organization
|
||||||
|
)
|
||||||
|
await updateChannel(
|
||||||
|
client,
|
||||||
|
orgId,
|
||||||
|
getValid(record, names.corporate_email),
|
||||||
|
contact.channelProvider.Email,
|
||||||
|
contact.class.Organization
|
||||||
|
)
|
||||||
|
await updateChannel(
|
||||||
|
client,
|
||||||
|
orgId,
|
||||||
|
getValid(record, names.workEmail),
|
||||||
|
contact.channelProvider.Email,
|
||||||
|
contact.class.Organization
|
||||||
|
)
|
||||||
|
|
||||||
|
const state = statesMap.get(record[names.processingStatus])
|
||||||
|
if (state !== undefined) {
|
||||||
|
const leadState =
|
||||||
|
state._class === task.class.State
|
||||||
|
? (state._id as Ref<State>)
|
||||||
|
: (statesMap.get(wonStateFrom) as unknown as Ref<State>)
|
||||||
|
const doneState =
|
||||||
|
state._class === task.class.WonState || state._class === task.class.LostState
|
||||||
|
? (state._id as Ref<DoneState>)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const orgLeadId = `imported-lead-${record.ID as string}` as Ref<Lead>
|
||||||
|
const orgLead = await client.findOne(lead.class.Lead, { _id: orgLeadId })
|
||||||
|
|
||||||
|
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,
|
||||||
|
funnelId,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const agreement = record[names.agreement] as string
|
||||||
|
|
||||||
|
if (agreement !== undefined) {
|
||||||
|
const agreements = agreement.split(',')
|
||||||
|
for (const r of agreements) {
|
||||||
|
try {
|
||||||
|
const url = (r ?? '').trim()
|
||||||
|
if (url.startsWith('http')) {
|
||||||
|
const lastpos = url.lastIndexOf('/')
|
||||||
|
const fname = url.substring(lastpos + 1)
|
||||||
|
|
||||||
|
const buffer = await got(url).buffer()
|
||||||
|
const blobId = (orgId + '_' + generateId()) as Ref<BlobData>
|
||||||
|
const type = mimetypes.contentType(fname)
|
||||||
|
const data: BlobData = {
|
||||||
|
_id: blobId,
|
||||||
|
space: contact.space.Contacts,
|
||||||
|
modifiedBy: client.txFactory.account,
|
||||||
|
modifiedOn: Date.now(),
|
||||||
|
_class: core.class.BlobData,
|
||||||
|
name: fname,
|
||||||
|
size: buffer.length,
|
||||||
|
type: type !== false ? type : 'unknown',
|
||||||
|
base64Data: buffer.toString('base64')
|
||||||
|
}
|
||||||
|
await connection.upload(DOMAIN_BLOB, [data])
|
||||||
|
|
||||||
|
await client.addCollection(
|
||||||
|
attachment.class.Attachment,
|
||||||
|
contact.space.Contacts,
|
||||||
|
orgId,
|
||||||
|
contact.class.Organization,
|
||||||
|
'attachments',
|
||||||
|
{
|
||||||
|
file: blobId,
|
||||||
|
name: fname,
|
||||||
|
size: buffer.length,
|
||||||
|
type: type !== false ? type : 'unknown',
|
||||||
|
lastModified: Date.now()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import contact, { ChannelProvider, combineName, Contact, EmployeeAccount, Person } from '@anticrm/contact'
|
import attachment, { Attachment } from '@anticrm/attachment'
|
||||||
|
import contact, { combineName, Contact, EmployeeAccount, Person } from '@anticrm/contact'
|
||||||
import core, {
|
import core, {
|
||||||
AnyAttribute,
|
AnyAttribute,
|
||||||
BackupClient,
|
BackupClient,
|
||||||
@ -34,19 +35,18 @@ import core, {
|
|||||||
} from '@anticrm/core'
|
} from '@anticrm/core'
|
||||||
import { Asset, getEmbeddedLabel, IntlString } from '@anticrm/platform'
|
import { Asset, getEmbeddedLabel, IntlString } from '@anticrm/platform'
|
||||||
import recruit, { Candidate } from '@anticrm/recruit'
|
import recruit, { Candidate } from '@anticrm/recruit'
|
||||||
|
import { ReconiDocument } from '@anticrm/rekoni'
|
||||||
|
import { generateToken } from '@anticrm/server-token'
|
||||||
import { connect } from '@anticrm/server-tool'
|
import { connect } from '@anticrm/server-tool'
|
||||||
import setting from '@anticrm/setting'
|
import setting from '@anticrm/setting'
|
||||||
import { readFile } from 'fs/promises'
|
import { readFile } from 'fs/promises'
|
||||||
import { parseCSV } from './parseCSV'
|
|
||||||
import { FieldType } from './types'
|
|
||||||
import { filled } from './utils'
|
|
||||||
import got from 'got'
|
import got from 'got'
|
||||||
import mimetypes from 'mime-types'
|
import mimetypes from 'mime-types'
|
||||||
import attachment, { Attachment } from '@anticrm/attachment'
|
|
||||||
import { generateToken } from '@anticrm/server-token'
|
|
||||||
import { recognize, updateContacts, updateSkills } from '../recruit'
|
import { recognize, updateContacts, updateSkills } from '../recruit'
|
||||||
import { ReconiDocument } from '@anticrm/rekoni'
|
|
||||||
import { findOrUpdateAttached } from '../utils'
|
import { findOrUpdateAttached } from '../utils'
|
||||||
|
import { parseCSV } from './parseCSV'
|
||||||
|
import { FieldType } from './types'
|
||||||
|
import { filled, getValid, updateChannel } from './utils'
|
||||||
|
|
||||||
const names = {
|
const names = {
|
||||||
status: 'Status',
|
status: 'Status',
|
||||||
@ -585,52 +585,29 @@ async function createTalants (
|
|||||||
dataLocationDetails
|
dataLocationDetails
|
||||||
)
|
)
|
||||||
|
|
||||||
function getValid (...names: string[]): string | undefined {
|
|
||||||
for (const o of names) {
|
|
||||||
const v = record[o]
|
|
||||||
if (v !== undefined && typeof v === 'string' && v.trim().length > 0) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateChannel (value: string | undefined, provider: Ref<ChannelProvider>): Promise<void> {
|
|
||||||
if (value === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const channels = await client.findAll(contact.class.Channel, { attachedTo: candidateId })
|
|
||||||
const emailPr = channels.find((it) => it.value === value)
|
|
||||||
if (emailPr === undefined) {
|
|
||||||
await client.addCollection(
|
|
||||||
contact.class.Channel,
|
|
||||||
contact.space.Contacts,
|
|
||||||
candidateId,
|
|
||||||
contact.class.Person,
|
|
||||||
'channels',
|
|
||||||
{
|
|
||||||
value,
|
|
||||||
provider
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateChannel(
|
await updateChannel(
|
||||||
|
client,
|
||||||
|
candidateId,
|
||||||
getValid(record, names.workEmail, names.homeEmail, names.newsletterEmail, names.otherEmail),
|
getValid(record, names.workEmail, names.homeEmail, names.newsletterEmail, names.otherEmail),
|
||||||
contact.channelProvider.Email
|
contact.channelProvider.Email
|
||||||
)
|
)
|
||||||
await updateChannel(getValid(record, names.webSite), contact.channelProvider.Homepage)
|
await updateChannel(client, candidateId, getValid(record, names.webSite), contact.channelProvider.Homepage)
|
||||||
await updateChannel(getValid(record, names.phone, names.phoneNumber), contact.channelProvider.Phone)
|
await updateChannel(
|
||||||
await updateChannel(getValid(record, names.telegram), contact.channelProvider.Telegram)
|
client,
|
||||||
|
candidateId,
|
||||||
|
getValid(record, names.phone, names.phoneNumber),
|
||||||
|
contact.channelProvider.Phone
|
||||||
|
)
|
||||||
|
await updateChannel(client, candidateId, getValid(record, names.telegram), contact.channelProvider.Telegram)
|
||||||
|
|
||||||
const ghval = getValid(record, names.githubPortfolio)
|
const ghval = getValid(record, names.githubPortfolio)
|
||||||
if (ghval?.includes('https://github.com') ?? false) {
|
if (ghval?.includes('https://github.com') ?? false) {
|
||||||
await updateChannel(ghval, contact.channelProvider.GitHub)
|
await updateChannel(client, candidateId, ghval, contact.channelProvider.GitHub)
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = getValid(record, names.profile)
|
const profile = getValid(record, names.profile)
|
||||||
if (profile?.includes('linkedin.com') ?? false) {
|
if (profile?.includes('linkedin.com') ?? false) {
|
||||||
await updateChannel(profile, contact.channelProvider.LinkedIn)
|
await updateChannel(client, candidateId, profile, contact.channelProvider.LinkedIn)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resume = record[names.resume] as string
|
const resume = record[names.resume] as string
|
||||||
|
@ -13,6 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import contact, { ChannelProvider, Contact } from '@anticrm/contact'
|
||||||
|
import { Class, Doc, Ref, TxOperations } from '@anticrm/core'
|
||||||
|
|
||||||
export function filled (obj: any, uniqKeys: string[]): any {
|
export function filled (obj: any, uniqKeys: string[]): any {
|
||||||
const result: Record<string, any> = {}
|
const result: Record<string, any> = {}
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
@ -26,3 +29,31 @@ export function filled (obj: any, uniqKeys: string[]): any {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateChannel (
|
||||||
|
client: TxOperations,
|
||||||
|
attachedTo: Ref<Contact>,
|
||||||
|
value: string | undefined,
|
||||||
|
provider: Ref<ChannelProvider>,
|
||||||
|
attachToClass: Ref<Class<Doc>> = contact.class.Person
|
||||||
|
): Promise<void> {
|
||||||
|
if (value === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const channels = await client.findAll(contact.class.Channel, { attachedTo })
|
||||||
|
const valueCh = channels.find((it) => it.value === value)
|
||||||
|
if (valueCh === undefined) {
|
||||||
|
await client.addCollection(contact.class.Channel, contact.space.Contacts, attachedTo, attachToClass, 'channels', {
|
||||||
|
value,
|
||||||
|
provider
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function getValid (record: any, ...names: string[]): string | undefined {
|
||||||
|
for (const o of names) {
|
||||||
|
const v = record[o]
|
||||||
|
if (v !== undefined && typeof v === 'string' && v.trim().length > 0) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -39,6 +39,7 @@ import { exit } from 'process'
|
|||||||
import { removeDuplicates } from './csv/duplicates'
|
import { removeDuplicates } from './csv/duplicates'
|
||||||
import { importLead } from './csv/lead-importer'
|
import { importLead } from './csv/lead-importer'
|
||||||
import { importLead2 } from './csv/lead-importer2'
|
import { importLead2 } from './csv/lead-importer2'
|
||||||
|
import { importOrgs } from './csv/org-importer'
|
||||||
import { importTalants } from './csv/talant-importer'
|
import { importTalants } from './csv/talant-importer'
|
||||||
import { rebuildElastic } from './elastic'
|
import { rebuildElastic } from './elastic'
|
||||||
import { importXml } from './importer'
|
import { importXml } from './importer'
|
||||||
@ -387,6 +388,13 @@ program
|
|||||||
return await importTalants(transactorUrl, workspace, fileName, rekoniUrl)
|
return await importTalants(transactorUrl, workspace, fileName, rekoniUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('import-org-csv <workspace> <fileName>')
|
||||||
|
.description('Import Organizations csv')
|
||||||
|
.action(async (workspace, fileName, cmd) => {
|
||||||
|
return await importOrgs(transactorUrl, workspace, fileName)
|
||||||
|
})
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('lead-duplicates <workspace>')
|
.command('lead-duplicates <workspace>')
|
||||||
.description('Find and remove duplicate organizations.')
|
.description('Find and remove duplicate organizations.')
|
||||||
|
Loading…
Reference in New Issue
Block a user