Jelajahi Sumber

feat(admin-ui): Implement order modification flow

Relates to #314
Michael Bromley 5 tahun lalu
induk
melakukan
d3e3a88423
52 mengubah file dengan 1991 tambahan dan 388 penghapusan
  1. 28 23
      packages/admin-ui/i18n-coverage.json
  2. 1 1
      packages/admin-ui/src/lib/core/src/common/detail-breadcrumb.ts
  3. 81 3
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  4. 45 0
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  5. 19 0
      packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts
  6. 67 0
      packages/admin-ui/src/lib/core/src/shared/components/address-form/address-form.component.html
  7. 0 0
      packages/admin-ui/src/lib/core/src/shared/components/address-form/address-form.component.scss
  8. 15 0
      packages/admin-ui/src/lib/core/src/shared/components/address-form/address-form.component.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.ts
  10. 2 0
      packages/admin-ui/src/lib/core/src/shared/pipes/state-i18n-token.pipe.ts
  11. 2 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  12. 5 67
      packages/admin-ui/src/lib/customer/src/components/address-detail-dialog/address-detail-dialog.component.html
  13. 22 0
      packages/admin-ui/src/lib/order/src/components/add-manual-payment-dialog/add-manual-payment-dialog.component.html
  14. 3 0
      packages/admin-ui/src/lib/order/src/components/add-manual-payment-dialog/add-manual-payment-dialog.component.scss
  15. 48 0
      packages/admin-ui/src/lib/order/src/components/add-manual-payment-dialog/add-manual-payment-dialog.component.ts
  16. 1 1
      packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.html
  17. 1 1
      packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.scss
  18. 14 0
      packages/admin-ui/src/lib/order/src/components/modification-detail/modification-detail.component.html
  19. 0 0
      packages/admin-ui/src/lib/order/src/components/modification-detail/modification-detail.component.scss
  20. 65 0
      packages/admin-ui/src/lib/order/src/components/modification-detail/modification-detail.component.ts
  21. 19 187
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html
  22. 1 55
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.scss
  23. 85 35
      packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts
  24. 29 0
      packages/admin-ui/src/lib/order/src/components/order-detail/transition-to-pre-modifying-state.ts
  25. 318 0
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html
  26. 10 0
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.scss
  27. 347 0
      packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts
  28. 29 0
      packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.html
  29. 0 0
      packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.scss
  30. 80 0
      packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.ts
  31. 26 1
      packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html
  32. 8 0
      packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts
  33. 1 1
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  34. 47 0
      packages/admin-ui/src/lib/order/src/components/order-table/order-table-mixin.scss
  35. 165 0
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html
  36. 15 0
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.scss
  37. 50 0
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.ts
  38. 1 1
      packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.html
  39. 1 1
      packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.scss
  40. 10 0
      packages/admin-ui/src/lib/order/src/order.module.ts
  41. 29 1
      packages/admin-ui/src/lib/order/src/order.routes.ts
  42. 1 1
      packages/admin-ui/src/lib/settings/src/components/test-order-builder/test-order-builder.component.html
  43. 31 1
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  44. 31 1
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  45. 31 1
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  46. 30 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  47. 53 3
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  48. 31 1
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  49. 31 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  50. 30 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  51. 30 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  52. 1 0
      packages/admin-ui/src/lib/static/styles/theme/_forms.scss

+ 28 - 23
packages/admin-ui/i18n-coverage.json

@@ -1,46 +1,51 @@
 {
-  "generatedOn": "2020-12-04T16:28:55.124Z",
-  "lastCommit": "8e5b1dd3b6d362fd9431d242e5e829419e6f3179",
+  "generatedOn": "2020-12-21T15:26:51.662Z",
+  "lastCommit": "3fda1021d033a38b878a6c6d8b5543bf0d554155",
   "translationStatus": {
     "cs": {
-      "tokenCount": 713,
-      "translatedCount": 685,
-      "percentage": 96
+      "tokenCount": 743,
+      "translatedCount": 687,
+      "percentage": 92
     },
     "de": {
-      "tokenCount": 713,
-      "translatedCount": 594,
-      "percentage": 83
+      "tokenCount": 743,
+      "translatedCount": 596,
+      "percentage": 80
     },
     "en": {
-      "tokenCount": 713,
-      "translatedCount": 713,
-      "percentage": 100
+      "tokenCount": 743,
+      "translatedCount": 739,
+      "percentage": 99
     },
     "es": {
-      "tokenCount": 713,
+      "tokenCount": 743,
       "translatedCount": 455,
-      "percentage": 64
+      "percentage": 61
+    },
+    "fr": {
+      "tokenCount": 743,
+      "translatedCount": 692,
+      "percentage": 93
     },
     "pl": {
-      "tokenCount": 713,
-      "translatedCount": 549,
-      "percentage": 77
+      "tokenCount": 743,
+      "translatedCount": 551,
+      "percentage": 74
     },
     "pt_BR": {
-      "tokenCount": 713,
-      "translatedCount": 640,
-      "percentage": 90
+      "tokenCount": 743,
+      "translatedCount": 642,
+      "percentage": 86
     },
     "zh_Hans": {
-      "tokenCount": 713,
+      "tokenCount": 743,
       "translatedCount": 533,
-      "percentage": 75
+      "percentage": 72
     },
     "zh_Hant": {
-      "tokenCount": 713,
+      "tokenCount": 743,
       "translatedCount": 533,
-      "percentage": 75
+      "percentage": 72
     }
   }
 }

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/detail-breadcrumb.ts

@@ -29,7 +29,7 @@ export function detailBreadcrumb<T>(options: {
                 },
                 {
                     label,
-                    link: [options.id],
+                    link: [options.route, options.id],
                 },
             ];
         }),

+ 81 - 3
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -3345,7 +3345,8 @@ export enum HistoryEntryType {
   ORDER_FULFILLMENT_TRANSITION = 'ORDER_FULFILLMENT_TRANSITION',
   ORDER_NOTE = 'ORDER_NOTE',
   ORDER_COUPON_APPLIED = 'ORDER_COUPON_APPLIED',
-  ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED'
+  ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
+  ORDER_MODIFIED = 'ORDER_MODIFIED'
 }
 
 export type HistoryEntryList = PaginatedList & {
@@ -5451,7 +5452,7 @@ export type RefundFragment = (
 
 export type OrderAddressFragment = (
   { __typename?: 'OrderAddress' }
-  & Pick<OrderAddress, 'fullName' | 'company' | 'streetLine1' | 'streetLine2' | 'city' | 'province' | 'postalCode' | 'country' | 'phoneNumber'>
+  & Pick<OrderAddress, 'fullName' | 'company' | 'streetLine1' | 'streetLine2' | 'city' | 'province' | 'postalCode' | 'country' | 'countryCode' | 'phoneNumber'>
 );
 
 export type OrderFragment = (
@@ -5541,7 +5542,23 @@ export type OrderDetailFragment = (
   )>>, fulfillments?: Maybe<Array<(
     { __typename?: 'Fulfillment' }
     & FulfillmentFragment
-  )>> }
+  )>>, modifications: Array<(
+    { __typename?: 'OrderModification' }
+    & Pick<OrderModification, 'id' | 'createdAt' | 'isSettled' | 'priceChange' | 'note'>
+    & { payment?: Maybe<(
+      { __typename?: 'Payment' }
+      & Pick<Payment, 'id' | 'amount'>
+    )>, orderItems?: Maybe<Array<(
+      { __typename?: 'OrderItem' }
+      & Pick<OrderItem, 'id'>
+    )>>, refund?: Maybe<(
+      { __typename?: 'Refund' }
+      & Pick<Refund, 'id' | 'paymentId' | 'total'>
+    )>, surcharges?: Maybe<Array<(
+      { __typename?: 'Surcharge' }
+      & Pick<Surcharge, 'id'>
+    )>> }
+  )> }
 );
 
 export type GetOrderListQueryVariables = Exact<{
@@ -5798,6 +5815,50 @@ export type GetOrderSummaryQuery = { orders: (
     )> }
   ) };
 
+export type ModifyOrderMutationVariables = Exact<{
+  input: ModifyOrderInput;
+}>;
+
+
+export type ModifyOrderMutation = { modifyOrder: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'NoChangesSpecifiedError' }
+    & ErrorResult_NoChangesSpecifiedError_Fragment
+  ) | (
+    { __typename?: 'OrderModificationStateError' }
+    & ErrorResult_OrderModificationStateError_Fragment
+  ) | (
+    { __typename?: 'PaymentMethodMissingError' }
+    & ErrorResult_PaymentMethodMissingError_Fragment
+  ) | (
+    { __typename?: 'RefundPaymentIdMissingError' }
+    & ErrorResult_RefundPaymentIdMissingError_Fragment
+  ) | (
+    { __typename?: 'OrderLimitError' }
+    & ErrorResult_OrderLimitError_Fragment
+  ) | (
+    { __typename?: 'NegativeQuantityError' }
+    & ErrorResult_NegativeQuantityError_Fragment
+  ) | (
+    { __typename?: 'InsufficientStockError' }
+    & ErrorResult_InsufficientStockError_Fragment
+  ) };
+
+export type AddManualPaymentMutationVariables = Exact<{
+  input: ManualPaymentInput;
+}>;
+
+
+export type AddManualPaymentMutation = { addManualPaymentToOrder: (
+    { __typename?: 'Order' }
+    & OrderDetailFragment
+  ) | (
+    { __typename?: 'ManualPaymentStateError' }
+    & ErrorResult_ManualPaymentStateError_Fragment
+  ) };
+
 export type AssetFragment = (
   { __typename?: 'Asset' }
   & Pick<Asset, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'fileSize' | 'mimeType' | 'type' | 'preview' | 'source' | 'width' | 'height'>
@@ -8108,6 +8169,11 @@ export namespace OrderDetail {
   export type Refunds = NonNullable<(NonNullable<NonNullable<(NonNullable<OrderDetailFragment['payments']>)[number]>['refunds']>)[number]>;
   export type OrderItems = NonNullable<(NonNullable<NonNullable<(NonNullable<NonNullable<(NonNullable<OrderDetailFragment['payments']>)[number]>['refunds']>)[number]>['orderItems']>)[number]>;
   export type Fulfillments = NonNullable<(NonNullable<OrderDetailFragment['fulfillments']>)[number]>;
+  export type Modifications = NonNullable<(NonNullable<OrderDetailFragment['modifications']>)[number]>;
+  export type Payment = (NonNullable<NonNullable<(NonNullable<OrderDetailFragment['modifications']>)[number]>['payment']>);
+  export type _OrderItems = NonNullable<(NonNullable<NonNullable<(NonNullable<OrderDetailFragment['modifications']>)[number]>['orderItems']>)[number]>;
+  export type Refund = (NonNullable<NonNullable<(NonNullable<OrderDetailFragment['modifications']>)[number]>['refund']>);
+  export type _Surcharges = NonNullable<(NonNullable<NonNullable<(NonNullable<OrderDetailFragment['modifications']>)[number]>['surcharges']>)[number]>;
 }
 
 export namespace GetOrderList {
@@ -8211,6 +8277,18 @@ export namespace GetOrderSummary {
   export type Items = NonNullable<(NonNullable<(NonNullable<GetOrderSummaryQuery['orders']>)['items']>)[number]>;
 }
 
+export namespace ModifyOrder {
+  export type Variables = ModifyOrderMutationVariables;
+  export type Mutation = ModifyOrderMutation;
+  export type ModifyOrder = (NonNullable<ModifyOrderMutation['modifyOrder']>);
+}
+
+export namespace AddManualPayment {
+  export type Variables = AddManualPaymentMutationVariables;
+  export type Mutation = AddManualPaymentMutation;
+  export type AddManualPaymentToOrder = (NonNullable<AddManualPaymentMutation['addManualPaymentToOrder']>);
+}
+
 export namespace Asset {
   export type Fragment = AssetFragment;
   export type FocalPoint = (NonNullable<AssetFragment['focalPoint']>);

+ 45 - 0
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -33,6 +33,7 @@ export const ORDER_ADDRESS_FRAGMENT = gql`
         province
         postalCode
         country
+        countryCode
         phoneNumber
     }
 `;
@@ -199,6 +200,28 @@ export const ORDER_DETAIL_FRAGMENT = gql`
         fulfillments {
             ...Fulfillment
         }
+        modifications {
+            id
+            createdAt
+            isSettled
+            priceChange
+            note
+            payment {
+                id
+                amount
+            }
+            orderItems {
+                id
+            }
+            refund {
+                id
+                paymentId
+                total
+            }
+            surcharges {
+                id
+            }
+        }
     }
     ${ADJUSTMENT_FRAGMENT}
     ${ORDER_ADDRESS_FRAGMENT}
@@ -396,3 +419,25 @@ export const GET_ORDER_SUMMARY = gql`
         }
     }
 `;
+
+export const MODIFY_ORDER = gql`
+    mutation ModifyOrder($input: ModifyOrderInput!) {
+        modifyOrder(input: $input) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;
+
+export const ADD_MANUAL_PAYMENT_TO_ORDER = gql`
+    mutation AddManualPayment($input: ManualPaymentInput!) {
+        addManualPaymentToOrder(input: $input) {
+            ...OrderDetail
+            ...ErrorResult
+        }
+    }
+    ${ORDER_DETAIL_FRAGMENT}
+    ${ERROR_RESULT_FRAGMENT}
+`;

+ 19 - 0
packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts

@@ -1,4 +1,5 @@
 import {
+    AddManualPayment,
     AddNoteToOrder,
     AddNoteToOrderInput,
     CancelOrder,
@@ -11,6 +12,9 @@ import {
     GetOrderList,
     GetOrderSummary,
     HistoryEntryListOptions,
+    ManualPaymentInput,
+    ModifyOrder,
+    ModifyOrderInput,
     OrderListOptions,
     RefundOrder,
     RefundOrderInput,
@@ -25,6 +29,7 @@ import {
     UpdateOrderNoteInput,
 } from '../../common/generated-types';
 import {
+    ADD_MANUAL_PAYMENT_TO_ORDER,
     ADD_NOTE_TO_ORDER,
     CANCEL_ORDER,
     CREATE_FULFILLMENT,
@@ -33,6 +38,7 @@ import {
     GET_ORDERS_LIST,
     GET_ORDER_HISTORY,
     GET_ORDER_SUMMARY,
+    MODIFY_ORDER,
     REFUND_ORDER,
     SETTLE_PAYMENT,
     SETTLE_REFUND,
@@ -165,4 +171,17 @@ export class OrderDataService {
             },
         );
     }
+
+    modifyOrder(input: ModifyOrderInput) {
+        return this.baseDataService.mutate<ModifyOrder.Mutation, ModifyOrder.Variables>(MODIFY_ORDER, {
+            input,
+        });
+    }
+
+    addManualPaymentToOrder(input: ManualPaymentInput) {
+        return this.baseDataService.mutate<AddManualPayment.Mutation, AddManualPayment.Variables>(
+            ADD_MANUAL_PAYMENT_TO_ORDER,
+            { input },
+        );
+    }
 }

+ 67 - 0
packages/admin-ui/src/lib/core/src/shared/components/address-form/address-form.component.html

@@ -0,0 +1,67 @@
+<form [formGroup]="formGroup">
+    <clr-input-container>
+        <label>{{ 'customer.full-name' | translate }}</label>
+        <input formControlName="fullName" type="text" clrInput />
+    </clr-input-container>
+
+    <div class="clr-row">
+        <div class="clr-col-md-4">
+            <clr-input-container>
+                <label>{{ 'customer.street-line-1' | translate }}</label>
+                <input formControlName="streetLine1" type="text" clrInput />
+            </clr-input-container>
+        </div>
+        <div class="clr-col-md-4">
+            <clr-input-container>
+                <label>{{ 'customer.street-line-2' | translate }}</label>
+                <input formControlName="streetLine2" type="text" clrInput />
+            </clr-input-container>
+        </div>
+    </div>
+    <div class="clr-row">
+        <div class="clr-col-md-4">
+            <clr-input-container>
+                <label>{{ 'customer.city' | translate }}</label>
+                <input formControlName="city" type="text" clrInput />
+            </clr-input-container>
+        </div>
+        <div class="clr-col-md-4">
+            <clr-input-container>
+                <label>{{ 'customer.province' | translate }}</label>
+                <input formControlName="province" type="text" clrInput />
+            </clr-input-container>
+        </div>
+    </div>
+    <div class="clr-row">
+        <div class="clr-col-md-4">
+            <clr-input-container>
+                <label>{{ 'customer.postal-code' | translate }}</label>
+                <input formControlName="postalCode" type="text" clrInput />
+            </clr-input-container>
+        </div>
+        <div class="clr-col-md-4">
+            <clr-input-container>
+                <label>{{ 'customer.country' | translate }}</label>
+                <select name="countryCode" formControlName="countryCode" clrInput clrSelect>
+                    <option *ngFor="let country of availableCountries" [value]="country.code">
+                        {{ country.name }}
+                    </option>
+                </select>
+            </clr-input-container>
+        </div>
+    </div>
+    <clr-input-container>
+        <label>{{ 'customer.phone-number' | translate }}</label>
+        <input formControlName="phoneNumber" type="text" clrInput />
+    </clr-input-container>
+    <section formGroupName="customFields" *ngIf="formGroup.get('customFields') as customFieldsGroup">
+        <label>{{ 'common.custom-fields' | translate }}</label>
+        <ng-container *ngFor="let customField of customFields">
+            <vdr-custom-field-control
+                entityName="Facet"
+                [customFieldsFormGroup]="customFieldsGroup"
+                [customField]="customField"
+            ></vdr-custom-field-control>
+        </ng-container>
+    </section>
+</form>

+ 0 - 0
packages/admin-ui/src/lib/core/src/shared/components/address-form/address-form.component.scss


+ 15 - 0
packages/admin-ui/src/lib/core/src/shared/components/address-form/address-form.component.ts

@@ -0,0 +1,15 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { CustomFieldConfig, GetAvailableCountries } from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'vdr-address-form',
+    templateUrl: './address-form.component.html',
+    styleUrls: ['./address-form.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AddressFormComponent {
+    @Input() customFields: CustomFieldConfig;
+    @Input() formGroup: FormGroup;
+    @Input() availableCountries: GetAvailableCountries.Items[];
+}

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.ts

@@ -3,7 +3,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 
 /**
  * A form input control which displays currency in decimal format, whilst working
- * with the intege cent value in the background.
+ * with the integer cent value in the background.
  */
 @Component({
     selector: 'vdr-currency-input',

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/state-i18n-token.pipe.ts

@@ -22,6 +22,8 @@ export class StateI18nTokenPipe implements PipeTransform {
         Failed: _('state.failed'),
         Error: _('state.error'),
         Declined: _('state.declined'),
+        Modifying: _('state.modifying'),
+        ArrangingAdditionalPayment: _('state.arranging-additional-payment'),
     };
     transform<T extends unknown>(value: T): T {
         if (typeof value === 'string') {

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

@@ -20,6 +20,7 @@ import {
     ActionBarLeftComponent,
     ActionBarRightComponent,
 } from './components/action-bar/action-bar.component';
+import { AddressFormComponent } from './components/address-form/address-form.component';
 import { AffixedInputComponent } from './components/affixed-input/affixed-input.component';
 import { PercentageSuffixInputComponent } from './components/affixed-input/percentage-suffix-input.component';
 import { AssetFileInputComponent } from './components/asset-file-input/asset-file-input.component';
@@ -191,6 +192,7 @@ const DECLARATIONS = [
     ProductSelectorComponent,
     HelpTooltipComponent,
     CustomerGroupFormInputComponent,
+    AddressFormComponent,
 ];
 
 const DYNAMIC_FORM_INPUTS = [

+ 5 - 67
packages/admin-ui/src/lib/customer/src/components/address-detail-dialog/address-detail-dialog.component.html

@@ -3,73 +3,11 @@
     <span *ngIf="addressForm.get('countryCode')?.value as countryCode"> {{ countryCode }}</span>
 </ng-template>
 
-<form [formGroup]="addressForm">
-    <clr-input-container>
-        <label>{{ 'customer.full-name' | translate }}</label>
-        <input formControlName="fullName" type="text" clrInput />
-    </clr-input-container>
-
-    <div class="clr-row">
-        <div class="clr-col-md-4">
-            <clr-input-container>
-                <label>{{ 'customer.street-line-1' | translate }}</label>
-                <input formControlName="streetLine1" type="text" clrInput />
-            </clr-input-container>
-        </div>
-        <div class="clr-col-md-4">
-            <clr-input-container>
-                <label>{{ 'customer.street-line-2' | translate }}</label>
-                <input formControlName="streetLine2" type="text" clrInput />
-            </clr-input-container>
-        </div>
-    </div>
-    <div class="clr-row">
-        <div class="clr-col-md-4">
-            <clr-input-container>
-                <label>{{ 'customer.city' | translate }}</label>
-                <input formControlName="city" type="text" clrInput />
-            </clr-input-container>
-        </div>
-        <div class="clr-col-md-4">
-            <clr-input-container>
-                <label>{{ 'customer.province' | translate }}</label>
-                <input formControlName="province" type="text" clrInput />
-            </clr-input-container>
-        </div>
-    </div>
-    <div class="clr-row">
-        <div class="clr-col-md-4">
-            <clr-input-container>
-                <label>{{ 'customer.postal-code' | translate }}</label>
-                <input formControlName="postalCode" type="text" clrInput />
-            </clr-input-container>
-        </div>
-        <div class="clr-col-md-4">
-            <clr-input-container>
-                <label>{{ 'customer.country' | translate }}</label>
-                <select name="countryCode" formControlName="countryCode" clrInput clrSelect>
-                    <option *ngFor="let country of availableCountries" [value]="country.code">
-                        {{ country.name }}
-                    </option>
-                </select>
-            </clr-input-container>
-        </div>
-    </div>
-    <clr-input-container>
-        <label>{{ 'customer.phone-number' | translate }}</label>
-        <input formControlName="phoneNumber" type="text" clrInput />
-    </clr-input-container>
-    <section formGroupName="customFields" *ngIf="addressForm.get('customFields') as customFieldsGroup">
-        <label>{{ 'common.custom-fields' | translate }}</label>
-        <ng-container *ngFor="let customField of customFields">
-            <vdr-custom-field-control
-                entityName="Facet"
-                [customFieldsFormGroup]="customFieldsGroup"
-                [customField]="customField"
-            ></vdr-custom-field-control>
-        </ng-container>
-    </section>
-</form>
+<vdr-address-form
+    [formGroup]="addressForm"
+    [availableCountries]="availableCountries"
+    [customFields]="customFields"
+></vdr-address-form>
 
 <ng-template vdrDialogButtons>
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>

+ 22 - 0
packages/admin-ui/src/lib/order/src/components/add-manual-payment-dialog/add-manual-payment-dialog.component.html

@@ -0,0 +1,22 @@
+<ng-template vdrDialogTitle>{{ 'order.add-payment-to-order' | translate }}</ng-template>
+<form [formGroup]="form">
+    <vdr-form-field [label]="'order.payment-method' | translate" for="method">
+        <ng-select
+            [items]="paymentMethods$ | async"
+            bindLabel="code"
+            autofocus
+            bindValue="code"
+            [addTag]="true"
+            formControlName="method"
+        ></ng-select>
+    </vdr-form-field>
+    <vdr-form-field [label]="'order.transaction-id' | translate" for="transactionId">
+        <input id="transactionId" type="text" formControlName="transactionId" />
+    </vdr-form-field>
+</form>
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit" (click)="submit()" class="btn btn-primary" [disabled]="form.invalid || form.pristine">
+        {{ 'order.add-payment' | translate }}  ({{ outstandingAmount / 100 | currency: currencyCode }})
+    </button>
+</ng-template>

+ 3 - 0
packages/admin-ui/src/lib/order/src/components/add-manual-payment-dialog/add-manual-payment-dialog.component.scss

@@ -0,0 +1,3 @@
+.ng-select {
+    min-width: 100%;
+}

+ 48 - 0
packages/admin-ui/src/lib/order/src/components/add-manual-payment-dialog/add-manual-payment-dialog.component.ts

@@ -0,0 +1,48 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import {
+    CurrencyCode,
+    DataService,
+    Dialog,
+    GetPaymentMethodList,
+    ManualPaymentInput,
+} from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+
+@Component({
+    selector: 'vdr-add-manual-payment-dialog',
+    templateUrl: './add-manual-payment-dialog.component.html',
+    styleUrls: ['./add-manual-payment-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AddManualPaymentDialogComponent implements OnInit, Dialog<Omit<ManualPaymentInput, 'orderId'>> {
+    // populated by ModalService call
+    outstandingAmount: number;
+    currencyCode: CurrencyCode;
+
+    resolveWith: (result?: Omit<ManualPaymentInput, 'orderId'>) => void;
+    form = new FormGroup({
+        method: new FormControl('', Validators.required),
+        transactionId: new FormControl('', Validators.required),
+    });
+    paymentMethods$: Observable<GetPaymentMethodList.Items[]>;
+    constructor(private dataService: DataService) {}
+
+    ngOnInit(): void {
+        this.paymentMethods$ = this.dataService.settings
+            .getPaymentMethods(999)
+            .mapSingle(data => data.paymentMethods.items);
+    }
+
+    submit() {
+        const formValue = this.form.value;
+        this.resolveWith({
+            method: formValue.method,
+            transactionId: formValue.transactionId,
+        });
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+}

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.html

@@ -1,7 +1,7 @@
 <ng-template vdrDialogTitle>{{ 'order.fulfill-order' | translate }}</ng-template>
 
 <div class="fulfillment-wrapper">
-    <div class="order-lines">
+    <div class="order-table">
         <table class="table">
             <thead>
                 <tr>

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.scss

@@ -23,7 +23,7 @@
             margin-top: 24px;
         }
     }
-    .order-lines {
+    .order-table {
         flex: 1;
         overflow-y: auto;
         table {

+ 14 - 0
packages/admin-ui/src/lib/order/src/components/modification-detail/modification-detail.component.html

@@ -0,0 +1,14 @@
+<vdr-labeled-data [label]="'common.ID' | translate">{{ modification.id }}</vdr-labeled-data>
+<vdr-labeled-data *ngIf="modification.note" [label]="'order.note' | translate">{{
+    modification.note
+}}</vdr-labeled-data>
+<vdr-labeled-data *ngFor="let surcharge of modification.surcharges" [label]="'order.surcharges' | translate">
+    {{ getSurcharge(surcharge.id)?.description }}
+    {{ getSurcharge(surcharge.id)?.priceWithTax / 100 | currency: order.currencyCode }}</vdr-labeled-data
+>
+<vdr-labeled-data *ngIf="getAddedItems().length" [label]="'order.added-items' | translate">
+    <vdr-simple-item-list [items]="getAddedItems()"></vdr-simple-item-list>
+</vdr-labeled-data>
+<vdr-labeled-data *ngIf="getRemovedItems().length" [label]="'order.removed-items' | translate">
+    <vdr-simple-item-list [items]="getRemovedItems()"></vdr-simple-item-list>
+</vdr-labeled-data>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/modification-detail/modification-detail.component.scss


+ 65 - 0
packages/admin-ui/src/lib/order/src/components/modification-detail/modification-detail.component.ts

@@ -0,0 +1,65 @@
+import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
+import { OrderDetail, OrderDetailFragment } from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'vdr-modification-detail',
+    templateUrl: './modification-detail.component.html',
+    styleUrls: ['./modification-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ModificationDetailComponent implements OnChanges {
+    @Input() order: OrderDetailFragment;
+    @Input() modification: OrderDetail.Modifications;
+    private addedItems = new Map<OrderDetail.Lines, number>();
+    private removedItems = new Map<OrderDetail.Lines, number>();
+
+    ngOnChanges(): void {
+        const { added, removed } = this.getModifiedLines();
+        this.addedItems = added;
+        this.removedItems = removed;
+    }
+
+    getSurcharge(id: string) {
+        return this.order.surcharges.find(m => m.id === id);
+    }
+
+    getAddedItems() {
+        return [...this.addedItems.entries()].map(([line, count]) => {
+            return { name: line.productVariant.name, quantity: count };
+        });
+    }
+
+    getRemovedItems() {
+        return [...this.removedItems.entries()].map(([line, count]) => {
+            return { name: line.productVariant.name, quantity: count };
+        });
+    }
+
+    private getModifiedLines() {
+        const added = new Map<OrderDetail.Lines, number>();
+        const removed = new Map<OrderDetail.Lines, number>();
+        for (const _item of this.modification.orderItems || []) {
+            const result = this.getOrderLineAndItem(_item.id);
+            if (result) {
+                const { line, item } = result;
+                if (item.cancelled) {
+                    const count = removed.get(line) ?? 0;
+                    removed.set(line, count + 1);
+                } else {
+                    const count = added.get(line) ?? 0;
+                    added.set(line, count + 1);
+                }
+            }
+        }
+        return { added, removed };
+    }
+
+    private getOrderLineAndItem(itemId: string) {
+        for (const line of this.order.lines) {
+            const item = line.items.find(i => i.id === itemId);
+            if (item) {
+                return { line, item };
+            }
+        }
+    }
+}

+ 19 - 187
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html

@@ -16,6 +16,14 @@
 
     <vdr-ab-right>
         <vdr-action-bar-items locationId="order-detail"></vdr-action-bar-items>
+        <button
+            class="btn btn-primary"
+            *ngIf="hasUnsettledModifications(order)"
+            (click)="addManualPayment(order)"
+        >
+            {{ 'order.add-payment-to-order' | translate }}
+            ({{ getOutstandingModificationAmount(order) / 100 | currency: order.currencyCode }})
+        </button>
         <button class="btn btn-primary" (click)="fulfillOrder()" [disabled]="!canAddFulfillment(order)">
             {{ 'order.fulfill-order' | translate }}
         </button>
@@ -24,6 +32,13 @@
                 <clr-icon shape="ellipsis-vertical"></clr-icon>
             </button>
             <vdr-dropdown-menu vdrPosition="bottom-right">
+                <ng-container *ngIf="order.nextStates.includes('Modifying')">
+                    <button type="button" class="btn" vdrDropdownItem (click)="transitionToModifying()">
+                        <clr-icon shape="pencil"></clr-icon>
+                        {{ 'order.modify-order' | translate }}
+                    </button>
+                    <div class="dropdown-divider"></div>
+                </ng-container>
                 <button
                     type="button"
                     class="btn"
@@ -57,193 +72,10 @@
 <div *ngIf="entity$ | async as order">
     <div class="clr-row">
         <div class="clr-col-lg-8">
-            <table class="order-lines table">
-                <thead>
-                    <tr>
-                        <th></th>
-                        <th>{{ 'order.product-name' | translate }}</th>
-                        <th>{{ 'order.product-sku' | translate }}</th>
-                        <th>{{ 'order.unit-price' | translate }}</th>
-                        <th>{{ 'order.quantity' | translate }}</th>
-                        <ng-container *ngFor="let customField of visibleOrderLineCustomFields">
-                            <th class="order-line-custom-field">
-                                <button
-                                    class="custom-field-header-button"
-                                    (click)="toggleOrderLineCustomFields()"
-                                    [title]="'common.hide-custom-fields' | translate"
-                                >
-                                    {{ customField | customFieldLabel }}
-                                </button>
-                            </th>
-                        </ng-container>
-                        <ng-container *ngIf="showElided">
-                            <th>
-                                <button
-                                    class="custom-field-header-button"
-                                    (click)="toggleOrderLineCustomFields()"
-                                    [title]="'common.display-custom-fields' | translate"
-                                >
-                                    <clr-icon
-                                        shape="ellipsis-horizontal"
-                                        class="custom-field-ellipsis"
-                                    ></clr-icon>
-                                </button>
-                            </th>
-                        </ng-container>
-                        <th>{{ 'order.total' | translate }}</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <tr
-                        *ngFor="let line of order.lines"
-                        class="order-line"
-                        [class.is-cancelled]="line.quantity === 0"
-                    >
-                        <td class="align-middle thumb">
-                            <img
-                                *ngIf="line.featuredAsset"
-                                [src]="line.featuredAsset | assetPreview: 'tiny'"
-                            />
-                        </td>
-                        <td class="align-middle name">{{ line.productVariant.name }}</td>
-                        <td class="align-middle sku">{{ line.productVariant.sku }}</td>
-                        <td class="align-middle unit-price">
-                            {{ line.unitPriceWithTax / 100 | currency: order.currencyCode }}
-                            <div class="net-price" [title]="'order.net-price' | translate">
-                                {{ line.unitPrice / 100 | currency: order.currencyCode }}
-                            </div>
-                        </td>
-                        <td class="align-middle quantity">
-                            {{ line.quantity }}
-                            <vdr-line-refunds [line]="line"></vdr-line-refunds>
-                            <vdr-line-fulfillment
-                                [line]="line"
-                                [orderState]="order.state"
-                            ></vdr-line-fulfillment>
-                        </td>
-                        <ng-container *ngFor="let customField of visibleOrderLineCustomFields">
-                            <td class="order-line-custom-field align-middle">
-                                <ng-container [ngSwitch]="customField.type">
-                                    <ng-template [ngSwitchCase]="'datetime'">
-                                        <span [title]="line.customFields[customField.name]">{{
-                                            line.customFields[customField.name] | date: 'short'
-                                        }}</span>
-                                    </ng-template>
-                                    <ng-template [ngSwitchCase]="'boolean'">
-                                        <ng-template [ngIf]="line.customFields[customField.name] === true">
-                                            <clr-icon shape="check"></clr-icon>
-                                        </ng-template>
-                                        <ng-template [ngIf]="line.customFields[customField.name] === false">
-                                            <clr-icon shape="times"></clr-icon>
-                                        </ng-template>
-                                    </ng-template>
-                                    <ng-template ngSwitchDefault>
-                                        {{ line.customFields[customField.name] }}
-                                    </ng-template>
-                                </ng-container>
-                            </td>
-                        </ng-container>
-                        <ng-container *ngIf="showElided"
-                            ><td class="order-line-custom-field align-middle">
-                                <clr-icon
-                                    shape="ellipsis-horizontal"
-                                    class="custom-field-ellipsis"
-                                ></clr-icon></td
-                        ></ng-container>
-                        <td class="align-middle total">
-                            {{ line.linePriceWithTax / 100 | currency: order.currencyCode }}
-                            <div class="net-price" [title]="'order.net-price' | translate">
-                                {{ line.linePrice / 100 | currency: order.currencyCode }}
-                            </div>
-
-                            <ng-container *ngIf="getLineDiscounts(line) as discounts">
-                                <vdr-dropdown *ngIf="discounts.length">
-                                    <div class="promotions-label" vdrDropdownTrigger>
-                                        {{ 'order.promotions-applied' | translate }}
-                                    </div>
-                                    <vdr-dropdown-menu>
-                                        <div class="line-promotion" *ngFor="let discount of discounts">
-                                            <a
-                                                class="promotion-name"
-                                                [routerLink]="getPromotionLink(discount)"
-                                                >{{ discount.description }}</a
-                                            >
-                                            <div class="promotion-amount">
-                                                {{ discount.amount / 100 | currency: order.currencyCode }}
-                                            </div>
-                                        </div>
-                                    </vdr-dropdown-menu>
-                                </vdr-dropdown>
-                            </ng-container>
-                        </td>
-                    </tr>
-                    <tr class="surcharge" *ngFor="let surcharge of order.surcharges">
-                        <td class="align-middle name left" colspan="2">{{ surcharge.description }}</td>
-                        <td class="align-middle sku">{{ surcharge.sku }}</td>
-                        <td class="align-middle"></td>
-                        <td [attr.colspan]="1 + visibleOrderLineCustomFields.length"></td>
-                        <ng-container *ngIf="showElided"><td></td></ng-container>
-                        <td class="align-middle total">
-                            {{ surcharge.priceWithTax / 100 | currency: order.currencyCode }}
-                            <div class="net-price" [title]="'order.net-price' | translate">
-                                {{ surcharge.price / 100 | currency: order.currencyCode }}
-                            </div>
-                        </td>
-                    </tr>
-                    <tr class="order-adjustment" *ngFor="let discount of order.discounts">
-                        <td
-                            [attr.colspan]="5 + visibleOrderLineCustomFields.length"
-                            class="left clr-align-middle"
-                        >
-                            <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
-                            <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
-                                couponCode
-                            }}</vdr-chip>
-                        </td>
-                        <ng-container *ngIf="showElided"><td></td></ng-container>
-                        <td class="clr-align-middle">
-                            {{ discount.amount / 100 | currency: order.currencyCode }}
-                        </td>
-                    </tr>
-                    <tr class="sub-total">
-                        <td class="left clr-align-middle">{{ 'order.sub-total' | translate }}</td>
-                        <td></td>
-                        <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
-                        <ng-container *ngIf="showElided"><td></td></ng-container>
-                        <td class="clr-align-middle">
-                            {{ order.subTotalWithTax / 100 | currency: order.currencyCode }}
-                            <div class="net-price" [title]="'order.net-price' | translate">
-                                {{ order.subTotal / 100 | currency: order.currencyCode }}
-                            </div>
-                        </td>
-                    </tr>
-                    <tr class="shipping">
-                        <td class="left clr-align-middle">{{ 'order.shipping' | translate }}</td>
-                        <td class="clr-align-middle">{{ order.shippingLines[0]?.shippingMethod?.name }}</td>
-                        <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
-                        <ng-container *ngIf="showElided"><td></td></ng-container>
-                        <td class="clr-align-middle">
-                            {{ order.shippingWithTax / 100 | currency: order.currencyCode }}
-                            <div class="net-price" [title]="'order.net-price' | translate">
-                                {{ order.shipping / 100 | currency: order.currencyCode }}
-                            </div>
-                        </td>
-                    </tr>
-                    <tr class="total">
-                        <td class="left clr-align-middle">{{ 'order.total' | translate }}</td>
-                        <td></td>
-                        <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
-                        <ng-container *ngIf="showElided"><td></td></ng-container>
-                        <td class="clr-align-middle">
-                            {{ order.totalWithTax / 100 | currency: order.currencyCode }}
-                            <div class="net-price" [title]="'order.net-price' | translate">
-                                {{ order.total / 100 | currency: order.currencyCode }}
-                            </div>
-                        </td>
-                    </tr>
-                </tbody>
-            </table>
-
+            <vdr-order-table
+                [order]="order"
+                [orderLineCustomFields]="orderLineCustomFields"
+            ></vdr-order-table>
             <h4>{{ 'order.tax-summary' | translate }}</h4>
             <table class="table">
                 <thead>

+ 1 - 55
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.scss

@@ -1,66 +1,12 @@
 @import "variables";
 @import "mixins";
+@import "../order-table/order-table-mixin";
 
 .shipping-address {
     list-style-type: none;
     line-height: 1.3em;
 }
 
-.order-lines {
-
-    .is-cancelled td {
-        text-decoration: line-through;
-        background-color: $color-grey-200;
-    }
-
-    .sub-total td {
-        border-top: 1px dashed $color-grey-300;
-    }
-
-    .total td {
-        font-weight: bold;
-        border-top: 1px dashed $color-grey-300;
-    }
-}
-
-.custom-field-header-button {
-    background: none;
-    margin: 0;
-    padding: 0;
-    border: none;
-    cursor: pointer;
-    color: $color-primary-600;
-}
-
-.order-line-custom-field {
-    background-color: $color-grey-100;
-
-    .custom-field-ellipsis {
-        color: $color-grey-300;
-    }
-}
-
-.net-price {
-    font-size: 11px;
-    color: $color-grey-400;
-}
-.promotions-label {
-    text-decoration: underline dotted $color-grey-300;
-    font-size: 11px;
-    margin-top: 6px;
-    cursor: pointer;
-    text-transform: lowercase;
-}
-
-.line-promotion {
-    display: flex;
-    justify-content: space-between;
-    padding: 6px 12px;
-    .promotion-amount {
-        margin-left: 12px;
-    }
-}
-
 .order-cards {
     h6 {
         margin-top: 6px;

+ 85 - 35
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -3,7 +3,6 @@ import { FormGroup } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
-    AdjustmentType,
     BaseDetailComponent,
     CancelOrder,
     CustomFieldConfig,
@@ -12,24 +11,30 @@ import {
     GetOrderHistory,
     GetOrderQuery,
     HistoryEntry,
+    HistoryEntryType,
     ModalService,
     NotificationService,
     Order,
     OrderDetail,
+    OrderDetailFragment,
     OrderLineFragment,
     RefundOrder,
     ServerConfigService,
     SortOrder,
 } from '@vendure/admin-ui/core';
+import { summate } from '@vendure/common/lib/shared-utils';
 import { EMPTY, merge, Observable, of, Subject } from 'rxjs';
 import { map, mapTo, startWith, switchMap, take } from 'rxjs/operators';
 
+import { AddManualPaymentDialogComponent } from '../add-manual-payment-dialog/add-manual-payment-dialog.component';
 import { CancelOrderDialogComponent } from '../cancel-order-dialog/cancel-order-dialog.component';
 import { FulfillOrderDialogComponent } from '../fulfill-order-dialog/fulfill-order-dialog.component';
 import { OrderProcessGraphDialogComponent } from '../order-process-graph-dialog/order-process-graph-dialog.component';
 import { RefundOrderDialogComponent } from '../refund-order-dialog/refund-order-dialog.component';
 import { SettleRefundDialogComponent } from '../settle-refund-dialog/settle-refund-dialog.component';
 
+import { transitionToPreModifyingState } from './transition-to-pre-modifying-state';
+
 @Component({
     selector: 'vdr-order-detail',
     templateUrl: './order-detail.component.html',
@@ -45,7 +50,6 @@ export class OrderDetailComponent
     fetchHistory = new Subject<void>();
     customFields: CustomFieldConfig[];
     orderLineCustomFields: CustomFieldConfig[];
-    orderLineCustomFieldsVisible = false;
     private readonly defaultStates = [
         'AddingItems',
         'ArrangingPayment',
@@ -54,6 +58,8 @@ export class OrderDetailComponent
         'PartiallyDelivered',
         'Delivered',
         'Cancelled',
+        'Modifying',
+        'ArrangingAdditionalPayment',
     ];
     constructor(
         router: Router,
@@ -67,19 +73,15 @@ export class OrderDetailComponent
         super(route, router, serverConfigService, dataService);
     }
 
-    get visibleOrderLineCustomFields(): CustomFieldConfig[] {
-        return this.orderLineCustomFieldsVisible ? this.orderLineCustomFields : [];
-    }
-
-    get showElided(): boolean {
-        return !this.orderLineCustomFieldsVisible && 0 < this.orderLineCustomFields.length;
-    }
-
     ngOnInit() {
         this.init();
+        this.entity$.pipe(take(1)).subscribe(order => {
+            if (order.state === 'Modifying') {
+                this.router.navigate(['./', 'modify'], { relativeTo: this.route });
+            }
+        });
         this.customFields = this.getCustomFieldConfig('Order');
         this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
-        this.orderLineCustomFieldsVisible = this.orderLineCustomFields.length < 2;
         this.history$ = this.fetchHistory.pipe(
             startWith(null),
             switchMap(() => {
@@ -106,19 +108,6 @@ export class OrderDetailComponent
         this.destroy();
     }
 
-    toggleOrderLineCustomFields() {
-        this.orderLineCustomFieldsVisible = !this.orderLineCustomFieldsVisible;
-    }
-
-    getLineDiscounts(line: OrderDetail.Lines) {
-        return line.discounts.filter(a => a.type === AdjustmentType.PROMOTION);
-    }
-
-    getPromotionLink(promotion: OrderDetail.Discounts): any[] {
-        const id = promotion.adjustmentSource.split(':')[1];
-        return ['/marketing', 'promotions', id];
-    }
-
     openStateDiagram() {
         this.entity$
             .pipe(
@@ -148,6 +137,20 @@ export class OrderDetailComponent
         });
     }
 
+    transitionToModifying() {
+        this.dataService.order
+            .transitionToState(this.id, 'Modifying')
+            .subscribe(({ transitionOrderToState }) => {
+                switch (transitionOrderToState?.__typename) {
+                    case 'Order':
+                        this.router.navigate(['./modify'], { relativeTo: this.route });
+                        break;
+                    case 'OrderStateTransitionError':
+                        this.notificationService.error(transitionOrderToState.transitionError);
+                }
+            });
+    }
+
     updateCustomFields(customFieldsValue: any) {
         this.dataService.order
             .updateOrderCustomFields({
@@ -159,17 +162,6 @@ export class OrderDetailComponent
             });
     }
 
-    getCouponCodeForAdjustment(
-        order: OrderDetail.Fragment,
-        promotionAdjustment: OrderDetail.Discounts,
-    ): string | undefined {
-        const id = promotionAdjustment.adjustmentSource.split(':')[1];
-        const promotion = order.promotions.find(p => p.id === id);
-        if (promotion) {
-            return promotion.couponCode || undefined;
-        }
-    }
-
     getOrderAddressLines(orderAddress?: { [key: string]: string }): string[] {
         if (!orderAddress) {
             return [];
@@ -205,12 +197,70 @@ export class OrderDetailComponent
             .every(item => !!item.fulfillment);
         return (
             !allItemsFulfilled &&
+            !this.hasUnsettledModifications(order) &&
             (order.nextStates.includes('Shipped') ||
                 order.nextStates.includes('PartiallyShipped') ||
                 order.nextStates.includes('Delivered'))
         );
     }
 
+    hasUnsettledModifications(order: OrderDetailFragment): boolean {
+        return 0 < order.modifications.filter(m => !m.isSettled).length;
+    }
+
+    getOutstandingModificationAmount(order: OrderDetailFragment): number {
+        return summate(
+            order.modifications.filter(m => !m.isSettled),
+            'priceChange',
+        );
+    }
+
+    addManualPayment(order: OrderDetailFragment) {
+        this.modalService
+            .fromComponent(AddManualPaymentDialogComponent, {
+                closable: true,
+                locals: {
+                    outstandingAmount: this.getOutstandingModificationAmount(order),
+                    currencyCode: order.currencyCode,
+                },
+            })
+            .pipe(
+                switchMap(result => {
+                    if (result) {
+                        return this.dataService.order.addManualPaymentToOrder({
+                            orderId: this.id,
+                            transactionId: result.transactionId,
+                            method: result.method,
+                            metadata: result.metadata || {},
+                        });
+                    } else {
+                        return EMPTY;
+                    }
+                }),
+                switchMap(({ addManualPaymentToOrder }) => {
+                    switch (addManualPaymentToOrder.__typename) {
+                        case 'Order':
+                            this.notificationService.success('order.add-payment-to-order-success');
+                            return transitionToPreModifyingState(this.dataService, order.id);
+                        case 'ManualPaymentStateError':
+                            this.notificationService.error(addManualPaymentToOrder.message);
+                            return EMPTY;
+                        default:
+                            return EMPTY;
+                    }
+                }),
+            )
+            .subscribe(({ transitionOrderToState }) => {
+                switch (transitionOrderToState?.__typename) {
+                    case 'Order':
+                        this.refetchOrder(transitionOrderToState);
+                        break;
+                    case 'OrderStateTransitionError':
+                        this.notificationService.error(transitionOrderToState.message);
+                }
+            });
+    }
+
     fulfillOrder() {
         this.entity$
             .pipe(

+ 29 - 0
packages/admin-ui/src/lib/order/src/components/order-detail/transition-to-pre-modifying-state.ts

@@ -0,0 +1,29 @@
+import { DataService, HistoryEntryType, SortOrder } from '@vendure/admin-ui/core';
+import { EMPTY } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+
+export function transitionToPreModifyingState(dataService: DataService, orderId: string) {
+    return dataService.order
+        .getOrderHistory(orderId, {
+            filter: {
+                type: {
+                    eq: HistoryEntryType.ORDER_STATE_TRANSITION,
+                },
+            },
+            sort: {
+                createdAt: SortOrder.DESC,
+            },
+        })
+        .mapSingle(result => result.order)
+        .pipe(
+            switchMap(result => {
+                const item = result?.history.items.find(i => i.data.to === 'Modifying');
+                if (item) {
+                    const originalState = item.data.from;
+                    return dataService.order.transitionToState(orderId, originalState);
+                } else {
+                    return EMPTY;
+                }
+            }),
+        );
+}

+ 318 - 0
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.html

@@ -0,0 +1,318 @@
+<vdr-action-bar *ngIf="entity$ | async as order">
+    <vdr-ab-left>
+        <div class="flex clr-align-items-center">
+            <vdr-entity-info [entity]="entity$ | async"></vdr-entity-info>
+            <vdr-order-state-label [state]="order.state"></vdr-order-state-label>
+        </div>
+    </vdr-ab-left>
+
+    <vdr-ab-right>
+        <button class="btn btn-secondary" (click)="transitionToPriorState(order)">
+            {{ 'order.cancel-modification' | translate }}
+        </button>
+    </vdr-ab-right>
+</vdr-action-bar>
+
+<div *ngIf="entity$ | async as order">
+    <div class="clr-row">
+        <div class="clr-col-lg-8">
+            <table class="order-table table">
+                <thead>
+                    <tr>
+                        <th></th>
+                        <th>{{ 'order.product-name' | translate }}</th>
+                        <th>{{ 'order.product-sku' | translate }}</th>
+                        <th>{{ 'order.unit-price' | translate }}</th>
+                        <th>{{ 'order.quantity' | translate }}</th>
+                        <th *ngIf="orderLineCustomFields.length">{{ 'common.custom-fields' | translate }}</th>
+                        <th>{{ 'order.total' | translate }}</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr
+                        *ngFor="let line of order.lines"
+                        class="order-line"
+                        [class.is-cancelled]="line.quantity === 0"
+                        [class.modified]="isLineModified(line)"
+                    >
+                        <td class="align-middle thumb">
+                            <img
+                                *ngIf="line.featuredAsset"
+                                [src]="line.featuredAsset | assetPreview: 'tiny'"
+                            />
+                        </td>
+                        <td class="align-middle name">{{ line.productVariant.name }}</td>
+                        <td class="align-middle sku">{{ line.productVariant.sku }}</td>
+                        <td class="align-middle unit-price">
+                            {{ line.unitPriceWithTax / 100 | currency: order.currencyCode }}
+                            <div class="net-price" [title]="'order.net-price' | translate">
+                                {{ line.unitPrice / 100 | currency: order.currencyCode }}
+                            </div>
+                        </td>
+                        <td class="align-middle quantity">
+                            <input
+                                class="clr-input"
+                                type="number"
+                                min="0"
+                                [value]="line.quantity"
+                                (input)="updateLineQuantity(line, $event.target.value)"
+                            />
+                            <vdr-line-refunds [line]="line"></vdr-line-refunds>
+                            <vdr-line-fulfillment
+                                [line]="line"
+                                [orderState]="order.state"
+                            ></vdr-line-fulfillment>
+                        </td>
+                        <td *ngIf="orderLineCustomFields.length" class="order-line-custom-field align-middle">
+                            <ng-container *ngFor="let customField of orderLineCustomFields">
+                                <vdr-labeled-data [label]="customField | customFieldLabel">{{
+                                    line.customFields[customField.name] || '-'
+                                }}</vdr-labeled-data>
+                            </ng-container>
+                        </td>
+                        <td class="align-middle total">
+                            {{ line.linePriceWithTax / 100 | currency: order.currencyCode }}
+                            <div class="net-price" [title]="'order.net-price' | translate">
+                                {{ line.linePrice / 100 | currency: order.currencyCode }}
+                            </div>
+                        </td>
+                    </tr>
+                    <tr
+                        *ngFor="let addedLine of addedLines; trackBy: trackByProductVariantId"
+                        class="modified"
+                    >
+                        <td class="align-middle thumb">
+                            <img
+                                *ngIf="addedLine.productAsset"
+                                [src]="addedLine.productAsset | assetPreview: 'tiny'"
+                            />
+                        </td>
+                        <td class="align-middle name">{{ addedLine.productVariantName }}</td>
+                        <td class="align-middle sku">{{ addedLine.sku }}</td>
+                        <td class="align-middle unit-price">
+                            {{ addedLine.priceWithTax / 100 | currency: order.currencyCode }}
+                            <div class="net-price" [title]="'order.net-price' | translate">
+                                {{ addedLine.price / 100 | currency: order.currencyCode }}
+                            </div>
+                        </td>
+                        <td class="align-middle quantity">
+                            <input
+                                class="clr-input"
+                                type="number"
+                                min="0"
+                                [value]="addedLine.quantity"
+                                (input)="updateAddedItemQuantity(addedLine, $event.target.value)"
+                            />
+                            <button class="icon-button" (click)="removeAddedItem(addedLine.productVariantId)">
+                                <clr-icon shape="trash"></clr-icon>
+                            </button>
+                        </td>
+                        <td *ngIf="orderLineCustomFields.length" class="order-line-custom-field align-middle">
+                            <!--  <ng-container *ngFor="let customField of orderLineCustomFields">
+                        <vdr-labeled-data [label]="customField | customFieldLabel">{{
+                            line.customFields[customField.name] || '-'
+                        }}</vdr-labeled-data>
+                    </ng-container>-->
+                        </td>
+                        <td class="align-middle total">
+                            {{
+                                (addedLine.priceWithTax * addedLine.quantity) / 100
+                                    | currency: order.currencyCode
+                            }}
+                            <div class="net-price" [title]="'order.net-price' | translate">
+                                {{
+                                    (addedLine.price * addedLine.quantity) / 100
+                                        | currency: order.currencyCode
+                                }}
+                            </div>
+                        </td>
+                    </tr>
+                    <tr class="surcharge" *ngFor="let surcharge of order.surcharges">
+                        <td class="align-middle name left" colspan="2">{{ surcharge.description }}</td>
+                        <td class="align-middle sku">{{ surcharge.sku }}</td>
+                        <td class="align-middle"></td>
+                        <td></td>
+                        <td *ngIf="orderLineCustomFields.length"></td>
+                        <td class="align-middle total">
+                            {{ surcharge.priceWithTax / 100 | currency: order.currencyCode }}
+                            <div class="net-price" [title]="'order.net-price' | translate">
+                                {{ surcharge.price / 100 | currency: order.currencyCode }}
+                            </div>
+                        </td>
+                    </tr>
+                    <tr
+                        class="surcharge modified"
+                        *ngFor="let surcharge of modifyOrderInput.surcharges; let i = index"
+                    >
+                        <td class="align-middle name left" colspan="2">
+                            {{ surcharge.description }}
+                            <button class="icon-button" (click)="removeSurcharge(i)">
+                                <clr-icon shape="trash"></clr-icon>
+                            </button>
+                        </td>
+                        <td class="align-middle sku">{{ surcharge.sku }}</td>
+                        <td class="align-middle"></td>
+                        <td></td>
+                        <td *ngIf="orderLineCustomFields.length"></td>
+                        <td class="align-middle total">
+                            <ng-container *ngIf="getSurchargePrices(surcharge) as surchargePrice">
+                                {{ surchargePrice.priceWithTax / 100 | currency: order.currencyCode }}
+                                <div class="net-price" [title]="'order.net-price' | translate">
+                                    {{ surchargePrice.price / 100 | currency: order.currencyCode }}
+                                </div>
+                            </ng-container>
+                        </td>
+                    </tr>
+                    <tr class="shipping">
+                        <td class="left clr-align-middle">{{ 'order.shipping' | translate }}</td>
+                        <td class="clr-align-middle">{{ order.shippingLines[0]?.shippingMethod?.name }}</td>
+                        <td colspan="3"></td>
+                        <td *ngIf="orderLineCustomFields.length"></td>
+                        <td class="clr-align-middle">
+                            {{ order.shippingWithTax / 100 | currency: order.currencyCode }}
+                            <div class="net-price" [title]="'order.net-price' | translate">
+                                {{ order.shipping / 100 | currency: order.currencyCode }}
+                            </div>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+
+            <h4 class="mb2">{{ 'order.modifications' | translate }}</h4>
+            <clr-accordion>
+                <clr-accordion-panel>
+                    <clr-accordion-title>{{ 'order.add-item-to-order' | translate }}</clr-accordion-title>
+                    <clr-accordion-content *clrIfExpanded>
+                        <vdr-product-selector (productSelected)="addItemToOrder($event)">
+                        </vdr-product-selector>
+                    </clr-accordion-content>
+                </clr-accordion-panel>
+
+                <clr-accordion-panel>
+                    <clr-accordion-title>{{ 'order.add-surcharge' | translate }}</clr-accordion-title>
+                    <clr-accordion-content *clrIfExpanded>
+                        <form [formGroup]="surchargeForm" (submit)="addSurcharge(surchargeForm.value)">
+                            <vdr-form-field [label]="'common.description' | translate" for="description"
+                                ><input
+                                    class="clr-input"
+                                    id="description"
+                                    type="text"
+                                    formControlName="description"
+                            /></vdr-form-field>
+                            <vdr-form-field [label]="'order.product-sku' | translate" for="sku"
+                                ><input class="clr-input" id="sku" type="text" formControlName="sku"
+                            /></vdr-form-field>
+                            <vdr-form-field [label]="'common.price' | translate" for="price"
+                                ><vdr-currency-input
+                                    [currencyCode]="order.currencyCode"
+                                    id="price"
+                                    formControlName="price"
+                                ></vdr-currency-input
+                            ></vdr-form-field>
+                            <vdr-form-field
+                                [label]="'catalog.price-includes-tax-at' | translate: { rate: surchargeForm.get('taxRate')?.value }"
+                                for="priceIncludesTax"
+                                ><input
+                                    id="priceIncludesTax"
+                                    type="checkbox"
+                                    clrCheckbox
+                                    formControlName="priceIncludesTax"
+                            /></vdr-form-field>
+                            <vdr-form-field [label]="'order.tax-rate' | translate" for="taxRate"
+                                ><vdr-affixed-input suffix="%"
+                                    ><input
+                                        class="clr-input"
+                                        id="taxRate"
+                                        type="number"
+                                        min="0"
+                                        max="100"
+                                        formControlName="taxRate" /></vdr-affixed-input
+                            ></vdr-form-field>
+                            <vdr-form-field [label]="'order.tax-description' | translate" for="taxDescription"
+                                ><input
+                                    class="clr-input"
+                                    id="taxDescription"
+                                    type="text"
+                                    formControlName="taxDescription"
+                            /></vdr-form-field>
+                            <button
+                                class="btn btn-secondary"
+                                [disabled]="surchargeForm.invalid || surchargeForm.pristine"
+                            >
+                                {{ 'order.add-surcharge' | translate }}
+                            </button>
+                        </form>
+                    </clr-accordion-content>
+                </clr-accordion-panel>
+                <clr-accordion-panel>
+                    <clr-accordion-title>{{ 'order.edit-shipping-address' | translate }}</clr-accordion-title>
+                    <clr-accordion-content *clrIfExpanded>
+                        <vdr-address-form
+                            [formGroup]="shippingAddressForm"
+                            [availableCountries]="availableCountries$ | async"
+                            [customFields]="addressCustomFields"
+                        ></vdr-address-form>
+                    </clr-accordion-content>
+                </clr-accordion-panel>
+                <clr-accordion-panel>
+                    <clr-accordion-title>{{ 'order.edit-billing-address' | translate }}</clr-accordion-title>
+                    <clr-accordion-content *clrIfExpanded>
+                        <vdr-address-form
+                            [formGroup]="billingAddressForm"
+                            [availableCountries]="availableCountries$ | async"
+                            [customFields]="addressCustomFields"
+                        ></vdr-address-form>
+                    </clr-accordion-content>
+                </clr-accordion-panel>
+            </clr-accordion>
+        </div>
+        <div class="clr-col-lg-4 order-cards">
+            <div class="card">
+                <div class="card-header">
+                    {{ 'order.modification-summary' | translate }}
+                </div>
+                <div class="card-block">
+                    <ul>
+                        <li *ngIf="modifyOrderInput.addItems?.length">
+                            {{
+                                'order.modification-adding-items'
+                                    | translate: { count: modifyOrderInput.addItems?.length }
+                            }}
+                        </li>
+                        <li *ngIf="modifyOrderInput.adjustOrderLines?.length">
+                            {{
+                                'order.modification-adjusting-lines'
+                                    | translate: { count: modifyOrderInput.adjustOrderLines?.length }
+                            }}
+                        </li>
+                        <li *ngIf="modifyOrderInput.surcharges?.length">
+                            {{
+                                'order.modification-adding-surcharges'
+                                    | translate: { count: modifyOrderInput.surcharges?.length }
+                            }}
+                        </li>
+                        <li *ngIf="shippingAddressForm.dirty">
+                            {{ 'order.modification-updating-shipping-address' | translate }}
+                        </li>
+                        <li *ngIf="billingAddressForm.dirty">
+                            {{ 'order.modification-updating-billing-address' | translate }}
+                        </li>
+                    </ul>
+                </div>
+                <div class="card-block">
+                    <label class="clr-control-label">{{ 'order.note' | translate }}</label>
+                    <textarea [(ngModel)]="note" name="note" clrTextarea required></textarea>
+                    <clr-checkbox-wrapper class="">
+                        <input type="checkbox" clrCheckbox [(ngModel)]="recalculateShipping" />
+                        <label>{{ 'order.modification-recalculate-shipping' | translate }}</label>
+                    </clr-checkbox-wrapper>
+                </div>
+                <div class="card-footer">
+                    <button class="btn btn-primary" [disabled]="!canPreviewChanges()" (click)="previewAndModify(order)">
+                        {{ 'order.preview-changes' | translate }}
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 10 - 0
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.scss

@@ -0,0 +1,10 @@
+@import "variables";
+@import "../order-table/order-table-mixin";
+
+.order-table {
+    @include order-table;
+
+    tr.modified td {
+        background-color: $color-warning-100;
+    }
+}

+ 347 - 0
packages/admin-ui/src/lib/order/src/components/order-editor/order-editor.component.ts

@@ -0,0 +1,347 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import {
+    BaseDetailComponent,
+    CustomFieldConfig,
+    DataService,
+    ErrorResult,
+    GetAvailableCountries,
+    HistoryEntryType,
+    LanguageCode,
+    ModalService,
+    ModifyOrderInput,
+    NotificationService,
+    OrderDetail,
+    ProductSelectorSearch,
+    ServerConfigService,
+    SortOrder,
+    SurchargeInput,
+} from '@vendure/admin-ui/core';
+import { assertNever, notNullOrUndefined } from '@vendure/common/lib/shared-utils';
+import { EMPTY, Observable, of } from 'rxjs';
+import { mapTo, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
+
+import { transitionToPreModifyingState } from '../order-detail/transition-to-pre-modifying-state';
+import {
+    OrderEditResultType,
+    OrderEditsPreviewDialogComponent,
+} from '../order-edits-preview-dialog/order-edits-preview-dialog.component';
+
+interface AddedLine {
+    productVariantId: string;
+    productAsset?: ProductSelectorSearch.ProductAsset | null;
+    productVariantName: string;
+    sku: string;
+    priceWithTax: number;
+    price: number;
+    quantity: number;
+}
+
+@Component({
+    selector: 'vdr-order-editor',
+    templateUrl: './order-editor.component.html',
+    styleUrls: ['./order-editor.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class OrderEditorComponent
+    extends BaseDetailComponent<OrderDetail.Fragment>
+    implements OnInit, OnDestroy {
+    availableCountries$: Observable<GetAvailableCountries.Items[]>;
+    addressCustomFields: CustomFieldConfig[];
+    detailForm = new FormGroup({});
+    orderLineCustomFields: CustomFieldConfig[];
+    modifyOrderInput: ModifyOrderInput = {
+        dryRun: true,
+        orderId: '',
+        addItems: [],
+        adjustOrderLines: [],
+        surcharges: [],
+        note: '',
+        updateShippingAddress: {},
+        updateBillingAddress: {},
+    };
+    surchargeForm: FormGroup;
+    shippingAddressForm: FormGroup;
+    billingAddressForm: FormGroup;
+    note = '';
+    recalculateShipping = true;
+    previousState: string;
+    private addedVariants = new Map<string, ProductSelectorSearch.Items>();
+
+    constructor(
+        router: Router,
+        route: ActivatedRoute,
+        serverConfigService: ServerConfigService,
+        private changeDetector: ChangeDetectorRef,
+        protected dataService: DataService,
+        private notificationService: NotificationService,
+        private modalService: ModalService,
+    ) {
+        super(route, router, serverConfigService, dataService);
+    }
+
+    get addedLines(): AddedLine[] {
+        const getSinglePriceValue = (price: ProductSelectorSearch.Price) =>
+            price.__typename === 'SinglePrice' ? price.value : 0;
+        return (this.modifyOrderInput.addItems || [])
+            .map(row => {
+                const variantInfo = this.addedVariants.get(row.productVariantId);
+                if (variantInfo) {
+                    return {
+                        ...variantInfo,
+                        price: getSinglePriceValue(variantInfo.price),
+                        priceWithTax: getSinglePriceValue(variantInfo.priceWithTax),
+                        quantity: row.quantity,
+                    };
+                }
+            })
+            .filter(notNullOrUndefined);
+    }
+
+    ngOnInit(): void {
+        this.init();
+        this.addressCustomFields = this.getCustomFieldConfig('Address');
+        this.modifyOrderInput.orderId = this.route.snapshot.paramMap.get('id') as string;
+        this.orderLineCustomFields = this.getCustomFieldConfig('OrderLine');
+        this.entity$.pipe(takeUntil(this.destroy$)).subscribe(order => {
+            this.surchargeForm = new FormGroup({
+                description: new FormControl('', Validators.required),
+                sku: new FormControl(''),
+                price: new FormControl(0, Validators.required),
+                priceIncludesTax: new FormControl(true),
+                taxRate: new FormControl(0),
+                taxDescription: new FormControl(''),
+            });
+            this.shippingAddressForm = new FormGroup({
+                fullName: new FormControl(order.shippingAddress?.fullName),
+                company: new FormControl(order.shippingAddress?.company),
+                streetLine1: new FormControl(order.shippingAddress?.streetLine1),
+                streetLine2: new FormControl(order.shippingAddress?.streetLine2),
+                city: new FormControl(order.shippingAddress?.city),
+                province: new FormControl(order.shippingAddress?.province),
+                postalCode: new FormControl(order.shippingAddress?.postalCode),
+                countryCode: new FormControl(order.shippingAddress?.countryCode),
+                phoneNumber: new FormControl(order.shippingAddress?.phoneNumber),
+            });
+            this.billingAddressForm = new FormGroup({
+                fullName: new FormControl(order.billingAddress?.fullName),
+                company: new FormControl(order.billingAddress?.company),
+                streetLine1: new FormControl(order.billingAddress?.streetLine1),
+                streetLine2: new FormControl(order.billingAddress?.streetLine2),
+                city: new FormControl(order.billingAddress?.city),
+                province: new FormControl(order.billingAddress?.province),
+                postalCode: new FormControl(order.billingAddress?.postalCode),
+                countryCode: new FormControl(order.billingAddress?.countryCode),
+                phoneNumber: new FormControl(order.billingAddress?.phoneNumber),
+            });
+        });
+        this.availableCountries$ = this.dataService.settings
+            .getAvailableCountries()
+            .mapSingle(result => result.countries.items)
+            .pipe(shareReplay(1));
+        this.dataService.order
+            .getOrderHistory(this.id, {
+                take: 1,
+                sort: {
+                    createdAt: SortOrder.DESC,
+                },
+                filter: { type: { eq: HistoryEntryType.ORDER_STATE_TRANSITION } },
+            })
+            .single$.subscribe(({ order }) => {
+                this.previousState = order?.history.items[0].data.from;
+            });
+    }
+
+    ngOnDestroy(): void {
+        this.destroy();
+    }
+
+    transitionToPriorState(order: OrderDetail.Fragment) {
+        transitionToPreModifyingState(this.dataService, order.id).subscribe(({ transitionOrderToState }) => {
+            switch (transitionOrderToState?.__typename) {
+                case 'Order':
+                    this.router.navigate(['..'], { relativeTo: this.route });
+                    break;
+                case 'OrderStateTransitionError':
+                    this.notificationService.error(transitionOrderToState?.transitionError);
+            }
+        });
+    }
+
+    canPreviewChanges(): boolean {
+        const { addItems, adjustOrderLines, surcharges } = this.modifyOrderInput;
+        return (
+            (!!addItems?.length ||
+                !!surcharges?.length ||
+                !!adjustOrderLines?.length ||
+                (this.shippingAddressForm.dirty && this.shippingAddressForm.valid) ||
+                (this.billingAddressForm.dirty && this.billingAddressForm.valid)) &&
+            this.note !== ''
+        );
+    }
+
+    isLineModified(line: OrderDetail.Lines): boolean {
+        return !!this.modifyOrderInput.adjustOrderLines?.find(
+            l => l.orderLineId === line.id && l.quantity !== line.quantity,
+        );
+    }
+
+    updateLineQuantity(line: OrderDetail.Lines, quantity: string) {
+        const { adjustOrderLines } = this.modifyOrderInput;
+        let row = adjustOrderLines?.find(l => l.orderLineId === line.id);
+        if (row && +quantity === line.quantity) {
+            // Remove the modification if the quantity is the same as
+            // the original order
+            adjustOrderLines?.splice(adjustOrderLines?.indexOf(row), 1);
+        }
+        if (!row) {
+            row = { orderLineId: line.id, quantity: +quantity };
+            adjustOrderLines?.push(row);
+        }
+        row.quantity = +quantity;
+    }
+
+    updateAddedItemQuantity(item: AddedLine, quantity: string) {
+        const row = this.modifyOrderInput.addItems?.find(l => l.productVariantId === item.productVariantId);
+        if (row) {
+            row.quantity = +quantity;
+        }
+    }
+
+    trackByProductVariantId(index: number, item: AddedLine) {
+        return item.productVariantId;
+    }
+
+    addItemToOrder(result: ProductSelectorSearch.Items) {
+        let row = this.modifyOrderInput.addItems?.find(l => l.productVariantId === result.productVariantId);
+        if (!row) {
+            row = { productVariantId: result.productVariantId, quantity: 1 };
+            this.modifyOrderInput.addItems?.push(row);
+        } else {
+            row.quantity++;
+        }
+        this.addedVariants.set(result.productVariantId, result);
+    }
+
+    removeAddedItem(productVariantId: string) {
+        this.modifyOrderInput.addItems = this.modifyOrderInput.addItems?.filter(
+            l => l.productVariantId !== productVariantId,
+        );
+    }
+
+    getSurchargePrices(surcharge: SurchargeInput) {
+        const priceWithTax = surcharge.priceIncludesTax
+            ? surcharge.price
+            : Math.round(surcharge.price * ((100 + (surcharge.taxRate || 0)) / 100));
+        const price = surcharge.priceIncludesTax
+            ? Math.round(surcharge.price / ((100 + (surcharge.taxRate || 0)) / 100))
+            : surcharge.price;
+        return {
+            price,
+            priceWithTax,
+        };
+    }
+
+    addSurcharge(value: any) {
+        this.modifyOrderInput.surcharges?.push(value);
+        this.surchargeForm.reset({
+            price: 0,
+            priceIncludesTax: true,
+            taxRate: 0,
+        });
+    }
+
+    removeSurcharge(index: number) {
+        this.modifyOrderInput.surcharges?.splice(index, 1);
+    }
+
+    previewAndModify(order: OrderDetail.Fragment) {
+        const input: ModifyOrderInput = {
+            ...this.modifyOrderInput,
+            dryRun: true,
+            note: this.note,
+            options: {
+                recalculateShipping: this.recalculateShipping,
+            },
+        };
+        const originalTotalWithTax = order.totalWithTax;
+        this.dataService.order
+            .modifyOrder(input)
+            .pipe(
+                switchMap(({ modifyOrder }) => {
+                    switch (modifyOrder.__typename) {
+                        case 'Order':
+                            return this.modalService.fromComponent(OrderEditsPreviewDialogComponent, {
+                                size: 'xl',
+                                closable: false,
+                                locals: {
+                                    originalTotalWithTax,
+                                    order: modifyOrder,
+                                    orderLineCustomFields: this.orderLineCustomFields,
+                                    modifyOrderInput: input,
+                                },
+                            });
+                        case 'InsufficientStockError':
+                        case 'NegativeQuantityError':
+                        case 'NoChangesSpecifiedError':
+                        case 'OrderLimitError':
+                        case 'OrderModificationStateError':
+                        case 'PaymentMethodMissingError':
+                        case 'RefundPaymentIdMissingError': {
+                            this.notificationService.error(modifyOrder.message);
+                            return of(false as const);
+                        }
+                        case null:
+                        case undefined:
+                            return of(false as const);
+                        default:
+                            assertNever(modifyOrder);
+                    }
+                }),
+                switchMap(result => {
+                    if (!result || result.result === OrderEditResultType.Cancel) {
+                        // re-fetch so that the preview values get overwritten in the cache.
+                        return this.dataService.order.getOrder(this.id).mapSingle(() => false);
+                    } else {
+                        // Do the modification
+                        const wetRunInput = {
+                            ...input,
+                            dryRun: false,
+                        };
+                        if (result.result === OrderEditResultType.Refund) {
+                            wetRunInput.refund = {
+                                paymentId: result.refundPaymentId,
+                                reason: result.refundNote,
+                            };
+                        }
+                        return this.dataService.order.modifyOrder(wetRunInput).pipe(
+                            switchMap(({ modifyOrder }) => {
+                                if (modifyOrder.__typename === 'Order') {
+                                    const priceDelta = modifyOrder.totalWithTax - originalTotalWithTax;
+                                    const nextState =
+                                        0 < priceDelta ? 'ArrangingAdditionalPayment' : this.previousState;
+
+                                    return this.dataService.order
+                                        .transitionToState(order.id, nextState)
+                                        .pipe(mapTo(true));
+                                } else {
+                                    this.notificationService.error((modifyOrder as ErrorResult).message);
+                                    return EMPTY;
+                                }
+                            }),
+                        );
+                    }
+                }),
+            )
+            .subscribe(result => {
+                if (result) {
+                    this.router.navigate(['../'], { relativeTo: this.route });
+                }
+            });
+    }
+
+    protected setFormValues(entity: OrderDetail.Fragment, languageCode: LanguageCode): void {
+        /* not used */
+    }
+}

+ 29 - 0
packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.html

@@ -0,0 +1,29 @@
+<ng-template vdrDialogTitle>{{ 'order.confirm-modifications' | translate }}</ng-template>
+<vdr-order-table [order]="order" [orderLineCustomFields]="orderLineCustomFields"></vdr-order-table>
+
+<h4 class="h4">
+    {{ 'order.modify-order-price-difference' | translate }}:
+    <strong>{{ priceDifference / 100 | currency: order.currencyCode }}</strong>
+</h4>
+<div *ngIf="priceDifference < 0">
+<clr-select-container>
+    <label>{{ 'order.payment-to-refund' | translate }}</label>
+    <select clrSelect name="options" [(ngModel)]="selectedPayment">
+        <option
+            *ngFor="let payment of order.payments"
+            [ngValue]="payment"
+        >
+            #{{ payment.id }} {{ payment.method }}:
+            {{ payment.amount / 100 | currency: order.currencyCode }}
+        </option>
+    </select>
+</clr-select-container>
+    <label class="clr-control-label">{{ 'order.refund-cancellation-reason' | translate }}</label>
+    <textarea [(ngModel)]="refundNote" name="refundNote" clrTextarea required></textarea>
+</div>
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit" (click)="submit()" [disabled]="priceDifference < 0 && !selectedPayment" class="btn btn-primary">
+        {{ 'common.confirm' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.scss


+ 80 - 0
packages/admin-ui/src/lib/order/src/components/order-edits-preview-dialog/order-edits-preview-dialog.component.ts

@@ -0,0 +1,80 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { CustomFieldConfig, Dialog, ModifyOrderInput, OrderDetail } from '@vendure/admin-ui/core';
+
+export enum OrderEditResultType {
+    Refund,
+    Payment,
+    PriceUnchanged,
+    Cancel,
+}
+
+interface OrderEditsRefundResult {
+    result: OrderEditResultType.Refund;
+    refundPaymentId: string;
+    refundNote?: string;
+}
+interface OrderEditsPaymentResult {
+    result: OrderEditResultType.Payment;
+}
+interface OrderEditsPriceUnchangedResult {
+    result: OrderEditResultType.PriceUnchanged;
+}
+interface OrderEditsCancelResult {
+    result: OrderEditResultType.Cancel;
+}
+type OrderEditResult =
+    | OrderEditsRefundResult
+    | OrderEditsPaymentResult
+    | OrderEditsPriceUnchangedResult
+    | OrderEditsCancelResult;
+
+@Component({
+    selector: 'vdr-order-edits-preview-dialog',
+    templateUrl: './order-edits-preview-dialog.component.html',
+    styleUrls: ['./order-edits-preview-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class OrderEditsPreviewDialogComponent implements OnInit, Dialog<OrderEditResult> {
+    // Passed in via the modalService
+    order: OrderDetail.Fragment;
+    originalTotalWithTax: number;
+    orderLineCustomFields: CustomFieldConfig[];
+    modifyOrderInput: ModifyOrderInput;
+
+    selectedPayment?: OrderDetail.Payments;
+    refundNote: string;
+    resolveWith: (result?: OrderEditResult) => void;
+
+    get priceDifference(): number {
+        return this.order.totalWithTax - this.originalTotalWithTax;
+    }
+
+    ngOnInit() {
+        this.refundNote = this.modifyOrderInput.note || '';
+    }
+
+    cancel() {
+        this.resolveWith({
+            result: OrderEditResultType.Cancel,
+        });
+    }
+
+    submit() {
+        if (0 < this.priceDifference) {
+            this.resolveWith({
+                result: OrderEditResultType.Payment,
+            });
+        } else if (this.priceDifference < 0) {
+            this.resolveWith({
+                result: OrderEditResultType.Refund,
+                // tslint:disable-next-line:no-non-null-assertion
+                refundPaymentId: this.selectedPayment!.id,
+                refundNote: this.refundNote,
+            });
+        } else {
+            this.resolveWith({
+                result: OrderEditResultType.PriceUnchanged,
+            });
+        }
+    }
+}

+ 26 - 1
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html

@@ -45,6 +45,27 @@
                     }}
                 </ng-template>
             </ng-container>
+            <ng-container *ngSwitchCase="type.ORDER_MODIFIED">
+                <div class="title">
+                    {{ 'order.history-order-modified' | translate }}
+                </div>
+                <ng-container *ngIf="getModification(entry.data.modificationId) as modification">
+                    {{ 'order.modify-order-price-difference' | translate }}:
+                    <strong>{{ modification.priceChange / 100 | currency: order.currencyCode }}</strong>
+                    <vdr-chip colorType="success" *ngIf="modification.isSettled">{{
+                        'order.modification-settled' | translate
+                    }}</vdr-chip>
+                    <vdr-chip colorType="error" *ngIf="!modification.isSettled">{{
+                        'order.modification-not-settled' | translate
+                    }}</vdr-chip>
+                    <vdr-history-entry-detail>
+                        <vdr-modification-detail
+                            [order]="order"
+                            [modification]="modification"
+                        ></vdr-modification-detail>
+                    </vdr-history-entry-detail>
+                </ng-container>
+            </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_PAYMENT_TRANSITION">
                 <ng-container *ngIf="entry.data.to === 'Settled'; else regularPaymentTransition">
                     <div class="title">
@@ -62,7 +83,11 @@
                     {{
                         'order.history-payment-transition'
                             | translate
-                                : { from: entry.data.from, to: entry.data.to, id: getPayment(entry)?.transactionId }
+                                : {
+                                      from: entry.data.from,
+                                      to: entry.data.to,
+                                      id: getPayment(entry)?.transactionId
+                                  }
                     }}
                 </ng-template>
             </ng-container>

+ 8 - 0
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts

@@ -68,6 +68,9 @@ export class OrderHistoryComponent {
         if (entry.type === HistoryEntryType.ORDER_NOTE) {
             return 'note';
         }
+        if (entry.type === HistoryEntryType.ORDER_MODIFIED) {
+            return 'pencil';
+        }
         if (entry.type === HistoryEntryType.ORDER_FULFILLMENT_TRANSITION) {
             if (entry.data.to === 'Shipped') {
                 return 'truck';
@@ -92,6 +95,7 @@ export class OrderHistoryComponent {
             case HistoryEntryType.ORDER_FULFILLMENT_TRANSITION:
                 return entry.data.to === 'Delivered' || entry.data.to === 'Shipped';
             case HistoryEntryType.ORDER_NOTE:
+            case HistoryEntryType.ORDER_MODIFIED:
                 return true;
             default:
                 return false;
@@ -132,6 +136,10 @@ export class OrderHistoryComponent {
         return Array.from(itemMap.entries()).map(([name, quantity]) => ({ name, quantity }));
     }
 
+    getModification(id: string) {
+        return this.order.modifications.find(m => m.id === id);
+    }
+
     getName(entry: GetOrderHistory.Items): string {
         const { administrator } = entry;
         if (administrator) {

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

@@ -104,7 +104,7 @@
             <vdr-table-row-action
                 iconShape="shopping-cart"
                 [label]="'common.open' | translate"
-                [linkTo]="['./', order.id]"
+                [linkTo]="order.state === 'Modifying' ? ['./', order.id, 'modify'] : ['./', order.id]"
             ></vdr-table-row-action>
         </td>
     </ng-template>

+ 47 - 0
packages/admin-ui/src/lib/order/src/components/order-table/order-table-mixin.scss

@@ -0,0 +1,47 @@
+@import "variables";
+
+@mixin order-table {
+
+    .is-cancelled td {
+        text-decoration: line-through;
+        background-color: $color-grey-200;
+    }
+
+    .sub-total td {
+        border-top: 1px dashed $color-grey-300;
+    }
+
+    .total td {
+        font-weight: bold;
+        border-top: 1px dashed $color-grey-300;
+    }
+
+    .order-line-custom-field {
+        background-color: $color-grey-100;
+
+        .custom-field-ellipsis {
+            color: $color-grey-300;
+        }
+    }
+
+    .net-price {
+        font-size: 11px;
+        color: $color-grey-400;
+    }
+    .promotions-label {
+        text-decoration: underline dotted $color-grey-300;
+        font-size: 11px;
+        margin-top: 6px;
+        cursor: pointer;
+        text-transform: lowercase;
+    }
+
+    .line-promotion {
+        display: flex;
+        justify-content: space-between;
+        padding: 6px 12px;
+        .promotion-amount {
+            margin-left: 12px;
+        }
+    }
+}

+ 165 - 0
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.html

@@ -0,0 +1,165 @@
+<table class="order-table table">
+    <thead>
+        <tr>
+            <th></th>
+            <th>{{ 'order.product-name' | translate }}</th>
+            <th>{{ 'order.product-sku' | translate }}</th>
+            <th>{{ 'order.unit-price' | translate }}</th>
+            <th>{{ 'order.quantity' | translate }}</th>
+            <ng-container *ngFor="let customField of visibleOrderLineCustomFields">
+                <th class="order-line-custom-field">
+                    <button
+                        class="custom-field-header-button"
+                        (click)="toggleOrderLineCustomFields()"
+                        [title]="'common.hide-custom-fields' | translate"
+                    >
+                        {{ customField | customFieldLabel }}
+                    </button>
+                </th>
+            </ng-container>
+            <ng-container *ngIf="showElided">
+                <th>
+                    <button
+                        class="custom-field-header-button"
+                        (click)="toggleOrderLineCustomFields()"
+                        [title]="'common.display-custom-fields' | translate"
+                    >
+                        <clr-icon shape="ellipsis-horizontal" class="custom-field-ellipsis"></clr-icon>
+                    </button>
+                </th>
+            </ng-container>
+            <th>{{ 'order.total' | translate }}</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr *ngFor="let line of order.lines" class="order-line" [class.is-cancelled]="line.quantity === 0">
+            <td class="align-middle thumb">
+                <img *ngIf="line.featuredAsset" [src]="line.featuredAsset | assetPreview: 'tiny'" />
+            </td>
+            <td class="align-middle name">{{ line.productVariant.name }}</td>
+            <td class="align-middle sku">{{ line.productVariant.sku }}</td>
+            <td class="align-middle unit-price">
+                {{ line.unitPriceWithTax / 100 | currency: order.currencyCode }}
+                <div class="net-price" [title]="'order.net-price' | translate">
+                    {{ line.unitPrice / 100 | currency: order.currencyCode }}
+                </div>
+            </td>
+            <td class="align-middle quantity">
+                {{ line.quantity }}
+                <vdr-line-refunds [line]="line"></vdr-line-refunds>
+                <vdr-line-fulfillment [line]="line" [orderState]="order.state"></vdr-line-fulfillment>
+            </td>
+            <ng-container *ngFor="let customField of visibleOrderLineCustomFields">
+                <td class="order-line-custom-field align-middle">
+                    <ng-container [ngSwitch]="customField.type">
+                        <ng-template [ngSwitchCase]="'datetime'">
+                            <span [title]="line.customFields[customField.name]">{{
+                                line.customFields[customField.name] | date: 'short'
+                            }}</span>
+                        </ng-template>
+                        <ng-template [ngSwitchCase]="'boolean'">
+                            <ng-template [ngIf]="line.customFields[customField.name] === true">
+                                <clr-icon shape="check"></clr-icon>
+                            </ng-template>
+                            <ng-template [ngIf]="line.customFields[customField.name] === false">
+                                <clr-icon shape="times"></clr-icon>
+                            </ng-template>
+                        </ng-template>
+                        <ng-template ngSwitchDefault>
+                            {{ line.customFields[customField.name] }}
+                        </ng-template>
+                    </ng-container>
+                </td>
+            </ng-container>
+            <ng-container *ngIf="showElided"
+                ><td class="order-line-custom-field align-middle">
+                    <clr-icon shape="ellipsis-horizontal" class="custom-field-ellipsis"></clr-icon></td
+            ></ng-container>
+            <td class="align-middle total">
+                {{ line.linePriceWithTax / 100 | currency: order.currencyCode }}
+                <div class="net-price" [title]="'order.net-price' | translate">
+                    {{ line.linePrice / 100 | currency: order.currencyCode }}
+                </div>
+
+                <ng-container *ngIf="getLineDiscounts(line) as discounts">
+                    <vdr-dropdown *ngIf="discounts.length">
+                        <div class="promotions-label" vdrDropdownTrigger>
+                            {{ 'order.promotions-applied' | translate }}
+                        </div>
+                        <vdr-dropdown-menu>
+                            <div class="line-promotion" *ngFor="let discount of discounts">
+                                <a class="promotion-name" [routerLink]="getPromotionLink(discount)">{{
+                                    discount.description
+                                }}</a>
+                                <div class="promotion-amount">
+                                    {{ discount.amount / 100 | currency: order.currencyCode }}
+                                </div>
+                            </div>
+                        </vdr-dropdown-menu>
+                    </vdr-dropdown>
+                </ng-container>
+            </td>
+        </tr>
+        <tr class="surcharge" *ngFor="let surcharge of order.surcharges">
+            <td class="align-middle name left" colspan="2">{{ surcharge.description }}</td>
+            <td class="align-middle sku">{{ surcharge.sku }}</td>
+            <td class="align-middle"></td>
+            <td [attr.colspan]="1 + visibleOrderLineCustomFields.length"></td>
+            <ng-container *ngIf="showElided"><td></td></ng-container>
+            <td class="align-middle total">
+                {{ surcharge.priceWithTax / 100 | currency: order.currencyCode }}
+                <div class="net-price" [title]="'order.net-price' | translate">
+                    {{ surcharge.price / 100 | currency: order.currencyCode }}
+                </div>
+            </td>
+        </tr>
+        <tr class="order-adjustment" *ngFor="let discount of order.discounts">
+            <td [attr.colspan]="5 + visibleOrderLineCustomFields.length" class="left clr-align-middle">
+                <a [routerLink]="getPromotionLink(discount)">{{ discount.description }}</a>
+                <vdr-chip *ngIf="getCouponCodeForAdjustment(order, discount) as couponCode">{{
+                    couponCode
+                }}</vdr-chip>
+            </td>
+            <ng-container *ngIf="showElided"><td></td></ng-container>
+            <td class="clr-align-middle">
+                {{ discount.amount / 100 | currency: order.currencyCode }}
+            </td>
+        </tr>
+        <tr class="sub-total">
+            <td class="left clr-align-middle">{{ 'order.sub-total' | translate }}</td>
+            <td></td>
+            <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
+            <ng-container *ngIf="showElided"><td></td></ng-container>
+            <td class="clr-align-middle">
+                {{ order.subTotalWithTax / 100 | currency: order.currencyCode }}
+                <div class="net-price" [title]="'order.net-price' | translate">
+                    {{ order.subTotal / 100 | currency: order.currencyCode }}
+                </div>
+            </td>
+        </tr>
+        <tr class="shipping">
+            <td class="left clr-align-middle">{{ 'order.shipping' | translate }}</td>
+            <td class="clr-align-middle">{{ order.shippingLines[0]?.shippingMethod?.name }}</td>
+            <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
+            <ng-container *ngIf="showElided"><td></td></ng-container>
+            <td class="clr-align-middle">
+                {{ order.shippingWithTax / 100 | currency: order.currencyCode }}
+                <div class="net-price" [title]="'order.net-price' | translate">
+                    {{ order.shipping / 100 | currency: order.currencyCode }}
+                </div>
+            </td>
+        </tr>
+        <tr class="total">
+            <td class="left clr-align-middle">{{ 'order.total' | translate }}</td>
+            <td></td>
+            <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
+            <ng-container *ngIf="showElided"><td></td></ng-container>
+            <td class="clr-align-middle">
+                {{ order.totalWithTax / 100 | currency: order.currencyCode }}
+                <div class="net-price" [title]="'order.net-price' | translate">
+                    {{ order.total / 100 | currency: order.currencyCode }}
+                </div>
+            </td>
+        </tr>
+    </tbody>
+</table>

+ 15 - 0
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.scss

@@ -0,0 +1,15 @@
+@import "variables";
+@import "order-table-mixin";
+
+.order-table {
+    @include order-table;
+}
+
+.custom-field-header-button {
+    background: none;
+    margin: 0;
+    padding: 0;
+    border: none;
+    cursor: pointer;
+    color: $color-primary-600;
+}

+ 50 - 0
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.ts

@@ -0,0 +1,50 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { AdjustmentType, CustomFieldConfig, OrderDetail } from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'vdr-order-table',
+    templateUrl: './order-table.component.html',
+    styleUrls: ['./order-table.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class OrderTableComponent implements OnInit {
+    @Input() order: OrderDetail.Fragment;
+    @Input() orderLineCustomFields: CustomFieldConfig[];
+    orderLineCustomFieldsVisible = false;
+
+    get visibleOrderLineCustomFields(): CustomFieldConfig[] {
+        return this.orderLineCustomFieldsVisible ? this.orderLineCustomFields : [];
+    }
+
+    get showElided(): boolean {
+        return !this.orderLineCustomFieldsVisible && 0 < this.orderLineCustomFields.length;
+    }
+
+    ngOnInit(): void {
+        this.orderLineCustomFieldsVisible = this.orderLineCustomFields.length < 2;
+    }
+
+    toggleOrderLineCustomFields() {
+        this.orderLineCustomFieldsVisible = !this.orderLineCustomFieldsVisible;
+    }
+
+    getLineDiscounts(line: OrderDetail.Lines) {
+        return line.discounts.filter(a => a.type === AdjustmentType.PROMOTION);
+    }
+
+    getPromotionLink(promotion: OrderDetail.Discounts): any[] {
+        const id = promotion.adjustmentSource.split(':')[1];
+        return ['/marketing', 'promotions', id];
+    }
+
+    getCouponCodeForAdjustment(
+        order: OrderDetail.Fragment,
+        promotionAdjustment: OrderDetail.Discounts,
+    ): string | undefined {
+        const id = promotionAdjustment.adjustmentSource.split(':')[1];
+        const promotion = order.promotions.find(p => p.id === id);
+        if (promotion) {
+            return promotion.couponCode || undefined;
+        }
+    }
+}

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.html

@@ -1,7 +1,7 @@
 <ng-template vdrDialogTitle>{{ 'order.refund-and-cancel-order' | translate }}</ng-template>
 
 <div class="refund-wrapper">
-    <div class="order-lines">
+    <div class="order-table">
         <table class="table">
             <thead>
                 <tr>

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/refund-order-dialog/refund-order-dialog.component.scss

@@ -8,7 +8,7 @@
 .refund-wrapper {
     flex: 1;
     flex-direction: column;
-    .order-lines {
+    .order-table {
         flex: 1;
         overflow-y: auto;
         table {

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

@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
 import { RouterModule } from '@angular/router';
 import { SharedModule } from '@vendure/admin-ui/core';
 
+import { AddManualPaymentDialogComponent } from './components/add-manual-payment-dialog/add-manual-payment-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 { FulfillmentCardComponent } from './components/fulfillment-card/fulfillment-card.component';
@@ -9,8 +10,11 @@ import { FulfillmentDetailComponent } from './components/fulfillment-detail/fulf
 import { FulfillmentStateLabelComponent } from './components/fulfillment-state-label/fulfillment-state-label.component';
 import { LineFulfillmentComponent } from './components/line-fulfillment/line-fulfillment.component';
 import { LineRefundsComponent } from './components/line-refunds/line-refunds.component';
+import { ModificationDetailComponent } from './components/modification-detail/modification-detail.component';
 import { OrderCustomFieldsCardComponent } from './components/order-custom-fields-card/order-custom-fields-card.component';
 import { OrderDetailComponent } from './components/order-detail/order-detail.component';
+import { OrderEditorComponent } from './components/order-editor/order-editor.component';
+import { OrderEditsPreviewDialogComponent } from './components/order-edits-preview-dialog/order-edits-preview-dialog.component';
 import { OrderHistoryComponent } from './components/order-history/order-history.component';
 import { OrderListComponent } from './components/order-list/order-list.component';
 import { OrderPaymentCardComponent } from './components/order-payment-card/order-payment-card.component';
@@ -18,6 +22,7 @@ import { OrderProcessGraphDialogComponent } from './components/order-process-gra
 import { OrderProcessEdgeComponent } from './components/order-process-graph/order-process-edge.component';
 import { OrderProcessGraphComponent } from './components/order-process-graph/order-process-graph.component';
 import { OrderProcessNodeComponent } from './components/order-process-graph/order-process-node.component';
+import { OrderTableComponent } from './components/order-table/order-table.component';
 import { PaymentDetailComponent } from './components/payment-detail/payment-detail.component';
 import { PaymentStateLabelComponent } from './components/payment-state-label/payment-state-label.component';
 import { RefundOrderDialogComponent } from './components/refund-order-dialog/refund-order-dialog.component';
@@ -51,6 +56,11 @@ import { orderRoutes } from './order.routes';
         OrderProcessGraphDialogComponent,
         FulfillmentStateLabelComponent,
         FulfillmentCardComponent,
+        OrderEditorComponent,
+        OrderTableComponent,
+        OrderEditsPreviewDialogComponent,
+        ModificationDetailComponent,
+        AddManualPaymentDialogComponent,
     ],
 })
 export class OrderModule {}

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

@@ -1,8 +1,16 @@
 import { Route } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { CanDeactivateDetailGuard, createResolveData, detailBreadcrumb, OrderDetail } from '@vendure/admin-ui/core';
+import {
+    BreadcrumbLabelLinkPair,
+    CanDeactivateDetailGuard,
+    createResolveData,
+    detailBreadcrumb,
+    OrderDetail,
+} from '@vendure/admin-ui/core';
+import { map } from 'rxjs/operators';
 
 import { OrderDetailComponent } from './components/order-detail/order-detail.component';
+import { OrderEditorComponent } from './components/order-editor/order-editor.component';
 import { OrderListComponent } from './components/order-list/order-list.component';
 import { OrderResolver } from './providers/routing/order-resolver';
 
@@ -23,6 +31,15 @@ export const orderRoutes: Route[] = [
             breadcrumb: orderBreadcrumb,
         },
     },
+    {
+        path: ':id/modify',
+        component: OrderEditorComponent,
+        resolve: createResolveData(OrderResolver),
+        // canDeactivate: [CanDeactivateDetailGuard],
+        data: {
+            breadcrumb: modifyingOrderBreadcrumb,
+        },
+    },
 ];
 
 export function orderBreadcrumb(data: any, params: any) {
@@ -34,3 +51,14 @@ export function orderBreadcrumb(data: any, params: any) {
         route: '',
     });
 }
+
+export function modifyingOrderBreadcrumb(data: any, params: any) {
+    return orderBreadcrumb(data, params).pipe(
+        map((breadcrumbs: BreadcrumbLabelLinkPair[]) => {
+            const modifiedBreadcrumbs = breadcrumbs.slice();
+            modifiedBreadcrumbs[0].link[0] = '../';
+            modifiedBreadcrumbs[1].link[0] = '../orders';
+            return modifiedBreadcrumbs.concat({ label: _('breadcrumb.modifying'), link: [''] });
+        }) as any,
+    );
+}

+ 1 - 1
packages/admin-ui/src/lib/settings/src/components/test-order-builder/test-order-builder.component.html

@@ -2,7 +2,7 @@
     <div class="card-header">
         {{ 'settings.test-order' | translate }}
     </div>
-    <table class="order-lines table" *ngIf="lines.length; else emptyPlaceholder">
+    <table class="order-table table" *ngIf="lines.length; else emptyPlaceholder">
         <thead>
             <tr>
                 <th></th>

+ 31 - 1
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -37,6 +37,7 @@
     "global-settings": "Všeobecné nastavení",
     "job-queue": "Fronta úloh",
     "manage-variants": "Správa variant",
+    "modifying": "",
     "orders": "Objednávky",
     "payment-methods": "Platební metody",
     "products": "Produkty",
@@ -549,22 +550,31 @@
     "zones": "Zóny"
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "Přidat poznámku",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "Částka",
     "apply-filters": "Filtrovat",
     "billing-address": "Fakturační adresa",
     "cancel": "Zrušit",
     "cancel-fulfillment": "Zrušit zpracování",
+    "cancel-modification": "",
     "cancel-order": "Zrušit objednávku",
     "cancel-reason-customer-request": "Požadavek zákazníka",
     "cancel-reason-not-available": "Neuveden",
     "cancel-selected-items": "Zrušit vybrané položky",
     "cancellation-reason": "Důvod zrušení",
     "cancelled-order-success": "Objednávka úspěšně zrušena",
+    "confirm-modifications": "",
     "contents": "Obsah",
     "create-fulfillment": "Zpracovat",
     "create-fulfillment-success": "Zpracováno",
     "customer": "Zákazník",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "Vlastní",
     "filter-preset-active": "Aktivní",
     "filter-preset-completed": "Uzavřené",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "Objednávka zrušena",
     "history-order-created": "Objednávka vytvořena",
     "history-order-fulfilled": "Objednávka zpracována",
+    "history-order-modified": "",
     "history-order-transition": "Order transitioned from {from} to {to}",
     "history-payment-settled": "Platba vyřízena",
     "history-payment-transition": "Platba #{id} proběhla od {from} pro {to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "Všechny položky zpracovány",
     "line-fulfillment-none": "Žádné položky zpracovány",
     "line-fulfillment-partial": "{ count } z { total } položek zpracováno",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "Čistá cena",
+    "note": "",
     "note-is-private": "Interní poznámka",
     "note-only-visible-to-administrators": "Pouze pro adminy",
     "note-visible-to-customer": "Pro adminy i zákazníka",
@@ -607,6 +631,7 @@
     "placed-at": "Datum objednávky",
     "placed-at-end": "Objednávky do",
     "placed-at-start": "Objednávky od",
+    "preview-changes": "",
     "product-name": "Název produktu",
     "product-sku": "SKU",
     "promotions-applied": "Promotions applied",
@@ -627,6 +652,7 @@
     "refund-total-error": "Částka refundace musí být mezi {min} a {max}",
     "refund-with-amount": "Refundovat {amount}",
     "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
+    "removed-items": "",
     "search-by-order-code": "Hledat na základě kódu objednávky",
     "set-fulfillment-state": "Označit jako {state}",
     "settle-payment": "Vypořádání platby",
@@ -641,7 +667,9 @@
     "state": "Stav",
     "sub-total": "Mezisoučet",
     "successfully-updated-fulfillment": "Zpracování aktualizováno",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "Košík",
     "all-orders": "Všechny objednávky",
+    "arranging-additional-payment": "",
     "arranging-payment": "Zřizování platby",
     "authorized": "Autorizováno",
     "cancelled": "Zrušeno",
@@ -720,6 +749,7 @@
     "delivered": "Doručeno",
     "error": "Chyba",
     "failed": "Selhalo",
+    "modifying": "",
     "partially-delivered": "Částečně doručeno",
     "partially-shipped": "Částečně expedováno",
     "payment-authorized": "Platba autorizovaná",
@@ -746,4 +776,4 @@
     "job-result": "Výsledek úlohy",
     "job-state": "Stav úlohy"
   }
-}
+}

+ 31 - 1
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -37,6 +37,7 @@
     "global-settings": "Globale Einstellungen",
     "job-queue": "Job-Warteschlange",
     "manage-variants": "Varianten verwalten",
+    "modifying": "",
     "orders": "Bestellungen",
     "payment-methods": "Zahlungsarten",
     "products": "Produkte",
@@ -549,22 +550,31 @@
     "zones": "Zonen"
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "Notiz hinzufügen",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "Betrag",
     "apply-filters": "",
     "billing-address": "",
     "cancel": "Abbrechen",
     "cancel-fulfillment": "",
+    "cancel-modification": "",
     "cancel-order": "Bestellung stornieren",
     "cancel-reason-customer-request": "Kundenanfrage",
     "cancel-reason-not-available": "Nicht verfügbar",
     "cancel-selected-items": "Auswahl aufheben",
     "cancellation-reason": "Stornierungsgrund",
     "cancelled-order-success": "Bestellung erfolgreich storniert",
+    "confirm-modifications": "",
     "contents": "Inhalt",
     "create-fulfillment": "Auftrag ausführen",
     "create-fulfillment-success": "Auftrag ausgeführt",
     "customer": "Kunde",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "Bestellung storniert",
     "history-order-created": "",
     "history-order-fulfilled": "Auftrag ausgeführt",
+    "history-order-modified": "",
     "history-order-transition": "Auftragsstatus von {from} nach {to}",
     "history-payment-settled": "Bezahlt",
     "history-payment-transition": "Zahlungsstatus (#{id}) von {from} nach {to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "Alle Positionen ausgeführt",
     "line-fulfillment-none": "Keine Positionen ausgeführt",
     "line-fulfillment-partial": "{ count } von { total } Positionen ausgeführt",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "Nettopreis",
+    "note": "",
     "note-is-private": "Notiz ist privat",
     "note-only-visible-to-administrators": "Nur für Administratoren sichtbar",
     "note-visible-to-customer": "Sichtbar für Administratoren und Kunden",
@@ -607,6 +631,7 @@
     "placed-at": "",
     "placed-at-end": "",
     "placed-at-start": "",
+    "preview-changes": "",
     "product-name": "Produktname",
     "product-sku": "Artikelnummer",
     "promotions-applied": "Aktivierte Werbeaktionen",
@@ -627,6 +652,7 @@
     "refund-total-error": "Die Erstattungssumme muss zwischen {min} und {max} liegen",
     "refund-with-amount": "Rückzahlung {amount}",
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
+    "removed-items": "",
     "search-by-order-code": "Suche nach Bestellcode",
     "set-fulfillment-state": "",
     "settle-payment": "Zahlung durchführen",
@@ -641,7 +667,9 @@
     "state": "Status",
     "sub-total": "Zwischensumme",
     "successfully-updated-fulfillment": "",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "Artikel hinzufügen",
     "all-orders": "",
+    "arranging-additional-payment": "",
     "arranging-payment": "Zahlung einrichten",
     "authorized": "",
     "cancelled": "Storniert",
@@ -720,6 +749,7 @@
     "delivered": "Ausgeführt",
     "error": "",
     "failed": "",
+    "modifying": "",
     "partially-delivered": "Teilweise ausgeführt",
     "partially-shipped": "",
     "payment-authorized": "Zahlung autorisiert",
@@ -746,4 +776,4 @@
     "job-result": "Job-Ergebnis",
     "job-state": "Job-Status"
   }
-}
+}

+ 31 - 1
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -37,6 +37,7 @@
     "global-settings": "Global settings",
     "job-queue": "Job queue",
     "manage-variants": "Manage variants",
+    "modifying": "Modifying",
     "orders": "Orders",
     "payment-methods": "Payment methods",
     "products": "Products",
@@ -549,22 +550,31 @@
     "zones": "Zones"
   },
   "order": {
+    "add-item-to-order": "Add item to order",
     "add-note": "Add note",
+    "add-payment": "Add payment",
+    "add-payment-to-order": "Add payment to order",
+    "add-surcharge": "Add surcharge",
+    "added-items": "Added items",
     "amount": "Amount",
     "apply-filters": "Apply filters",
     "billing-address": "Billing address",
     "cancel": "Cancel",
     "cancel-fulfillment": "Cancel fulfillment",
+    "cancel-modification": "Cancel modification",
     "cancel-order": "Cancel order",
     "cancel-reason-customer-request": "Customer request",
     "cancel-reason-not-available": "Not available",
     "cancel-selected-items": "Cancel selected items",
     "cancellation-reason": "Cancellation reason",
     "cancelled-order-success": "Successfully cancelled order",
+    "confirm-modifications": "Confirm modifications",
     "contents": "Contents",
     "create-fulfillment": "Create fulfillment",
     "create-fulfillment-success": "Created fulfillment",
     "customer": "Customer",
+    "edit-billing-address": "Edit billing address",
+    "edit-shipping-address": "Edit shipping address",
     "filter-custom": "Custom",
     "filter-preset-active": "Active",
     "filter-preset-completed": "Completed",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "Order cancelled",
     "history-order-created": "Order created",
     "history-order-fulfilled": "Order fulfilled",
+    "history-order-modified": "Order modified",
     "history-order-transition": "Order transitioned from {from} to {to}",
     "history-payment-settled": "Payment settled",
     "history-payment-transition": "Payment #{id} transitioned from {from} to {to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "All items fulfilled",
     "line-fulfillment-none": "No items fulfilled",
     "line-fulfillment-partial": "{ count } of { total } items fulfilled",
+    "modification-adding-items": "Adding {count} {count, plural, one {item} other {items}}",
+    "modification-adding-surcharges": "Adding {count} {count, plural, one {surcharge} other {surcharges}}",
+    "modification-adjusting-lines": "Adjusting {count} {count, plural, one {line} other {lines}}",
+    "modification-not-settled": "Not settled",
+    "modification-recalculate-shipping": "Recalculate shipping",
+    "modification-settled": "Settled",
+    "modification-summary": "Summary of modifications",
+    "modification-updating-billing-address": "Updating billing address",
+    "modification-updating-shipping-address": "Updating shipping address",
+    "modifications": "Modifications",
+    "modify-order": "Modify order",
+    "modify-order-price-difference": "Price difference",
     "net-price": "Net price",
+    "note": "Note",
     "note-is-private": "Note is private",
     "note-only-visible-to-administrators": "Visible to admins only",
     "note-visible-to-customer": "Visible to admins and customer",
@@ -607,6 +631,7 @@
     "placed-at": "Placed at",
     "placed-at-end": "Placed at - until",
     "placed-at-start": "Placed at - from",
+    "preview-changes": "Preview changes",
     "product-name": "Product name",
     "product-sku": "SKU",
     "promotions-applied": "Promotions applied",
@@ -627,6 +652,7 @@
     "refund-total-error": "Refund total must be between {min} and {max}",
     "refund-with-amount": "Refund {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
+    "removed-items": "Removed items",
     "search-by-order-code": "Search by order code",
     "set-fulfillment-state": "Mark as {state}",
     "settle-payment": "Settle payment",
@@ -641,7 +667,9 @@
     "state": "State",
     "sub-total": "Sub total",
     "successfully-updated-fulfillment": "Successfully updated fulfillment",
+    "surcharges": "Surcharges",
     "tax-base": "Tax base",
+    "tax-description": "Tax description",
     "tax-rate": "Tax rate",
     "tax-summary": "Tax summary",
     "tax-total": "Tax total",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "Adding items",
     "all-orders": "All order states",
+    "arranging-additional-payment": "Arranging additional payment",
     "arranging-payment": "Arranging payment",
     "authorized": "Authorized",
     "cancelled": "Cancelled",
@@ -720,6 +749,7 @@
     "delivered": "Delivered",
     "error": "Error",
     "failed": "Failed",
+    "modifying": "Modifying",
     "partially-delivered": "Partially delivered",
     "partially-shipped": "Partially shipped",
     "payment-authorized": "Payment authorized",
@@ -746,4 +776,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

+ 30 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -37,6 +37,7 @@
     "global-settings": "Ajustes globales",
     "job-queue": "Cola de trabajos",
     "manage-variants": "Gestionar variantes",
+    "modifying": "",
     "orders": "Pedidos",
     "payment-methods": "Métodos de pago",
     "products": "Productos",
@@ -549,22 +550,31 @@
     "zones": "Zonas"
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "Precio",
     "apply-filters": "",
     "billing-address": "",
     "cancel": "",
     "cancel-fulfillment": "",
+    "cancel-modification": "",
     "cancel-order": "",
     "cancel-reason-customer-request": "",
     "cancel-reason-not-available": "",
     "cancel-selected-items": "",
     "cancellation-reason": "",
     "cancelled-order-success": "",
+    "confirm-modifications": "",
     "contents": "",
     "create-fulfillment": "",
     "create-fulfillment-success": "",
     "customer": "Cliente",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "",
     "history-order-created": "",
     "history-order-fulfilled": "",
+    "history-order-modified": "",
     "history-order-transition": "",
     "history-payment-settled": "",
     "history-payment-transition": "",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "",
     "line-fulfillment-none": "",
     "line-fulfillment-partial": "",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "",
+    "note": "",
     "note-is-private": "",
     "note-only-visible-to-administrators": "",
     "note-visible-to-customer": "",
@@ -607,6 +631,7 @@
     "placed-at": "",
     "placed-at-end": "",
     "placed-at-start": "",
+    "preview-changes": "",
     "product-name": "Nombre del producto",
     "product-sku": "SKU",
     "promotions-applied": "",
@@ -627,6 +652,7 @@
     "refund-total-error": "",
     "refund-with-amount": "",
     "refunded-count": "",
+    "removed-items": "",
     "search-by-order-code": "",
     "set-fulfillment-state": "",
     "settle-payment": "",
@@ -641,7 +667,9 @@
     "state": "Estado",
     "sub-total": "Sub total",
     "successfully-updated-fulfillment": "",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "",
     "all-orders": "",
+    "arranging-additional-payment": "",
     "arranging-payment": "",
     "authorized": "",
     "cancelled": "",
@@ -720,6 +749,7 @@
     "delivered": "",
     "error": "",
     "failed": "",
+    "modifying": "",
     "partially-delivered": "",
     "partially-shipped": "",
     "payment-authorized": "",

+ 53 - 3
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -37,6 +37,7 @@
     "global-settings": "Paramètres globaux",
     "job-queue": "File d'attente de tâches",
     "manage-variants": "Gestion des variations",
+    "modifying": "",
     "orders": "Commandes",
     "payment-methods": "Modes de paiement",
     "products": "Produits",
@@ -57,6 +58,8 @@
     "assign-products-to-channel": "Attribuer les produits au canal",
     "assign-to-channel": "Attribuer au canal",
     "assign-to-named-channel": "Attribuer à { channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "collection-contents": "Contenu de la Collection",
     "confirm-adding-options-delete-default-body": "L'ajout d'options à ce produit supprimera les variations existantes par défaut. Voulez-vous continuer ?",
@@ -99,6 +102,8 @@
     "no-selection": "Pas de sélection",
     "notify-remove-product-from-channel-error": "retrait du produit du canal échoué",
     "notify-remove-product-from-channel-success": "Retrait du produit du canal réussi",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "Option",
     "option-name": "Nom de l'option",
     "option-values": "Valeurs de l'option",
@@ -121,6 +126,7 @@
     "remove-from-channel": "Retirer du canal",
     "remove-option": "Retirer l'option",
     "remove-product-from-channel": "Retirer le produit du canal",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "Chercher le terme",
     "search-product-name-or-code": "Chercher par nom de produit ou code",
     "sku": "UGS",
@@ -287,6 +293,16 @@
     "verified": "Verifié",
     "view-group-members": "Voir les membres du groupe"
   },
+  "dashboard": {
+    "add-widget": "",
+    "latest-orders": "",
+    "orders-summary": "",
+    "remove-widget": "",
+    "total-order-value": "",
+    "total-orders": "",
+    "widget-resize": "",
+    "widget-width": ""
+  },
   "datetime": {
     "ago-days": "Il y a {count, plural, one {1 jour} other {{count} jours}}",
     "ago-hours": "Il y a {count, plural, one {1 h} other {{count} h}}",
@@ -534,22 +550,31 @@
     "zones": "Zones"
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "Ajouter note",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "Quantité",
     "apply-filters": "Applique les filtres",
     "billing-address": "Adresse de facturation",
     "cancel": "Annuler",
     "cancel-fulfillment": "Annuler préparation",
+    "cancel-modification": "",
     "cancel-order": "Annuler la commande",
     "cancel-reason-customer-request": "Demande du client",
     "cancel-reason-not-available": "Pas disponible",
     "cancel-selected-items": "Annuler les articles selectionnés",
     "cancellation-reason": "Raison de l'annulation",
     "cancelled-order-success": "Commande annulée",
+    "confirm-modifications": "",
     "contents": "Contenu",
     "create-fulfillment": "Créer préparation",
     "create-fulfillment-success": "Préparation créée",
     "customer": "Client",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "Personnalisé",
     "filter-preset-active": "Active",
     "filter-preset-completed": "Terminée",
@@ -569,6 +594,7 @@
     "history-order-cancelled": "Commande annulée",
     "history-order-created": "Commande créée",
     "history-order-fulfilled": "Commande préparée",
+    "history-order-modified": "",
     "history-order-transition": "Commande transférée de {from} à {to}",
     "history-payment-settled": "paiement validé",
     "history-payment-transition": "paiement #{id} transféré de {from} à {to}",
@@ -577,7 +603,20 @@
     "line-fulfillment-all": "Tous les articles préparés",
     "line-fulfillment-none": "Aucun article préparé",
     "line-fulfillment-partial": "{ count } sur { total } {count, plural, one {article préparé} other {articles préparés}}",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "Prix net",
+    "note": "",
     "note-is-private": "La note est privée",
     "note-only-visible-to-administrators": "Visible par les admins uniquement",
     "note-visible-to-customer": "Visible par les admins et le client",
@@ -592,26 +631,28 @@
     "placed-at": "Placé à",
     "placed-at-end": "Placé à - jusqu'à",
     "placed-at-start": "Placé à - depuis",
+    "preview-changes": "",
     "product-name": "Nom du produit",
     "product-sku": "UGS",
     "promotions-applied": "Promotions utilisées",
+    "prorated-unit-price": "",
     "quantity": "Quantité",
     "refund": "Remboursement",
     "refund-adjustment": "Ajustement",
     "refund-and-cancel-order": "Rembourser et annuler la commande",
+    "refund-cancellation-reason": "",
+    "refund-cancellation-reason-required": "",
     "refund-metadata": "Métadonnées de rembousement",
-    "refund-order": "Rembourser la commande",
     "refund-order-success": "Commande remboursée",
     "refund-reason": "Raison du remboursement",
     "refund-reason-customer-request": "Demande du client",
     "refund-reason-not-available": "Non disponible",
-    "refund-reason-required": "La raison du remboursement est requise",
     "refund-shipping": "Rembourser la livraison",
     "refund-total": "Remboursement total",
     "refund-total-error": "Le remboursement total doit être entre {min} et {max}",
     "refund-with-amount": "Rembourser {amount}",
     "refunded-count": "{count} {count, plural, one {article remboursé} other {articles remboursés}}",
-    "return-to-stock": "Retour dans le stock",
+    "removed-items": "",
     "search-by-order-code": "Chercher par numéro de commande",
     "set-fulfillment-state": "Marquer {state}",
     "settle-payment": "Régler le paiement",
@@ -626,6 +667,12 @@
     "state": "Etat",
     "sub-total": "Sous total",
     "successfully-updated-fulfillment": "Préparation mise à jour",
+    "surcharges": "",
+    "tax-base": "",
+    "tax-description": "",
+    "tax-rate": "",
+    "tax-summary": "",
+    "tax-total": "",
     "total": "Total",
     "tracking-code": "Numéro de suivi",
     "transaction-id": "Numéro de transaction",
@@ -659,6 +706,7 @@
     "email-address": "Adresse email",
     "filter-by-member-name": "Filtrer par pays",
     "first-name": "Prénom",
+    "fulfillment-handler": "",
     "global-out-of-stock-threshold": "Limite de rupture de stock globale",
     "global-out-of-stock-threshold-tooltip": "Régler le niveau de stock à partir duquel la variante est considéré en rupture de stock. Renseigner une valeur négative permet d'accepter des commandes en attente. La valeur peut être régler individuellement par variante de produit.",
     "last-name": "Nom",
@@ -692,6 +740,7 @@
   "state": {
     "adding-items": "Ajout d'articles",
     "all-orders": "Tous les états de commande",
+    "arranging-additional-payment": "",
     "arranging-payment": "Paiement en cours",
     "authorized": "Autorisé",
     "cancelled": "Annulé",
@@ -700,6 +749,7 @@
     "delivered": "Livré",
     "error": "Erreur",
     "failed": "Echec",
+    "modifying": "",
     "partially-delivered": "Partiellement livré",
     "partially-shipped": "Partiellement expédié",
     "payment-authorized": "Paiement autorisé",

+ 31 - 1
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -37,6 +37,7 @@
     "global-settings": "Ustawienia globalne",
     "job-queue": "Kolejka zadań",
     "manage-variants": "Zarządzaj wariantami",
+    "modifying": "",
     "orders": "Zamówienia",
     "payment-methods": "Metody płatności",
     "products": "Produkty",
@@ -549,22 +550,31 @@
     "zones": ""
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "Dodaj notatke",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "Ilość",
     "apply-filters": "",
     "billing-address": "",
     "cancel": "Anuluj",
     "cancel-fulfillment": "",
+    "cancel-modification": "",
     "cancel-order": "Anuluj zamówienie",
     "cancel-reason-customer-request": "Prośba klienta",
     "cancel-reason-not-available": "Niedostępny",
     "cancel-selected-items": "Anuluj zaznaczone",
     "cancellation-reason": "Powód anulowania",
     "cancelled-order-success": "Pomyślnie anulowano zamówienie",
+    "confirm-modifications": "",
     "contents": "Zawartość",
     "create-fulfillment": "Utwórz wypełnienie",
     "create-fulfillment-success": "Utworzono wypełnienie pomyślnie",
     "customer": "Klient",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "Zamówienie anulowane",
     "history-order-created": "",
     "history-order-fulfilled": "Zamówienie zrealizowane",
+    "history-order-modified": "",
     "history-order-transition": "Zamówienie wysłane z {from} do {to}",
     "history-payment-settled": "Opłacono",
     "history-payment-transition": "Płatność #{id} wysłana z {from} do {to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "Wrzystkie zamówienia zrealizowane",
     "line-fulfillment-none": "Brak zrealizowany zamówień",
     "line-fulfillment-partial": "{ count } z { total } zrealizowanych",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "Cena netto",
+    "note": "",
     "note-is-private": "Notatka jest prywatna",
     "note-only-visible-to-administrators": "Widoczne tylko dla administratora",
     "note-visible-to-customer": "Widoczne dla administratora i klienta",
@@ -607,6 +631,7 @@
     "placed-at": "",
     "placed-at-end": "",
     "placed-at-start": "",
+    "preview-changes": "",
     "product-name": "Nazwa produktu",
     "product-sku": "SKU",
     "promotions-applied": "Zastosowane promocje",
@@ -627,6 +652,7 @@
     "refund-total-error": "Wartość zwrotu musi być pomiędzy {min} i {max}",
     "refund-with-amount": "Zwróć {amount}",
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
+    "removed-items": "",
     "search-by-order-code": "Szukaj po numerze zamówienia",
     "set-fulfillment-state": "",
     "settle-payment": "Rozlicz płatność",
@@ -641,7 +667,9 @@
     "state": "Status",
     "sub-total": "Sub total",
     "successfully-updated-fulfillment": "",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "Dodawanie",
     "all-orders": "",
+    "arranging-additional-payment": "",
     "arranging-payment": "Oczekiwanie na płatność",
     "authorized": "",
     "cancelled": "Anulowano",
@@ -720,6 +749,7 @@
     "delivered": "Zrealizowano",
     "error": "",
     "failed": "",
+    "modifying": "",
     "partially-delivered": "Częściowo zrealizowano",
     "partially-shipped": "",
     "payment-authorized": "Płatność zaakceptowana",
@@ -746,4 +776,4 @@
     "job-result": "Rezultat zlecenia",
     "job-state": "Status zlecenia"
   }
-}
+}

+ 31 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -37,6 +37,7 @@
     "global-settings": "Configurações globais",
     "job-queue": "Fila de tabalho",
     "manage-variants": "Gerenciamento de variantes",
+    "modifying": "",
     "orders": "Pedidos",
     "payment-methods": "Métodos de pagamentos",
     "products": "Produtos",
@@ -549,22 +550,31 @@
     "zones": "Zonas"
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "Adicionar nota",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "Total",
     "apply-filters": "",
     "billing-address": "Endereço de cobrança",
     "cancel": "Cancelar",
     "cancel-fulfillment": "",
+    "cancel-modification": "",
     "cancel-order": "Cancelar Pedido",
     "cancel-reason-customer-request": "Pedido do cliente",
     "cancel-reason-not-available": "Não disponível",
     "cancel-selected-items": "Cancelar itens selecionados",
     "cancellation-reason": "Motivo do cancelamento",
     "cancelled-order-success": "Pedido cancelado com sucesso",
+    "confirm-modifications": "",
     "contents": "Conteúdo",
     "create-fulfillment": "Criar a execução",
     "create-fulfillment-success": "Execução criada",
     "customer": "Cliente",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "Pedido cancelado",
     "history-order-created": "",
     "history-order-fulfilled": "Pedido realizado",
+    "history-order-modified": "",
     "history-order-transition": "Pedido transferido de {from} para {to}",
     "history-payment-settled": "Pagamento concluído",
     "history-payment-transition": "Pagamento #{id} transferido de {from} para {to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "Todos os itens executados",
     "line-fulfillment-none": "Nenhum ítem executado",
     "line-fulfillment-partial": "{ count } of { total } itens executados",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "Preço líquido",
+    "note": "",
     "note-is-private": "Nota é privada",
     "note-only-visible-to-administrators": "Visível somente para administradores",
     "note-visible-to-customer": "Visível para administradores e clientes",
@@ -607,6 +631,7 @@
     "placed-at": "",
     "placed-at-end": "",
     "placed-at-start": "",
+    "preview-changes": "",
     "product-name": "Nome do produto",
     "product-sku": "SKU",
     "promotions-applied": "Promoções aplicadas",
@@ -627,6 +652,7 @@
     "refund-total-error": "Total do reembolso deve ser entre {min} e {max}",
     "refund-with-amount": "Reembolso {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
+    "removed-items": "",
     "search-by-order-code": "Buscar por código do pedido",
     "set-fulfillment-state": "",
     "settle-payment": "Liquidar pagamento",
@@ -641,7 +667,9 @@
     "state": "Estado",
     "sub-total": "Subtotal",
     "successfully-updated-fulfillment": "",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "Criando itens",
     "all-orders": "",
+    "arranging-additional-payment": "",
     "arranging-payment": "Organização de pagamento",
     "authorized": "",
     "cancelled": "Cancelado",
@@ -720,6 +749,7 @@
     "delivered": "Realizado",
     "error": "",
     "failed": "",
+    "modifying": "",
     "partially-delivered": "Parcialmente realizado",
     "partially-shipped": "",
     "payment-authorized": "Pagamento autorizado",
@@ -746,4 +776,4 @@
     "job-result": "Resultado do trabalho",
     "job-state": "Estado do trabalho"
   }
-}
+}

+ 30 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -37,6 +37,7 @@
     "global-settings": "语言设置",
     "job-queue": "",
     "manage-variants": "商品规格管理",
+    "modifying": "",
     "orders": "订单管理",
     "payment-methods": "支付管理",
     "products": "商品列表",
@@ -549,22 +550,31 @@
     "zones": ""
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "添加备注",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "金额",
     "apply-filters": "",
     "billing-address": "",
     "cancel": "取消",
     "cancel-fulfillment": "",
+    "cancel-modification": "",
     "cancel-order": "取消订单",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "产品无库存",
     "cancel-selected-items": "取消已选",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "订单成功取消",
+    "confirm-modifications": "",
     "contents": "具体内容",
     "create-fulfillment": "确认配货",
     "create-fulfillment-success": "确认配货成功",
     "customer": "客户",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "订单已取消",
     "history-order-created": "",
     "history-order-fulfilled": "订单已配货",
+    "history-order-modified": "",
     "history-order-transition": "订单状态从{from}更新至{to}",
     "history-payment-settled": "已结算付款",
     "history-payment-transition": "付款交易 #{id} 状态从{from}更新至{to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "订单已全部配货完成",
     "line-fulfillment-none": "无订单配货记录",
     "line-fulfillment-partial": "总共{ total }个订单项,{ count }个已配货",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "净价",
+    "note": "",
     "note-is-private": "隐藏备注",
     "note-only-visible-to-administrators": "仅管理员可见",
     "note-visible-to-customer": "管理员及客户可见",
@@ -607,6 +631,7 @@
     "placed-at": "",
     "placed-at-end": "",
     "placed-at-start": "",
+    "preview-changes": "",
     "product-name": "产品名称",
     "product-sku": "库存编码",
     "promotions-applied": "已使用代金券",
@@ -627,6 +652,7 @@
     "refund-total-error": "退款总计必须大于{min}并少于{max}之间",
     "refund-with-amount": "退款金额{amount}",
     "refunded-count": "{count}个商品已退款",
+    "removed-items": "",
     "search-by-order-code": "输入要搜索的订单编号",
     "set-fulfillment-state": "",
     "settle-payment": "结算付款",
@@ -641,7 +667,9 @@
     "state": "状态",
     "sub-total": "小计金额",
     "successfully-updated-fulfillment": "",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "正在选择商品",
     "all-orders": "",
+    "arranging-additional-payment": "",
     "arranging-payment": "正在付款",
     "authorized": "",
     "cancelled": "已取消",
@@ -720,6 +749,7 @@
     "delivered": "已完成",
     "error": "",
     "failed": "",
+    "modifying": "",
     "partially-delivered": "部分配货",
     "partially-shipped": "",
     "payment-authorized": "已授权支付",

+ 30 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -37,6 +37,7 @@
     "global-settings": "語言",
     "job-queue": "",
     "manage-variants": "商品規格管理",
+    "modifying": "",
     "orders": "訂單管理",
     "payment-methods": "支付方式",
     "products": "商品",
@@ -549,22 +550,31 @@
     "zones": ""
   },
   "order": {
+    "add-item-to-order": "",
     "add-note": "新增備注",
+    "add-payment": "",
+    "add-payment-to-order": "",
+    "add-surcharge": "",
+    "added-items": "",
     "amount": "金額",
     "apply-filters": "",
     "billing-address": "",
     "cancel": "取消",
     "cancel-fulfillment": "",
+    "cancel-modification": "",
     "cancel-order": "取消訂單",
     "cancel-reason-customer-request": "客户要求",
     "cancel-reason-not-available": "產品無庫存",
     "cancel-selected-items": "取消已選",
     "cancellation-reason": "取消原因",
     "cancelled-order-success": "訂單取消成功",
+    "confirm-modifications": "",
     "contents": "内容",
     "create-fulfillment": "確認配貨",
     "create-fulfillment-success": "確認配貨成功",
     "customer": "客户",
+    "edit-billing-address": "",
+    "edit-shipping-address": "",
     "filter-custom": "",
     "filter-preset-active": "",
     "filter-preset-completed": "",
@@ -584,6 +594,7 @@
     "history-order-cancelled": "訂單已取消",
     "history-order-created": "",
     "history-order-fulfilled": "訂單已配貨",
+    "history-order-modified": "",
     "history-order-transition": "訂單狀態從{from}更新至{to}",
     "history-payment-settled": "已結算付款",
     "history-payment-transition": "付款交易 #{id} 狀態從{from}更新至{to}",
@@ -592,7 +603,20 @@
     "line-fulfillment-all": "訂單已全部配貨完成",
     "line-fulfillment-none": "無訂單配貨記錄",
     "line-fulfillment-partial": "總共{ total }個訂單項,{ count }個已配貨",
+    "modification-adding-items": "",
+    "modification-adding-surcharges": "",
+    "modification-adjusting-lines": "",
+    "modification-not-settled": "",
+    "modification-recalculate-shipping": "",
+    "modification-settled": "",
+    "modification-summary": "",
+    "modification-updating-billing-address": "",
+    "modification-updating-shipping-address": "",
+    "modifications": "",
+    "modify-order": "",
+    "modify-order-price-difference": "",
     "net-price": "淨價",
+    "note": "",
     "note-is-private": "隱藏備注",
     "note-only-visible-to-administrators": "僅管理員可瀏覽",
     "note-visible-to-customer": "管理員及客户可瀏覽",
@@ -607,6 +631,7 @@
     "placed-at": "",
     "placed-at-end": "",
     "placed-at-start": "",
+    "preview-changes": "",
     "product-name": "產品名稱",
     "product-sku": "庫存編碼",
     "promotions-applied": "已使用優惠券",
@@ -627,6 +652,7 @@
     "refund-total-error": "退款總計必須大於{min}並少遊{max}之間",
     "refund-with-amount": "退款金額{amount}",
     "refunded-count": "{count}個商品已退款",
+    "removed-items": "",
     "search-by-order-code": "輸入要搜索的訂單編號",
     "set-fulfillment-state": "",
     "settle-payment": "結算付款",
@@ -641,7 +667,9 @@
     "state": "狀態",
     "sub-total": "小計金額",
     "successfully-updated-fulfillment": "",
+    "surcharges": "",
     "tax-base": "",
+    "tax-description": "",
     "tax-rate": "",
     "tax-summary": "",
     "tax-total": "",
@@ -712,6 +740,7 @@
   "state": {
     "adding-items": "正在選擇商品",
     "all-orders": "",
+    "arranging-additional-payment": "",
     "arranging-payment": "正在付款",
     "authorized": "",
     "cancelled": "已取消",
@@ -720,6 +749,7 @@
     "delivered": "已完成",
     "error": "",
     "failed": "",
+    "modifying": "",
     "partially-delivered": "部分配貨",
     "partially-shipped": "",
     "payment-authorized": "已授權支付",

+ 1 - 0
packages/admin-ui/src/lib/static/styles/theme/_forms.scss

@@ -93,6 +93,7 @@ select {
 
 .ng-select .ng-select-container .ng-value-container {
     padding-top: 0;
+    min-width: 60px;
     .ng-value {
         margin: 0 6px 0 0;
     }