mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 17:05:16 +03:00
Show Person Already Exists for Person/Candidates (#1059)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
bddd5f0628
commit
5d5d1a9acf
@ -44,6 +44,7 @@
|
|||||||
"FacebookPlaceholder": "https://fb.com/jappleseed",
|
"FacebookPlaceholder": "https://fb.com/jappleseed",
|
||||||
"Facebook": "Facebook",
|
"Facebook": "Facebook",
|
||||||
"SocialLinks": "Socail links",
|
"SocialLinks": "Socail links",
|
||||||
"ViewActivity": "View activity"
|
"ViewActivity": "View activity",
|
||||||
|
"PersonAlreadyExists": "Person already exists..."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -44,6 +44,7 @@
|
|||||||
"FacebookPlaceholder": "https://fb.com/jappleseed",
|
"FacebookPlaceholder": "https://fb.com/jappleseed",
|
||||||
"Facebook": "Facebook",
|
"Facebook": "Facebook",
|
||||||
"SocialLinks": "Контактная информация",
|
"SocialLinks": "Контактная информация",
|
||||||
"ViewActivity": "Посмотреть активность"
|
"ViewActivity": "Посмотреть активность",
|
||||||
|
"PersonAlreadyExists": "Контакт уже существует..."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||||
// Copyright © 2021 Hardcore Engineering Inc.
|
// Copyright © 2021 Hardcore Engineering Inc.
|
||||||
@ -16,17 +15,18 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { AttachedData, Data, generateId } from '@anticrm/core'
|
import { AttachedData, Data, FindResult, generateId } from '@anticrm/core'
|
||||||
import { getResource } from '@anticrm/platform'
|
import { getResource } from '@anticrm/platform'
|
||||||
|
|
||||||
import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
|
import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
|
||||||
|
|
||||||
import attachment from '@anticrm/attachment'
|
import attachment from '@anticrm/attachment'
|
||||||
import { EditBox } from '@anticrm/ui'
|
import { EditBox, IconInfo, Label } from '@anticrm/ui'
|
||||||
|
|
||||||
import { Channel, combineName, Person } from '@anticrm/contact'
|
import { Channel, combineName, findPerson, Person } from '@anticrm/contact'
|
||||||
import contact from '../plugin'
|
import contact from '../plugin'
|
||||||
import Channels from './Channels.svelte'
|
import Channels from './Channels.svelte'
|
||||||
|
import PersonPresenter from './PersonPresenter.svelte'
|
||||||
|
|
||||||
let firstName = ''
|
let firstName = ''
|
||||||
let lastName = ''
|
let lastName = ''
|
||||||
@ -52,9 +52,7 @@
|
|||||||
|
|
||||||
async function createPerson () {
|
async function createPerson () {
|
||||||
const uploadFile = await getResource(attachment.helper.UploadFile)
|
const uploadFile = await getResource(attachment.helper.UploadFile)
|
||||||
const avatarProp = avatar !== undefined
|
const avatarProp = avatar !== undefined ? { avatar: await uploadFile(avatar) } : {}
|
||||||
? { avatar: await uploadFile(avatar) }
|
|
||||||
: {}
|
|
||||||
|
|
||||||
const person: Data<Person> = {
|
const person: Data<Person> = {
|
||||||
name: combineName(firstName, lastName),
|
name: combineName(firstName, lastName),
|
||||||
@ -74,30 +72,57 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let channels: AttachedData<Channel>[] = []
|
let channels: AttachedData<Channel>[] = []
|
||||||
|
|
||||||
|
let matches: FindResult<Person> = []
|
||||||
|
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
|
||||||
|
matches = p
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label={contact.string.CreatePerson}
|
label={contact.string.CreatePerson}
|
||||||
okAction={createPerson}
|
okAction={createPerson}
|
||||||
canSave={firstName.length > 0 && lastName.length > 0}
|
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
|
||||||
bind:space={contact.space.Contacts}
|
bind:space={contact.space.Contacts}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{#if matches.length > 0}
|
||||||
|
<div class="flex-row update-container ERROR">
|
||||||
|
<div class="flex mb-2">
|
||||||
|
<IconInfo size={'small'} />
|
||||||
|
<div class="text-sm ml-2 overflow-label">
|
||||||
|
<Label label={contact.string.PersonAlreadyExists} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PersonPresenter value={matches[0]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
|
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
<div class="fs-title"><EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} /></div>
|
<div class="fs-title">
|
||||||
<div class="fs-title mb-1"><EditBox placeholder={contact.string.PersonLastNamePlaceholder} maxWidth="12rem" bind:value={lastName} /></div>
|
<EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} />
|
||||||
<div class="text-sm"><EditBox placeholder={contact.string.PersonLocationPlaceholder} maxWidth="12rem" bind:value={object.city} /></div>
|
</div>
|
||||||
|
<div class="fs-title mb-1">
|
||||||
|
<EditBox placeholder={contact.string.PersonLastNamePlaceholder} maxWidth="12rem" bind:value={lastName} />
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<EditBox placeholder={contact.string.PersonLocationPlaceholder} maxWidth="12rem" bind:value={object.city} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-row-center channels">
|
<div class="flex-row-center channels">
|
||||||
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
|
<Channels
|
||||||
|
bind:channels
|
||||||
|
on:change={(e) => {
|
||||||
|
channels = e.detail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -105,4 +130,23 @@
|
|||||||
.channels {
|
.channels {
|
||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
.update-container {
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-right: -1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--theme-content-color);
|
||||||
|
&.WARNING {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
&.ERROR {
|
||||||
|
color: var(--system-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
border: 1px dashed var(--theme-zone-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,9 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import type { Account, AttachedData, AttachedDoc, Class, Client, Data, Doc, FindResult, Ref, Space, UXObject } from '@anticrm/core'
|
||||||
|
import type { Asset, Plugin } from '@anticrm/platform'
|
||||||
import { IntlString, plugin } from '@anticrm/platform'
|
import { IntlString, plugin } from '@anticrm/platform'
|
||||||
import type { Plugin, Asset } from '@anticrm/platform'
|
|
||||||
import type { Doc, Ref, Class, UXObject, Space, Account, AttachedDoc } from '@anticrm/core'
|
|
||||||
import type { AnyComponent } from '@anticrm/ui'
|
import type { AnyComponent } from '@anticrm/ui'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,7 +119,10 @@ export function formatName (name: string): string {
|
|||||||
*/
|
*/
|
||||||
export const contactId = 'contact' as Plugin
|
export const contactId = 'contact' as Plugin
|
||||||
|
|
||||||
export default plugin(contactId, {
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
const contactPlugin = plugin(contactId, {
|
||||||
class: {
|
class: {
|
||||||
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
|
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
|
||||||
Channel: '' as Ref<Class<Channel>>,
|
Channel: '' as Ref<Class<Channel>>,
|
||||||
@ -166,5 +169,70 @@ export default plugin(contactId, {
|
|||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
Contacts: '' as Ref<Doc>
|
Contacts: '' as Ref<Doc>
|
||||||
|
},
|
||||||
|
string: {
|
||||||
|
PersonAlreadyExists: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export default contactPlugin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function findPerson (client: Client, person: Data<Person>, channels: AttachedData<Channel>[]): Promise<FindResult<Person>> {
|
||||||
|
if (channels.length === 0 || person.name.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// Take only first part of first name for match.
|
||||||
|
const values = channels.map(it => it.value)
|
||||||
|
|
||||||
|
// Same name persons
|
||||||
|
|
||||||
|
const potentialChannels = await client.findAll(contactPlugin.class.Channel, { value: { $in: values } })
|
||||||
|
let potentialPersonIds = Array.from(new Set(potentialChannels.map(it => it.attachedTo as Ref<Person>)).values())
|
||||||
|
|
||||||
|
if (potentialPersonIds.length === 0) {
|
||||||
|
const firstName = getFirstName(person.name).split(' ').shift() ?? ''
|
||||||
|
const lastName = getLastName(person.name)
|
||||||
|
// try match using just first/last name
|
||||||
|
potentialPersonIds = (await client.findAll(contactPlugin.class.Person, { name: { $like: `${lastName}%${firstName}%` } })).map(it => it._id)
|
||||||
|
if (potentialPersonIds.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const potentialPersons: FindResult<Person> = await client.findAll(contactPlugin.class.Person, { _id: { $in: potentialPersonIds } }, {
|
||||||
|
lookup: {
|
||||||
|
_id: {
|
||||||
|
channels: contactPlugin.class.Channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result: FindResult<Person> = []
|
||||||
|
|
||||||
|
for (const c of potentialPersons) {
|
||||||
|
let matches = 0
|
||||||
|
if (c.name === person.name) {
|
||||||
|
matches++
|
||||||
|
}
|
||||||
|
if (c.city === person.city) {
|
||||||
|
matches++
|
||||||
|
}
|
||||||
|
for (const ch of c.$lookup?.channels as Channel[] ?? []) {
|
||||||
|
for (const chc of channels) {
|
||||||
|
if (chc.provider === ch.provider && chc.value === ch.value.trim()) {
|
||||||
|
// We have matched value
|
||||||
|
matches += 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches >= 2) {
|
||||||
|
result.push(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -14,12 +14,24 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import attachment from '@anticrm/attachment'
|
import attachment from '@anticrm/attachment'
|
||||||
import contact, { Channel, ChannelProvider, combineName, Person } from '@anticrm/contact'
|
import contact, { Channel, ChannelProvider, combineName, findPerson, Person } from '@anticrm/contact'
|
||||||
import { Channels } from '@anticrm/contact-resources'
|
import { Channels } from '@anticrm/contact-resources'
|
||||||
import { Account, AttachedData, Data, Doc, generateId, MixinData, Ref, TxProcessor } from '@anticrm/core'
|
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
|
||||||
|
import {
|
||||||
|
Account,
|
||||||
|
AttachedData,
|
||||||
|
Data,
|
||||||
|
Doc,
|
||||||
|
FindResult,
|
||||||
|
generateId,
|
||||||
|
Hierarchy,
|
||||||
|
MixinData,
|
||||||
|
Ref,
|
||||||
|
TxProcessor
|
||||||
|
} from '@anticrm/core'
|
||||||
import login from '@anticrm/login'
|
import login from '@anticrm/login'
|
||||||
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
|
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
|
||||||
import {
|
import presentation, {
|
||||||
Card,
|
Card,
|
||||||
createQuery,
|
createQuery,
|
||||||
EditableAvatar,
|
EditableAvatar,
|
||||||
@ -34,9 +46,12 @@
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
EditBox,
|
EditBox,
|
||||||
getColorNumberByText, IconFile as FileIcon,
|
getColorNumberByText,
|
||||||
|
IconFile as FileIcon,
|
||||||
|
IconInfo,
|
||||||
Label,
|
Label,
|
||||||
Link, showPopup,
|
Link,
|
||||||
|
showPopup,
|
||||||
Spinner
|
Spinner
|
||||||
} from '@anticrm/ui'
|
} from '@anticrm/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
@ -156,7 +171,12 @@
|
|||||||
|
|
||||||
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
|
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
|
||||||
// Tag elements
|
// Tag elements
|
||||||
const skillTagElements = new Map((await client.findAll(tags.class.TagElement, { _id: { $in: skills.map(it => it.tag) } })).map(it => ([it._id, it])))
|
const skillTagElements = new Map(
|
||||||
|
(await client.findAll(tags.class.TagElement, { _id: { $in: skills.map((it) => it.tag) } })).map((it) => [
|
||||||
|
it._id,
|
||||||
|
it
|
||||||
|
])
|
||||||
|
)
|
||||||
for (const skill of skills) {
|
for (const skill of skills) {
|
||||||
// Create update tag if missing
|
// Create update tag if missing
|
||||||
if (!skillTagElements.has(skill.tag)) {
|
if (!skillTagElements.has(skill.tag)) {
|
||||||
@ -246,10 +266,12 @@
|
|||||||
// Create skills
|
// Create skills
|
||||||
await elementsPromise
|
await elementsPromise
|
||||||
|
|
||||||
|
|
||||||
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
|
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
|
||||||
const categoriesMap = new Map(Array.from(categories.map(it => ([it._id, it]))))
|
const categoriesMap = new Map(Array.from(categories.map((it) => [it._id, it])))
|
||||||
|
|
||||||
const newSkills:TagReference[] = []
|
const newSkills: TagReference[] = []
|
||||||
|
|
||||||
// Create missing tag elemnts
|
// Create missing tag elemnts
|
||||||
for (const s of doc.skills ?? []) {
|
for (const s of doc.skills ?? []) {
|
||||||
const title = s.trim().toLowerCase()
|
const title = s.trim().toLowerCase()
|
||||||
@ -356,29 +378,50 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
|
let matches: FindResult<Person> = []
|
||||||
|
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
|
||||||
|
matches = p
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
label={recruit.string.CreateCandidate}
|
label={recruit.string.CreateCandidate}
|
||||||
okAction={createCandidate}
|
okAction={createCandidate}
|
||||||
canSave={firstName.length > 0 && lastName.length > 0}
|
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
|
||||||
space={contact.space.Contacts}
|
space={contact.space.Contacts}
|
||||||
on:close={() => {
|
on:close={() => {
|
||||||
dispatch('close')
|
dispatch('close')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<!-- <StatusComponent slot="error" status={{ severity: Severity.ERROR, code: 'Can’t save the object because it already exists' }} /> -->
|
{#if matches.length > 0}
|
||||||
|
<div class="flex-row update-container ERROR">
|
||||||
|
<div class="flex mb-2">
|
||||||
|
<IconInfo size={'small'} />
|
||||||
|
<div class="text-sm ml-2 overflow-label">
|
||||||
|
<Label label={contact.string.PersonAlreadyExists} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PersonPresenter value={matches[0]} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<div class="mr-4">
|
<div class="mr-4">
|
||||||
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
|
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
<div class="fs-title"><EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} /></div>
|
<div class="fs-title">
|
||||||
<div class="fs-title mb-1"><EditBox placeholder={recruit.string.PersonLastNamePlaceholder} maxWidth="10rem" bind:value={lastName} /></div>
|
<EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} />
|
||||||
<div class="text-sm"><EditBox placeholder={recruit.string.Title} maxWidth="10rem" bind:value={object.title} /></div>
|
</div>
|
||||||
<div class="text-sm"><EditBox placeholder={recruit.string.Location} maxWidth="10rem" bind:value={object.city} /></div>
|
<div class="fs-title mb-1">
|
||||||
|
<EditBox placeholder={recruit.string.PersonLastNamePlaceholder} maxWidth="10rem" bind:value={lastName} />
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<EditBox placeholder={recruit.string.Title} maxWidth="10rem" bind:value={object.title} />
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<EditBox placeholder={recruit.string.Location} maxWidth="10rem" bind:value={object.city} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -490,4 +533,23 @@
|
|||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.update-container {
|
||||||
|
margin-left: -1rem;
|
||||||
|
margin-right: -1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--theme-content-color);
|
||||||
|
&.WARNING {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
&.ERROR {
|
||||||
|
color: var(--system-error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
border: 1px dashed var(--theme-zone-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user