mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-14 20:39:03 +00:00
Allow to create customer for Organization (#1696)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
6c8afa1fda
commit
9266a61c98
@ -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,
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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 |
@ -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`))
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
36
plugins/lead-resources/src/components/NewItemsHeader.svelte
Normal file
36
plugins/lead-resources/src/components/NewItemsHeader.svelte
Normal 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>
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user