Browse Source

feat(admin-ui): Display customer history in detail view

Relates to #343
Michael Bromley 5 years ago
parent
commit
8eea7d6e8c
19 changed files with 395 additions and 9 deletions
  1. 56 1
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 31 0
      packages/admin-ui/src/lib/core/src/data/definitions/customer-definitions.ts
  3. 28 0
      packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts
  4. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.html
  5. 0 0
      packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.scss
  6. 0 0
      packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.ts
  7. 3 0
      packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss
  8. 2 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  9. 5 0
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html
  10. 34 1
      packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts
  11. 104 0
      packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.html
  12. 34 0
      packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.scss
  13. 69 0
      packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.ts
  14. 1 1
      packages/admin-ui/src/lib/customer/src/components/select-customer-group-dialog/select-customer-group-dialog.component.html
  15. 2 0
      packages/admin-ui/src/lib/customer/src/customer.module.ts
  16. 6 0
      packages/admin-ui/src/lib/customer/src/public_api.ts
  17. 0 2
      packages/admin-ui/src/lib/order/src/order.module.ts
  18. 1 1
      packages/admin-ui/src/lib/order/src/public_api.ts
  19. 18 2
      packages/admin-ui/src/lib/static/i18n-messages/en.json

+ 56 - 1
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1284,10 +1284,11 @@ export enum HistoryEntryType {
   CUSTOMER_REGISTERED = 'CUSTOMER_REGISTERED',
   CUSTOMER_REGISTERED = 'CUSTOMER_REGISTERED',
   CUSTOMER_VERIFIED = 'CUSTOMER_VERIFIED',
   CUSTOMER_VERIFIED = 'CUSTOMER_VERIFIED',
   CUSTOMER_DETAIL_UPDATED = 'CUSTOMER_DETAIL_UPDATED',
   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_CREATED = 'CUSTOMER_ADDRESS_CREATED',
   CUSTOMER_ADDRESS_UPDATED = 'CUSTOMER_ADDRESS_UPDATED',
   CUSTOMER_ADDRESS_UPDATED = 'CUSTOMER_ADDRESS_UPDATED',
   CUSTOMER_ADDRESS_DELETED = 'CUSTOMER_ADDRESS_DELETED',
   CUSTOMER_ADDRESS_DELETED = 'CUSTOMER_ADDRESS_DELETED',
-  CUSTOMER_ORDER_PLACED = 'CUSTOMER_ORDER_PLACED',
   CUSTOMER_PASSWORD_UPDATED = 'CUSTOMER_PASSWORD_UPDATED',
   CUSTOMER_PASSWORD_UPDATED = 'CUSTOMER_PASSWORD_UPDATED',
   CUSTOMER_PASSWORD_RESET_REQUESTED = 'CUSTOMER_PASSWORD_RESET_REQUESTED',
   CUSTOMER_PASSWORD_RESET_REQUESTED = 'CUSTOMER_PASSWORD_RESET_REQUESTED',
   CUSTOMER_PASSWORD_RESET_VERIFIED = 'CUSTOMER_PASSWORD_RESET_VERIFIED',
   CUSTOMER_PASSWORD_RESET_VERIFIED = 'CUSTOMER_PASSWORD_RESET_VERIFIED',
@@ -4478,6 +4479,45 @@ export type RemoveCustomersFromGroupMutation = (
   ) }
   ) }
 );
 );
 
 
+export type GetCustomerHistoryQueryVariables = {
+  id: Scalars['ID'];
+  options?: Maybe<HistoryEntryListOptions>;
+};
+
+
+export type GetCustomerHistoryQuery = (
+  { __typename?: 'Query' }
+  & { customer?: Maybe<(
+    { __typename?: 'Customer' }
+    & Pick<Customer, 'id'>
+    & { history: (
+      { __typename?: 'HistoryEntryList' }
+      & Pick<HistoryEntryList, 'totalItems'>
+      & { items: Array<(
+        { __typename?: 'HistoryEntry' }
+        & Pick<HistoryEntry, 'id' | 'type' | 'createdAt' | 'isPublic' | 'data'>
+        & { administrator?: Maybe<(
+          { __typename?: 'Administrator' }
+          & Pick<Administrator, 'id' | 'firstName' | 'lastName'>
+        )> }
+      )> }
+    ) }
+  )> }
+);
+
+export type AddNoteToCustomerMutationVariables = {
+  input: AddNoteToCustomerInput;
+};
+
+
+export type AddNoteToCustomerMutation = (
+  { __typename?: 'Mutation' }
+  & { addNoteToCustomer: (
+    { __typename?: 'Customer' }
+    & Pick<Customer, 'id'>
+  ) }
+);
+
 export type FacetValueFragment = (
 export type FacetValueFragment = (
   { __typename?: 'FacetValue' }
   { __typename?: 'FacetValue' }
   & Pick<FacetValue, 'id' | 'createdAt' | 'updatedAt' | 'languageCode' | 'code' | 'name'>
   & Pick<FacetValue, 'id' | 'createdAt' | 'updatedAt' | 'languageCode' | 'code' | 'name'>
@@ -6895,6 +6935,21 @@ export namespace RemoveCustomersFromGroup {
   export type RemoveCustomersFromGroup = RemoveCustomersFromGroupMutation['removeCustomersFromGroup'];
   export type RemoveCustomersFromGroup = RemoveCustomersFromGroupMutation['removeCustomersFromGroup'];
 }
 }
 
 
+export namespace GetCustomerHistory {
+  export type Variables = GetCustomerHistoryQueryVariables;
+  export type Query = GetCustomerHistoryQuery;
+  export type Customer = (NonNullable<GetCustomerHistoryQuery['customer']>);
+  export type History = (NonNullable<GetCustomerHistoryQuery['customer']>)['history'];
+  export type Items = (NonNullable<(NonNullable<GetCustomerHistoryQuery['customer']>)['history']['items'][0]>);
+  export type Administrator = (NonNullable<(NonNullable<(NonNullable<GetCustomerHistoryQuery['customer']>)['history']['items'][0]>)['administrator']>);
+}
+
+export namespace AddNoteToCustomer {
+  export type Variables = AddNoteToCustomerMutationVariables;
+  export type Mutation = AddNoteToCustomerMutation;
+  export type AddNoteToCustomer = AddNoteToCustomerMutation['addNoteToCustomer'];
+}
+
 export namespace FacetValue {
 export namespace FacetValue {
   export type Fragment = FacetValueFragment;
   export type Fragment = FacetValueFragment;
   export type Translations = (NonNullable<FacetValueFragment['translations'][0]>);
   export type Translations = (NonNullable<FacetValueFragment['translations'][0]>);

+ 31 - 0
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
+        }
+    }
+`;

+ 28 - 0
packages/admin-ui/src/lib/core/src/data/providers/customer-data.service.ts

@@ -1,5 +1,6 @@
 import {
 import {
     AddCustomersToGroup,
     AddCustomersToGroup,
+    AddNoteToCustomer,
     CreateAddressInput,
     CreateAddressInput,
     CreateCustomer,
     CreateCustomer,
     CreateCustomerAddress,
     CreateCustomerAddress,
@@ -12,7 +13,9 @@ import {
     GetCustomer,
     GetCustomer,
     GetCustomerGroups,
     GetCustomerGroups,
     GetCustomerGroupWithCustomers,
     GetCustomerGroupWithCustomers,
+    GetCustomerHistory,
     GetCustomerList,
     GetCustomerList,
+    HistoryEntryListOptions,
     OrderListOptions,
     OrderListOptions,
     RemoveCustomersFromGroup,
     RemoveCustomersFromGroup,
     UpdateAddressInput,
     UpdateAddressInput,
@@ -24,6 +27,7 @@ import {
 } from '../../common/generated-types';
 } from '../../common/generated-types';
 import {
 import {
     ADD_CUSTOMERS_TO_GROUP,
     ADD_CUSTOMERS_TO_GROUP,
+    ADD_NOTE_TO_CUSTOMER,
     CREATE_CUSTOMER,
     CREATE_CUSTOMER,
     CREATE_CUSTOMER_ADDRESS,
     CREATE_CUSTOMER_ADDRESS,
     CREATE_CUSTOMER_GROUP,
     CREATE_CUSTOMER_GROUP,
@@ -31,6 +35,7 @@ import {
     GET_CUSTOMER,
     GET_CUSTOMER,
     GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
     GET_CUSTOMER_GROUP_WITH_CUSTOMERS,
     GET_CUSTOMER_GROUPS,
     GET_CUSTOMER_GROUPS,
+    GET_CUSTOMER_HISTORY,
     GET_CUSTOMER_LIST,
     GET_CUSTOMER_LIST,
     REMOVE_CUSTOMERS_FROM_GROUP,
     REMOVE_CUSTOMERS_FROM_GROUP,
     UPDATE_CUSTOMER,
     UPDATE_CUSTOMER,
@@ -173,4 +178,27 @@ export class CustomerDataService {
             customerIds,
             customerIds,
         });
         });
     }
     }
+
+    getCustomerHistory(id: string, options?: HistoryEntryListOptions) {
+        return this.baseDataService.query<GetCustomerHistory.Query, GetCustomerHistory.Variables>(
+            GET_CUSTOMER_HISTORY,
+            {
+                id,
+                options,
+            },
+        );
+    }
+
+    addNoteToCustomer(customerId: string, note: string) {
+        return this.baseDataService.mutate<AddNoteToCustomer.Mutation, AddNoteToCustomer.Variables>(
+            ADD_NOTE_TO_CUSTOMER,
+            {
+                input: {
+                    note,
+                    isPublic: false,
+                    id: customerId,
+                },
+            },
+        );
+    }
 }
 }

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.html → packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.html

@@ -1,7 +1,7 @@
 <vdr-dropdown>
 <vdr-dropdown>
     <button class="btn btn-link btn-sm details-button" vdrDropdownTrigger>
     <button class="btn btn-link btn-sm details-button" vdrDropdownTrigger>
         <clr-icon shape="details" size="12"></clr-icon>
         <clr-icon shape="details" size="12"></clr-icon>
-        {{ 'order.details' | translate }}
+        {{ 'common.details' | translate }}
     </button>
     </button>
     <vdr-dropdown-menu>
     <vdr-dropdown-menu>
         <div class="entry-dropdown">
         <div class="entry-dropdown">

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.scss → packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.scss


+ 0 - 0
packages/admin-ui/src/lib/order/src/components/history-entry-detail/history-entry-detail.component.ts → packages/admin-ui/src/lib/core/src/shared/components/history-entry-detail/history-entry-detail.component.ts


+ 3 - 0
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss

@@ -119,6 +119,9 @@
 }
 }
 
 
 .warning {
 .warning {
+    .timeline, .timeline .custom-icon {
+        color: $color-warning-400;
+    }
     .timeline:after {
     .timeline:after {
         background-color: $color-warning-200;
         background-color: $color-warning-200;
         border: 1px solid $color-warning-400;
         border: 1px solid $color-warning-400;

+ 2 - 0
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 { FormFieldComponent } from './components/form-field/form-field.component';
 import { FormItemComponent } from './components/form-item/form-item.component';
 import { FormItemComponent } from './components/form-item/form-item.component';
 import { FormattedAddressComponent } from './components/formatted-address/formatted-address.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 { ItemsPerPageControlsComponent } from './components/items-per-page-controls/items-per-page-controls.component';
 import { LabeledDataComponent } from './components/labeled-data/labeled-data.component';
 import { LabeledDataComponent } from './components/labeled-data/labeled-data.component';
 import { LanguageSelectorComponent } from './components/language-selector/language-selector.component';
 import { LanguageSelectorComponent } from './components/language-selector/language-selector.component';
@@ -168,6 +169,7 @@ const DECLARATIONS = [
     DurationPipe,
     DurationPipe,
     EmptyPlaceholderComponent,
     EmptyPlaceholderComponent,
     TimelineEntryComponent,
     TimelineEntryComponent,
+    HistoryEntryDetailComponent,
 ];
 ];
 
 
 @NgModule({
 @NgModule({

+ 5 - 0
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html

@@ -141,3 +141,8 @@
         </vdr-data-table>
         </vdr-data-table>
     </div>
     </div>
 </div>
 </div>
+<div class="clr-row" *ngIf="!(isNew$ | async)">
+    <div class="clr-col-md-6">
+        <vdr-customer-history [customer]="entity$ | async" [history]="history$ | async" (addNote)="addNoteToCustomer($event)"></vdr-customer-history>
+    </div>
+</div>

+ 34 - 1
packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.ts

@@ -11,15 +11,27 @@ import {
     DataService,
     DataService,
     GetAvailableCountries,
     GetAvailableCountries,
     GetCustomer,
     GetCustomer,
+    GetCustomerHistory,
     GetCustomerQuery,
     GetCustomerQuery,
     ModalService,
     ModalService,
     NotificationService,
     NotificationService,
     ServerConfigService,
     ServerConfigService,
+    SortOrder,
     UpdateCustomerInput,
     UpdateCustomerInput,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { EMPTY, forkJoin, from, Observable, Subject } from 'rxjs';
 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';
 import { SelectCustomerGroupDialogComponent } from '../select-customer-group-dialog/select-customer-group-dialog.component';
 
 
@@ -38,6 +50,8 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
     availableCountries$: Observable<GetAvailableCountries.Items[]>;
     availableCountries$: Observable<GetAvailableCountries.Items[]>;
     orders$: Observable<GetCustomer.Items[]>;
     orders$: Observable<GetCustomer.Items[]>;
     ordersCount$: Observable<number>;
     ordersCount$: Observable<number>;
+    history$: Observable<GetCustomerHistory.Items[] | undefined>;
+    fetchHistory = new Subject<void>();
     defaultShippingAddressId: string;
     defaultShippingAddressId: string;
     defaultBillingAddressId: string;
     defaultBillingAddressId: string;
     addressDefaultsUpdated = false;
     addressDefaultsUpdated = false;
@@ -84,6 +98,18 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
         const customerWithUpdates$ = this.entity$.pipe(merge(this.orderListUpdates$));
         const customerWithUpdates$ = this.entity$.pipe(merge(this.orderListUpdates$));
         this.orders$ = customerWithUpdates$.pipe(map((customer) => customer.orders.items));
         this.orders$ = customerWithUpdates$.pipe(map((customer) => customer.orders.items));
         this.ordersCount$ = this.entity$.pipe(map((customer) => customer.orders.totalItems));
         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() {
     ngOnDestroy() {
@@ -238,6 +264,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                     this.detailForm.markAsPristine();
                     this.detailForm.markAsPristine();
                     this.addressDefaultsUpdated = false;
                     this.addressDefaultsUpdated = false;
                     this.changeDetector.markForCheck();
                     this.changeDetector.markForCheck();
+                    this.fetchHistory.next();
                 },
                 },
                 (err) => {
                 (err) => {
                     this.notificationService.error(_('common.notify-update-error'), {
                     this.notificationService.error(_('common.notify-update-error'), {
@@ -265,6 +292,7 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                 },
                 },
                 complete: () => {
                 complete: () => {
                     this.dataService.customer.getCustomer(this.id, { take: 0 }).single$.subscribe();
                     this.dataService.customer.getCustomer(this.id, { take: 0 }).single$.subscribe();
+                    this.fetchHistory.next();
                 },
                 },
             });
             });
     }
     }
@@ -291,9 +319,14 @@ export class CustomerDetailComponent extends BaseDetailComponent<CustomerWithOrd
                     customerCount: 1,
                     customerCount: 1,
                     groupName: group.name,
                     groupName: group.name,
                 });
                 });
+                this.fetchHistory.next();
             });
             });
     }
     }
 
 
+    addNoteToCustomer({ note }: { note: string }) {
+        this.dataService.customer.addNoteToCustomer(this.id, note).subscribe(() => this.fetchHistory.next());
+    }
+
     protected setFormValues(entity: Customer.Fragment): void {
     protected setFormValues(entity: Customer.Fragment): void {
         const customerGroup = this.detailForm.get('customer');
         const customerGroup = this.detailForm.get('customer');
         if (customerGroup) {
         if (customerGroup) {

+ 104 - 0
packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.html

@@ -0,0 +1,104 @@
+<h4>{{ 'customer.customer-history' | translate }}</h4>
+<div class="entry-list">
+    <vdr-timeline-entry iconShape="note" displayType="muted">
+        <div class="note-entry">
+            <textarea [(ngModel)]="note" name="note" class="note"></textarea>
+            <button class="btn btn-secondary" [disabled]="!note" (click)="addNoteToCustomer()">
+                {{ 'order.add-note' | translate }}
+            </button>
+        </div>
+    </vdr-timeline-entry>
+    <vdr-timeline-entry
+        *ngFor="let entry of history"
+        [displayType]="getDisplayType(entry)"
+        [iconShape]="getTimelineIcon(entry)"
+        [createdAt]="entry.createdAt"
+        [name]="getName(entry)"
+    >
+        <ng-container [ngSwitch]="entry.type">
+            <ng-container *ngSwitchCase="type.CUSTOMER_REGISTERED">
+                {{ 'customer.history-customer-registered' | translate }}
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_VERIFIED">
+                <div class="title">
+                    {{ 'customer.history-customer-verified' | translate }}
+                </div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_DETAIL_UPDATED">
+
+                <div class="flex">
+                    {{ 'customer.history-customer-detail-updated' | translate }}
+                    <vdr-history-entry-detail>
+                        <vdr-object-tree [value]="entry.data.input"></vdr-object-tree>
+                    </vdr-history-entry-detail>
+                </div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_ADDED_TO_GROUP">
+                {{ 'customer.history-customer-added-to-group' | translate: { groupName: entry.data.groupName } }}
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_REMOVED_FROM_GROUP">
+                {{ 'customer.history-customer-removed-from-group' | translate: { groupName: entry.data.groupName } }}
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_CREATED">
+                {{ 'customer.history-customer-address-created' | translate }}
+                <div class="flex">
+                    <div class="address-string">{{ entry.data.address }}</div>
+                </div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_UPDATED">
+                {{ 'customer.history-customer-address-updated' | translate }}
+                <div class="flex">
+                    <div class="address-string">{{ entry.data.address }}</div>
+                    <vdr-history-entry-detail>
+                        <vdr-object-tree [value]="entry.data.input"></vdr-object-tree>
+                    </vdr-history-entry-detail>
+                </div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_DELETED">
+                {{ 'customer.history-customer-address-deleted' | translate }}
+                <div class="address-string">{{ entry.data.address }}</div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_UPDATED">
+                {{ 'customer.history-customer-password-updated' | translate }}
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_RESET_REQUESTED">
+                {{ 'customer.history-customer-password-reset-requested' | translate }}
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_RESET_VERIFIED">
+                {{ 'customer.history-customer-password-reset-verified' | translate }}
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_EMAIL_UPDATE_REQUESTED">
+                <div class="flex">
+                    {{ 'customer.history-customer-email-update-requested' | translate }}
+                    <vdr-history-entry-detail>
+                        <vdr-labeled-data [label]="'customer.old-email-address' | translate">{{
+                            entry.data.oldEmailAddress
+                        }}</vdr-labeled-data>
+                        <vdr-labeled-data [label]="'customer.new-email-address' | translate">{{
+                            entry.data.newEmailAddress
+                        }}</vdr-labeled-data>
+                    </vdr-history-entry-detail>
+                </div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_EMAIL_UPDATE_VERIFIED">
+                <div class="flex">
+                    {{ 'customer.history-customer-email-update-verified' | translate }}
+                    <vdr-history-entry-detail>
+                        <vdr-labeled-data [label]="'customer.old-email-address' | translate">{{
+                            entry.data.oldEmailAddress
+                        }}</vdr-labeled-data>
+                        <vdr-labeled-data [label]="'customer.new-email-address' | translate">{{
+                            entry.data.newEmailAddress
+                        }}</vdr-labeled-data>
+                    </vdr-history-entry-detail>
+                </div>
+            </ng-container>
+            <ng-container *ngSwitchCase="type.CUSTOMER_NOTE">
+                <div class="note-text">
+                    {{ entry.data.note }}
+                </div>
+            </ng-container>
+        </ng-container>
+    </vdr-timeline-entry>
+    <vdr-timeline-entry [isLast]="true"></vdr-timeline-entry>
+</div>

+ 34 - 0
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;
+}

+ 69 - 0
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 = '';
+    }
+}

+ 1 - 1
packages/admin-ui/src/lib/customer/src/components/select-customer-group-dialog/select-customer-group-dialog.component.html

@@ -1,5 +1,5 @@
 <ng-template vdrDialogTitle>
 <ng-template vdrDialogTitle>
-    {{ 'customer.add-customer-to-group' }}
+    {{ 'customer.add-customer-to-group' | translate }}
 </ng-template>
 </ng-template>
 
 
 <ng-select
 <ng-select

+ 2 - 0
packages/admin-ui/src/lib/customer/src/customer.module.ts

@@ -8,6 +8,7 @@ import { CustomerDetailComponent } from './components/customer-detail/customer-d
 import { CustomerGroupDetailDialogComponent } from './components/customer-group-detail-dialog/customer-group-detail-dialog.component';
 import { CustomerGroupDetailDialogComponent } from './components/customer-group-detail-dialog/customer-group-detail-dialog.component';
 import { CustomerGroupListComponent } from './components/customer-group-list/customer-group-list.component';
 import { CustomerGroupListComponent } from './components/customer-group-list/customer-group-list.component';
 import { CustomerGroupMemberListComponent } from './components/customer-group-member-list/customer-group-member-list.component';
 import { CustomerGroupMemberListComponent } from './components/customer-group-member-list/customer-group-member-list.component';
+import { CustomerHistoryComponent } from './components/customer-history/customer-history.component';
 import { CustomerListComponent } from './components/customer-list/customer-list.component';
 import { CustomerListComponent } from './components/customer-list/customer-list.component';
 import { CustomerStatusLabelComponent } from './components/customer-status-label/customer-status-label.component';
 import { CustomerStatusLabelComponent } from './components/customer-status-label/customer-status-label.component';
 import { SelectCustomerGroupDialogComponent } from './components/select-customer-group-dialog/select-customer-group-dialog.component';
 import { SelectCustomerGroupDialogComponent } from './components/select-customer-group-dialog/select-customer-group-dialog.component';
@@ -25,6 +26,7 @@ import { customerRoutes } from './customer.routes';
         AddCustomerToGroupDialogComponent,
         AddCustomerToGroupDialogComponent,
         CustomerGroupMemberListComponent,
         CustomerGroupMemberListComponent,
         SelectCustomerGroupDialogComponent,
         SelectCustomerGroupDialogComponent,
+        CustomerHistoryComponent,
     ],
     ],
     exports: [AddressCardComponent],
     exports: [AddressCardComponent],
 })
 })

+ 6 - 0
packages/admin-ui/src/lib/customer/src/public_api.ts

@@ -1,8 +1,14 @@
 // This file was generated by the build-public-api.ts script
 // This file was generated by the build-public-api.ts script
+export * from './components/add-customer-to-group-dialog/add-customer-to-group-dialog.component';
 export * from './components/address-card/address-card.component';
 export * from './components/address-card/address-card.component';
 export * from './components/customer-detail/customer-detail.component';
 export * from './components/customer-detail/customer-detail.component';
+export * from './components/customer-group-detail-dialog/customer-group-detail-dialog.component';
+export * from './components/customer-group-list/customer-group-list.component';
+export * from './components/customer-group-member-list/customer-group-member-list.component';
+export * from './components/customer-history/customer-history.component';
 export * from './components/customer-list/customer-list.component';
 export * from './components/customer-list/customer-list.component';
 export * from './components/customer-status-label/customer-status-label.component';
 export * from './components/customer-status-label/customer-status-label.component';
+export * from './components/select-customer-group-dialog/select-customer-group-dialog.component';
 export * from './customer.module';
 export * from './customer.module';
 export * from './customer.routes';
 export * from './customer.routes';
 export * from './providers/routing/customer-resolver';
 export * from './providers/routing/customer-resolver';

+ 0 - 2
packages/admin-ui/src/lib/order/src/order.module.ts

@@ -5,7 +5,6 @@ import { SharedModule } from '@vendure/admin-ui/core';
 import { CancelOrderDialogComponent } from './components/cancel-order-dialog/cancel-order-dialog.component';
 import { CancelOrderDialogComponent } from './components/cancel-order-dialog/cancel-order-dialog.component';
 import { FulfillOrderDialogComponent } from './components/fulfill-order-dialog/fulfill-order-dialog.component';
 import { FulfillOrderDialogComponent } from './components/fulfill-order-dialog/fulfill-order-dialog.component';
 import { FulfillmentDetailComponent } from './components/fulfillment-detail/fulfillment-detail.component';
 import { FulfillmentDetailComponent } from './components/fulfillment-detail/fulfillment-detail.component';
-import { HistoryEntryDetailComponent } from './components/history-entry-detail/history-entry-detail.component';
 import { LineFulfillmentComponent } from './components/line-fulfillment/line-fulfillment.component';
 import { LineFulfillmentComponent } from './components/line-fulfillment/line-fulfillment.component';
 import { LineRefundsComponent } from './components/line-refunds/line-refunds.component';
 import { LineRefundsComponent } from './components/line-refunds/line-refunds.component';
 import { OrderCustomFieldsCardComponent } from './components/order-custom-fields-card/order-custom-fields-card.component';
 import { OrderCustomFieldsCardComponent } from './components/order-custom-fields-card/order-custom-fields-card.component';
@@ -38,7 +37,6 @@ import { orderRoutes } from './order.routes';
         OrderHistoryComponent,
         OrderHistoryComponent,
         FulfillmentDetailComponent,
         FulfillmentDetailComponent,
         PaymentDetailComponent,
         PaymentDetailComponent,
-        HistoryEntryDetailComponent,
         SimpleItemListComponent,
         SimpleItemListComponent,
         OrderCustomFieldsCardComponent,
         OrderCustomFieldsCardComponent,
     ],
     ],

+ 1 - 1
packages/admin-ui/src/lib/order/src/public_api.ts

@@ -2,7 +2,7 @@
 export * from './components/cancel-order-dialog/cancel-order-dialog.component';
 export * from './components/cancel-order-dialog/cancel-order-dialog.component';
 export * from './components/fulfill-order-dialog/fulfill-order-dialog.component';
 export * from './components/fulfill-order-dialog/fulfill-order-dialog.component';
 export * from './components/fulfillment-detail/fulfillment-detail.component';
 export * from './components/fulfillment-detail/fulfillment-detail.component';
-export * from './components/history-entry-detail/history-entry-detail.component';
+export * from '../../core/src/shared/components/history-entry-detail/history-entry-detail.component';
 export * from './components/line-fulfillment/line-fulfillment.component';
 export * from './components/line-fulfillment/line-fulfillment.component';
 export * from './components/line-refunds/line-refunds.component';
 export * from './components/line-refunds/line-refunds.component';
 export * from './components/order-custom-fields-card/order-custom-fields-card.component';
 export * from './components/order-custom-fields-card/order-custom-fields-card.component';

+ 18 - 2
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -147,6 +147,7 @@
     "default-language": "Default language",
     "default-language": "Default language",
     "delete": "Delete",
     "delete": "Delete",
     "description": "Description",
     "description": "Description",
+    "details": "Details",
     "disabled": "Disabled",
     "disabled": "Disabled",
     "discard-changes": "Discard changes",
     "discard-changes": "Discard changes",
     "display-custom-fields": "Display custom fields",
     "display-custom-fields": "Display custom fields",
@@ -212,6 +213,7 @@
     "create-new-customer": "Create new customer",
     "create-new-customer": "Create new customer",
     "create-new-customer-group": "Create new customer group",
     "create-new-customer-group": "Create new customer group",
     "customer-groups": "Customer groups",
     "customer-groups": "Customer groups",
+    "customer-history": "Customer history",
     "customer-type": "Customer type",
     "customer-type": "Customer type",
     "default-billing-address": "Default billing",
     "default-billing-address": "Default billing",
     "default-shipping-address": "Default shipping",
     "default-shipping-address": "Default shipping",
@@ -220,10 +222,25 @@
     "first-name": "First name",
     "first-name": "First name",
     "full-name": "Full name",
     "full-name": "Full name",
     "guest": "Guest",
     "guest": "Guest",
+    "history-customer-added-to-group": "Customer added to group \"{ groupName }\"",
+    "history-customer-address-created": "Address created",
+    "history-customer-address-deleted": "Address deleted",
+    "history-customer-address-updated": "Address updated",
+    "history-customer-detail-updated": "Customer details updated",
+    "history-customer-email-update-requested": "Email address update requested",
+    "history-customer-email-update-verified": "Email address update verified",
+    "history-customer-password-reset-requested": "Password reset requested",
+    "history-customer-password-reset-verified": "Password reset verified",
+    "history-customer-password-updated": "Password updated",
+    "history-customer-registered": "Customer registered",
+    "history-customer-removed-from-group": "Customer removed from group \"{ groupName }\"",
+    "history-customer-verified": "Customer verified",
     "last-name": "Last name",
     "last-name": "Last name",
     "name": "Name",
     "name": "Name",
+    "new-email-address": "New email address",
     "no-orders-placed": "No orders placed",
     "no-orders-placed": "No orders placed",
     "not-a-member-of-any-groups": "This customer is not a member of any groups",
     "not-a-member-of-any-groups": "This customer is not a member of any groups",
+    "old-email-address": "Old email address",
     "orders": "Orders",
     "orders": "Orders",
     "password": "Password",
     "password": "Password",
     "phone-number": "Phone number",
     "phone-number": "Phone number",
@@ -529,7 +546,6 @@
     "create-fulfillment": "Create fulfillment",
     "create-fulfillment": "Create fulfillment",
     "create-fulfillment-success": "Created fulfillment",
     "create-fulfillment-success": "Created fulfillment",
     "customer": "Customer",
     "customer": "Customer",
-    "details": "Details",
     "fulfill": "Fulfill",
     "fulfill": "Fulfill",
     "fulfill-order": "Fulfill order",
     "fulfill-order": "Fulfill order",
     "fulfillment": "Fulfillment",
     "fulfillment": "Fulfillment",
@@ -686,4 +702,4 @@
     "job-result": "Job result",
     "job-result": "Job result",
     "job-state": "Job state"
     "job-state": "Job state"
   }
   }
-}
+}