Browse Source

feat(admin-ui): Add tax category controls to ProductVariant form

Michael Bromley 7 years ago
parent
commit
688cf97ba6

+ 1 - 0
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -72,6 +72,7 @@
 
             <vdr-product-variants-list [variants]="variants$ | async"
                                        [productVariantsFormArray]="productForm.get('variants')"
+                                       [taxCategories]="taxCategories$ | async"
                                        #productVariantsList>
                 <button class="btn btn-sm btn-secondary" (click)="selectFacetValue(productVariantsList.selectedVariantIds)">
                     {{ 'catalog.apply-facets' | translate }}

+ 11 - 2
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { combineLatest, EMPTY, forkJoin, Observable } from 'rxjs';
 import { map, mergeMap, take } from 'rxjs/operators';
 import {
+    AdjustmentSource,
     CreateProductInput,
     LanguageCode,
     ProductWithVariants,
@@ -33,6 +34,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     implements OnInit, OnDestroy {
     product$: Observable<ProductWithVariants.Fragment>;
     variants$: Observable<ProductWithVariants.Variants[]>;
+    taxCategories$: Observable<AdjustmentSource.Fragment[]>;
     customFields: CustomFieldConfig[];
     customVariantFields: CustomFieldConfig[];
     productForm: FormGroup;
@@ -67,6 +69,9 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         this.init();
         this.product$ = this.entity$;
         this.variants$ = this.product$.pipe(map(product => product.variants));
+        this.taxCategories$ = this.dataService.adjustmentSource
+            .getTaxCategories()
+            .mapSingle(data => data.adjustmentSources.items);
     }
 
     ngOnDestroy() {
@@ -240,6 +245,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                     sku: variant.sku,
                     name: variantTranslation ? variantTranslation.name : '',
                     price: variant.price,
+                    priceBeforeTax: variant.priceBeforeTax,
+                    taxCategoryId: variant.taxCategory.id,
                 };
 
                 const existing = variantsFormArray.at(i);
@@ -296,12 +303,14 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         }
         return dirtyVariants
             .map((variant, i) => {
-                return createUpdatedTranslatable(
+                const updated = createUpdatedTranslatable(
                     variant,
                     dirtyVariantValues[i],
                     this.customVariantFields,
                     languageCode,
-                );
+                ) as UpdateProductVariantInput;
+                updated.taxCategoryId = dirtyVariantValues[i].taxCategoryId;
+                return updated;
             })
             .filter(notNullOrUndefined);
     }

+ 68 - 41
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html

@@ -1,46 +1,73 @@
 <div class="with-selected">
+    <vdr-select-toggle size="small"
+                       [selected]="areAllSelected()"
+                       (selectedChange)="toggleSelectAll()"></vdr-select-toggle>
     <ng-container *ngIf="selectedVariantIds.length">
         <label>{{ 'catalog.with-selected' | translate }}:</label><ng-content></ng-content>
     </ng-container>
 </div>
-<table class="variants-list table">
-    <thead>
-    <tr>
-        <th>
-            <vdr-select-toggle size="small"
-                               [selected]="areAllSelected()"
-                               (selectedChange)="toggleSelectAll()"></vdr-select-toggle>
-        </th>
-        <th>{{ 'catalog.sku' | translate }}</th>
-        <th>{{ 'catalog.name' | translate }}</th>
-        <th>{{ 'catalog.facets' | translate }}</th>
-        <th>{{ 'catalog.options' | translate }}</th>
-        <th>{{ 'catalog.price' | translate }}</th>
-    </tr>
-    </thead>
-    <tbody>
-    <tr class="variant"
-        *ngFor="let variant of variants; let i = index">
-        <td>
-            <vdr-select-toggle size="small"
-                               [selected]="isVariantSelected(variant.id)"
-                               (selectedChange)="toggleSelectVariant(variant.id)"></vdr-select-toggle>
-        </td>
-        <td>
-            <input type="text" [formControl]="formArray.get([i, 'sku'])">
-        </td>
-        <td>
-            <input type="text" [formControl]="formArray.get([i, 'name'])">
-        </td>
-        <td>
-            <vdr-chip *ngFor="let facetValue of variant.facetValues">{{ facetValue.name }}</vdr-chip>
-        </td>
-        <td>
-            <vdr-chip *ngFor="let option of variant.options">{{ option.name }}</vdr-chip>
-        </td>
-        <td>
-            <vdr-currency-input [formControl]="formArray.get([i, 'price'])"></vdr-currency-input>
-        </td>
-    </tr>
-    </tbody>
-</table>
+<div class="variants-list">
+    <div class="variant-container card" *ngFor="let variant of variants; let i = index">
+        <div class="card-block header-row">
+            <div class="toggle">
+                <vdr-select-toggle size="small"
+                                   [selected]="isVariantSelected(variant.id)"
+                                   (selectedChange)="toggleSelectVariant(variant.id)"></vdr-select-toggle>
+            </div>
+            <div class="options">
+                <vdr-chip *ngFor="let option of variant.options">{{ option.name }}</vdr-chip>
+            </div>
+            <div class="flex-spacer"></div>
+            <div class="facets">
+                <div *ngIf="variant.facetValues.length">{{ 'catalog.facets' | translate }}:</div>
+                <div *ngIf="!variant.facetValues.length">{{ 'catalog.no-facets' | translate }}</div>
+                <vdr-chip *ngFor="let facetValue of variant.facetValues">{{ facetValue.name }}</vdr-chip>
+            </div>
+        </div>
+        <div class="card-block">
+            <div class="details">
+                <div class="sku">
+                    <clr-input-container>
+                        <label>{{ 'catalog.sku' | translate }}</label>
+                        <input clrInput type="text" [formControl]="formArray.get([i, 'sku'])">
+                    </clr-input-container>
+                </div>
+                <div class="name">
+                    <clr-input-container>
+                        <label>{{ 'catalog.name' | translate }}</label>
+                        <input clrInput type="text" [formControl]="formArray.get([i, 'name'])">
+                    </clr-input-container>
+                </div>
+            </div>
+            <div class="pricing">
+                <div class="price-before-tax">
+                    <clr-input-container>
+                        <label>{{ 'catalog.price-before-tax' | translate }}</label>
+                        <vdr-currency-input clrInput
+                                            [formControl]="formArray.get([i, 'priceBeforeTax'])"
+                                            (input)="setPrice(i)"></vdr-currency-input>
+                    </clr-input-container>
+                </div>
+                <div class="tax-category">
+                    <clr-select-container>
+                        <label>{{ 'catalog.tax-category' | translate }}</label>
+                        <select clrSelect name="options"
+                                [formControl]="formArray.get([i, 'taxCategoryId'])"
+                                (change)="setPrice(i)">
+                            <option *ngFor="let taxCategory of taxCategories"
+                                    [value]="taxCategory.id">{{ taxCategory.name }}</option>
+                        </select>
+                    </clr-select-container>
+                </div>
+                <div class="price">
+                    <clr-input-container>
+                        <label>{{ 'catalog.price' | translate }}</label>
+                        <vdr-currency-input clrInput
+                                            [formControl]="formArray.get([i, 'price'])"
+                                            (input)="setPreTaxPrice(i)"></vdr-currency-input>
+                    </clr-input-container>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 46 - 3
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.scss

@@ -1,10 +1,53 @@
+@import "variables";
+
 .with-selected {
-    min-height: 40px;
+    display: flex;
+    min-height: 52px;
+    align-items: center;
+    border: 1px solid $color-grey-2;
+    border-radius: 3px;
+    padding: 6px 18px;
+
+    vdr-select-toggle {
+        margin-right: 12px;
+    }
+
     > label {
         margin-right: 12px;
     }
 }
 
-.table {
-    margin-top: 0;
+.variant-container {
+    .header-row {
+        display: flex;
+        align-items: center;
+    }
+
+    .flex-spacer {
+        flex: 1;
+    }
+
+    .clr-form-control {
+        margin-top: 0;
+    }
+
+    .facets {
+        display: flex;
+        align-items: center;
+    }
+
+    .details {
+        display: flex;
+        margin-bottom: 24px;
+        > div {
+            margin-right: 12px;
+        }
+    }
+
+    .pricing {
+        display: flex;
+        > div {
+            margin-right: 12px;
+        }
+    }
 }

+ 43 - 2
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.ts

@@ -1,6 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
-import { FormArray } from '@angular/forms';
-import { ProductWithVariants } from 'shared/generated-types';
+import { FormArray, FormControl } from '@angular/forms';
+import { AdjustmentSource, ProductWithVariants } from 'shared/generated-types';
 
 @Component({
     selector: 'vdr-product-variants-list',
@@ -11,6 +11,7 @@ import { ProductWithVariants } from 'shared/generated-types';
 export class ProductVariantsListComponent {
     @Input('productVariantsFormArray') formArray: FormArray;
     @Input() variants: ProductWithVariants.Variants[];
+    @Input() taxCategories: AdjustmentSource.Fragment[];
     selectedVariantIds: string[] = [];
 
     areAllSelected(): boolean {
@@ -37,4 +38,44 @@ export class ProductVariantsListComponent {
     isVariantSelected(variantId: string): boolean {
         return -1 < this.selectedVariantIds.indexOf(variantId);
     }
+
+    /**
+     * Set the priceBeforeTax value whenever the price is changed based on the current taxRate.
+     */
+    setPreTaxPrice(index: number) {
+        const { preTaxPriceControl, postTaxPriceControl, taxRate } = this.getPriceControlsAndTaxRate(index);
+        preTaxPriceControl.setValue(Math.round(postTaxPriceControl.value / (1 + taxRate / 100)));
+    }
+
+    /**
+     * Set the price (including tax) value whenever the priceBeforeTax or the taxRate is changed.
+     */
+    setPrice(index: number) {
+        const { preTaxPriceControl, postTaxPriceControl, taxRate } = this.getPriceControlsAndTaxRate(index);
+        postTaxPriceControl.setValue(Math.round(preTaxPriceControl.value * (1 + taxRate / 100)));
+    }
+
+    private getPriceControlsAndTaxRate(
+        index: number,
+    ): {
+        preTaxPriceControl: FormControl;
+        postTaxPriceControl: FormControl;
+        taxRate: number;
+    } {
+        const preTaxPriceControl = this.formArray.get([index, 'priceBeforeTax']);
+        const postTaxPriceControl = this.formArray.get([index, 'price']);
+        const taxCategoryIdControl = this.formArray.get([index, 'taxCategoryId']);
+        if (preTaxPriceControl && postTaxPriceControl && taxCategoryIdControl) {
+            const taxCategory = this.taxCategories.find(tc => tc.id === taxCategoryIdControl.value);
+            if (taxCategory) {
+                const taxRate = Number(taxCategory.actions[0].args[0].value);
+                return {
+                    preTaxPriceControl: preTaxPriceControl as FormControl,
+                    postTaxPriceControl: postTaxPriceControl as FormControl,
+                    taxRate,
+                };
+            }
+        }
+        throw new Error(`Could not find the corresponding form controls.`);
+    }
 }

+ 13 - 1
admin-ui/src/app/data/definitions/product-definitions.ts

@@ -18,6 +18,12 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
         languageCode
         name
         price
+        priceBeforeTax
+        taxCategory {
+            id
+            name
+            taxRate
+        }
         sku
         options {
             id
@@ -111,9 +117,15 @@ export const CREATE_PRODUCT = gql`
 `;
 
 export const GENERATE_PRODUCT_VARIANTS = gql`
-    mutation GenerateProductVariants($productId: ID!, $defaultPrice: Int, $defaultSku: String) {
+    mutation GenerateProductVariants(
+        $productId: ID!
+        $defaultTaxCategoryId: ID
+        $defaultPrice: Int
+        $defaultSku: String
+    ) {
         generateVariantsForProduct(
             productId: $productId
+            defaultTaxCategoryId: $defaultTaxCategoryId
             defaultPrice: $defaultPrice
             defaultSku: $defaultSku
         ) {

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

@@ -90,7 +90,9 @@ export class ProductDataService {
 
     updateProductVariants(variants: UpdateProductVariantInput[]) {
         const input: UpdateProductVariants.Variables = {
-            input: variants.map(pick(['id', 'translations', 'sku', 'price'])),
+            input: variants.map(
+                pick(['id', 'translations', 'sku', 'price', 'priceBeforeTax', 'taxCategoryId']),
+            ),
         };
         return this.baseDataService.mutate<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
             UPDATE_PRODUCT_VARIANTS,

+ 3 - 1
admin-ui/src/i18n-messages/en.json

@@ -34,6 +34,7 @@
     "generate-variants-default-only": "This product does not have options",
     "generate-variants-with-options": "This product has options",
     "name": "Name",
+    "no-facets": "No facets",
     "no-featured-asset": "No featured asset",
     "no-selection": "No selection",
     "notify-create-assets-success": "Created {count, plural, one {new Asset} other {{count} new Assets}}",
@@ -42,9 +43,9 @@
     "option-group-name": "Option group name",
     "option-group-options-label": "Options",
     "option-group-options-tooltip": "Enter each option on a new line in the default language ({ defaultLanguage })",
-    "options": "Options",
     "original-asset-size": "Source size",
     "price": "Price",
+    "price-before-tax": "Price before tax",
     "product": "Product",
     "product-name": "Product name",
     "product-option-groups": "Option groups",
@@ -56,6 +57,7 @@
     "set-as-featured-asset": "Set as featured asset",
     "sku": "SKU",
     "slug": "Slug",
+    "tax-category": "Tax category",
     "truncated-options-count": "{count} further {count, plural, one {option} other {options}}",
     "upload-assets": "Upload assets",
     "values": "Values",

+ 1 - 0
docs/diagrams/full-class-diagram.puml

@@ -108,5 +108,6 @@ AdjustmentSource o-- Channel
 Product o-- Channel
 ProductVariant *-- "1..*" ProductVariantPrice
 ProductVariantPrice o-- Channel
+ProductVariantPrice "taxCategory" -- AdjustmentSource
 
 @enduml