Allow to create customer for Organization (#1696)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-05-09 00:55:51 +07:00 committed by GitHub
parent 6c8afa1fda
commit 9266a61c98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 191 additions and 74 deletions

View File

@ -149,10 +149,11 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: { _id: { channels: contact.class.Channel } }
lookup: { _id: { channels: contact.class.Channel }, _class: core.class.Class }
},
config: [
'',
'$lookup._class.label',
'city',
{
presenter: attachment.component.AttachmentsPresenter,

View File

@ -20,7 +20,7 @@ import type { Customer, Funnel, Lead } from '@anticrm/lead'
import { Builder, Collection, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import attachment from '@anticrm/model-attachment'
import chunter from '@anticrm/model-chunter'
import contact, { TPerson } from '@anticrm/model-contact'
import contact, { TContact } from '@anticrm/model-contact'
import core from '@anticrm/model-core'
import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view, { createAction } from '@anticrm/model-view'
@ -53,7 +53,7 @@ export class TLead extends TTask implements Lead {
@Mixin(lead.mixin.Customer, contact.class.Contact)
@UX(lead.string.Customer, lead.icon.LeadApplication)
export class TCustomer extends TPerson implements Customer {
export class TCustomer extends TContact implements Customer {
@Prop(Collection(lead.class.Lead), lead.string.Leads)
leads?: number
@ -112,7 +112,8 @@ export function createModel (builder: Builder): void {
createComponent: lead.component.CreateFunnel
}
]
}
},
navHeaderComponent: lead.component.NewItemsHeader
},
lead.app.Lead
)
@ -122,10 +123,11 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.Table,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: { _id: { channels: contact.class.Channel } } as any
lookup: { _id: { channels: contact.class.Channel }, _class: core.class.Class }
} as FindOptions<Doc>, // TODO: fix
config: [
'',
'$lookup._class.label',
{ key: 'leads', presenter: lead.component.LeadsPresenter, label: lead.string.Leads },
'modifiedOn',
'$lookup.channels'

View File

@ -42,7 +42,8 @@ export default mergeIds(leadId, lead, {
LeadPresenter: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
Customers: '' as AnyComponent,
Leads: '' as AnyComponent
Leads: '' as AnyComponent,
NewItemsHeader: '' as AnyComponent
},
space: {
DefaultFunnel: '' as Ref<Space>

View File

@ -41,10 +41,10 @@
<form class="antiCard dialog" on:submit|preventDefault={() => {}}>
<div class="antiCard-header">
<div class="antiCard-header__title-wrap">
{#if spaceClass && spaceLabel && spacePlaceholder}
{#if (spaceClass && spaceLabel && spacePlaceholder) || $$slots.space}
{#if $$slots.space}
<slot name="space" />
{:else}
{:else if spaceClass && spaceLabel && spacePlaceholder}
<SpaceSelect _class={spaceClass} {spaceQuery} label={spaceLabel} bind:value={space} />
{/if}
<span class="antiCard-header__divider"></span>
@ -78,7 +78,9 @@
kind={'primary'}
on:click={() => {
okAction()
dispatch('close')
if (!createMore) {
dispatch('close')
}
}}
/>
</div>

View File

@ -77,7 +77,7 @@
display: flex;
justify-content: stretch;
align-items: stretch;
margin: .5rem .75rem .75rem;
margin: .5rem 0.5rem .75rem 1rem;
}
.antiNav-element {
flex-shrink: 0;

View File

@ -9,4 +9,10 @@
<symbol id="lead" viewBox="0 0 16 16">
<path d="M13.2,9.4h-2.8l0.3-2.7h2.5c0.3,0,0.5-0.2,0.5-0.5s-0.2-0.5-0.5-0.5h-2.4l0.4-3.2c0-0.3-0.2-0.5-0.4-0.6 c-0.3,0-0.5,0.2-0.6,0.4L9.8,5.6H7.1l0.4-3.2c0-0.3-0.2-0.5-0.4-0.6c-0.3,0-0.5,0.2-0.6,0.4L6.1,5.6H3.2c-0.3,0-0.5,0.2-0.5,0.5 s0.2,0.5,0.5,0.5h2.8L5.6,9.4H3.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h2.4l-0.4,3.2c0,0.3,0.2,0.5,0.4,0.6c0,0,0,0,0.1,0 c0.3,0,0.5-0.2,0.5-0.4l0.4-3.3h2.7l-0.4,3.2c0,0.3,0.2,0.5,0.4,0.6c0,0,0,0,0.1,0c0.3,0,0.5-0.2,0.5-0.4l0.4-3.3h2.9 c0.3,0,0.5-0.2,0.5-0.5S13.4,9.4,13.2,9.4z M6.6,9.4l0.3-2.7h2.7L9.4,9.4H6.6z"/>
</symbol>
<symbol id="new-customer" viewBox="0 0 16 16">
<path d="M14.7826 3.26359C15.1313 2.69123 15.0606 1.93115 14.5705 1.43492C14.0757 0.933932 13.3153 0.865765 12.7483 1.23041C13.2123 2.09277 13.9198 2.79999 14.7826 3.26359Z"/>
<path d="M11.8171 2.11829L6.78472 7.18C7.77457 7.47517 8.57699 8.21908 8.95006 9.18198L13.9064 4.20208C13.0535 3.68436 12.3369 2.9696 11.8171 2.11829Z"/>
<path d="M7.91486 10.1761C7.80538 9.1186 6.91913 8.30487 5.8592 8.29183C5.53827 8.92804 5.21105 9.90848 5.01729 10.5311C4.93355 10.8002 5.16675 11.0527 5.44262 10.9905C6.16831 10.8268 7.36057 10.5217 7.91486 10.1761Z"/>
<path d="M4.75 2C2.67893 2 1 3.67893 1 5.75V11.25C1 13.3211 2.67893 15 4.75 15H10.2501C12.3212 15 14.0001 13.3211 14.0001 11.25V8C14.0001 7.58579 13.6643 7.25 13.2501 7.25C12.8359 7.25 12.5001 7.58579 12.5001 8V11.25C12.5001 12.4926 11.4927 13.5 10.2501 13.5H4.75C3.50736 13.5 2.5 12.4926 2.5 11.25V5.75C2.5 4.50736 3.50736 3.5 4.75 3.5H7C7.41421 3.5 7.75 3.16421 7.75 2.75C7.75 2.33579 7.41421 2 7 2H4.75Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -21,7 +21,8 @@ const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(lead.icon, {
Funnel: `${icons}#funnel`,
Lead: `${icons}#lead`,
LeadApplication: `${icons}#leadapplication`
LeadApplication: `${icons}#leadapplication`,
CreateCustomer: `${icons}#new-customer`
})
addStringsLoader(leadId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -14,53 +14,58 @@
-->
<script lang="ts">
import attachment from '@anticrm/attachment'
import { Channel, combineName, findPerson, Person } from '@anticrm/contact'
import { Channel, combineName, Contact, findPerson } from '@anticrm/contact'
import { ChannelsDropdown } from '@anticrm/contact-resources'
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
import contact from '@anticrm/contact-resources/src/plugin'
import { AttachedData, Data, generateId, MixinData, Ref } from '@anticrm/core'
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref } from '@anticrm/core'
import type { Customer } from '@anticrm/lead'
import { getResource } from '@anticrm/platform'
import { Card, EditableAvatar, getClient } from '@anticrm/presentation'
import { EditBox, IconInfo, Label } from '@anticrm/ui'
import { Button, EditBox, eventToHTMLElement, IconInfo, Label, SelectPopup, showPopup } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import lead from '../plugin'
let firstName = ''
let lastName = ''
let createMore: boolean = false
export function canClose (): boolean {
return firstName === '' && lastName === ''
}
const object: Customer = {
let object: Customer = {
_class: contact.class.Person
} as Customer
const dispatch = createEventDispatcher()
const client = getClient()
const customerId = generateId()
let customerId = generateId()
let channels: AttachedData<Channel>[] = []
let avatar: File | undefined
function formatName (targetClass: Ref<Class<Doc>>, firstName: string, lastName: string, objectName: string): string {
return targetClass === contact.class.Person ? combineName(firstName, lastName) : objectName
}
async function createCustomer () {
const candidate: Data<Person> = {
name: combineName(firstName, lastName),
const candidate: Data<Contact> = {
name: formatName(targetClass._id, firstName, lastName, object.name),
city: object.city
}
if (avatar !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile)
candidate.avatar = await uploadFile(avatar)
}
const candidateData: MixinData<Person, Customer> = {
const candidateData: MixinData<Contact, Customer> = {
description: object.description
}
const id = await client.createDoc(contact.class.Person, contact.space.Contacts, candidate, customerId)
const id = await client.createDoc(targetClass._id, contact.space.Contacts, candidate, customerId)
await client.createMixin(
id as Ref<Person>,
contact.class.Person,
id as Ref<Contact>,
targetClass._id,
contact.space.Contacts,
lead.mixin.Customer,
candidateData
@ -71,7 +76,7 @@
contact.class.Channel,
contact.space.Contacts,
customerId,
contact.class.Person,
targetClass._id,
'channels',
{
value: channel.value,
@ -80,7 +85,17 @@
)
}
dispatch('close')
if (createMore) {
// Prepare for next
object = {
_class: targetClass._id
} as Customer
customerId = generateId()
avatar = undefined
firstName = ''
lastName = ''
channels = []
}
}
function onAvatarDone (e: any) {
@ -89,65 +104,127 @@
avatar = file
}
let matches: Person[] = []
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
let matches: Contact[] = []
$: findPerson(
client,
{ ...object, name: formatName(targetClass._id, firstName, lastName, object.name) },
channels
).then((p) => {
matches = p
})
function removeAvatar (): void {
avatar = undefined
}
const targets = [
client.getModel().getObject(contact.class.Person),
client.getModel().getObject(contact.class.Organization)
]
let targetClass = targets[0]
function selectTarget (evt: MouseEvent): void {
showPopup(
SelectPopup,
{
value: targets.map((it) => ({ id: it._id, label: it.label, icon: it.icon })),
placeholder: contact.string.Contacts,
searchable: false
},
eventToHTMLElement(evt),
(ref) => {
if (ref != null) {
const newT = targets.find((it) => it._id === ref)
if (newT !== undefined) {
if (targetClass._id !== newT._id) {
targetClass = newT
object.name = ''
firstName = ''
lastName = ''
customerId = generateId()
avatar = undefined
}
}
}
}
)
}
$: canSave = formatName(targetClass._id, firstName, lastName, object.name).length > 0
</script>
<Card
label={lead.string.CreateCustomer}
okAction={createCustomer}
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
{canSave}
space={contact.space.Contacts}
on:close={() => {
dispatch('close')
}}
bind:createMore
>
<div class="flex-between flex-row-top">
<div class="flex-col flex-grow">
<EditBox
placeholder={contact.string.PersonFirstNamePlaceholder}
bind:value={firstName}
kind={'large-style'}
maxWidth={'32rem'}
focus
/>
<EditBox
placeholder={contact.string.PersonLastNamePlaceholder}
bind:value={lastName}
kind={'large-style'}
maxWidth={'32rem'}
/>
<div class="mt-1">
<svelte:fragment slot="space">
<Button
icon={targetClass.icon}
label={targetClass.label}
size={'small'}
kind={'no-border'}
on:click={selectTarget}
/>
</svelte:fragment>
{#if targetClass._id === contact.class.Person}
<div class="flex-between flex-row-top mt-2 mb-2">
<div class="flex-col flex-grow">
<EditBox
placeholder={contact.string.PersonLocationPlaceholder}
bind:value={object.city}
placeholder={contact.string.PersonFirstNamePlaceholder}
bind:value={firstName}
kind={'large-style'}
maxWidth={'32rem'}
focus
/>
<EditBox
placeholder={contact.string.PersonLastNamePlaceholder}
bind:value={lastName}
kind={'large-style'}
maxWidth={'32rem'}
/>
<div class="mt-1">
<EditBox
placeholder={contact.string.PersonLocationPlaceholder}
bind:value={object.city}
kind={'small-style'}
maxWidth={'32rem'}
/>
</div>
<EditBox
placeholder={lead.string.IssueDescriptionPlaceholder}
bind:value={object.description}
kind={'small-style'}
maxWidth={'32rem'}
/>
</div>
<div class="ml-4 flex">
<EditableAvatar
bind:direct={avatar}
avatar={object.avatar}
size={'large'}
on:remove={removeAvatar}
on:done={onAvatarDone}
/>
</div>
</div>
{:else}
<div class="flex-row-center clear-mins mt-2 mb-2">
<div class="mr-3">
<Button icon={contact.icon.Company} size={'medium'} kind={'link-bordered'} disabled />
</div>
<EditBox
placeholder={lead.string.IssueDescriptionPlaceholder}
bind:value={object.description}
kind={'small-style'}
maxWidth={'32rem'}
placeholder={contact.string.OrganizationNamePlaceholder}
bind:value={object.name}
maxWidth={'37.5rem'}
kind={'large-style'}
focus
/>
</div>
<div class="ml-4 flex">
<EditableAvatar
bind:direct={avatar}
avatar={object.avatar}
size={'large'}
on:remove={removeAvatar}
on:done={onAvatarDone}
/>
</div>
</div>
{/if}
<svelte:fragment slot="pool">
<ChannelsDropdown bind:value={channels} editable />
</svelte:fragment>

View File

@ -16,11 +16,10 @@
<script lang="ts">
import { Doc, DocumentQuery } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Button, Icon, IconAdd, Label, Scroller, SearchEdit, showPopup } from '@anticrm/ui'
import { Icon, Label, Scroller, SearchEdit } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { Table } from '@anticrm/view-resources'
import lead from '../plugin'
import CreateCustomer from './CreateCustomer.svelte'
let search = ''
let resultQuery: DocumentQuery<Doc> = {}
@ -34,10 +33,6 @@
function updateResultQuery (search: string): void {
resultQuery = search === '' ? {} : { $search: search }
}
function showCreateDialog (ev: Event) {
showPopup(CreateCustomer, {}, 'top')
}
</script>
<div class="ac-header full">
@ -52,13 +47,6 @@
updateResultQuery(search)
}}
/>
<Button
icon={IconAdd}
label={lead.string.CreateCustomerLabel}
kind={'primary'}
on:click={(ev) => showCreateDialog(ev)}
/>
</div>
<Scroller tableFade>

View File

@ -0,0 +1,36 @@
<!--
// 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.
-->
<script lang="ts">
import { Button, showPopup } from '@anticrm/ui'
import lead from '../plugin'
import CreateCustomer from './CreateCustomer.svelte'
async function newIssue (): Promise<void> {
showPopup(CreateCustomer, {}, 'top')
}
</script>
<div class="antiNav-subheader gap-2">
<div class="flex-grow text-md">
<Button
icon={lead.icon.CreateCustomer}
label={lead.string.CreateCustomerLabel}
justify={'left'}
width={'100%'}
on:click={newIssue}
/>
</div>
<!-- <Button icon={lead.icon.Magnifier} on:click={async () => {}} /> -->
</div>

View File

@ -25,6 +25,7 @@ import Leads from './components/Leads.svelte'
import LeadsPresenter from './components/LeadsPresenter.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import CreateCustomer from './components/CreateCustomer.svelte'
import NewItemsHeader from './components/NewItemsHeader.svelte'
export default async (): Promise<Resources> => ({
component: {
@ -37,6 +38,7 @@ export default async (): Promise<Resources> => ({
Customers,
LeadsPresenter,
Leads,
CreateCustomer
CreateCustomer,
NewItemsHeader
}
})

View File

@ -70,7 +70,8 @@ const lead = plugin(leadId, {
icon: {
Funnel: '' as Asset,
Lead: '' as Asset,
LeadApplication: '' as Asset
LeadApplication: '' as Asset,
CreateCustomer: '' as Asset
},
space: {
FunnelTemplates: '' as Ref<KanbanTemplateSpace>