diff --git a/packages/admin-ui/src/lib/core/src/common/generated-types.ts b/packages/admin-ui/src/lib/core/src/common/generated-types.ts index b5a15cb972..fe463ff7d7 100644 --- a/packages/admin-ui/src/lib/core/src/common/generated-types.ts +++ b/packages/admin-ui/src/lib/core/src/common/generated-types.ts @@ -1284,10 +1284,11 @@ export enum HistoryEntryType { CUSTOMER_REGISTERED = 'CUSTOMER_REGISTERED', CUSTOMER_VERIFIED = 'CUSTOMER_VERIFIED', CUSTOMER_DETAIL_UPDATED = 'CUSTOMER_DETAIL_UPDATED', + CUSTOMER_ADDED_TO_GROUP = 'CUSTOMER_ADDED_TO_GROUP', + CUSTOMER_REMOVED_FROM_GROUP = 'CUSTOMER_REMOVED_FROM_GROUP', CUSTOMER_ADDRESS_CREATED = 'CUSTOMER_ADDRESS_CREATED', CUSTOMER_ADDRESS_UPDATED = 'CUSTOMER_ADDRESS_UPDATED', CUSTOMER_ADDRESS_DELETED = 'CUSTOMER_ADDRESS_DELETED', - CUSTOMER_ORDER_PLACED = 'CUSTOMER_ORDER_PLACED', CUSTOMER_PASSWORD_UPDATED = 'CUSTOMER_PASSWORD_UPDATED', CUSTOMER_PASSWORD_RESET_REQUESTED = 'CUSTOMER_PASSWORD_RESET_REQUESTED', CUSTOMER_PASSWORD_RESET_VERIFIED = 'CUSTOMER_PASSWORD_RESET_VERIFIED', @@ -4478,6 +4479,45 @@ export type RemoveCustomersFromGroupMutation = ( ) } ); +export type GetCustomerHistoryQueryVariables = { + id: Scalars['ID']; + options?: Maybe; +}; + + +export type GetCustomerHistoryQuery = ( + { __typename?: 'Query' } + & { customer?: Maybe<( + { __typename?: 'Customer' } + & Pick + & { history: ( + { __typename?: 'HistoryEntryList' } + & Pick + & { items: Array<( + { __typename?: 'HistoryEntry' } + & Pick + & { administrator?: Maybe<( + { __typename?: 'Administrator' } + & Pick + )> } + )> } + ) } + )> } +); + +export type AddNoteToCustomerMutationVariables = { + input: AddNoteToCustomerInput; +}; + + +export type AddNoteToCustomerMutation = ( + { __typename?: 'Mutation' } + & { addNoteToCustomer: ( + { __typename?: 'Customer' } + & Pick + ) } +); + export type FacetValueFragment = ( { __typename?: 'FacetValue' } & Pick @@ -6895,6 +6935,21 @@ export namespace RemoveCustomersFromGroup { export type RemoveCustomersFromGroup = RemoveCustomersFromGroupMutation['removeCustomersFromGroup']; } +export namespace GetCustomerHistory { + export type Variables = GetCustomerHistoryQueryVariables; + export type Query = GetCustomerHistoryQuery; + export type Customer = (NonNullable); + export type History = (NonNullable)['history']; + export type Items = (NonNullable<(NonNullable)['history']['items'][0]>); + export type Administrator = (NonNullable<(NonNullable<(NonNullable)['history']['items'][0]>)['administrator']>); +} + +export namespace AddNoteToCustomer { + export type Variables = AddNoteToCustomerMutationVariables; + export type Mutation = AddNoteToCustomerMutation; + export type AddNoteToCustomer = AddNoteToCustomerMutation['addNoteToCustomer']; +} + export namespace FacetValue { export type Fragment = FacetValueFragment; export type Translations = (NonNullable); diff --git a/packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts b/packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts index 8e76442c24..5cd59d6bf4 100644 --- a/packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts +++ b/packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts @@ -215,3 +215,34 @@ export const REMOVE_CUSTOMERS_FROM_GROUP = gql` } } `; + +export const GET_CUSTOMER_HISTORY = gql` + query GetCustomerHistory($id: ID!, $options: HistoryEntryListOptions) { + customer(id: $id) { + id + history(options: $options) { + totalItems + items { + id + type + createdAt + isPublic + administrator { + id + firstName + lastName + } + data + } + } + } + } +`; + +export const ADD_NOTE_TO_CUSTOMER = gql` + mutation AddNoteToCustomer($input: AddNoteToCustomerInput!) { + addNoteToCustomer(input: $input) { + id + } + } +`; diff --git a/packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts b/packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts index 52f55a5385..5769b20880 100644 --- a/packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts +++ b/packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts @@ -1,5 +1,6 @@ import { AddCustomersToGroup, + AddNoteToCustomer, CreateAddressInput, CreateCustomer, CreateCustomerAddress, @@ -12,7 +13,9 @@ import { GetCustomer, GetCustomerGroups, GetCustomerGroupWithCustomers, + GetCustomerHistory, GetCustomerList, + HistoryEntryListOptions, OrderListOptions, RemoveCustomersFromGroup, UpdateAddressInput, @@ -24,6 +27,7 @@ import { } from '../../common/generated-types'; import { ADD_CUSTOMERS_TO_GROUP, + ADD_NOTE_TO_CUSTOMER, CREATE_CUSTOMER, CREATE_CUSTOMER_ADDRESS, CREATE_CUSTOMER_GROUP, @@ -31,6 +35,7 @@ import { GET_CUSTOMER, GET_CUSTOMER_GROUP_WITH_CUSTOMERS, GET_CUSTOMER_GROUPS, + GET_CUSTOMER_HISTORY, GET_CUSTOMER_LIST, REMOVE_CUSTOMERS_FROM_GROUP, UPDATE_CUSTOMER, @@ -173,4 +178,27 @@ export class CustomerDataService { customerIds, }); } + + getCustomerHistory(id: string, options?: HistoryEntryListOptions) { + return this.baseDataService.query( + GET_CUSTOMER_HISTORY, + { + id, + options, + }, + ); + } + + addNoteToCustomer(customerId: string, note: string) { + return this.baseDataService.mutate( + ADD_NOTE_TO_CUSTOMER, + { + input: { + note, + isPublic: false, + id: customerId, + }, + }, + ); + } } diff --git a/packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.html b/packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.html similarity index 87% rename from packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.html rename to packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.html index a917462da4..c00eba460f 100644 --- a/packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.html @@ -1,7 +1,7 @@
diff --git a/packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.scss similarity index 100% rename from packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.scss rename to packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.scss diff --git a/packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.ts similarity index 100% rename from packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.ts rename to packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.ts diff --git a/packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss index 4731c5f520..9fb94dfeba 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss @@ -119,6 +119,9 @@ } .warning { + .timeline, .timeline .custom-icon { + color: $color-warning-400; + } .timeline:after { background-color: $color-warning-200; border: 1px solid $color-warning-400; diff --git a/packages/admin-ui/src/lib/core/src/shared/shared.module.ts b/packages/admin-ui/src/lib/core/src/shared/shared.module.ts index e6aa641417..eccf02f13d 100644 --- a/packages/admin-ui/src/lib/core/src/shared/shared.module.ts +++ b/packages/admin-ui/src/lib/core/src/shared/shared.module.ts @@ -51,6 +51,7 @@ import { FormFieldControlDirective } from './components/form-field/form-field-co import { FormFieldComponent } from './components/form-field/form-field.component'; import { FormItemComponent } from './components/form-item/form-item.component'; import { FormattedAddressComponent } from './components/formatted-address/formatted-address.component'; +import { HistoryEntryDetailComponent } from './components/history-entry-detail/history-entry-detail.component'; import { ItemsPerPageControlsComponent } from './components/items-per-page-controls/items-per-page-controls.component'; import { LabeledDataComponent } from './components/labeled-data/labeled-data.component'; import { LanguageSelectorComponent } from './components/language-selector/language-selector.component'; @@ -168,6 +169,7 @@ const DECLARATIONS = [ DurationPipe, EmptyPlaceholderComponent, TimelineEntryComponent, + HistoryEntryDetailComponent, ]; @NgModule({ diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html index 329c24d95e..fe5b77a257 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html +++ b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html @@ -141,3 +141,8 @@

{{ 'customer.orders' | translate }}

+
+
+ +
+
diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts index 3cab0252b6..278ce4a363 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts +++ b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts @@ -11,15 +11,27 @@ import { DataService, GetAvailableCountries, GetCustomer, + GetCustomerHistory, GetCustomerQuery, ModalService, NotificationService, ServerConfigService, + SortOrder, UpdateCustomerInput, } from '@vendure/admin-ui/core'; import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; import { EMPTY, forkJoin, from, Observable, Subject } from 'rxjs'; -import { concatMap, filter, map, merge, mergeMap, shareReplay, switchMap, take } from 'rxjs/operators'; +import { + concatMap, + filter, + map, + merge, + mergeMap, + shareReplay, + startWith, + switchMap, + take, +} from 'rxjs/operators'; import { SelectCustomerGroupDialogComponent } from '../select-customer-group-dialog/select-customer-group-dialog.component'; @@ -38,6 +50,8 @@ export class CustomerDetailComponent extends BaseDetailComponent; orders$: Observable; ordersCount$: Observable; + history$: Observable; + fetchHistory = new Subject(); defaultShippingAddressId: string; defaultBillingAddressId: string; addressDefaultsUpdated = false; @@ -84,6 +98,18 @@ export class CustomerDetailComponent extends BaseDetailComponent customer.orders.items)); this.ordersCount$ = this.entity$.pipe(map((customer) => customer.orders.totalItems)); + this.history$ = this.fetchHistory.pipe( + startWith(null), + switchMap(() => { + return this.dataService.customer + .getCustomerHistory(this.id, { + sort: { + createdAt: SortOrder.DESC, + }, + }) + .mapStream((data) => data.customer?.history.items); + }), + ); } ngOnDestroy() { @@ -238,6 +264,7 @@ export class CustomerDetailComponent extends BaseDetailComponent { this.notificationService.error(_('common.notify-update-error'), { @@ -265,6 +292,7 @@ export class CustomerDetailComponent extends BaseDetailComponent { this.dataService.customer.getCustomer(this.id, { take: 0 }).single$.subscribe(); + this.fetchHistory.next(); }, }); } @@ -291,9 +319,14 @@ export class CustomerDetailComponent extends BaseDetailComponent this.fetchHistory.next()); + } + protected setFormValues(entity: Customer.Fragment): void { const customerGroup = this.detailForm.get('customer'); if (customerGroup) { diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.html b/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.html new file mode 100644 index 0000000000..418f6d0c2f --- /dev/null +++ b/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.html @@ -0,0 +1,104 @@ +

{{ 'customer.customer-history' | translate }}

+
+ +
+ + +
+
+ + + + {{ 'customer.history-customer-registered' | translate }} + + +
+ {{ 'customer.history-customer-verified' | translate }} +
+
+ + +
+ {{ 'customer.history-customer-detail-updated' | translate }} + + + +
+
+ + {{ 'customer.history-customer-added-to-group' | translate: { groupName: entry.data.groupName } }} + + + {{ 'customer.history-customer-removed-from-group' | translate: { groupName: entry.data.groupName } }} + + + {{ 'customer.history-customer-address-created' | translate }} +
+
{{ entry.data.address }}
+
+
+ + {{ 'customer.history-customer-address-updated' | translate }} +
+
{{ entry.data.address }}
+ + + +
+
+ + {{ 'customer.history-customer-address-deleted' | translate }} +
{{ entry.data.address }}
+
+ + {{ 'customer.history-customer-password-updated' | translate }} + + + {{ 'customer.history-customer-password-reset-requested' | translate }} + + + {{ 'customer.history-customer-password-reset-verified' | translate }} + + +
+ {{ 'customer.history-customer-email-update-requested' | translate }} + + {{ + entry.data.oldEmailAddress + }} + {{ + entry.data.newEmailAddress + }} + +
+
+ +
+ {{ 'customer.history-customer-email-update-verified' | translate }} + + {{ + entry.data.oldEmailAddress + }} + {{ + entry.data.newEmailAddress + }} + +
+
+ +
+ {{ entry.data.note }} +
+
+
+
+ +
diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.scss b/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.scss new file mode 100644 index 0000000000..1f2cab0b3a --- /dev/null +++ b/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.scss @@ -0,0 +1,34 @@ +@import "variables"; + +.entry-list { + margin-top: 24px; + margin-left: 24px; + margin-right: 12px; +} + +.note-entry { + display: flex; + align-items: center; + .note { + flex: 1; + } + + button { + margin: 0; + } +} +textarea.note { + flex: 1; + height: 36px; + border-radius: 3px; + margin-right: 6px; +} +.note-text { + color: $color-grey-800; + white-space: pre-wrap; +} + +.address-string { + font-size: smaller; + color: $color-grey-500; +} diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.ts b/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.ts new file mode 100644 index 0000000000..9859e3225e --- /dev/null +++ b/packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.ts @@ -0,0 +1,69 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { Customer, GetCustomerHistory, HistoryEntryType, TimelineDisplayType } from '@vendure/admin-ui/core'; + +@Component({ + selector: 'vdr-customer-history', + templateUrl: './customer-history.component.html', + styleUrls: ['./customer-history.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CustomerHistoryComponent { + @Input() customer: Customer.Fragment; + @Input() history: GetCustomerHistory.Items[]; + @Output() addNote = new EventEmitter<{ note: string }>(); + note = ''; + readonly type = HistoryEntryType; + + getDisplayType(entry: GetCustomerHistory.Items): TimelineDisplayType { + switch (entry.type) { + case HistoryEntryType.CUSTOMER_VERIFIED: + case HistoryEntryType.CUSTOMER_EMAIL_UPDATE_VERIFIED: + case HistoryEntryType.CUSTOMER_PASSWORD_RESET_VERIFIED: + return 'success'; + case HistoryEntryType.CUSTOMER_REGISTERED: + return 'muted'; + case HistoryEntryType.CUSTOMER_REMOVED_FROM_GROUP: + return 'error'; + default: + return 'default'; + } + } + + getTimelineIcon(entry: GetCustomerHistory.Items): string | [string, string] | undefined { + switch (entry.type) { + case HistoryEntryType.CUSTOMER_REGISTERED: + return 'user'; + case HistoryEntryType.CUSTOMER_VERIFIED: + return ['assign-user', 'is-solid']; + case HistoryEntryType.CUSTOMER_NOTE: + return 'note'; + case HistoryEntryType.CUSTOMER_ADDED_TO_GROUP: + case HistoryEntryType.CUSTOMER_REMOVED_FROM_GROUP: + return 'users'; + } + } + + isFeatured(entry: GetCustomerHistory.Items): boolean { + switch (entry.type) { + case HistoryEntryType.CUSTOMER_REGISTERED: + case HistoryEntryType.CUSTOMER_VERIFIED: + return true; + default: + return false; + } + } + + getName(entry: GetCustomerHistory.Items): string { + const { administrator } = entry; + if (administrator) { + return `${administrator.firstName} ${administrator.lastName}`; + } else { + return `${this.customer.firstName} ${this.customer.lastName}`; + } + } + + addNoteToCustomer() { + this.addNote.emit({ note: this.note }); + this.note = ''; + } +} diff --git a/packages/admin-ui/src/lib/customer/src/components/select-customer-group-dialog/select-customer-group-dialog.component.html b/packages/admin-ui/src/lib/customer/src/components/select-customer-group-dialog/select-customer-group-dialog.component.html index a741957149..6b9cdad8a5 100644 --- a/packages/admin-ui/src/lib/customer/src/components/select-customer-group-dialog/select-customer-group-dialog.component.html +++ b/packages/admin-ui/src/lib/customer/src/components/select-customer-group-dialog/select-customer-group-dialog.component.html @@ -1,5 +1,5 @@ - {{ 'customer.add-customer-to-group' }} + {{ 'customer.add-customer-to-group' | translate }}