Procházet zdrojové kódy

feat(admin-ui): Set currencyCode per channel, display correctly

Closes #57
Michael Bromley před 7 roky
rodič
revize
2c7c01709c
19 změnil soubory, kde provedl 128 přidání a 14 odebrání
  1. 2 0
      admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html
  2. 1 1
      admin-ui/src/app/catalog/components/variant-price-detail/variant-price-detail.component.html
  3. 1 0
      admin-ui/src/app/catalog/components/variant-price-detail/variant-price-detail.component.ts
  4. 2 0
      admin-ui/src/app/data/definitions/order-definitions.ts
  5. 1 0
      admin-ui/src/app/data/definitions/product-definitions.ts
  6. 1 0
      admin-ui/src/app/data/definitions/settings-definitions.ts
  7. 8 8
      admin-ui/src/app/order/components/order-detail/order-detail.component.html
  8. 1 1
      admin-ui/src/app/order/components/order-list/order-list.component.html
  9. 8 0
      admin-ui/src/app/settings/components/channel-detail/channel-detail.component.html
  10. 13 0
      admin-ui/src/app/settings/components/channel-detail/channel-detail.component.ts
  11. 2 1
      admin-ui/src/app/settings/providers/routing/channel-resolver.ts
  12. 1 1
      admin-ui/src/app/shared/components/currency-input/currency-input.component.html
  13. 2 0
      admin-ui/src/app/shared/components/currency-input/currency-input.component.spec.ts
  14. 1 0
      admin-ui/src/app/shared/components/currency-input/currency-input.component.ts
  15. 37 0
      admin-ui/src/app/shared/pipes/currency-name.pipe.spec.ts
  16. 40 0
      admin-ui/src/app/shared/pipes/currency-name.pipe.ts
  17. 2 0
      admin-ui/src/app/shared/shared.module.ts
  18. 3 2
      admin-ui/src/i18n-messages/en.json
  19. 2 0
      shared/generated-types.ts

+ 2 - 0
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html

@@ -80,12 +80,14 @@
                                 <label>{{ 'catalog.price' | translate }}</label>
                                 <vdr-currency-input
                                     clrInput
+                                    [currencyCode]="variant.currencyCode"
                                     [formControl]="formArray.get([i, 'price'])"
                                 ></vdr-currency-input>
                             </clr-input-container>
                         </div>
                         <vdr-variant-price-detail
                             [price]="formArray.get([i, 'price'])!.value"
+                            [currencyCode]="variant.currencyCode"
                             [priceIncludesTax]="variant.priceIncludesTax"
                             [taxCategoryId]="formArray.get([i, 'taxCategoryId'])!.value"
                         ></vdr-variant-price-detail>

+ 1 - 1
admin-ui/src/app/catalog/components/variant-price-detail/variant-price-detail.component.html

@@ -5,6 +5,6 @@
 <div *ngIf="!priceIncludesTax">
     {{
         'catalog.price-with-tax-in-default-zone'
-            | translate: { price: grossPrice$ | async | currency, rate: taxRate$ | async }
+            | translate: { price: grossPrice$ | async | currency: currencyCode, rate: taxRate$ | async }
     }}
 </div>

+ 1 - 0
admin-ui/src/app/catalog/components/variant-price-detail/variant-price-detail.component.ts

@@ -13,6 +13,7 @@ import { DataService } from '../../../data/providers/data.service';
 export class VariantPriceDetailComponent implements OnInit, OnChanges {
     @Input() priceIncludesTax: boolean;
     @Input() price: number;
+    @Input() currencyCode: string;
     @Input() taxCategoryId: string;
 
     grossPrice$: Observable<number>;

+ 2 - 0
admin-ui/src/app/data/definitions/order-definitions.ts

@@ -31,6 +31,7 @@ export const ORDER_FRAGMENT = gql`
         code
         state
         total
+        currencyCode
         customer {
             id
             firstName
@@ -80,6 +81,7 @@ export const ORDER_WITH_LINES_FRAGMENT = gql`
         subTotal
         subTotalBeforeTax
         totalBeforeTax
+        currencyCode
         shipping
         shippingMethod {
             id

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

@@ -18,6 +18,7 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
         languageCode
         name
         price
+        currencyCode
         priceIncludesTax
         priceWithTax
         taxRateApplied {

+ 1 - 0
admin-ui/src/app/data/definitions/settings-definitions.ts

@@ -234,6 +234,7 @@ export const CHANNEL_FRAGMENT = gql`
         code
         token
         pricesIncludeTax
+        currencyCode
         defaultLanguageCode
         defaultShippingZone {
             id

+ 8 - 8
admin-ui/src/app/order/components/order-detail/order-detail.component.html

@@ -59,7 +59,7 @@
                         </tr>
                         <tr>
                             <th>{{ 'order.amount' | translate }}</th>
-                            <td>{{ payment.amount / 100 | currency }}</td>
+                            <td>{{ payment.amount / 100 | currency: order.currencyCode }}</td>
                         </tr>
                         <tr>
                             <th>{{ 'order.transaction-id' | translate }}</th>
@@ -97,9 +97,9 @@
             <td class="thumb"><img [src]="line.featuredAsset.preview + '?preset=tiny'" /></td>
             <td class="name">{{ line.productVariant.name }}</td>
             <td class="sku">{{ line.productVariant.sku }}</td>
-            <td class="unit-price">{{ line.unitPriceWithTax / 100 | currency }}</td>
+            <td class="unit-price">{{ line.unitPriceWithTax / 100 | currency: order.currencyCode }}</td>
             <td class="quantity">{{ line.quantity }}</td>
-            <td class="total">{{ line.totalPrice / 100 | currency }}</td>
+            <td class="total">{{ line.totalPrice / 100 | currency: order.currencyCode }}</td>
         </tr>
         <tr class="sub-total">
             <td class="left">{{ 'order.sub-total' | translate }}</td>
@@ -107,17 +107,17 @@
             <td></td>
             <td></td>
             <td></td>
-            <td>{{ order.subTotal / 100 | currency }}</td>
+            <td>{{ order.subTotal / 100 | currency: order.currencyCode }}</td>
         </tr>
         <tr class="order-ajustment" *ngFor="let adjustment of order.adjustments">
             <td colspan="5" class="left">{{ adjustment.description }}</td>
-            <td>{{ adjustment.amount / 100 | currency }}</td>
+            <td>{{ adjustment.amount / 100 | currency: order.currencyCode }}</td>
         </tr>
         <tr class="shipping">
             <td class="left">{{ 'order.shipping' | translate }}</td>
-            <td>{{ order.shippingMethod.description }}</td>
+            <td>{{ order.shippingMethod?.description }}</td>
             <td colspan="3"></td>
-            <td>{{ order.shipping / 100 | currency }}</td>
+            <td>{{ order.shipping / 100 | currency: order.currencyCode }}</td>
         </tr>
         <tr class="total">
             <td class="left">{{ 'order.total' | translate }}</td>
@@ -125,7 +125,7 @@
             <td></td>
             <td></td>
             <td></td>
-            <td>{{ order.total / 100 | currency }}</td>
+            <td>{{ order.total / 100 | currency: order.currencyCode }}</td>
         </tr>
     </table>
 </div>

+ 1 - 1
admin-ui/src/app/order/components/order-list/order-list.component.html

@@ -29,7 +29,7 @@
         <td class="left">{{ order.code }}</td>
         <td class="left"><vdr-customer-label [customer]="order.customer"></vdr-customer-label></td>
         <td class="left">{{ order.state }}</td>
-        <td class="left">{{ order.total / 100 | currency }}</td>
+        <td class="left">{{ order.total / 100 | currency: order.currencyCode }}</td>
         <td class="left">{{ order.updatedAt | date: 'medium' }}</td>
         <td class="right">
             <vdr-table-row-action

+ 8 - 0
admin-ui/src/app/settings/components/channel-detail/channel-detail.component.html

@@ -23,6 +23,14 @@
         <vdr-form-field [label]="'common.code' | translate" for="code">
             <input id="code" type="text" formControlName="code" />
         </vdr-form-field>
+        <vdr-form-field [label]="'settings.channel-token' | translate" for="token">
+            <input id="token" type="text" formControlName="token" />
+        </vdr-form-field>
+        <vdr-form-field [label]="'settings.currency' | translate" for="defaultTaxZoneId">
+            <select clrSelect name="currencyCode" formControlName="currencyCode">
+                <option *ngFor="let code of currencyCodes" [value]="code">{{ code | currencyName }}</option>
+            </select>
+        </vdr-form-field>
         <vdr-form-field [label]="'settings.prices-include-tax' | translate" for="pricesIncludeTax">
             <div class="toggle-switch">
                 <input type="checkbox" id="pricesIncludeTax" formControlName="pricesIncludeTax" />

+ 13 - 0
admin-ui/src/app/settings/components/channel-detail/channel-detail.component.ts

@@ -6,10 +6,12 @@ import { mergeMap, take } from 'rxjs/operators';
 import {
     Channel,
     CreateChannelInput,
+    CurrencyCode,
     GetZones,
     LanguageCode,
     UpdateChannelInput,
 } from 'shared/generated-types';
+import { DEFAULT_CHANNEL_CODE } from 'shared/shared-constants';
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
@@ -27,6 +29,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
     implements OnInit, OnDestroy {
     zones$: Observable<GetZones.Zones[]>;
     detailForm: FormGroup;
+    currencyCodes = Object.values(CurrencyCode);
 
     constructor(
         router: Router,
@@ -42,6 +45,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
             code: ['', Validators.required],
             token: ['', Validators.required],
             pricesIncludeTax: [false],
+            currencyCode: [''],
             defaultShippingZoneId: [''],
             defaultTaxZoneId: [''],
         });
@@ -68,6 +72,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
         const input = {
             code: formValue.code,
             pricesIncludeTax: formValue.pricesIncludeTax,
+            currencyCode: formValue.currencyCode,
             defaultShippingZoneId: formValue.defaultShippingZoneId,
             defaultTaxZoneId: formValue.defaultTaxZoneId,
         } as CreateChannelInput;
@@ -101,6 +106,7 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
                         id: channel.id,
                         code: formValue.code,
                         pricesIncludeTax: formValue.pricesIncludeTax,
+                        currencyCode: formValue.currencyCode,
                         defaultShippingZoneId: formValue.defaultShippingZoneId,
                         defaultTaxZoneId: formValue.defaultTaxZoneId,
                     } as UpdateChannelInput;
@@ -131,8 +137,15 @@ export class ChannelDetailComponent extends BaseDetailComponent<Channel.Fragment
             code: entity.code,
             token: entity.token,
             pricesIncludeTax: entity.pricesIncludeTax,
+            currencyCode: entity.currencyCode,
             defaultShippingZoneId: entity.defaultShippingZone ? entity.defaultShippingZone.id : '',
             defaultTaxZoneId: entity.defaultTaxZone ? entity.defaultTaxZone.id : '',
         });
+        if (entity.code === DEFAULT_CHANNEL_CODE) {
+            const codeControl = this.detailForm.get('code');
+            if (codeControl) {
+                codeControl.disable();
+            }
+        }
     }
 }

+ 2 - 1
admin-ui/src/app/settings/providers/routing/channel-resolver.ts

@@ -1,5 +1,5 @@
 import { Injectable } from '@angular/core';
-import { Channel } from 'shared/generated-types';
+import { Channel, CurrencyCode } from 'shared/generated-types';
 
 import { BaseEntityResolver } from '../../../common/base-entity-resolver';
 import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
@@ -18,6 +18,7 @@ export class ChannelResolver extends BaseEntityResolver<Channel.Fragment> {
                 code: '',
                 token: '',
                 pricesIncludeTax: false,
+                currencyCode: CurrencyCode.USD,
                 defaultLanguageCode: getDefaultLanguage(),
                 defaultShippingZone: {} as any,
                 defaultTaxZone: {} as any,

+ 1 - 1
admin-ui/src/app/shared/components/currency-input/currency-input.component.html

@@ -1,4 +1,4 @@
-<vdr-affixed-input [prefix]="currencySymbol">
+<vdr-affixed-input [prefix]="currencyCode | currencyName: 'symbol'">
     <input
         type="number"
         step="0.01"

+ 2 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.spec.ts

@@ -4,6 +4,7 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core
 import { FormsModule } from '@angular/forms';
 import { By } from '@angular/platform-browser';
 
+import { CurrencyNamePipe } from '../../pipes/currency-name.pipe';
 import { AffixedInputComponent } from '../affixed-input/affixed-input.component';
 
 import { CurrencyInputComponent } from './currency-input.component';
@@ -17,6 +18,7 @@ describe('CurrencyInputComponent', () => {
                 TestSimpleComponent,
                 CurrencyInputComponent,
                 AffixedInputComponent,
+                CurrencyNamePipe,
             ],
         }).compileComponents();
     }));

+ 1 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.ts

@@ -23,6 +23,7 @@ export class CurrencyInputComponent implements ControlValueAccessor, OnChanges {
     @Input() disabled = false;
     @Input() readonly = false;
     @Input() value: number;
+    @Input() currencyCode = '';
     onChange: (val: any) => void;
     onTouch: () => void;
     _decimalValue: string;

+ 37 - 0
admin-ui/src/app/shared/pipes/currency-name.pipe.spec.ts

@@ -0,0 +1,37 @@
+import { CurrencyNamePipe } from './currency-name.pipe';
+
+describe('CurrencyNamePipe', () => {
+    const pipe = new CurrencyNamePipe();
+    it('full output', () => {
+        expect(pipe.transform('usd')).toBe('US dollars ($)');
+        expect(pipe.transform('gbp')).toBe('British pounds (£)');
+        expect(pipe.transform('CNY')).toBe('Chinese yuan (CN¥)');
+    });
+
+    it('name output', () => {
+        expect(pipe.transform('usd', 'name')).toBe('US dollars');
+        expect(pipe.transform('gbp', 'name')).toBe('British pounds');
+        expect(pipe.transform('CNY', 'name')).toBe('Chinese yuan');
+    });
+
+    it('symbol output', () => {
+        expect(pipe.transform('usd', 'symbol')).toBe('$');
+        expect(pipe.transform('gbp', 'symbol')).toBe('£');
+        expect(pipe.transform('CNY', 'symbol')).toBe('CN¥');
+    });
+
+    it('returns code for unknown codes', () => {
+        expect(pipe.transform('zzz')).toBe('ZZZ (ZZZ)');
+    });
+
+    it('returns empty string for empty input', () => {
+        expect(pipe.transform('')).toBe('');
+        expect(pipe.transform(null)).toBe('');
+        expect(pipe.transform(undefined)).toBe('');
+    });
+
+    it('returns warning for invalid input', () => {
+        expect(pipe.transform({} as any)).toBe('Invalid currencyCode "[object Object]"');
+        expect(pipe.transform(false as any)).toBe('Invalid currencyCode "false"');
+    });
+});

+ 40 - 0
admin-ui/src/app/shared/pipes/currency-name.pipe.ts

@@ -0,0 +1,40 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+/**
+ * Displays a human-readable name for a given ISO 4217 currency code.
+ */
+@Pipe({
+    name: 'currencyName',
+})
+export class CurrencyNamePipe implements PipeTransform {
+    transform(value: any, display: 'full' | 'symbol' | 'name' = 'full'): any {
+        if (value == null || value === '') {
+            return '';
+        }
+        if (typeof value !== 'string') {
+            return `Invalid currencyCode "${value as any}"`;
+        }
+        let name = '';
+        let symbol = '';
+
+        if (display === 'full' || display === 'name') {
+            name = new Intl.NumberFormat('en', {
+                style: 'currency',
+                currency: value,
+                currencyDisplay: 'name',
+            })
+                .format(undefined as any)
+                .replace(/\s*NaN\s*/, '');
+        }
+        if (display === 'full' || display === 'symbol') {
+            symbol = new Intl.NumberFormat('en', {
+                style: 'currency',
+                currency: value,
+                currencyDisplay: 'symbol',
+            })
+                .format(undefined as any)
+                .replace(/\s*NaN\s*/, '');
+        }
+        return display === 'full' ? `${name} (${symbol})` : display === 'name' ? name : symbol;
+    }
+}

+ 2 - 0
admin-ui/src/app/shared/shared.module.ts

@@ -38,6 +38,7 @@ import { RichTextEditorComponent } from './components/rich-text-editor/rich-text
 import { SelectToggleComponent } from './components/select-toggle/select-toggle.component';
 import { TableRowActionComponent } from './components/table-row-action/table-row-action.component';
 import { BackgroundColorFromDirective } from './directives/background-color-from.directive';
+import { CurrencyNamePipe } from './pipes/currency-name.pipe';
 import { FileSizePipe } from './pipes/file-size.pipe';
 import { ModalService } from './providers/modal/modal.service';
 import { CanDeactivateDetailGuard } from './providers/routing/can-deactivate-detail-guard';
@@ -63,6 +64,7 @@ const DECLARATIONS = [
     ChipComponent,
     ConfirmNavigationDialogComponent,
     CurrencyInputComponent,
+    CurrencyNamePipe,
     CustomerLabelComponent,
     CustomFieldControlComponent,
     DataTableComponent,

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

@@ -133,8 +133,7 @@
     "403-forbidden": "Your session has expired. Please log in",
     "could-not-connect-to-server": "Could not connect to the Vendure server at { url }",
     "facet-value-form-values-do-not-match": "The number of values in the facet form does not match the actual number of values",
-    "product-variant-form-values-do-not-match": "The number of variants in the product form does not match the actual number of variants",
-    "this-field-is-required": "This field is required"
+    "product-variant-form-values-do-not-match": "The number of variants in the product form does not match the actual number of variants"
   },
   "marketing": {
     "actions": "Actions",
@@ -187,6 +186,7 @@
     "add-countries-to-zone-success": "Added { countryCount } {countryCount, plural, one {country} other {countries}} to zone \"{ zoneName }\"",
     "administrator": "Administrator",
     "catalog": "Catalog",
+    "channel-token": "Channel token",
     "create": "Create",
     "create-new-channel": "Create new channel",
     "create-new-country": "Create new country",
@@ -195,6 +195,7 @@
     "create-new-tax-category": "Create tax category",
     "create-new-tax-rate": "Create new tax rate",
     "create-zone": "Create zone",
+    "currency": "Currency",
     "customer": "Customer",
     "default-shipping-zone": "Default shipping zone",
     "default-tax-zone": "Default tax zone",

+ 2 - 0
shared/generated-types.ts

@@ -6554,6 +6554,7 @@ export namespace Order {
         code: string;
         state: string;
         total: number;
+        currencyCode: CurrencyCode;
         customer?: Customer | null;
     };
 
@@ -6580,6 +6581,7 @@ export namespace OrderWithLines {
         subTotal: number;
         subTotalBeforeTax: number;
         totalBeforeTax: number;
+        currencyCode: CurrencyCode;
         shipping: number;
         shippingMethod?: ShippingMethod | null;
         shippingAddress?: ShippingAddress | null;