Browse Source

feat(admin-ui): Create CustomerModule with basic list/detail views

Michael Bromley 7 years ago
parent
commit
489b0e99bb

+ 4 - 0
admin-ui/src/app/app.routes.ts

@@ -23,6 +23,10 @@ export const routes: Route[] = [
                 path: 'catalog',
                 loadChildren: './catalog/catalog.module#CatalogModule',
             },
+            {
+                path: 'customer',
+                loadChildren: './customer/customer.module#CustomerModule',
+            },
             {
                 path: 'orders',
                 loadChildren: './order/order.module#OrderModule',

+ 13 - 0
admin-ui/src/app/core/components/main-nav/main-nav.component.html

@@ -44,6 +44,19 @@
                 </li>
             </ul>
         </section>
+        <section class="nav-group">
+            <input id="tabexample2" type="checkbox">
+            <label for="tabexample2">{{ 'nav.customers' | translate }}</label>
+            <ul class="nav-list">
+                <li>
+                    <a class="nav-link"
+                       [routerLink]="['/customer', 'customers']"
+                       routerLinkActive="active">
+                        <clr-icon shape="user" size="20"></clr-icon>{{ 'nav.customers' | translate }}
+                    </a>
+                </li>
+            </ul>
+        </section>
         <section class="nav-group">
             <input id="tabexample2" type="checkbox">
             <label for="tabexample2">{{ 'nav.marketing' | translate }}</label>

+ 37 - 0
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.html

@@ -0,0 +1,37 @@
+<vdr-action-bar>
+    <vdr-ab-left>
+    </vdr-ab-left>
+
+
+    <vdr-ab-right>
+        <button class="btn btn-primary"
+                *ngIf="isNew$ | async; else updateButton"
+                (click)="create()"
+                [disabled]="customerForm.invalid || customerForm.pristine">{{ 'common.create' | translate }}</button>
+        <ng-template #updateButton>
+            <button class="btn btn-primary"
+                    (click)="save()"
+                    [disabled]="customerForm.invalid || customerForm.pristine">{{ 'common.update' | translate }}</button>
+        </ng-template>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<form class="form" [formGroup]="customerForm" >
+    <section class="form-block">
+        <vdr-form-field [label]="'customer.firstName' | translate" for="firstName" [readOnlyToggle]="true">
+            <input id="firstName" type="text" formControlName="firstName">
+        </vdr-form-field>
+        <vdr-form-field [label]="'customer.lastName' | translate" for="lastName" [readOnlyToggle]="true">
+            <input id="lastName" type="text" formControlName="lastName">
+        </vdr-form-field>
+
+        <section formGroupName="customFields" *ngIf="customFields.length">
+            <label>{{ 'common.custom-fields' }}</label>
+            <ng-container *ngFor="let customField of customFields">
+                <vdr-custom-field-control *ngIf="customFieldIsSet(customField.name)"
+                                          [customFieldsFormGroup]="customerForm.get(['facet', 'customFields'])"
+                                          [customField]="customField"></vdr-custom-field-control>
+            </ng-container>
+        </section>
+    </section>
+</form>

+ 0 - 0
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.scss


+ 71 - 0
admin-ui/src/app/customer/components/customer-detail/customer-detail.component.ts

@@ -0,0 +1,71 @@
+import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { map } from 'rxjs/operators';
+import { Customer } from 'shared/generated-types';
+import { CustomFieldConfig } from 'shared/shared-types';
+
+import { BaseDetailComponent } from '../../../common/base-detail.component';
+import { ServerConfigService } from '../../../data/server-config';
+
+@Component({
+    selector: 'vdr-customer-detail',
+    templateUrl: './customer-detail.component.html',
+    styleUrls: ['./customer-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CustomerDetailComponent extends BaseDetailComponent<Customer.Fragment>
+    implements OnInit, OnDestroy {
+    customerForm: FormGroup;
+    customFields: CustomFieldConfig[];
+
+    constructor(
+        route: ActivatedRoute,
+        router: Router,
+        serverConfigService: ServerConfigService,
+        private formBuilder: FormBuilder,
+    ) {
+        super(route, router, serverConfigService);
+
+        this.customFields = this.getCustomFieldConfig('Customer');
+        this.customerForm = this.formBuilder.group({
+            firstName: ['', Validators.required],
+            lastName: ['', Validators.required],
+            customFields: this.formBuilder.group(
+                this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+            ),
+        });
+    }
+
+    ngOnInit() {
+        this.init();
+    }
+
+    ngOnDestroy() {
+        this.destroy();
+    }
+
+    customFieldIsSet(name: string): boolean {
+        return !!this.customerForm.get(['customFields', name]);
+    }
+
+    protected setFormValues(entity: Customer.Fragment): void {
+        this.customerForm.patchValue({
+            firstName: entity.firstName,
+            lastName: entity.lastName,
+        });
+
+        if (this.customFields.length) {
+            const customFieldsGroup = this.customerForm.get(['customFields']) as FormGroup;
+
+            for (const fieldDef of this.customFields) {
+                const key = fieldDef.name;
+                const value = (entity as any).customFields[key];
+                const control = customFieldsGroup.get(key);
+                if (control) {
+                    control.patchValue(value);
+                }
+            }
+        }
+    }
+}

+ 38 - 0
admin-ui/src/app/customer/components/customer-list/customer-list.component.html

@@ -0,0 +1,38 @@
+<vdr-action-bar>
+    <vdr-ab-right>
+        <a class="btn btn-primary" [routerLink]="['./create']">
+            <clr-icon shape="plus"></clr-icon>
+            {{ 'customer.create-new-customer' | translate }}
+        </a>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<vdr-data-table [items]="items$ | async"
+                [itemsPerPage]="itemsPerPage$ | async"
+                [totalItems]="totalItems$ | async"
+                [currentPage]="currentPage$ | async"
+                (pageChange)="setPageNumber($event)"
+                (itemsPerPageChange)="setItemsPerPage($event)">
+    <vdr-dt-column>{{ 'common.ID' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'customer.name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'customer.email-address' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'customer.customer-type' | translate }}</vdr-dt-column>
+    <vdr-dt-column></vdr-dt-column>
+    <ng-template let-customer="item">
+        <td class="left">{{ customer.id }}</td>
+        <td class="left">{{ customer.firstName }} {{ customer.lastName }}</td>
+        <td class="left">{{ customer.emailAddress }}</td>
+        <td class="left">
+            <vdr-chip *ngIf="customer.user.id">
+                <clr-icon shape="check-circle" class="registered-user-icon"></clr-icon> {{ 'customer.registered' | translate }}
+            </vdr-chip>
+            <vdr-chip *ngIf="!customer.user.id">{{ 'customer.guest' | translate }}</vdr-chip>
+        </td>
+        <td class="right">
+            <vdr-table-row-action iconShape="edit"
+                                  [label]="'common.edit' | translate"
+                                  [linkTo]="['./', customer.id]">
+            </vdr-table-row-action>
+        </td>
+    </ng-template>
+</vdr-data-table>

+ 5 - 0
admin-ui/src/app/customer/components/customer-list/customer-list.component.scss

@@ -0,0 +1,5 @@
+@import "variables";
+
+.registered-user-icon {
+    color: $color-success;
+}

+ 21 - 0
admin-ui/src/app/customer/components/customer-list/customer-list.component.ts

@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { GetCustomerList } from 'shared/generated-types';
+
+import { BaseListComponent } from '../../../common/base-list.component';
+import { DataService } from '../../../data/providers/data.service';
+
+@Component({
+    selector: 'vdr-customer-list',
+    templateUrl: './customer-list.component.html',
+    styleUrls: ['./customer-list.component.scss'],
+})
+export class CustomerListComponent extends BaseListComponent<GetCustomerList.Query, GetCustomerList.Items> {
+    constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) => this.dataService.customer.getCustomerList(...args),
+            data => data.customers,
+        );
+    }
+}

+ 16 - 0
admin-ui/src/app/customer/customer.module.ts

@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { SharedModule } from '../shared/shared.module';
+
+import { CustomerDetailComponent } from './components/customer-detail/customer-detail.component';
+import { CustomerListComponent } from './components/customer-list/customer-list.component';
+import { customerRoutes } from './customer.routes';
+import { CustomerResolver } from './providers/routing/customer-resolver';
+
+@NgModule({
+    imports: [SharedModule, RouterModule.forChild(customerRoutes)],
+    declarations: [CustomerListComponent, CustomerDetailComponent],
+    providers: [CustomerResolver],
+})
+export class CustomerModule {}

+ 39 - 0
admin-ui/src/app/customer/customer.routes.ts

@@ -0,0 +1,39 @@
+import { Route } from '@angular/router';
+import { Customer } from 'shared/generated-types';
+
+import { createResolveData } from '../common/base-entity-resolver';
+import { detailBreadcrumb } from '../common/detail-breadcrumb';
+import { _ } from '../core/providers/i18n/mark-for-extraction';
+
+import { CustomerDetailComponent } from './components/customer-detail/customer-detail.component';
+import { CustomerListComponent } from './components/customer-list/customer-list.component';
+import { CustomerResolver } from './providers/routing/customer-resolver';
+
+export const customerRoutes: Route[] = [
+    {
+        path: 'customers',
+        component: CustomerListComponent,
+        pathMatch: '',
+        data: {
+            breadcrumb: _('breadcrumb.customers'),
+        },
+    },
+    {
+        path: 'customers/:id',
+        component: CustomerDetailComponent,
+        resolve: createResolveData(CustomerResolver),
+        data: {
+            breadcrumb: customerBreadcrumb,
+        },
+    },
+];
+
+export function customerBreadcrumb(data: any, params: any) {
+    return detailBreadcrumb<Customer.Fragment>({
+        entity: data.entity,
+        id: params.id,
+        breadcrumbKey: 'breadcrumb.customers',
+        getName: customer => `${customer.firstName} ${customer.lastName}`,
+        route: 'customers',
+    });
+}

+ 21 - 0
admin-ui/src/app/customer/providers/routing/customer-resolver.ts

@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+import { Customer } from 'shared/generated-types';
+
+import { BaseEntityResolver } from '../../../common/base-entity-resolver';
+import { DataService } from '../../../data/providers/data.service';
+
+@Injectable()
+export class CustomerResolver extends BaseEntityResolver<Customer.Fragment> {
+    constructor(private dataService: DataService) {
+        super(
+            {
+                __typename: 'Customer',
+                id: '',
+                firstName: '',
+                lastName: '',
+                emailAddress: '',
+            },
+            id => this.dataService.customer.getCustomer(id).mapStream(data => data.customer),
+        );
+    }
+}

+ 41 - 0
admin-ui/src/app/data/definitions/customer-definitions.ts

@@ -0,0 +1,41 @@
+import gql from 'graphql-tag';
+
+export const CUSTOMER_FRAGMENT = gql`
+    fragment Customer on Customer {
+        id
+        firstName
+        lastName
+        emailAddress
+        user {
+            id
+            identifier
+            lastLogin
+        }
+    }
+`;
+
+export const GET_CUSTOMER_LIST = gql`
+    query GetCustomerList($options: CustomerListOptions!) {
+        customers(options: $options) {
+            items {
+                id
+                firstName
+                lastName
+                emailAddress
+                user {
+                    id
+                }
+            }
+            totalItems
+        }
+    }
+`;
+
+export const GET_CUSTOMER = gql`
+    query GetCustomer($id: ID!) {
+        customer(id: $id) {
+            ...Customer
+        }
+    }
+    ${CUSTOMER_FRAGMENT}
+`;

+ 25 - 0
admin-ui/src/app/data/providers/customer-data.service.ts

@@ -0,0 +1,25 @@
+import { GetCustomer, GetCustomerList } from 'shared/generated-types';
+
+import { GET_CUSTOMER, GET_CUSTOMER_LIST } from '../definitions/customer-definitions';
+
+import { BaseDataService } from './base-data.service';
+
+export class CustomerDataService {
+    constructor(private baseDataService: BaseDataService) {}
+
+    getCustomerList(take: number = 10, skip: number = 0) {
+        return this.baseDataService.query<GetCustomerList.Query, GetCustomerList.Variables>(
+            GET_CUSTOMER_LIST,
+            {
+                options: {
+                    take,
+                    skip,
+                },
+            },
+        );
+    }
+
+    getCustomer(id: string) {
+        return this.baseDataService.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, { id });
+    }
+}

+ 4 - 0
admin-ui/src/app/data/providers/data.service.mock.ts

@@ -30,6 +30,10 @@ export function spyObservable(name: string, returnValue: any = {}): jasmine.Spy
 }
 
 export class MockDataService implements DataServiceMock {
+    customer = {
+        getCustomerList: spyQueryResult('getCustomerList'),
+        getCustomer: spyQueryResult('getCustomer'),
+    };
     promotion = {
         getPromotions: spyQueryResult('getPromotions'),
         getPromotion: spyQueryResult('getPromotion'),

+ 3 - 0
admin-ui/src/app/data/providers/data.service.ts

@@ -4,6 +4,7 @@ import { AdministratorDataService } from './administrator-data.service';
 import { AuthDataService } from './auth-data.service';
 import { BaseDataService } from './base-data.service';
 import { ClientDataService } from './client-data.service';
+import { CustomerDataService } from './customer-data.service';
 import { FacetDataService } from './facet-data.service';
 import { OrderDataService } from './order-data.service';
 import { ProductDataService } from './product-data.service';
@@ -20,6 +21,7 @@ export class DataService {
     facet: FacetDataService;
     order: OrderDataService;
     settings: SettingsDataService;
+    customer: CustomerDataService;
 
     constructor(baseDataService: BaseDataService) {
         this.promotion = new PromotionDataService(baseDataService);
@@ -30,5 +32,6 @@ export class DataService {
         this.facet = new FacetDataService(baseDataService);
         this.order = new OrderDataService(baseDataService);
         this.settings = new SettingsDataService(baseDataService);
+        this.customer = new CustomerDataService(baseDataService);
     }
 }

+ 12 - 0
admin-ui/src/i18n-messages/en.json

@@ -7,6 +7,7 @@
     "assets": "Assets",
     "channels": "Channels",
     "countries": "Countries",
+    "customers": "Customers",
     "dashboard": "Dashboard",
     "facets": "Facets",
     "orders": "Orders",
@@ -99,6 +100,16 @@
     "updated-at": "Updated at",
     "username": "Username"
   },
+  "customer": {
+    "create-new-customer": "Create new customer",
+    "customer-type": "Customer type",
+    "email-address": "Email address",
+    "firstName": "First name",
+    "guest": "Guest",
+    "lastName": "Last name",
+    "name": "Name",
+    "registered": "Registered"
+  },
   "error": {
     "401-unauthorized": "Invalid login. Please try again",
     "403-forbidden": "Your session has expired. Please log in",
@@ -121,6 +132,7 @@
     "categories": "Categories",
     "channels": "Channels",
     "countries": "Countries",
+    "customers": "Customers",
     "facets": "Facets",
     "marketing": "Marketing",
     "orders": "Orders",

+ 62 - 0
shared/generated-types.ts

@@ -4019,6 +4019,50 @@ export namespace GetServerConfig {
     };
 }
 
+export namespace GetCustomerList {
+    export type Variables = {
+        options: CustomerListOptions;
+    };
+
+    export type Query = {
+        __typename?: 'Query';
+        customers: Customers;
+    };
+
+    export type Customers = {
+        __typename?: 'CustomerList';
+        items: Items[];
+        totalItems: number;
+    };
+
+    export type Items = {
+        __typename?: 'Customer';
+        id: string;
+        firstName: string;
+        lastName: string;
+        emailAddress: string;
+        user?: User | null;
+    };
+
+    export type User = {
+        __typename?: 'User';
+        id: string;
+    };
+}
+
+export namespace GetCustomer {
+    export type Variables = {
+        id: string;
+    };
+
+    export type Query = {
+        __typename?: 'Query';
+        customer?: Customer | null;
+    };
+
+    export type Customer = Customer.Fragment;
+}
+
 export namespace CreateFacet {
     export type Variables = {
         input: CreateFacetInput;
@@ -4819,6 +4863,24 @@ export namespace CurrentUser {
     };
 }
 
+export namespace Customer {
+    export type Fragment = {
+        __typename?: 'Customer';
+        id: string;
+        firstName: string;
+        lastName: string;
+        emailAddress: string;
+        user?: User | null;
+    };
+
+    export type User = {
+        __typename?: 'User';
+        id: string;
+        identifier: string;
+        lastLogin?: string | null;
+    };
+}
+
 export namespace FacetValue {
     export type Fragment = {
         __typename?: 'FacetValue';