Browse Source

feat(admin-ui): Add date range & coupon code controls to PromotionDetail

Relates to #174
Michael Bromley 6 years ago
parent
commit
48def65f14

+ 11 - 1
packages/admin-ui/src/app/common/generated-types.ts

@@ -527,6 +527,7 @@ export type CreatePromotionInput = {
   startsAt?: Maybe<Scalars['DateTime']>,
   endsAt?: Maybe<Scalars['DateTime']>,
   couponCode?: Maybe<Scalars['String']>,
+  perCustomerUsageLimit?: Maybe<Scalars['Int']>,
   conditions: Array<ConfigurableOperationInput>,
   actions: Array<ConfigurableOperationInput>,
 };
@@ -2183,18 +2184,23 @@ export type Order = Node & {
   id: Scalars['ID'],
   createdAt: Scalars['DateTime'],
   updatedAt: Scalars['DateTime'],
+  /** A unique code for the Order */
   code: Scalars['String'],
   state: Scalars['String'],
+  /** An order is active as long as the payment process has not been completed */
   active: Scalars['Boolean'],
   customer?: Maybe<Customer>,
   shippingAddress?: Maybe<OrderAddress>,
   billingAddress?: Maybe<OrderAddress>,
   lines: Array<OrderLine>,
+  /** Order-level adjustments to the order total, such as discounts from promotions */
   adjustments: Array<Adjustment>,
   couponCodes: Array<Scalars['String']>,
+  promotions: Array<Promotion>,
   payments?: Maybe<Array<Payment>>,
   fulfillments?: Maybe<Array<Fulfillment>>,
   subTotalBeforeTax: Scalars['Int'],
+  /** The subTotal is the total of the OrderLines, before order-level promotions and shipping has been applied. */
   subTotal: Scalars['Int'],
   currencyCode: CurrencyCode,
   shipping: Scalars['Int'],
@@ -2636,6 +2642,7 @@ export type Promotion = Node & {
   startsAt?: Maybe<Scalars['DateTime']>,
   endsAt?: Maybe<Scalars['DateTime']>,
   couponCode?: Maybe<Scalars['String']>,
+  perCustomerUsageLimit?: Maybe<Scalars['Int']>,
   name: Scalars['String'],
   enabled: Scalars['Boolean'],
   conditions: Array<ConfigurableOperation>,
@@ -2648,6 +2655,7 @@ export type PromotionFilterParameter = {
   startsAt?: Maybe<DateOperators>,
   endsAt?: Maybe<DateOperators>,
   couponCode?: Maybe<StringOperators>,
+  perCustomerUsageLimit?: Maybe<NumberOperators>,
   name?: Maybe<StringOperators>,
   enabled?: Maybe<BooleanOperators>,
 };
@@ -2672,6 +2680,7 @@ export type PromotionSortParameter = {
   startsAt?: Maybe<SortOrder>,
   endsAt?: Maybe<SortOrder>,
   couponCode?: Maybe<SortOrder>,
+  perCustomerUsageLimit?: Maybe<SortOrder>,
   name?: Maybe<SortOrder>,
 };
 
@@ -3419,6 +3428,7 @@ export type UpdatePromotionInput = {
   startsAt?: Maybe<Scalars['DateTime']>,
   endsAt?: Maybe<Scalars['DateTime']>,
   couponCode?: Maybe<Scalars['String']>,
+  perCustomerUsageLimit?: Maybe<Scalars['Int']>,
   conditions?: Maybe<Array<ConfigurableOperationInput>>,
   actions?: Maybe<Array<ConfigurableOperationInput>>,
 };
@@ -4009,7 +4019,7 @@ export type GetProductVariantOptionsQueryVariables = {
 
 export type GetProductVariantOptionsQuery = ({ __typename?: 'Query' } & { product: Maybe<({ __typename?: 'Product' } & Pick<Product, 'id' | 'createdAt' | 'updatedAt' | 'name'> & { optionGroups: Array<({ __typename?: 'ProductOptionGroup' } & Pick<ProductOptionGroup, 'id' | 'name' | 'code'> & { options: Array<({ __typename?: 'ProductOption' } & Pick<ProductOption, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'code'>)> })>, variants: Array<({ __typename?: 'ProductVariant' } & Pick<ProductVariant, 'id' | 'createdAt' | 'updatedAt' | 'enabled' | 'name' | 'sku' | 'price' | 'stockOnHand' | 'enabled'> & { options: Array<({ __typename?: 'ProductOption' } & Pick<ProductOption, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'code' | 'groupId'>)> })> })> });
 
-export type PromotionFragment = ({ __typename?: 'Promotion' } & Pick<Promotion, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'enabled'> & { conditions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, actions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> });
+export type PromotionFragment = ({ __typename?: 'Promotion' } & Pick<Promotion, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'enabled' | 'couponCode' | 'perCustomerUsageLimit' | 'startsAt' | 'endsAt'> & { conditions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, actions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> });
 
 export type GetPromotionListQueryVariables = {
   options?: Maybe<PromotionListOptions>

+ 3 - 3
packages/admin-ui/src/app/common/introspection-result.ts

@@ -121,6 +121,9 @@ const result: IntrospectionResultData = {
                     {
                         name: 'Address',
                     },
+                    {
+                        name: 'Promotion',
+                    },
                     {
                         name: 'Payment',
                     },
@@ -166,9 +169,6 @@ const result: IntrospectionResultData = {
                     {
                         name: 'Product',
                     },
-                    {
-                        name: 'Promotion',
-                    },
                 ],
             },
             {

+ 4 - 0
packages/admin-ui/src/app/data/definitions/promotion-definitions.ts

@@ -9,6 +9,10 @@ export const PROMOTION_FRAGMENT = gql`
         updatedAt
         name
         enabled
+        couponCode
+        perCustomerUsageLimit
+        startsAt
+        endsAt
         conditions {
             ...ConfigurableOperation
         }

+ 37 - 6
packages/admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html

@@ -3,12 +3,7 @@
         <div class="flex clr-align-items-center">
             <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
             <clr-toggle-wrapper *vdrIfPermissions="'UpdatePromotion'">
-                <input
-                    type="checkbox"
-                    clrToggle
-                    name="enabled"
-                    [formControl]="detailForm.get(['enabled'])"
-                />
+                <input type="checkbox" clrToggle name="enabled" [formControl]="detailForm.get(['enabled'])" />
                 <label>{{ 'common.enabled' | translate }}</label>
             </clr-toggle-wrapper>
         </div>
@@ -46,6 +41,42 @@
             formControlName="name"
         />
     </vdr-form-field>
+    <vdr-form-field [label]="'marketing.starts-at' | translate" for="startsAt">
+        <input
+            type="datetime-local"
+            name="startsAt"
+            [value]="detailForm.get('startsAt')?.value | date: 'yyyy-MM-ddTHH:mm:ss'"
+            (change)="updateDateTime(detailForm.get('startsAt')!, $event)"
+            [readonly]="!('UpdatePromotion' | hasPermission)"
+        />
+    </vdr-form-field>
+    <vdr-form-field [label]="'marketing.ends-at' | translate" for="endsAt">
+        <input
+            type="datetime-local"
+            name="endsAt"
+            [value]="detailForm.get('endsAt')?.value | date: 'yyyy-MM-ddTHH:mm:ss'"
+            (change)="updateDateTime(detailForm.get('endsAt')!, $event)"
+            [readonly]="!('UpdatePromotion' | hasPermission)"
+        />
+    </vdr-form-field>
+    <vdr-form-field [label]="'marketing.coupon-code' | translate" for="couponCode">
+        <input
+            id="couponCode"
+            [readonly]="!('UpdatePromotion' | hasPermission)"
+            type="text"
+            formControlName="couponCode"
+        />
+    </vdr-form-field>
+    <vdr-form-field [label]="'marketing.per-customer-limit' | translate" for="perCustomerUsageLimit">
+        <input
+            id="perCustomerUsageLimit"
+            [readonly]="!('UpdatePromotion' | hasPermission)"
+            type="number"
+            min="1"
+            max="999"
+            formControlName="perCustomerUsageLimit"
+        />
+    </vdr-form-field>
 
     <div class="clr-row">
         <div class="clr-col" formArrayName="conditions">

+ 30 - 3
packages/admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
-import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { Observable } from 'rxjs';
 import { mergeMap, shareReplay, take } from 'rxjs/operators';
@@ -53,6 +53,10 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         this.detailForm = this.formBuilder.group({
             name: ['', Validators.required],
             enabled: true,
+            couponCode: null,
+            perCustomerUsageLimit: null,
+            startsAt: null,
+            endsAt: null,
             conditions: this.formBuilder.array([]),
             actions: this.formBuilder.array([]),
         });
@@ -103,7 +107,7 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         return (
             this.detailForm.dirty &&
             this.detailForm.valid &&
-            this.conditions.length !== 0 &&
+            (this.conditions.length !== 0 || this.detailForm.value.couponCode) &&
             this.actions.length !== 0
         );
     }
@@ -132,6 +136,14 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         return this.detailForm.get(key) as FormArray;
     }
 
+    // TODO: Remove this once a dedicated cross-browser datetime picker
+    // exists. See https://github.com/vendure-ecommerce/vendure/issues/181
+    updateDateTime(formControl: AbstractControl, event: Event) {
+        const value = (event.target as HTMLInputElement).value;
+        formControl.setValue(value ? new Date(value).toISOString() : null, { emitEvent: true });
+        formControl.parent.markAsDirty();
+    }
+
     create() {
         if (!this.detailForm.dirty) {
             return;
@@ -140,6 +152,10 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         const input: CreatePromotionInput = {
             name: formValue.name,
             enabled: true,
+            couponCode: formValue.couponCode,
+            perCustomerUsageLimit: formValue.perCustomerUsageLimit,
+            startsAt: formValue.startsAt,
+            endsAt: formValue.endsAt,
             conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
             actions: this.mapOperationsToInputs(this.actions, formValue.actions),
         };
@@ -171,6 +187,10 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
                         id: promotion.id,
                         name: formValue.name,
                         enabled: formValue.enabled,
+                        couponCode: formValue.couponCode,
+                        perCustomerUsageLimit: formValue.perCustomerUsageLimit,
+                        startsAt: formValue.startsAt,
+                        endsAt: formValue.endsAt,
                         conditions: this.mapOperationsToInputs(this.conditions, formValue.conditions),
                         actions: this.mapOperationsToInputs(this.actions, formValue.actions),
                     };
@@ -197,7 +217,14 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
      * Update the form values when the entity changes.
      */
     protected setFormValues(entity: Promotion.Fragment, languageCode: LanguageCode): void {
-        this.detailForm.patchValue({ name: entity.name, enabled: entity.enabled });
+        this.detailForm.patchValue({
+            name: entity.name,
+            enabled: entity.enabled,
+            couponCode: entity.couponCode,
+            perCustomerUsageLimit: entity.perCustomerUsageLimit,
+            startsAt: entity.startsAt,
+            endsAt: entity.endsAt,
+        });
         entity.conditions.forEach(o => {
             this.addOperation('conditions', o);
         });

+ 1 - 1
packages/admin-ui/src/app/shared/components/form-field/form-field.component.html

@@ -1,7 +1,7 @@
 <div
     class="form-group"
     [class.no-label]="!label"
-    [class.clr-error]="formFieldControl?.formControlName.invalid"
+    [class.clr-error]="formFieldControl?.formControlName?.invalid"
 >
     <label *ngIf="label" [for]="for" class="clr-control-label">
         {{ label }}

+ 5 - 2
packages/admin-ui/src/i18n-messages/en.json

@@ -149,7 +149,6 @@
     "select": "Select...",
     "there-are-unsaved-changes": "There are unsaved changes. Navigating away will cause these changes to be lost.",
     "update": "Update",
-    "updated": "Updated",
     "updated-at": "Updated at",
     "username": "Username"
   },
@@ -384,7 +383,11 @@
     "add-action": "Add action",
     "add-condition": "Add condition",
     "conditions": "Conditions",
-    "create-new-promotion": "Create new promotion"
+    "coupon-code": "Coupon code",
+    "create-new-promotion": "Create new promotion",
+    "ends-at": "Ends at",
+    "per-customer-limit": "Per-customer limit",
+    "starts-at": "Starts at"
   },
   "nav": {
     "administrators": "Administrators",

+ 13 - 0
packages/admin-ui/src/styles/theme/_forms.scss

@@ -50,6 +50,19 @@ input, select {
     max-height: none !important;
 }
 
+.clr-input-wrapper {
+    max-height: none !important;
+}
+
+.clr-input-group {
+    height: initial !important;
+    border-bottom: none;
+    &:focus, &.clr-focus {
+        background: none;
+        border-bottom: none;
+    }
+}
+
 .tooltip.tooltip-validation::before {
     top: 10px !important;
 }