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",
|
||||
"Facebook": "Facebook",
|
||||
"SocialLinks": "Socail links",
|
||||
"ViewActivity": "View activity"
|
||||
"ViewActivity": "View activity",
|
||||
"PersonAlreadyExists": "Person already exists..."
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@
|
||||
"FacebookPlaceholder": "https://fb.com/jappleseed",
|
||||
"Facebook": "Facebook",
|
||||
"SocialLinks": "Контактная информация",
|
||||
"ViewActivity": "Посмотреть активность"
|
||||
"ViewActivity": "Посмотреть активность",
|
||||
"PersonAlreadyExists": "Контакт уже существует..."
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
@ -16,17 +15,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 { getClient, Card, EditableAvatar } from '@anticrm/presentation'
|
||||
|
||||
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 Channels from './Channels.svelte'
|
||||
import PersonPresenter from './PersonPresenter.svelte'
|
||||
|
||||
let firstName = ''
|
||||
let lastName = ''
|
||||
@ -52,9 +52,7 @@
|
||||
|
||||
async function createPerson () {
|
||||
const uploadFile = await getResource(attachment.helper.UploadFile)
|
||||
const avatarProp = avatar !== undefined
|
||||
? { avatar: await uploadFile(avatar) }
|
||||
: {}
|
||||
const avatarProp = avatar !== undefined ? { avatar: await uploadFile(avatar) } : {}
|
||||
|
||||
const person: Data<Person> = {
|
||||
name: combineName(firstName, lastName),
|
||||
@ -74,30 +72,57 @@
|
||||
}
|
||||
|
||||
let channels: AttachedData<Channel>[] = []
|
||||
|
||||
let matches: FindResult<Person> = []
|
||||
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
|
||||
matches = p
|
||||
})
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={contact.string.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}
|
||||
on: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="mr-4">
|
||||
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="fs-title"><EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} /></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 class="fs-title">
|
||||
<EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} />
|
||||
</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 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>
|
||||
</Card>
|
||||
|
||||
@ -105,4 +130,23 @@
|
||||
.channels {
|
||||
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>
|
||||
|
@ -13,9 +13,9 @@
|
||||
// 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 type { Plugin, Asset } from '@anticrm/platform'
|
||||
import type { Doc, Ref, Class, UXObject, Space, Account, AttachedDoc } from '@anticrm/core'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
/**
|
||||
@ -119,7 +119,10 @@ export function formatName (name: string): string {
|
||||
*/
|
||||
export const contactId = 'contact' as Plugin
|
||||
|
||||
export default plugin(contactId, {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
const contactPlugin = plugin(contactId, {
|
||||
class: {
|
||||
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
|
||||
Channel: '' as Ref<Class<Channel>>,
|
||||
@ -166,5 +169,70 @@ export default plugin(contactId, {
|
||||
},
|
||||
app: {
|
||||
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">
|
||||
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 { 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 { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
|
||||
import {
|
||||
import presentation, {
|
||||
Card,
|
||||
createQuery,
|
||||
EditableAvatar,
|
||||
@ -34,9 +46,12 @@
|
||||
import {
|
||||
Component,
|
||||
EditBox,
|
||||
getColorNumberByText, IconFile as FileIcon,
|
||||
getColorNumberByText,
|
||||
IconFile as FileIcon,
|
||||
IconInfo,
|
||||
Label,
|
||||
Link, showPopup,
|
||||
Link,
|
||||
showPopup,
|
||||
Spinner
|
||||
} from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -156,7 +171,12 @@
|
||||
|
||||
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
|
||||
// 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) {
|
||||
// Create update tag if missing
|
||||
if (!skillTagElements.has(skill.tag)) {
|
||||
@ -246,10 +266,12 @@
|
||||
// Create skills
|
||||
await elementsPromise
|
||||
|
||||
|
||||
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 newSkills:TagReference[] = []
|
||||
const categoriesMap = new Map(Array.from(categories.map((it) => [it._id, it])))
|
||||
|
||||
const newSkills: TagReference[] = []
|
||||
|
||||
// Create missing tag elemnts
|
||||
for (const s of doc.skills ?? []) {
|
||||
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
|
||||
label={recruit.string.CreateCandidate}
|
||||
okAction={createCandidate}
|
||||
canSave={firstName.length > 0 && lastName.length > 0}
|
||||
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
|
||||
space={contact.space.Contacts}
|
||||
on: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="mr-4">
|
||||
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="fs-title"><EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} /></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 class="fs-title">
|
||||
<EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} />
|
||||
</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>
|
||||
|
||||
@ -490,4 +533,23 @@
|
||||
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>
|
||||
|
Loading…
Reference in New Issue
Block a user