From 36ca61cbcea07f6247f491b0f3b70b8f5fcb1d09 Mon Sep 17 00:00:00 2001
From: Stefan Hauke
Date: Wed, 12 Jun 2024 09:18:59 +0200
Subject: [PATCH 1/8] feat: introduce Formly number field (quantity input with
+ and - buttons)
---
docs/guides/formly.md | 37 +++++++++--------
.../number-field/number-field.component.html | 35 ++++++++++++++++
.../number-field/number-field.component.scss | 31 ++++++++++++++
.../number-field/number-field.component.ts | 41 +++++++++++++++++++
src/app/shared/formly/types/types.module.ts | 7 ++++
src/assets/i18n/de_DE.json | 2 +
src/assets/i18n/en_US.json | 2 +
src/assets/i18n/fr_FR.json | 2 +
8 files changed, 139 insertions(+), 18 deletions(-)
create mode 100644 src/app/shared/formly/types/number-field/number-field.component.html
create mode 100644 src/app/shared/formly/types/number-field/number-field.component.scss
create mode 100644 src/app/shared/formly/types/number-field/number-field.component.ts
diff --git a/docs/guides/formly.md b/docs/guides/formly.md
index 46b5fef81e..7de22ef7c0 100644
--- a/docs/guides/formly.md
+++ b/docs/guides/formly.md
@@ -61,7 +61,7 @@ A configuration for a form containing only a basic input field could be defined
```typescript
const fields: FormlyFieldConfig[] = [
{
- type: 'ish-input-field',
+ type: 'ish-text-input-field',
key: 'example-input',
props: {
required: true,
@@ -254,23 +254,24 @@ Refer to the tables below for an overview of these parts.
- Template option `inputClass`: These CSS class(es) will be added to all input/select/textarea/text tags.
- Template option `ariaLabel`: Adds an aria-label to all input/select/textarea tags.
-| Name | Description | Relevant props |
-| --------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| ish-text-input-field | Basic input field, supports all text types | `type`: 'text (default),'email','tel','password'. `mask`: input mask for a needed pattern (see [ngx-mask](https://www.npmjs.com/package/ngx-mask) for more information) |
-| ish-select-field | Basic select field | `options`: `{ value: any; label: string}[]` or Observable. `placeholder`: Translation key or string for the default selection. `optionsTranslateDisabled`: Disables options label translation (placeholder is still translated). |
-| ish-textarea-field | Basic textarea field | `cols` & `rows`: Specifies the dimensions of the textarea |
-| ish-checkbox-field | Basic checkbox input | `title`: Title for a checkbox |
-| ish-email-field | Email input field that automatically adds an e-mail validator and error messages | ---- |
-| ish-password-field | Password input field that automatically adds a password validator and error messages | ---- |
-| ish-phone-field | Phone number input field that automatically adds a phone number validator and error messages | ---- |
-| ish-fieldset-field | Wraps fields in a `` tag for styling | `fieldsetClass`: Class that will be added to the fieldset tag. `childClass`: Class that will be added to the child div. `legend`: Legend element that will be added to the fieldset, use the value as the legend text. `legendClass`: Class that will be added to the legend tag. |
-| ish-captcha-field | Includes the `` component and adds the relevant `formControls` to the form | `topic`: Topic that will be passed to the Captcha component. |
-| ish-radio-field | Basic radio input | ---- |
-| ish-radio-group-field | Radio button group inline for price type selection | `opts`: Array of label/value pairs |
-| ish-plain-text-field | Only display the form value | ---- |
-| ish-html-text-field | Only display the form value as html | ---- |
-| ish-date-picker-field | Basic datepicker | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `isSatExcluded`: Specifies if saturdays can be disabled. `isSunExcluded`: Specifies if sundays can be disabled. |
-| ish-date-range-picker-field | Datepicker with range | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `startDate`: The start date. `placeholder`: Placeholder that displays the date format in the input field. |
+| Name | Description | Relevant props |
+| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| ish-text-input-field | Basic input field, supports all text types | `type`: 'text (default),'email','tel','password'. `mask`: input mask for a needed pattern (see [ngx-mask](https://www.npmjs.com/package/ngx-mask) for more information) |
+| ish-select-field | Basic select field | `options`: `{ value: any; label: string}[]` or Observable. `placeholder`: Translation key or string for the default selection. `optionsTranslateDisabled`: Disables options label translation (placeholder is still translated). |
+| ish-textarea-field | Basic textarea field | `cols` & `rows`: Specifies the dimensions of the textarea |
+| ish-checkbox-field | Basic checkbox input | `title`: Title for a checkbox |
+| ish-email-field | Email input field that automatically adds an e-mail validator and error messages | ---- |
+| ish-password-field | Password input field that automatically adds a password validator and error messages | ---- |
+| ish-phone-field | Phone number input field that automatically adds a phone number validator and error messages | ---- |
+| ish-fieldset-field | Wraps fields in a `` tag for styling | `fieldsetClass`: Class that will be added to the fieldset tag. `childClass`: Class that will be added to the child div. `legend`: Legend element that will be added to the fieldset, use the value as the legend text. `legendClass`: Class that will be added to the legend tag. |
+| ish-captcha-field | Includes the `` component and adds the relevant `formControls` to the form | `topic`: Topic that will be passed to the Captcha component. |
+| ish-radio-field | Basic radio input | ---- |
+| ish-radio-group-field | Radio button group inline for price type selection | `opts`: Array of label/value pairs |
+| ish-plain-text-field | Only display the form value | ---- |
+| ish-html-text-field | Only display the form value as html | ---- |
+| ish-date-picker-field | Basic datepicker | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `isSatExcluded`: Specifies if saturdays can be disabled. `isSunExcluded`: Specifies if sundays can be disabled. |
+| ish-date-range-picker-field | Datepicker with range | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `startDate`: The start date. `placeholder`: Placeholder that displays the date format in the input field. |
+| ish-number-field | Basic number input field for smaller Integer numbers, with `+` and `-` buttons (use `ish-text-input-field` with `mask` for larger numbers) | `min`, `max` and `step` input configuration is considered by the in-/decrease buttons |
### Wrappers
diff --git a/src/app/shared/formly/types/number-field/number-field.component.html b/src/app/shared/formly/types/number-field/number-field.component.html
new file mode 100644
index 0000000000..496b0c27c3
--- /dev/null
+++ b/src/app/shared/formly/types/number-field/number-field.component.html
@@ -0,0 +1,35 @@
+
+
+ number.decrease.text
+
+
+ number.increase.text
+
+
+
diff --git a/src/app/shared/formly/types/number-field/number-field.component.scss b/src/app/shared/formly/types/number-field/number-field.component.scss
new file mode 100644
index 0000000000..97d347ebca
--- /dev/null
+++ b/src/app/shared/formly/types/number-field/number-field.component.scss
@@ -0,0 +1,31 @@
+.counter-input {
+ position: relative;
+
+ button {
+ position: absolute;
+ width: auto;
+ margin: 0;
+ }
+
+ .decrease-button {
+ left: 0;
+ }
+
+ .increase-button {
+ right: 0;
+ }
+
+ // https://www.w3schools.com/howto/howto_css_hide_arrow_number.asp
+
+ /* Chrome, Safari, Edge, Opera */
+ input::-webkit-outer-spin-button,
+ input::-webkit-inner-spin-button {
+ margin: 0;
+ appearance: none;
+ }
+
+ /* Firefox */
+ input[type='number'] {
+ appearance: textfield;
+ }
+}
diff --git a/src/app/shared/formly/types/number-field/number-field.component.ts b/src/app/shared/formly/types/number-field/number-field.component.ts
new file mode 100644
index 0000000000..d13d2df3a6
--- /dev/null
+++ b/src/app/shared/formly/types/number-field/number-field.component.ts
@@ -0,0 +1,41 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+/**
+ * Type for a number field
+ *
+ * @defaultWrappers form-field-horizontal & validation
+ */
+@Component({
+ selector: 'ish-number-field',
+ templateUrl: './number-field.component.html',
+ styleUrls: ['./number-field.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NumberFieldComponent extends FieldType implements OnInit {
+ cannotIncrease = false;
+ cannotDecrease = false;
+
+ ngOnInit(): void {
+ this.evaluateButtonDisabled();
+ }
+
+ increase() {
+ this.formControl.setValue(
+ Number.parseInt(this.formControl.value) + (this.field.props.step ? this.field.props.step : 1)
+ );
+ this.evaluateButtonDisabled();
+ }
+
+ decrease() {
+ this.formControl.setValue(
+ Number.parseInt(this.formControl.value) - (this.field.props.step ? this.field.props.step : 1)
+ );
+ this.evaluateButtonDisabled();
+ }
+
+ private evaluateButtonDisabled() {
+ this.cannotDecrease = this.field.props.min && this.formControl.value <= this.field.props.min;
+ this.cannotIncrease = this.field.props.max && this.formControl.value >= this.field.props.max;
+ }
+}
diff --git a/src/app/shared/formly/types/types.module.ts b/src/app/shared/formly/types/types.module.ts
index 15d5b68a62..7c945f31bd 100644
--- a/src/app/shared/formly/types/types.module.ts
+++ b/src/app/shared/formly/types/types.module.ts
@@ -26,6 +26,7 @@ import { LocalizedParserFormatter } from './date-picker-field/localized-parser-f
import { DateRangePickerFieldComponent } from './date-range-picker-field/date-range-picker-field.component';
import { FieldsetFieldComponent } from './fieldset-field/fieldset-field.component';
import { HtmlTextFieldComponent } from './html-text-field/html-text-field.component';
+import { NumberFieldComponent } from './number-field/number-field.component';
import { PlainTextFieldComponent } from './plain-text-field/plain-text-field.component';
import { RadioFieldComponent } from './radio-field/radio-field.component';
import { RadioGroupFieldComponent } from './radio-group-field/radio-group-field.component';
@@ -46,6 +47,7 @@ const fieldComponents = [
SelectFieldComponent,
TextareaFieldComponent,
TextInputFieldComponent,
+ NumberFieldComponent,
];
@NgModule({
@@ -173,6 +175,11 @@ const fieldComponents = [
component: DateRangePickerFieldComponent,
wrappers: ['form-field-horizontal', 'validation'],
},
+ {
+ name: 'ish-number-field',
+ component: NumberFieldComponent,
+ wrappers: ['form-field-horizontal', 'validation'],
+ },
],
}),
],
diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json
index cabaa91284..5e79253df0 100644
--- a/src/assets/i18n/de_DE.json
+++ b/src/assets/i18n/de_DE.json
@@ -958,6 +958,8 @@
"navigation.paging.go_to_page.label": "Zu Seite {{0}} gehen",
"navigation.paging.next_page.label": "Zur nächsten Seite gehen",
"navigation.paging.previous_page.label": "Zur vorherigen Seite gehen",
+ "number.decrease.text": "-",
+ "number.increase.text": "+",
"order.tracking.error": "Leider konnte keine Bestellung mit Ihren Daten gefunden werden.",
"order_template.create.heading": "Bestellvorlage anlegen",
"payment.error.PaymentInstrumentAlreadyExists": "Das Zahlungsmittel konnte nicht angelegt werden. Zahlungsdaten mit den angegebenen Parametern sind bereits vorhanden.",
diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json
index 539eacbbb2..62692574ee 100644
--- a/src/assets/i18n/en_US.json
+++ b/src/assets/i18n/en_US.json
@@ -958,6 +958,8 @@
"navigation.paging.go_to_page.label": "Go to page {{0}}",
"navigation.paging.next_page.label": "Go to next page",
"navigation.paging.previous_page.label": "Go to previous page",
+ "number.decrease.text": "-",
+ "number.increase.text": "+",
"order.tracking.error": "Unfortunately, we could not locate an order with the information you provided.",
"order_template.create.heading": "Create order template",
"payment.error.PaymentInstrumentAlreadyExists": "The payment instrument could not be created. Payment data with the given parameters already exists.",
diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json
index 35ca67bf76..1894a72979 100644
--- a/src/assets/i18n/fr_FR.json
+++ b/src/assets/i18n/fr_FR.json
@@ -958,6 +958,8 @@
"navigation.paging.go_to_page.label": "Aller à la page {{0}}",
"navigation.paging.next_page.label": "Aller à la page suivante",
"navigation.paging.previous_page.label": "Aller à la page précédente",
+ "number.decrease.text": "-",
+ "number.increase.text": "+",
"order.tracking.error": "Malheureusement, nous n’avons pas pu localiser une commande avec les informations que vous avez fournies.",
"order_template.create.heading": "Créer un modèle de commande",
"payment.error.PaymentInstrumentAlreadyExists": "Le moyen de paiement n’a pas pu être mis en place. Les données de paiement avec les paramètres fournis existent déjà.",
From 2cba66944741d8e05fcf5c24cfee97cb021166cc Mon Sep 17 00:00:00 2001
From: Stefan Hauke
Date: Mon, 24 Jun 2024 22:37:54 +0200
Subject: [PATCH 2/8] feat: introduce Formly information field for rendering
any freestyle text within generated forms
---
docs/guides/formly.md | 1 +
.../information-field.component.html | 4 ++++
.../information-field.component.ts | 13 +++++++++++++
src/app/shared/formly/types/types.module.ts | 6 ++++++
4 files changed, 24 insertions(+)
create mode 100644 src/app/shared/formly/types/information-field/information-field.component.html
create mode 100644 src/app/shared/formly/types/information-field/information-field.component.ts
diff --git a/docs/guides/formly.md b/docs/guides/formly.md
index 7de22ef7c0..a114aa95ae 100644
--- a/docs/guides/formly.md
+++ b/docs/guides/formly.md
@@ -272,6 +272,7 @@ Refer to the tables below for an overview of these parts.
| ish-date-picker-field | Basic datepicker | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `isSatExcluded`: Specifies if saturdays can be disabled. `isSunExcluded`: Specifies if sundays can be disabled. |
| ish-date-range-picker-field | Datepicker with range | `minDays`: Computes the minDate by adding the minimum allowed days to today. `maxDays`: Computes the maxDate by adding the maximum allowed days to today. `startDate`: The start date. `placeholder`: Placeholder that displays the date format in the input field. |
| ish-number-field | Basic number input field for smaller Integer numbers, with `+` and `-` buttons (use `ish-text-input-field` with `mask` for larger numbers) | `min`, `max` and `step` input configuration is considered by the in-/decrease buttons |
+| ish-information-field | Include any freestyle text within a Formly generated form (HTML content is supported) | provide text via `localizationKey` or just plain `text` and adapt the styling via `containerClass` |
### Wrappers
diff --git a/src/app/shared/formly/types/information-field/information-field.component.html b/src/app/shared/formly/types/information-field/information-field.component.html
new file mode 100644
index 0000000000..6bae178c14
--- /dev/null
+++ b/src/app/shared/formly/types/information-field/information-field.component.html
@@ -0,0 +1,4 @@
+
diff --git a/src/app/shared/formly/types/information-field/information-field.component.ts b/src/app/shared/formly/types/information-field/information-field.component.ts
new file mode 100644
index 0000000000..269d112779
--- /dev/null
+++ b/src/app/shared/formly/types/information-field/information-field.component.ts
@@ -0,0 +1,13 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+/**
+ * Include any freestyle text within a Formly generated form (HTML content is supported).
+ * Provide text via `localizationKey` or just plain `text` and adapt the styling via `containerClass`.
+ */
+@Component({
+ selector: 'ish-information-field',
+ templateUrl: './information-field.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class InformationFieldComponent extends FieldType {}
diff --git a/src/app/shared/formly/types/types.module.ts b/src/app/shared/formly/types/types.module.ts
index 7c945f31bd..0847132500 100644
--- a/src/app/shared/formly/types/types.module.ts
+++ b/src/app/shared/formly/types/types.module.ts
@@ -26,6 +26,7 @@ import { LocalizedParserFormatter } from './date-picker-field/localized-parser-f
import { DateRangePickerFieldComponent } from './date-range-picker-field/date-range-picker-field.component';
import { FieldsetFieldComponent } from './fieldset-field/fieldset-field.component';
import { HtmlTextFieldComponent } from './html-text-field/html-text-field.component';
+import { InformationFieldComponent } from './information-field/information-field.component';
import { NumberFieldComponent } from './number-field/number-field.component';
import { PlainTextFieldComponent } from './plain-text-field/plain-text-field.component';
import { RadioFieldComponent } from './radio-field/radio-field.component';
@@ -48,6 +49,7 @@ const fieldComponents = [
TextareaFieldComponent,
TextInputFieldComponent,
NumberFieldComponent,
+ InformationFieldComponent,
];
@NgModule({
@@ -180,6 +182,10 @@ const fieldComponents = [
component: NumberFieldComponent,
wrappers: ['form-field-horizontal', 'validation'],
},
+ {
+ name: 'ish-information-field',
+ component: InformationFieldComponent,
+ },
],
}),
],
From 366d63984b2d4611abac7131defd5ff087a6bf27 Mon Sep 17 00:00:00 2001
From: Susanne Schneider
Date: Thu, 26 Sep 2024 20:17:50 +0200
Subject: [PATCH 3/8] feat: introduce Switch component (UI element for imidiate
status changes)
* implemented as standalone component
---
.../common/switch/switch.component.html | 13 ++++++
.../common/switch/switch.component.scss | 23 ++++++++++
.../common/switch/switch.component.spec.ts | 25 +++++++++++
.../common/switch/switch.component.ts | 42 +++++++++++++++++++
4 files changed, 103 insertions(+)
create mode 100644 src/app/shared/components/common/switch/switch.component.html
create mode 100644 src/app/shared/components/common/switch/switch.component.scss
create mode 100644 src/app/shared/components/common/switch/switch.component.spec.ts
create mode 100644 src/app/shared/components/common/switch/switch.component.ts
diff --git a/src/app/shared/components/common/switch/switch.component.html b/src/app/shared/components/common/switch/switch.component.html
new file mode 100644
index 0000000000..d44a6b3945
--- /dev/null
+++ b/src/app/shared/components/common/switch/switch.component.html
@@ -0,0 +1,13 @@
+
+
+ {{ activeState ? labelActive : labelInactive }}
+
diff --git a/src/app/shared/components/common/switch/switch.component.scss b/src/app/shared/components/common/switch/switch.component.scss
new file mode 100644
index 0000000000..fb76d6d7d9
--- /dev/null
+++ b/src/app/shared/components/common/switch/switch.component.scss
@@ -0,0 +1,23 @@
+@import 'variables';
+
+.custom-control-input ~ .custom-control-label::before {
+ background-color: $text-color-quaternary;
+ border-color: $text-color-quaternary;
+}
+
+.custom-control-input:disabled ~ .custom-control-label::before {
+ background-color: $text-color-quinary;
+}
+
+.custom-control-input:checked ~ .custom-control-label::before {
+ background-color: $CORPORATE-PRIMARY;
+ border-color: $CORPORATE-PRIMARY;
+}
+
+.custom-control-input:checked:disabled ~ .custom-control-label::before {
+ background-color: $CORPORATE-LIGHT;
+}
+
+.custom-switch .custom-control-label::after {
+ background-color: $white;
+}
diff --git a/src/app/shared/components/common/switch/switch.component.spec.ts b/src/app/shared/components/common/switch/switch.component.spec.ts
new file mode 100644
index 0000000000..4b20ec3e72
--- /dev/null
+++ b/src/app/shared/components/common/switch/switch.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SwitchComponent } from './switch.component';
+
+describe('Switch Component', () => {
+ let component: SwitchComponent;
+ let fixture: ComponentFixture;
+ let element: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({}).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SwitchComponent);
+ component = fixture.componentInstance;
+ element = fixture.nativeElement;
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ expect(element).toBeTruthy();
+ expect(() => fixture.detectChanges()).not.toThrow();
+ });
+});
diff --git a/src/app/shared/components/common/switch/switch.component.ts b/src/app/shared/components/common/switch/switch.component.ts
new file mode 100644
index 0000000000..93d72e4909
--- /dev/null
+++ b/src/app/shared/components/common/switch/switch.component.ts
@@ -0,0 +1,42 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
+import { v4 as uuid } from 'uuid';
+
+@Component({
+ selector: 'ish-switch',
+ templateUrl: './switch.component.html',
+ styleUrls: ['./switch.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+})
+/**
+ * The Switch Component is a reusable component that allows toggling between two states for a given object.
+ * The bootstrap switch component is used to render the switch.
+ *
+ * @example
+ *
+ */
+export class SwitchComponent implements OnChanges {
+ // id is not required but can be used to identify the switch context
+ @Input() id: string = uuid();
+ @Input() active = false;
+ @Input() labelActive = '';
+ @Input() labelInactive = '';
+ @Input() disabled = false;
+
+ @Output() toggleSwitch = new EventEmitter<{ active: boolean; id: string }>();
+
+ activeState: boolean;
+
+ ngOnChanges() {
+ this.activeState = this.active;
+ }
+
+ toggleState() {
+ this.activeState = !this.activeState;
+ this.toggleSwitch.emit({ active: this.activeState, id: this.id });
+ }
+}
From dc4f4df5f8447308ae8d39a0b335960d856d3ba2 Mon Sep 17 00:00:00 2001
From: Stefan Hauke
Date: Thu, 26 Sep 2024 20:21:41 +0200
Subject: [PATCH 4/8] feat: introduce Server Setting route guard
* checks for an enabled server setting before routing
---
src/app/core/guards/server-setting.guard.ts | 30 +++++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 src/app/core/guards/server-setting.guard.ts
diff --git a/src/app/core/guards/server-setting.guard.ts b/src/app/core/guards/server-setting.guard.ts
new file mode 100644
index 0000000000..6ee70eea06
--- /dev/null
+++ b/src/app/core/guards/server-setting.guard.ts
@@ -0,0 +1,30 @@
+import { inject } from '@angular/core';
+import { ActivatedRouteSnapshot, Router } from '@angular/router';
+import { map } from 'rxjs';
+
+import { AppFacade } from 'ish-core/facades/app.facade';
+import { HttpStatusCodeService } from 'ish-core/utils/http-status-code/http-status-code.service';
+
+/**
+ * Routes only to the page if the configured server setting at the route is enabled
+ */
+export function serverSettingGuard(route: ActivatedRouteSnapshot) {
+ const appFacade = inject(AppFacade);
+ const router = inject(Router);
+ const httpStatusCodeService = inject(HttpStatusCodeService);
+
+ return appFacade.serverSetting$(route.data.serverSetting).pipe(
+ map(enabled => {
+ if (!enabled) {
+ httpStatusCodeService.setStatus(404, false);
+ return router.createUrlTree(['/error'], {
+ queryParams: {
+ error: 'server-setting-deactivated',
+ value: route.data.serverSetting,
+ },
+ });
+ }
+ return true;
+ })
+ );
+}
From 74b600e43f95b1086addc7bc41bae6dc8fd43de7 Mon Sep 17 00:00:00 2001
From: Stefan Hauke
Date: Thu, 26 Sep 2024 20:23:17 +0200
Subject: [PATCH 5/8] feat: order recurrence (subscription support)
* create recurring order during checkout
* handle recurring orders in requisition/approval listings
* recurring orders listing in my account
* recurring order details
Co-authored-by: Susanne Schneider
---
...heckout-receipt-requisition.component.html | 2 +-
.../requisitions-list.component.html | 14 +-
.../requisition/requisition.interface.ts | 1 +
.../requisition/requisition.mapper.spec.ts | 2 +
.../models/requisition/requisition.mapper.ts | 3 +-
.../models/requisition/requisition.model.ts | 1 +
.../requisition-buyer-approval.component.html | 8 +-
...sition-cost-center-approval.component.html | 12 +-
.../requisition-summary.component.html | 18 +-
src/app/core/facades/account.facade.ts | 32 ++-
src/app/core/facades/checkout.facade.ts | 25 +-
src/app/core/icon.module.ts | 2 +
.../core/models/basket/basket.interface.ts | 2 +
src/app/core/models/basket/basket.mapper.ts | 1 +
src/app/core/models/basket/basket.model.ts | 2 +
.../breadcrumb-item.interface.ts | 1 +
src/app/core/models/order/order.interface.ts | 3 +-
src/app/core/models/order/order.mapper.ts | 1 +
src/app/core/models/order/order.model.ts | 3 +-
src/app/core/models/price/price.interface.ts | 5 +
.../models/recurrence/recurrence.model.ts | 6 +
.../recurring-order.interface.ts | 69 +++++
.../recurring-order.mapper.spec.ts | 31 +++
.../recurring-order/recurring-order.mapper.ts | 126 +++++++++
.../recurring-order/recurring-order.model.ts | 31 +++
src/app/core/pipes.module.ts | 2 +
src/app/core/pipes/frequency.pipe.ts | 35 +++
.../core/services/basket/basket.service.ts | 4 +-
.../recurring-orders.service.spec.ts | 28 ++
.../recurring-orders.service.ts | 104 +++++++
.../store/customer/basket/basket.actions.ts | 7 +
.../store/customer/basket/basket.effects.ts | 12 +
.../store/customer/customer-store.module.ts | 4 +
.../store/customer/customer-store.spec.ts | 2 +
src/app/core/store/customer/customer-store.ts | 2 +
.../store/customer/orders/orders.actions.ts | 5 +-
.../customer/orders/orders.effects.spec.ts | 8 +-
.../store/customer/orders/orders.effects.ts | 13 +-
.../store/customer/orders/orders.reducer.ts | 10 +-
.../store/customer/recurring-orders/index.ts | 3 +
.../recurring-orders.actions.ts | 28 ++
.../recurring-orders.effects.spec.ts | 50 ++++
.../recurring-orders.effects.ts | 134 ++++++++++
.../recurring-orders.reducer.ts | 59 ++++
.../recurring-orders.selectors.spec.ts | 41 +++
.../recurring-orders.selectors.ts | 32 +++
.../account-order.component.html | 7 +
...ccount-recurring-order-page.component.html | 200 ++++++++++++++
...unt-recurring-order-page.component.spec.ts | 64 +++++
.../account-recurring-order-page.component.ts | 51 ++++
.../account-recurring-order-page.module.ts | 15 ++
...count-recurring-orders-page.component.html | 28 ++
...nt-recurring-orders-page.component.spec.ts | 55 ++++
...account-recurring-orders-page.component.ts | 55 ++++
.../account-recurring-orders-page.module.ts | 26 ++
.../recurring-order-list.component.html | 158 +++++++++++
.../recurring-order-list.component.spec.ts | 37 +++
.../recurring-order-list.component.ts | 47 ++++
.../account-navigation.items.b2c.ts | 6 +
.../account-navigation.items.ts | 6 +
src/app/pages/account/account-page.module.ts | 13 +
...asket-order-recurrence-edit.component.html | 35 +++
...asket-order-recurrence-edit.component.scss | 12 +
...et-order-recurrence-edit.component.spec.ts | 33 +++
.../basket-order-recurrence-edit.component.ts | 253 ++++++++++++++++++
src/app/pages/basket/basket-page.module.ts | 2 +
.../shopping-basket-payment.component.html | 35 +--
.../shopping-basket.component.html | 5 +
.../shopping-basket.component.spec.ts | 4 +-
.../checkout-address.component.html | 2 +
.../checkout-address.component.spec.ts | 2 +
.../checkout-payment.component.html | 3 +
.../checkout-payment.component.spec.ts | 2 +
.../checkout-receipt-order.component.html | 22 +-
.../checkout-receipt-order.component.ts | 3 +-
.../checkout-receipt-page.component.ts | 5 +-
.../checkout-receipt.component.html | 21 +-
.../checkout-receipt.component.ts | 3 +-
.../checkout-review.component.html | 40 ++-
.../checkout-shipping-page.component.html | 4 +
.../checkout-shipping-page.component.spec.ts | 2 +
.../basket-approval-info.component.html | 1 +
...asket-desired-delivery-date.component.html | 36 +--
...et-desired-delivery-date.component.spec.ts | 17 +-
.../basket-recurrence-summary.component.html | 5 +
.../basket-recurrence-summary.component.scss | 5 +
...asket-recurrence-summary.component.spec.ts | 27 ++
.../basket-recurrence-summary.component.ts | 13 +
.../breadcrumb/breadcrumb.component.html | 10 +-
.../order-list/order-list.component.html | 6 +
.../order-recurrence.component.html | 16 ++
.../order-recurrence.component.scss | 9 +
.../order-recurrence.component.spec.ts | 27 ++
.../order-recurrence.component.ts | 15 ++
src/app/shared/shared.module.ts | 4 +
src/assets/i18n/de_DE.json | 82 +++++-
src/assets/i18n/en_US.json | 84 +++++-
src/assets/i18n/fr_FR.json | 82 +++++-
src/styles/global/global.scss | 4 +
src/styles/pages/checkout/shopping-cart.scss | 5 -
100 files changed, 2576 insertions(+), 112 deletions(-)
create mode 100644 src/app/core/models/recurrence/recurrence.model.ts
create mode 100644 src/app/core/models/recurring-order/recurring-order.interface.ts
create mode 100644 src/app/core/models/recurring-order/recurring-order.mapper.spec.ts
create mode 100644 src/app/core/models/recurring-order/recurring-order.mapper.ts
create mode 100644 src/app/core/models/recurring-order/recurring-order.model.ts
create mode 100644 src/app/core/pipes/frequency.pipe.ts
create mode 100644 src/app/core/services/recurring-orders/recurring-orders.service.spec.ts
create mode 100644 src/app/core/services/recurring-orders/recurring-orders.service.ts
create mode 100644 src/app/core/store/customer/recurring-orders/index.ts
create mode 100644 src/app/core/store/customer/recurring-orders/recurring-orders.actions.ts
create mode 100644 src/app/core/store/customer/recurring-orders/recurring-orders.effects.spec.ts
create mode 100644 src/app/core/store/customer/recurring-orders/recurring-orders.effects.ts
create mode 100644 src/app/core/store/customer/recurring-orders/recurring-orders.reducer.ts
create mode 100644 src/app/core/store/customer/recurring-orders/recurring-orders.selectors.spec.ts
create mode 100644 src/app/core/store/customer/recurring-orders/recurring-orders.selectors.ts
create mode 100644 src/app/pages/account-recurring-order/account-recurring-order-page.component.html
create mode 100644 src/app/pages/account-recurring-order/account-recurring-order-page.component.spec.ts
create mode 100644 src/app/pages/account-recurring-order/account-recurring-order-page.component.ts
create mode 100644 src/app/pages/account-recurring-order/account-recurring-order-page.module.ts
create mode 100644 src/app/pages/account-recurring-orders/account-recurring-orders-page.component.html
create mode 100644 src/app/pages/account-recurring-orders/account-recurring-orders-page.component.spec.ts
create mode 100644 src/app/pages/account-recurring-orders/account-recurring-orders-page.component.ts
create mode 100644 src/app/pages/account-recurring-orders/account-recurring-orders-page.module.ts
create mode 100644 src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.html
create mode 100644 src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.spec.ts
create mode 100644 src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.ts
create mode 100644 src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.html
create mode 100644 src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.scss
create mode 100644 src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.spec.ts
create mode 100644 src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.ts
create mode 100644 src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.html
create mode 100644 src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.scss
create mode 100644 src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.spec.ts
create mode 100644 src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.ts
create mode 100644 src/app/shared/components/order/order-recurrence/order-recurrence.component.html
create mode 100644 src/app/shared/components/order/order-recurrence/order-recurrence.component.scss
create mode 100644 src/app/shared/components/order/order-recurrence/order-recurrence.component.spec.ts
create mode 100644 src/app/shared/components/order/order-recurrence/order-recurrence.component.ts
diff --git a/projects/requisition-management/src/app/components/checkout-receipt-requisition/checkout-receipt-requisition.component.html b/projects/requisition-management/src/app/components/checkout-receipt-requisition/checkout-receipt-requisition.component.html
index 5d7d9c382b..68236bebdb 100644
--- a/projects/requisition-management/src/app/components/checkout-receipt-requisition/checkout-receipt-requisition.component.html
+++ b/projects/requisition-management/src/app/components/checkout-receipt-requisition/checkout-receipt-requisition.component.html
@@ -10,7 +10,7 @@
{{ req.requisitionNo }} {{ req.requisitionNo || req.recurringOrderDocumentNo }}
diff --git a/projects/requisition-management/src/app/components/requisitions-list/requisitions-list.component.html b/projects/requisition-management/src/app/components/requisitions-list/requisitions-list.component.html
index 77aa060eac..199c965f8a 100644
--- a/projects/requisition-management/src/app/components/requisitions-list/requisitions-list.component.html
+++ b/projects/requisition-management/src/app/components/requisitions-list/requisitions-list.component.html
@@ -19,8 +19,14 @@
[routerLink]="[requisition.id, { status: status }]"
[attr.aria-label]="'account.approvallist.table.id_of_order.aria_label' | translate"
>
- {{ requisition.requisitionNo }}
+ {{ requisition.requisitionNo || requisition.recurringOrderDocumentNo }}
+
@@ -34,7 +40,9 @@
*cdkCellDef="let requisition"
[attr.data-label]="'account.approvallist.table.no_of_order' | translate"
>
- {{ requisition.orderNo }}
+
+ {{ requisition.orderNo || requisition.recurringOrderDocumentNo }}
+
@@ -48,7 +56,7 @@
*cdkCellDef="let requisition"
[attr.data-label]="'account.approvallist.table.no_of_order' | translate"
>
- {{ requisition.orderNo }}
+ {{ requisition.orderNo || requisition.recurringOrderDocumentNo }}
diff --git a/projects/requisition-management/src/app/models/requisition/requisition.interface.ts b/projects/requisition-management/src/app/models/requisition/requisition.interface.ts
index 938471e8b5..015dc07a2a 100644
--- a/projects/requisition-management/src/app/models/requisition/requisition.interface.ts
+++ b/projects/requisition-management/src/app/models/requisition/requisition.interface.ts
@@ -9,6 +9,7 @@ import { RequisitionApproval, RequisitionUserBudget } from './requisition.model'
export interface RequisitionBaseData extends BasketBaseData {
requisitionNo: string;
orderNo?: string;
+ recurringOrderDocumentNo?: string;
order?: {
itemId: string;
};
diff --git a/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts b/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts
index a30c4893bc..063ac4d79c 100644
--- a/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts
+++ b/projects/requisition-management/src/app/models/requisition/requisition.mapper.spec.ts
@@ -111,6 +111,8 @@ describe('Requisition Mapper', () => {
"payment": undefined,
"promotionCodes": undefined,
"purchaseCurrency": "USD",
+ "recurrence": undefined,
+ "recurringOrderDocumentNo": undefined,
"requisitionNo": "0001",
"systemRejectErrors": [
"some message",
diff --git a/projects/requisition-management/src/app/models/requisition/requisition.mapper.ts b/projects/requisition-management/src/app/models/requisition/requisition.mapper.ts
index bae189a477..7163693e1f 100644
--- a/projects/requisition-management/src/app/models/requisition/requisition.mapper.ts
+++ b/projects/requisition-management/src/app/models/requisition/requisition.mapper.ts
@@ -24,6 +24,7 @@ export class RequisitionMapper {
id: data.id,
requisitionNo: data.requisitionNo,
orderNo: data.orderNo,
+ recurringOrderDocumentNo: data.recurringOrderDocumentNo,
creationDate: data.creationDate,
userBudget: this.fromUserBudgets(data.userBudgets, data.purchaseCurrency),
lineItemCount: data.lineItemCount,
@@ -64,7 +65,7 @@ export class RequisitionMapper {
return (
payload.data
/* filter requisitions that didn't need an approval */
- .filter(data => data.requisitionNo)
+ .filter(data => data.requisitionNo || data.recurringOrderDocumentNo)
.map(data => ({
...this.fromData({ ...payload, data }),
totals: {
diff --git a/projects/requisition-management/src/app/models/requisition/requisition.model.ts b/projects/requisition-management/src/app/models/requisition/requisition.model.ts
index 8cbf85b392..04b50b4b24 100644
--- a/projects/requisition-management/src/app/models/requisition/requisition.model.ts
+++ b/projects/requisition-management/src/app/models/requisition/requisition.model.ts
@@ -39,6 +39,7 @@ type RequisitionBasket = Omit, 'approval'>;
export interface Requisition extends RequisitionBasket {
requisitionNo: string;
orderNo?: string;
+ recurringOrderDocumentNo?: string;
creationDate: number;
lineItemCount: number;
diff --git a/projects/requisition-management/src/app/pages/requisition-detail/requisition-buyer-approval/requisition-buyer-approval.component.html b/projects/requisition-management/src/app/pages/requisition-detail/requisition-buyer-approval/requisition-buyer-approval.component.html
index bc7a32622c..7a27dacd04 100644
--- a/projects/requisition-management/src/app/pages/requisition-detail/requisition-buyer-approval/requisition-buyer-approval.component.html
+++ b/projects/requisition-management/src/app/pages/requisition-detail/requisition-buyer-approval/requisition-buyer-approval.component.html
@@ -59,7 +59,8 @@
*ngIf="requisition.approval.statusCode !== 'APPROVED'; else leftBudgetDisplay"
class="row dl-horizontal dl-separator"
>
- {{ 'approval.detailspage.budget.including_order.label' | translate }}
+
+ {{ 'approval.detailspage.budget.including_order.label' | translate : { recurring: !requisition.requisitionNo } }}
1 }">
{{
requisition.userBudget?.spentBudgetIncludingThisRequisition || requisition.totals.total
@@ -87,5 +88,10 @@
/>
+
diff --git a/projects/requisition-management/src/app/pages/requisition-detail/requisition-cost-center-approval/requisition-cost-center-approval.component.html b/projects/requisition-management/src/app/pages/requisition-detail/requisition-cost-center-approval/requisition-cost-center-approval.component.html
index 181dd76046..7482512d5a 100644
--- a/projects/requisition-management/src/app/pages/requisition-detail/requisition-cost-center-approval/requisition-cost-center-approval.component.html
+++ b/projects/requisition-management/src/app/pages/requisition-detail/requisition-cost-center-approval/requisition-cost-center-approval.component.html
@@ -39,7 +39,8 @@
- {{ 'approval.detailspage.budget.including_order.label' | translate }}
+
+ {{ 'approval.detailspage.budget.including_order.label' | translate : { recurring: !requisition.requisitionNo } }}
1 }">
{{ ccVal.spentBudgetIncludingThisRequisition | ishPrice }} ({{
ccVal.spentPercentageIncludingThisRequisition | percent
@@ -79,7 +80,8 @@
- {{ 'approval.detailspage.budget.including_order.label' | translate }}
+
+ {{ 'approval.detailspage.budget.including_order.label' | translate : { recurring: !requisition.requisitionNo } }}
1 }">
{{ bVal.spentBudgetIncludingThisRequisition | ishPrice }} ({{
bVal.spentPercentageIncludingThisRequisition | percent
@@ -91,6 +93,12 @@
+
+
diff --git a/projects/requisition-management/src/app/pages/requisition-detail/requisition-summary/requisition-summary.component.html b/projects/requisition-management/src/app/pages/requisition-detail/requisition-summary/requisition-summary.component.html
index 9690bb667e..b199f74070 100644
--- a/projects/requisition-management/src/app/pages/requisition-detail/requisition-summary/requisition-summary.component.html
+++ b/projects/requisition-management/src/app/pages/requisition-detail/requisition-summary/requisition-summary.component.html
@@ -1,15 +1,21 @@
{{ 'approval.detailspage.order.request_id' | translate }}
- {{ requisition.requisitionNo }}
+ {{ requisition.requisitionNo || requisition.recurringOrderDocumentNo }}
- {{ 'approval.detailspage.order_reference_id.label' | translate }}
+
+ {{ 'approval.detailspage.order_reference_id.label' | translate : { recurring: !requisition.requisitionNo } }}
- {{
- requisition.orderNo
- }}
- {{ requisition.orderNo }}
+
+ {{ requisition.orderNo || requisition.recurringOrderDocumentNo }}
+
+
+ {{ requisition.orderNo || requisition.recurringOrderDocumentNo }}
+
diff --git a/src/app/core/facades/account.facade.ts b/src/app/core/facades/account.facade.ts
index 9e009c2912..5165e54754 100644
--- a/src/app/core/facades/account.facade.ts
+++ b/src/app/core/facades/account.facade.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable, Subject, of } from 'rxjs';
-import { map, switchMap, take, tap } from 'rxjs/operators';
+import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators';
import { Address } from 'ish-core/models/address/address.model';
import { Credentials } from 'ish-core/models/credentials/credentials.model';
@@ -13,6 +13,7 @@ import { PasswordReminder } from 'ish-core/models/password-reminder/password-rem
import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model';
import { User } from 'ish-core/models/user/user.model';
import { MessagesPayloadType } from 'ish-core/store/core/messages';
+import { selectQueryParam } from 'ish-core/store/core/router';
import { getServerConfigParameter } from 'ish-core/store/core/server-config';
import {
createCustomerAddress,
@@ -38,6 +39,13 @@ import {
loadMoreOrders,
loadOrders,
} from 'ish-core/store/customer/orders';
+import {
+ getRecurringOrders,
+ getRecurringOrdersError,
+ getRecurringOrdersLoading,
+ getSelectedRecurringOrder,
+ recurringOrdersActions,
+} from 'ish-core/store/customer/recurring-orders';
import {
cancelRegistration,
getSsoRegistrationCancelled,
@@ -195,6 +203,28 @@ export class AccountFacade {
ordersLoading$ = this.store.pipe(select(getOrdersLoading));
ordersError$ = this.store.pipe(select(getOrdersError));
+ // RECURRING ORDERS
+
+ recurringOrdersContext$ = this.store.pipe(select(selectQueryParam('context')), distinctUntilChanged());
+ selectedRecurringOrder$ = this.store.pipe(select(getSelectedRecurringOrder));
+ recurringOrdersLoading$ = this.store.pipe(select(getRecurringOrdersLoading));
+ recurringOrdersError$ = this.store.pipe(select(getRecurringOrdersError));
+
+ recurringOrders$() {
+ return this.recurringOrdersContext$.pipe(
+ tap(context => this.store.dispatch(recurringOrdersActions.loadRecurringOrders({ context }))),
+ switchMap(context => this.store.pipe(select(getRecurringOrders(context))))
+ );
+ }
+
+ deleteRecurringOrder(id: string): void {
+ this.store.dispatch(recurringOrdersActions.deleteRecurringOrder({ recurringOrderId: id }));
+ }
+
+ setActiveRecurringOrder(recurringOrderId: string, active: boolean) {
+ this.store.dispatch(recurringOrdersActions.updateRecurringOrder({ recurringOrderId, active }));
+ }
+
// PAYMENT
private eligiblePaymentMethods$ = this.store.pipe(select(getUserPaymentMethods));
diff --git a/src/app/core/facades/checkout.facade.ts b/src/app/core/facades/checkout.facade.ts
index 745dff3429..266d46955f 100644
--- a/src/app/core/facades/checkout.facade.ts
+++ b/src/app/core/facades/checkout.facade.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Store, createSelector, select } from '@ngrx/store';
import { formatISO } from 'date-fns';
-import { Subject, combineLatest, merge } from 'rxjs';
+import { Subject, combineLatest, iif, merge } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map, sample, switchMap, take, tap } from 'rxjs/operators';
import { Address } from 'ish-core/models/address/address.model';
@@ -10,7 +10,8 @@ import { CheckoutStepType } from 'ish-core/models/checkout/checkout-step.type';
import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-update.model';
import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model';
import { PriceType } from 'ish-core/models/price/price.model';
-import { selectRouteData } from 'ish-core/store/core/router';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
+import { selectQueryParam, selectRouteData } from 'ish-core/store/core/router';
import { getServerConfigParameter } from 'ish-core/store/core/server-config';
import {
addMessageToMerchant,
@@ -55,10 +56,12 @@ import {
updateBasketAddress,
updateBasketCostCenter,
updateBasketItem,
+ updateBasketRecurrence,
updateBasketShippingMethod,
updateConcardisCvcLastUpdated,
} from 'ish-core/store/customer/basket';
import { getOrdersError, getSelectedOrder } from 'ish-core/store/customer/orders';
+import { getRecurringOrder } from 'ish-core/store/customer/recurring-orders';
import { getLoggedInUser, getUserCostCenters, loadUserCostCenters } from 'ish-core/store/customer/user';
import { whenFalsy, whenTruthy } from 'ish-core/utils/operators';
@@ -146,6 +149,10 @@ export class CheckoutFacade {
this.store.dispatch(updateBasketCostCenter({ costCenter }));
}
+ updateBasketRecurrence(recurrence: Recurrence) {
+ this.store.dispatch(updateBasketRecurrence({ recurrence }));
+ }
+
updateBasketExternalOrderReference(externalOrderReference: string) {
this.store.dispatch(updateBasket({ update: { externalOrderReference } }));
}
@@ -167,7 +174,19 @@ export class CheckoutFacade {
private ordersError$ = this.store.pipe(select(getOrdersError));
basketOrOrdersError$ = merge(this.basketError$, this.ordersError$);
- selectedOrder$ = this.store.pipe(select(getSelectedOrder));
+
+ submittedOrder$ = this.store.pipe(
+ select(selectQueryParam('recurringOrderId')),
+ switchMap(recurringOrderId =>
+ iif(
+ () => !!recurringOrderId,
+ // fetch recurring order information if recurringOrderId is present
+ this.store.pipe(select(getRecurringOrder(recurringOrderId))),
+ // otherwise fetch the information for a standard order
+ this.store.pipe(select(getSelectedOrder))
+ )
+ )
+ );
// SHIPPING
diff --git a/src/app/core/icon.module.ts b/src/app/core/icon.module.ts
index 272f8101ac..01cc5e063f 100644
--- a/src/app/core/icon.module.ts
+++ b/src/app/core/icon.module.ts
@@ -42,6 +42,7 @@ import {
faPlus,
faPrint,
faQuestionCircle,
+ faRepeat,
faRightFromBracket,
faSearch,
faShoppingCart,
@@ -106,6 +107,7 @@ export class IconModule {
faPlus,
faPrint,
faQuestionCircle,
+ faRepeat,
faRightFromBracket,
faSearch,
faShoppingCart,
diff --git a/src/app/core/models/basket/basket.interface.ts b/src/app/core/models/basket/basket.interface.ts
index b90adbe20d..58efeea503 100644
--- a/src/app/core/models/basket/basket.interface.ts
+++ b/src/app/core/models/basket/basket.interface.ts
@@ -10,6 +10,7 @@ import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-in
import { PaymentMethodBaseData } from 'ish-core/models/payment-method/payment-method.interface';
import { PaymentData } from 'ish-core/models/payment/payment.interface';
import { PriceItemData } from 'ish-core/models/price-item/price-item.interface';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
import { ShippingMethodData } from 'ish-core/models/shipping-method/shipping-method.interface';
export interface BasketBaseData {
@@ -55,6 +56,7 @@ export interface BasketBaseData {
firstName: string;
lastName: string;
};
+ recurrence?: Recurrence;
}
export interface BasketIncludedData {
diff --git a/src/app/core/models/basket/basket.mapper.ts b/src/app/core/models/basket/basket.mapper.ts
index a044d8f264..203567db77 100644
--- a/src/app/core/models/basket/basket.mapper.ts
+++ b/src/app/core/models/basket/basket.mapper.ts
@@ -74,6 +74,7 @@ export class BasketMapper {
user: data.buyer,
externalOrderReference: data.externalOrderReference,
messageToMerchant: data.messageToMerchant,
+ recurrence: data.recurrence,
};
}
diff --git a/src/app/core/models/basket/basket.model.ts b/src/app/core/models/basket/basket.model.ts
index 550fb9c709..c0d9fc0016 100644
--- a/src/app/core/models/basket/basket.model.ts
+++ b/src/app/core/models/basket/basket.model.ts
@@ -6,6 +6,7 @@ import { BasketTotal } from 'ish-core/models/basket-total/basket-total.model';
import { BasketValidationResultType } from 'ish-core/models/basket-validation/basket-validation.model';
import { LineItem, LineItemView } from 'ish-core/models/line-item/line-item.model';
import { Payment } from 'ish-core/models/payment/payment.model';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
import { ShippingMethod } from 'ish-core/models/shipping-method/shipping-method.model';
export interface AbstractBasket {
@@ -36,6 +37,7 @@ export interface AbstractBasket {
};
externalOrderReference?: string;
messageToMerchant?: string;
+ recurrence?: Recurrence;
}
export type Basket = AbstractBasket;
diff --git a/src/app/core/models/breadcrumb-item/breadcrumb-item.interface.ts b/src/app/core/models/breadcrumb-item/breadcrumb-item.interface.ts
index a2e8cbbbde..516367feef 100644
--- a/src/app/core/models/breadcrumb-item/breadcrumb-item.interface.ts
+++ b/src/app/core/models/breadcrumb-item/breadcrumb-item.interface.ts
@@ -2,4 +2,5 @@ export interface BreadcrumbItem {
key?: string;
text?: string;
link?: string | unknown[];
+ linkParams?: { [key: string]: string };
}
diff --git a/src/app/core/models/order/order.interface.ts b/src/app/core/models/order/order.interface.ts
index 8fe539f8d2..1061f13f8c 100644
--- a/src/app/core/models/order/order.interface.ts
+++ b/src/app/core/models/order/order.interface.ts
@@ -17,7 +17,7 @@ export interface OrderBaseData extends BasketBaseData {
status: 'COMPLETED' | 'ROLLED_BACK' | 'STOPPED' | 'CONTINUE';
stopAction?: {
type: 'Redirect' | 'Workflow';
- exitReason?: 'waiting_for_pending_payments' | 'redirect_urls_required';
+ exitReason?: 'waiting_for_pending_payments' | 'redirect_urls_required' | 'recurring.order';
redirectUrl?: string;
};
};
@@ -25,6 +25,7 @@ export interface OrderBaseData extends BasketBaseData {
status: string;
basket: string;
requisitionDocumentNo?: string;
+ recurringOrderID?: string;
attributes?: Attribute[];
taxIdentificationNumber?: string;
}
diff --git a/src/app/core/models/order/order.mapper.ts b/src/app/core/models/order/order.mapper.ts
index 17e58bf052..837a78aa73 100644
--- a/src/app/core/models/order/order.mapper.ts
+++ b/src/app/core/models/order/order.mapper.ts
@@ -23,6 +23,7 @@ export class OrderMapper {
statusCode: data.statusCode,
status: data.status,
requisitionNo: data.requisitionDocumentNo,
+ recurringOrderID: data.recurringOrderID,
approval:
data.attributes &&
AttributeHelper.getAttributeValueByAttributeName(
diff --git a/src/app/core/models/order/order.model.ts b/src/app/core/models/order/order.model.ts
index f100cc1e2f..e887529314 100644
--- a/src/app/core/models/order/order.model.ts
+++ b/src/app/core/models/order/order.model.ts
@@ -16,7 +16,7 @@ export interface Order extends OrderBasket {
status: 'COMPLETED' | 'ROLLED_BACK' | 'STOPPED' | 'CONTINUE';
stopAction?: {
type: 'Redirect' | 'Workflow';
- exitReason?: 'waiting_for_pending_payments' | 'redirect_urls_required';
+ exitReason?: 'waiting_for_pending_payments' | 'redirect_urls_required' | 'recurring.order';
redirectUrl?: string;
};
};
@@ -28,4 +28,5 @@ export interface Order extends OrderBasket {
date: number;
};
requisitionNo?: string;
+ recurringOrderID?: string;
}
diff --git a/src/app/core/models/price/price.interface.ts b/src/app/core/models/price/price.interface.ts
index 8092206d7a..cee4665b18 100644
--- a/src/app/core/models/price/price.interface.ts
+++ b/src/app/core/models/price/price.interface.ts
@@ -2,3 +2,8 @@ export interface PriceData {
value: number;
currency: string;
}
+
+export interface PriceAmountData {
+ amount: number;
+ currency: string;
+}
diff --git a/src/app/core/models/recurrence/recurrence.model.ts b/src/app/core/models/recurrence/recurrence.model.ts
new file mode 100644
index 0000000000..cb4e0a0f70
--- /dev/null
+++ b/src/app/core/models/recurrence/recurrence.model.ts
@@ -0,0 +1,6 @@
+export interface Recurrence {
+ interval: string;
+ startDate: string;
+ endDate?: string;
+ repetitions?: number;
+}
diff --git a/src/app/core/models/recurring-order/recurring-order.interface.ts b/src/app/core/models/recurring-order/recurring-order.interface.ts
new file mode 100644
index 0000000000..89729cbd54
--- /dev/null
+++ b/src/app/core/models/recurring-order/recurring-order.interface.ts
@@ -0,0 +1,69 @@
+import { AddressData } from 'ish-core/models/address/address.interface';
+import { BasketApprover } from 'ish-core/models/basket-approval/basket-approval.model';
+import { BasketTotalData } from 'ish-core/models/basket-total/basket-total.interface';
+import { LineItemData } from 'ish-core/models/line-item/line-item.interface';
+import { PaymentMethodBaseData } from 'ish-core/models/payment-method/payment-method.interface';
+import { PaymentData } from 'ish-core/models/payment/payment.interface';
+import { PriceAmountData } from 'ish-core/models/price/price.interface';
+import { ShippingMethod } from 'ish-core/models/shipping-method/shipping-method.model';
+
+export interface RecurringOrderData extends Omit {
+ orderCount?: number;
+ totals: BasketTotalData;
+ // error flag if the recurring orders was set to inactive by the system
+ error?: boolean;
+ errorCode?: string;
+ statusCode?: string;
+
+ lineItems: LineItemData[];
+ shippingBuckets?: [
+ {
+ shipToAddress: string;
+ shippingMethod: string;
+ }
+ ];
+ shippingMethods?: ShippingMethod[];
+
+ addresses?: AddressData[];
+ invoiceToAddress?: string;
+
+ approvalStatuses?: { approvalDate: number; approver: BasketApprover; statusCode: string }[];
+
+ costCenterID?: string;
+ costCenterName?: string;
+
+ payments?: PaymentData[];
+ paymentMethods?: PaymentMethodBaseData[];
+ lastOrders?: [{ id: string; documentNumber: string; creationDate: string }];
+}
+
+export interface RecurringOrderListData {
+ id: string;
+ number: string;
+ active: boolean;
+ expired: boolean;
+
+ interval: string;
+ startDate: string;
+ endDate?: string;
+ repetitions?: number;
+
+ creationDate: string;
+ lastOrderDate?: string;
+ nextOrderDate?: string;
+
+ buyer: BuyerData;
+ itemCount: number;
+ totalNet: PriceAmountData;
+ totalGross: PriceAmountData;
+}
+
+interface BuyerData {
+ accountID: string;
+ companyName?: string;
+ customerNo: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ userNo: string;
+}
diff --git a/src/app/core/models/recurring-order/recurring-order.mapper.spec.ts b/src/app/core/models/recurring-order/recurring-order.mapper.spec.ts
new file mode 100644
index 0000000000..3079ace6d3
--- /dev/null
+++ b/src/app/core/models/recurring-order/recurring-order.mapper.spec.ts
@@ -0,0 +1,31 @@
+import { LineItemData } from 'ish-core/models/line-item/line-item.interface';
+
+import { RecurringOrderData } from './recurring-order.interface';
+import { RecurringOrderMapper } from './recurring-order.mapper';
+import { RecurringOrder } from './recurring-order.model';
+
+describe('Recurring Order Mapper', () => {
+ const recurringOrderData = {
+ number: '0000045',
+ id: 'TEwK8ITr4AQAAAGRGCQADlo0',
+ buyer: {
+ companyName: 'Oil Corp',
+ },
+ lineItems: [
+ {
+ calculated: false,
+ } as LineItemData,
+ ],
+ payments: [{ id: 'payment_1' }],
+ paymentMethods: [{ id: 'payment_method_1' }],
+ } as RecurringOrderData;
+
+ describe('fromData', () => {
+ let recurringOrder: RecurringOrder;
+
+ it(`should return RecurringOrder when getting RecurringOrderData`, () => {
+ recurringOrder = RecurringOrderMapper.fromData(recurringOrderData);
+ expect(recurringOrder).toBeTruthy();
+ });
+ });
+});
diff --git a/src/app/core/models/recurring-order/recurring-order.mapper.ts b/src/app/core/models/recurring-order/recurring-order.mapper.ts
new file mode 100644
index 0000000000..cbfe627056
--- /dev/null
+++ b/src/app/core/models/recurring-order/recurring-order.mapper.ts
@@ -0,0 +1,126 @@
+import { Injectable } from '@angular/core';
+
+import { BasketBaseData } from 'ish-core/models/basket/basket.interface';
+import { BasketMapper } from 'ish-core/models/basket/basket.mapper';
+import { LineItemMapper } from 'ish-core/models/line-item/line-item.mapper';
+import { PaymentMapper } from 'ish-core/models/payment/payment.mapper';
+
+import { RecurringOrderData, RecurringOrderListData } from './recurring-order.interface';
+import { RecurringOrder } from './recurring-order.model';
+
+@Injectable({ providedIn: 'root' })
+export class RecurringOrderMapper {
+ static fromListData(recurringOrderData: RecurringOrderListData[]): RecurringOrder[] {
+ if (!recurringOrderData.length) {
+ return [];
+ }
+ return recurringOrderData.map(data => ({
+ id: data.id,
+ documentNo: data.number,
+ active: data.active,
+ expired: data.expired,
+
+ recurrence: {
+ interval: data.interval,
+ startDate: data.startDate,
+ endDate: data.endDate,
+ repetitions: data.repetitions,
+ },
+
+ creationDate: data.creationDate,
+ lastOrderDate: data.lastOrderDate,
+ nextOrderDate: data.nextOrderDate,
+
+ customerNo: data.buyer.customerNo,
+ email: data.buyer.email,
+ user: {
+ email: data.buyer.email,
+ firstName: data.buyer.firstName,
+ lastName: data.buyer.lastName,
+ companyName: data.buyer.companyName,
+ },
+
+ totals: {
+ total: {
+ type: 'PriceItem',
+ gross: data.totalGross.amount,
+ net: data.totalNet.amount,
+ currency: data.totalGross.currency,
+ },
+ // required properties for 'totals'
+ itemTotal: undefined,
+ isEstimated: false,
+ },
+ }));
+ }
+
+ static fromData(data: RecurringOrderData): RecurringOrder {
+ if (!data) {
+ return;
+ }
+
+ return {
+ id: data.id,
+ documentNo: data.number,
+ active: data.active,
+ expired: data.expired,
+ error: data.error,
+ errorCode: data.errorCode,
+ statusCode: data.statusCode,
+
+ recurrence: {
+ interval: data.interval,
+ startDate: data.startDate,
+ endDate: data.endDate,
+ repetitions: data.repetitions,
+ },
+
+ creationDate: data.creationDate,
+ lastOrderDate: data.lastOrderDate,
+ nextOrderDate: data.nextOrderDate,
+ orderCount: data.orderCount,
+ costCenterId: data.costCenterID,
+ costCenterName: data.costCenterName,
+
+ customerNo: data.buyer.customerNo,
+ email: data.buyer.email,
+ user: {
+ email: data.buyer.email,
+ firstName: data.buyer.firstName,
+ lastName: data.buyer.lastName,
+ companyName: data.buyer.companyName,
+ },
+
+ invoiceToAddress: data.addresses?.find(address => address.urn === data.invoiceToAddress),
+ commonShipToAddress: data.addresses?.find(address => address.urn === data.shippingBuckets?.[0]?.shipToAddress),
+ commonShippingMethod: data.shippingMethods?.find(
+ methods => methods.id === data.shippingBuckets?.[0]?.shippingMethod
+ ),
+
+ payment: PaymentMapper.fromIncludeData(data.payments[0], data.paymentMethods[0], undefined),
+
+ approvalStatuses: data.approvalStatuses
+ ?.map(status => ({
+ approvalDate: status.approvalDate,
+ approver: status.approver,
+ statusCode: status.statusCode,
+ }))
+ .filter(
+ (value, index, self) =>
+ index ===
+ self.findIndex(
+ t =>
+ t.approvalDate === value.approvalDate &&
+ t.approver.firstName === value.approver.firstName &&
+ t.approver.lastName === value.approver.lastName
+ )
+ ),
+
+ lastPlacedOrders: data.lastOrders,
+
+ lineItems: data.lineItems.map(lineItem => LineItemMapper.fromData(lineItem)),
+
+ totals: BasketMapper.getTotals(data as unknown as BasketBaseData),
+ };
+ }
+}
diff --git a/src/app/core/models/recurring-order/recurring-order.model.ts b/src/app/core/models/recurring-order/recurring-order.model.ts
new file mode 100644
index 0000000000..478abee449
--- /dev/null
+++ b/src/app/core/models/recurring-order/recurring-order.model.ts
@@ -0,0 +1,31 @@
+import { BasketApprover } from 'ish-core/models/basket-approval/basket-approval.model';
+import { AbstractBasket } from 'ish-core/models/basket/basket.model';
+import { LineItem } from 'ish-core/models/line-item/line-item.model';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
+import { User } from 'ish-core/models/user/user.model';
+
+type OrderBasket = Omit, 'approval'>;
+
+export interface RecurringOrder extends OrderBasket {
+ documentNo: string;
+ active: boolean;
+ expired: boolean;
+ // error flag if the recurring orders was set to inactive by the system
+ error?: boolean;
+ errorCode?: string;
+ statusCode?: string;
+
+ recurrence: Recurrence;
+
+ creationDate: string;
+ lastOrderDate?: string;
+ nextOrderDate?: string;
+ orderCount?: number;
+ costCenterId?: string;
+ costCenterName?: string;
+ approvalStatuses?: { approvalDate: number; approver: BasketApprover; statusCode: string }[];
+
+ lastPlacedOrders?: { id: string; documentNumber: string; creationDate: string }[];
+
+ user: User & { companyName: string };
+}
diff --git a/src/app/core/pipes.module.ts b/src/app/core/pipes.module.ts
index ed532a3500..7b598f3b79 100644
--- a/src/app/core/pipes.module.ts
+++ b/src/app/core/pipes.module.ts
@@ -4,6 +4,7 @@ import { AttributeToStringPipe } from './models/attribute/attribute.pipe';
import { PricePipe } from './models/price/price.pipe';
import { DatePipe } from './pipes/date.pipe';
import { FeatureTogglePipe } from './pipes/feature-toggle.pipe';
+import { FrequencyPipe } from './pipes/frequency.pipe';
import { HighlightPipe } from './pipes/highlight.pipe';
import { HtmlEncodePipe } from './pipes/html-encode.pipe';
import { MakeHrefPipe } from './pipes/make-href.pipe';
@@ -20,6 +21,7 @@ const pipes = [
ContentPageRoutePipe,
DatePipe,
FeatureTogglePipe,
+ FrequencyPipe,
HighlightPipe,
HtmlEncodePipe,
MakeHrefPipe,
diff --git a/src/app/core/pipes/frequency.pipe.ts b/src/app/core/pipes/frequency.pipe.ts
new file mode 100644
index 0000000000..4453273417
--- /dev/null
+++ b/src/app/core/pipes/frequency.pipe.ts
@@ -0,0 +1,35 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+
+/**
+ * The frequency pipe converts a string of special interval format to the corresponding display string
+ * example: interval 'P21D' returns '3 Week(s)'
+ */
+@Pipe({ name: 'ishFrequency' })
+export class FrequencyPipe implements PipeTransform {
+ constructor(private translate: TranslateService) {}
+
+ transform(interval: string): string {
+ let period = interval.slice(-1).toUpperCase();
+ let duration = parseInt(interval.slice(1, -1), 10);
+
+ // convert days to weeks if possible since the API only returns daily, monthly or yearly intervals
+ if (period === 'D' && duration % 7 === 0) {
+ period = 'W';
+ duration = duration / 7;
+ }
+
+ switch (period) {
+ case 'D':
+ return this.translate.instant('order.recurrence.period.days', { 0: duration });
+ case 'W':
+ return this.translate.instant('order.recurrence.period.weeks', { 0: duration });
+ case 'M':
+ return this.translate.instant('order.recurrence.period.months', { 0: duration });
+ case 'Y':
+ return this.translate.instant('order.recurrence.period.years', { 0: duration });
+ default:
+ return interval;
+ }
+ }
+}
diff --git a/src/app/core/services/basket/basket.service.ts b/src/app/core/services/basket/basket.service.ts
index 3968a11220..a450bc89a7 100644
--- a/src/app/core/services/basket/basket.service.ts
+++ b/src/app/core/services/basket/basket.service.ts
@@ -17,6 +17,7 @@ import { BasketValidation, BasketValidationScopeType } from 'ish-core/models/bas
import { BasketBaseData, BasketData } from 'ish-core/models/basket/basket.interface';
import { BasketMapper } from 'ish-core/models/basket/basket.mapper';
import { Basket } from 'ish-core/models/basket/basket.model';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
import { ShippingMethodData } from 'ish-core/models/shipping-method/shipping-method.interface';
import { ShippingMethodMapper } from 'ish-core/models/shipping-method/shipping-method.mapper';
import { ShippingMethod } from 'ish-core/models/shipping-method/shipping-method.model';
@@ -30,7 +31,8 @@ export type BasketUpdateType =
| { costCenter: string }
| { externalOrderReference: string }
| { invoiceToAddress: string }
- | { messageToMerchant: string };
+ | { messageToMerchant: string }
+ | { recurrence: Recurrence };
/**
* The Basket Service handles the interaction with the 'baskets' REST API.
diff --git a/src/app/core/services/recurring-orders/recurring-orders.service.spec.ts b/src/app/core/services/recurring-orders/recurring-orders.service.spec.ts
new file mode 100644
index 0000000000..a00c675d57
--- /dev/null
+++ b/src/app/core/services/recurring-orders/recurring-orders.service.spec.ts
@@ -0,0 +1,28 @@
+import { TestBed } from '@angular/core/testing';
+import { provideMockStore } from '@ngrx/store/testing';
+import { instance, mock } from 'ts-mockito';
+
+import { ApiService } from 'ish-core/services/api/api.service';
+import { getLoggedInCustomer } from 'ish-core/store/customer/user';
+
+import { RecurringOrdersService } from './recurring-orders.service';
+
+describe('Recurring Orders Service', () => {
+ let apiServiceMock: ApiService;
+ let recurringOrdersService: RecurringOrdersService;
+
+ beforeEach(() => {
+ apiServiceMock = mock(ApiService);
+ TestBed.configureTestingModule({
+ providers: [
+ { provide: ApiService, useFactory: () => instance(apiServiceMock) },
+ provideMockStore({ selectors: [{ selector: getLoggedInCustomer, value: undefined }] }),
+ ],
+ });
+ recurringOrdersService = TestBed.inject(RecurringOrdersService);
+ });
+
+ it('should be created', () => {
+ expect(recurringOrdersService).toBeTruthy();
+ });
+});
diff --git a/src/app/core/services/recurring-orders/recurring-orders.service.ts b/src/app/core/services/recurring-orders/recurring-orders.service.ts
new file mode 100644
index 0000000000..a4062f52cc
--- /dev/null
+++ b/src/app/core/services/recurring-orders/recurring-orders.service.ts
@@ -0,0 +1,104 @@
+import { HttpHeaders } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Store, select } from '@ngrx/store';
+import { combineLatest, filter, map, switchMap, take, throwError } from 'rxjs';
+
+import { RecurringOrderData, RecurringOrderListData } from 'ish-core/models/recurring-order/recurring-order.interface';
+import { RecurringOrderMapper } from 'ish-core/models/recurring-order/recurring-order.mapper';
+import { ApiService, unpackEnvelope } from 'ish-core/services/api/api.service';
+import { getLoggedInCustomer, getLoggedInUser } from 'ish-core/store/customer/user';
+
+@Injectable({ providedIn: 'root' })
+export class RecurringOrdersService {
+ constructor(private apiService: ApiService, private store: Store) {}
+
+ private recurringOrderHeaderV2 = new HttpHeaders({ Accept: 'application/vnd.intershop.recurringorder.v2+json' });
+
+ private currentCustomerAndUser$ = combineLatest([
+ this.store.pipe(select(getLoggedInCustomer)),
+ this.store.pipe(select(getLoggedInUser)),
+ ]).pipe(
+ filter(([customer, user]) => !!customer && !!user),
+ take(1)
+ );
+
+ private getRecurringOrdersEndpoint(context?: string) {
+ return this.currentCustomerAndUser$.pipe(
+ map(([customer, user]) => {
+ let apiEndpoint: string = undefined;
+ if (customer.isBusinessCustomer) {
+ if (context === 'ADMIN') {
+ apiEndpoint = `customers/${this.apiService.encodeResourceId(customer.customerNo)}/recurringorders`;
+ // admin users could request user specific recurring orders - not used in UI yet
+ // } else if (context) {
+ // apiEndpoint = `customers/${this.apiService.encodeResourceId(
+ // customer.customerNo
+ // )}/users/${this.apiService.encodeResourceId(context)}/recurringorders`;
+ } else {
+ apiEndpoint = `customers/${this.apiService.encodeResourceId(
+ customer.customerNo
+ )}/users/${this.apiService.encodeResourceId(user.login)}/recurringorders`;
+ }
+ } else {
+ apiEndpoint = `privatecustomers/${this.apiService.encodeResourceId(customer.customerNo)}/recurringorders`;
+ }
+ return apiEndpoint;
+ })
+ );
+ }
+
+ getRecurringOrders(context?: string) {
+ return this.getRecurringOrdersEndpoint(context).pipe(
+ switchMap(apiEndpoint =>
+ this.apiService.get(apiEndpoint, { headers: this.recurringOrderHeaderV2 }).pipe(
+ unpackEnvelope('data'),
+ map(data => RecurringOrderMapper.fromListData(data))
+ )
+ )
+ );
+ }
+
+ getRecurringOrder(recurringOrderId: string, context?: string) {
+ return this.getRecurringOrdersEndpoint(context).pipe(
+ switchMap(apiEndpoint =>
+ this.apiService
+ .get<{ data: RecurringOrderData }>(`${apiEndpoint}/${this.apiService.encodeResourceId(recurringOrderId)}`, {
+ headers: this.recurringOrderHeaderV2,
+ })
+ .pipe(map(data => RecurringOrderMapper.fromData(data.data)))
+ )
+ );
+ }
+
+ updateRecurringOrder(recurringOrderId: string, active: boolean, context?: string) {
+ if (!recurringOrderId) {
+ return throwError(() => new Error('updateRecurringOrder() called without recurringOrderId'));
+ }
+
+ return this.getRecurringOrdersEndpoint(context).pipe(
+ switchMap(apiEndpoint =>
+ this.apiService
+ .patch<{ data: RecurringOrderData }>(
+ `${apiEndpoint}/${this.apiService.encodeResourceId(recurringOrderId)}`,
+ { active },
+ { headers: this.recurringOrderHeaderV2 }
+ )
+ .pipe(map(data => RecurringOrderMapper.fromData(data.data)))
+ )
+ );
+ }
+
+ deleteRecurringOrder(recurringOrderId: string, context?: string) {
+ if (!recurringOrderId) {
+ return throwError(() => new Error('deleteRecurringOrder() called without recurringOrderId'));
+ }
+
+ return this.getRecurringOrdersEndpoint(context).pipe(
+ switchMap(apiEndpoint =>
+ this.apiService.delete(`${apiEndpoint}/${this.apiService.encodeResourceId(recurringOrderId)}`, {
+ headers: this.recurringOrderHeaderV2,
+ })
+ )
+ );
+ }
+}
diff --git a/src/app/core/store/customer/basket/basket.actions.ts b/src/app/core/store/customer/basket/basket.actions.ts
index 661fcb2100..7c0a8cd1ce 100644
--- a/src/app/core/store/customer/basket/basket.actions.ts
+++ b/src/app/core/store/customer/basket/basket.actions.ts
@@ -12,6 +12,7 @@ import { LineItemUpdate } from 'ish-core/models/line-item-update/line-item-updat
import { AddLineItemType, LineItem } from 'ish-core/models/line-item/line-item.model';
import { PaymentInstrument } from 'ish-core/models/payment-instrument/payment-instrument.model';
import { PaymentMethod } from 'ish-core/models/payment-method/payment-method.model';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
import { ShippingMethod } from 'ish-core/models/shipping-method/shipping-method.model';
import { BasketUpdateType } from 'ish-core/services/basket/basket.service';
import { httpError, payload } from 'ish-core/utils/ngrx-creators';
@@ -68,6 +69,12 @@ export const updateBasketCostCenter = createAction(
'[Basket] Assign a Cost Center at Basket ',
payload<{ costCenter: string }>()
);
+
+export const updateBasketRecurrence = createAction(
+ '[Basket] Set the Recurrence Information at Basket ',
+ payload<{ recurrence: Recurrence }>()
+);
+
export const addMessageToMerchant = createAction(
'[Basket] Message to Merchant',
payload<{ messageToMerchant: string }>()
diff --git a/src/app/core/store/customer/basket/basket.effects.ts b/src/app/core/store/customer/basket/basket.effects.ts
index 30c59b4bfe..c6e416cee3 100644
--- a/src/app/core/store/customer/basket/basket.effects.ts
+++ b/src/app/core/store/customer/basket/basket.effects.ts
@@ -63,6 +63,7 @@ import {
updateBasket,
updateBasketCostCenter,
updateBasketFail,
+ updateBasketRecurrence,
updateBasketShippingMethod,
} from './basket.actions';
import { getCurrentBasket, getCurrentBasketId } from './basket.selectors';
@@ -211,6 +212,17 @@ export class BasketEffects {
)
);
+ /**
+ * Sets the recurrence at the current basket.
+ */
+ updateBasketRecurrence$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(updateBasketRecurrence),
+ mapToPayloadProperty('recurrence'),
+ map(recurrence => updateBasket({ update: { recurrence } }))
+ )
+ );
+
/**
* Sets a message to merchant at the current basket.
*/
diff --git a/src/app/core/store/customer/customer-store.module.ts b/src/app/core/store/customer/customer-store.module.ts
index f46ac2df8c..ba513a4e63 100644
--- a/src/app/core/store/customer/customer-store.module.ts
+++ b/src/app/core/store/customer/customer-store.module.ts
@@ -22,6 +22,8 @@ import { dataRequestsReducer } from './data-requests/data-requests.reducer';
import { OrdersEffects } from './orders/orders.effects';
import { ordersReducer } from './orders/orders.reducer';
import { OrganizationManagementEffects } from './organization-management/organization-management.effects';
+import { RecurringOrdersEffects } from './recurring-orders/recurring-orders.effects';
+import { recurringOrdersReducer } from './recurring-orders/recurring-orders.reducer';
import { RequisitionManagementEffects } from './requisition-management/requisition-management.effects';
import { SsoRegistrationEffects } from './sso-registration/sso-registration.effects';
import { ssoRegistrationReducer } from './sso-registration/sso-registration.reducer';
@@ -33,6 +35,7 @@ const customerReducers: ActionReducerMap = {
user: userReducer,
addresses: addressesReducer,
orders: ordersReducer,
+ recurringOrders: recurringOrdersReducer,
basket: basketReducer,
authorization: authorizationReducer,
ssoRegistration: ssoRegistrationReducer,
@@ -48,6 +51,7 @@ const customerEffects = [
BasketPromotionCodeEffects,
BasketValidationEffects,
OrdersEffects,
+ RecurringOrdersEffects,
UserEffects,
AuthorizationEffects,
OrganizationManagementEffects,
diff --git a/src/app/core/store/customer/customer-store.spec.ts b/src/app/core/store/customer/customer-store.spec.ts
index faa98e54fd..2d1d64c411 100644
--- a/src/app/core/store/customer/customer-store.spec.ts
+++ b/src/app/core/store/customer/customer-store.spec.ts
@@ -27,6 +27,7 @@ import { PaymentService } from 'ish-core/services/payment/payment.service';
import { PricesService } from 'ish-core/services/prices/prices.service';
import { ProductsService } from 'ish-core/services/products/products.service';
import { PromotionsService } from 'ish-core/services/promotions/promotions.service';
+import { RecurringOrdersService } from 'ish-core/services/recurring-orders/recurring-orders.service';
import { SuggestService } from 'ish-core/services/suggest/suggest.service';
import { TokenService } from 'ish-core/services/token/token.service';
import { UserService } from 'ish-core/services/user/user.service';
@@ -186,6 +187,7 @@ describe('Customer Store', () => {
{ provide: PricesService, useFactory: () => instance(productPriceServiceMock) },
{ provide: ProductsService, useFactory: () => instance(productsServiceMock) },
{ provide: PromotionsService, useFactory: () => instance(promotionsServiceMock) },
+ { provide: RecurringOrdersService, useFactory: () => instance(mock(RecurringOrdersService)) },
{ provide: SuggestService, useFactory: () => instance(mock(SuggestService)) },
{ provide: TokenService, useFactory: () => instance(mock(TokenService)) },
{ provide: UserService, useFactory: () => instance(userServiceMock) },
diff --git a/src/app/core/store/customer/customer-store.ts b/src/app/core/store/customer/customer-store.ts
index 84c00b8663..a0d2589a73 100644
--- a/src/app/core/store/customer/customer-store.ts
+++ b/src/app/core/store/customer/customer-store.ts
@@ -6,6 +6,7 @@ import { AddressesState } from './addresses/addresses.reducer';
import { BasketState } from './basket/basket.reducer';
import { DataRequestsState } from './data-requests/data-requests.reducer';
import { OrdersState } from './orders/orders.reducer';
+import { RecurringOrdersState } from './recurring-orders/recurring-orders.reducer';
import { SsoRegistrationState } from './sso-registration/sso-registration.reducer';
import { UserState } from './user/user.reducer';
@@ -13,6 +14,7 @@ export interface CustomerState {
user: UserState;
addresses: AddressesState;
orders: OrdersState;
+ recurringOrders: RecurringOrdersState;
basket: BasketState;
authorization: Authorization;
ssoRegistration: SsoRegistrationState;
diff --git a/src/app/core/store/customer/orders/orders.actions.ts b/src/app/core/store/customer/orders/orders.actions.ts
index 5040f57771..65d14123c1 100644
--- a/src/app/core/store/customer/orders/orders.actions.ts
+++ b/src/app/core/store/customer/orders/orders.actions.ts
@@ -9,7 +9,10 @@ export const createOrder = createAction('[Orders Internal] Create Order');
export const createOrderFail = createAction('[Orders API] Create Order Fail', httpError());
-export const createOrderSuccess = createAction('[Orders API] Create Order Success', payload<{ order: Order }>());
+export const createOrderSuccess = createAction(
+ '[Orders API] Create Order Success',
+ payload<{ order: Order; basketId: string }>()
+);
export const loadOrders = createAction('[Orders] Load Orders', payload<{ query: OrderListQuery }>());
diff --git a/src/app/core/store/customer/orders/orders.effects.spec.ts b/src/app/core/store/customer/orders/orders.effects.spec.ts
index f2cf8577e1..954ab9349c 100644
--- a/src/app/core/store/customer/orders/orders.effects.spec.ts
+++ b/src/app/core/store/customer/orders/orders.effects.spec.ts
@@ -112,7 +112,7 @@ describe('Orders Effects', () => {
const basketId = BasketMockData.getBasket().id;
const newOrder = { id: basketId } as Order;
const action = createOrder();
- const completion = createOrderSuccess({ order: newOrder });
+ const completion = createOrderSuccess({ order: newOrder, basketId: 'BID' });
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });
@@ -134,7 +134,7 @@ describe('Orders Effects', () => {
describe('continueAfterOrderCreation', () => {
it('should navigate to /checkout/receipt after CreateOrderSuccess if there is no redirect required', fakeAsync(() => {
- const action = createOrderSuccess({ order: { id: '123' } as Order });
+ const action = createOrderSuccess({ order: { id: '123' } as Order, basketId: 'BID' });
actions$ = of(action);
effects.continueAfterOrderCreation$.subscribe({ next: noop, error: fail, complete: noop });
@@ -156,6 +156,7 @@ describe('Orders Effects', () => {
id: '123',
orderCreation: { status: 'STOPPED', stopAction: { type: 'Redirect', redirectUrl: 'http://test' } },
} as Order,
+ basketId: 'BID',
});
actions$ = of(action);
@@ -175,6 +176,7 @@ describe('Orders Effects', () => {
orderCreation: { status: 'ROLLED_BACK' },
infos: [{ message: 'Info' }],
} as Order,
+ basketId: 'BID',
});
actions$ = of(action);
@@ -383,7 +385,7 @@ describe('Orders Effects', () => {
});
it('should trigger SelectOrderAfterRedirect action if checkout payment/receipt page is called with query param "redirect" and an order is available', done => {
- store.dispatch(createOrderSuccess({ order }));
+ store.dispatch(createOrderSuccess({ order, basketId: 'BID' }));
router.navigate(['checkout', 'receipt'], {
queryParams: { redirect: 'success', param1: 123, orderId: order.id },
diff --git a/src/app/core/store/customer/orders/orders.effects.ts b/src/app/core/store/customer/orders/orders.effects.ts
index ac82663175..df2b695a38 100644
--- a/src/app/core/store/customer/orders/orders.effects.ts
+++ b/src/app/core/store/customer/orders/orders.effects.ts
@@ -52,7 +52,7 @@ export class OrdersEffects {
concatLatestFrom(() => this.store.pipe(select(getCurrentBasketId))),
mergeMap(([, basketId]) =>
this.orderService.createOrder(basketId, true).pipe(
- map(order => createOrderSuccess({ order })),
+ map(order => createOrderSuccess({ order, basketId })),
mapErrorToAction(createOrderFail)
)
)
@@ -66,9 +66,9 @@ export class OrdersEffects {
() =>
this.actions$.pipe(
ofType(createOrderSuccess),
- mapToPayloadProperty('order'),
- filter(order => !order?.orderCreation || order.orderCreation.status !== 'ROLLED_BACK'),
- concatMap(order => {
+ mapToPayload(),
+ filter(({ order }) => !order?.orderCreation || order.orderCreation.status !== 'ROLLED_BACK'),
+ concatMap(({ order, basketId }) => {
if (
order.orderCreation &&
order.orderCreation.status === 'STOPPED' &&
@@ -77,6 +77,11 @@ export class OrdersEffects {
) {
location.assign(order.orderCreation.stopAction.redirectUrl);
return EMPTY;
+ } else if (
+ order.orderCreation?.status === 'STOPPED' &&
+ order.orderCreation.stopAction.exitReason === 'recurring.order'
+ ) {
+ return from(this.router.navigate(['/checkout/receipt'], { queryParams: { recurringOrderId: basketId } }));
} else {
return from(this.router.navigate(['/checkout/receipt'], { queryParams: { orderId: order.id } }));
}
diff --git a/src/app/core/store/customer/orders/orders.reducer.ts b/src/app/core/store/customer/orders/orders.reducer.ts
index 8b62c66cac..5cb4577c9c 100644
--- a/src/app/core/store/customer/orders/orders.reducer.ts
+++ b/src/app/core/store/customer/orders/orders.reducer.ts
@@ -56,10 +56,12 @@ export const ordersReducer = createReducer(
on(createOrderSuccess, loadOrderSuccess, (state, action) => {
const { order } = action.payload;
- return {
- ...orderAdapter.upsertOne(order, state),
- selected: order.id,
- };
+ return order.id
+ ? {
+ ...orderAdapter.upsertOne(order, state),
+ selected: order.id,
+ }
+ : state;
}),
on(loadOrdersSuccess, (state, action) => {
const { orders, query, allRetrieved } = action.payload;
diff --git a/src/app/core/store/customer/recurring-orders/index.ts b/src/app/core/store/customer/recurring-orders/index.ts
new file mode 100644
index 0000000000..5dc19edf96
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/index.ts
@@ -0,0 +1,3 @@
+// API to access ngrx recurringOrders state
+export * from './recurring-orders.actions';
+export * from './recurring-orders.selectors';
diff --git a/src/app/core/store/customer/recurring-orders/recurring-orders.actions.ts b/src/app/core/store/customer/recurring-orders/recurring-orders.actions.ts
new file mode 100644
index 0000000000..3485dabb6a
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/recurring-orders.actions.ts
@@ -0,0 +1,28 @@
+import { createActionGroup } from '@ngrx/store';
+
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { httpError, payload } from 'ish-core/utils/ngrx-creators';
+
+export const recurringOrdersActions = createActionGroup({
+ source: 'Recurring Orders',
+ events: {
+ 'Load Recurring Orders': payload<{ context: string }>(),
+ 'Load Recurring Order': payload<{ recurringOrderId: string }>(),
+ 'Update Recurring Order': payload<{ recurringOrderId: string; active: boolean }>(),
+ 'Delete Recurring Order': payload<{ recurringOrderId: string }>(),
+ },
+});
+
+export const recurringOrdersApiActions = createActionGroup({
+ source: 'Recurring Orders API',
+ events: {
+ 'Load Recurring Orders Success': payload<{ recurringOrders: RecurringOrder[]; context: string }>(),
+ 'Load Recurring Orders Fail': httpError<{}>(),
+ 'Load Recurring Order Success': payload<{ recurringOrder: RecurringOrder }>(),
+ 'Load Recurring Order Fail': httpError<{}>(),
+ 'Update Recurring Order Success': payload<{ recurringOrder: RecurringOrder }>(),
+ 'Update Recurring Order Fail': httpError<{}>(),
+ 'Delete Recurring Order Success': payload<{ recurringOrderId: string }>(),
+ 'Delete Recurring Order Fail': httpError<{}>(),
+ },
+});
diff --git a/src/app/core/store/customer/recurring-orders/recurring-orders.effects.spec.ts b/src/app/core/store/customer/recurring-orders/recurring-orders.effects.spec.ts
new file mode 100644
index 0000000000..c243e34061
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/recurring-orders.effects.spec.ts
@@ -0,0 +1,50 @@
+import { TestBed } from '@angular/core/testing';
+import { provideMockActions } from '@ngrx/effects/testing';
+import { Action } from '@ngrx/store';
+import { TranslateModule } from '@ngx-translate/core';
+import { Observable, of } from 'rxjs';
+import { anything, instance, mock, verify, when } from 'ts-mockito';
+
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { RecurringOrdersService } from 'ish-core/services/recurring-orders/recurring-orders.service';
+import { CoreStoreModule } from 'ish-core/store/core/core-store.module';
+
+import { recurringOrdersActions } from './recurring-orders.actions';
+import { RecurringOrdersEffects } from './recurring-orders.effects';
+
+describe('Recurring Orders Effects', () => {
+ let actions$: Observable;
+ let effects: RecurringOrdersEffects;
+ let recurringOrdersServiceMock: RecurringOrdersService;
+
+ const order = { documentNo: '0000001', id: '1', active: true, lineItems: [] } as RecurringOrder;
+ const recurringOrders = [order, { number: '0000002', id: '2', active: true, lineItems: [] }] as RecurringOrder[];
+
+ beforeEach(() => {
+ recurringOrdersServiceMock = mock(RecurringOrdersService);
+ when(recurringOrdersServiceMock.getRecurringOrders(anything())).thenReturn(of(recurringOrders));
+
+ TestBed.configureTestingModule({
+ imports: [CoreStoreModule.forTesting(), TranslateModule.forRoot()],
+ providers: [
+ { provide: RecurringOrdersService, useFactory: () => instance(recurringOrdersServiceMock) },
+ provideMockActions(() => actions$),
+ RecurringOrdersEffects,
+ ],
+ });
+
+ effects = TestBed.inject(RecurringOrdersEffects);
+ });
+
+ describe('loadRecurringOrders$', () => {
+ it('should call the RecurringOrderService for loadRecurringOrders', done => {
+ const action = recurringOrdersActions.loadRecurringOrders({ context: 'MY' });
+ actions$ = of(action);
+
+ effects.loadRecurringOrders$.subscribe(() => {
+ verify(recurringOrdersServiceMock.getRecurringOrders(anything())).once();
+ done();
+ });
+ });
+ });
+});
diff --git a/src/app/core/store/customer/recurring-orders/recurring-orders.effects.ts b/src/app/core/store/customer/recurring-orders/recurring-orders.effects.ts
new file mode 100644
index 0000000000..766fb5d0be
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/recurring-orders.effects.ts
@@ -0,0 +1,134 @@
+import { Injectable } from '@angular/core';
+import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
+import { routerNavigatedAction } from '@ngrx/router-store';
+import { Store, select } from '@ngrx/store';
+import { TranslateService } from '@ngx-translate/core';
+import { merge } from 'rxjs';
+import { concatMap, map, mergeMap, switchMap } from 'rxjs/operators';
+
+import { RecurringOrdersService } from 'ish-core/services/recurring-orders/recurring-orders.service';
+import { displaySuccessMessage } from 'ish-core/store/core/messages';
+import { ofUrl, selectQueryParam, selectRouteParam } from 'ish-core/store/core/router';
+import { setBreadcrumbData } from 'ish-core/store/core/viewconf';
+import { mapErrorToAction, mapToPayload, mapToPayloadProperty, whenTruthy } from 'ish-core/utils/operators';
+
+import { recurringOrdersActions, recurringOrdersApiActions } from './recurring-orders.actions';
+import { getRecurringOrder, getSelectedRecurringOrder } from './recurring-orders.selectors';
+
+@Injectable()
+export class RecurringOrdersEffects {
+ constructor(
+ private actions$: Actions,
+ private recurringOrdersService: RecurringOrdersService,
+ private store: Store,
+ private translateService: TranslateService
+ ) {}
+
+ loadRecurringOrders$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(recurringOrdersActions.loadRecurringOrders),
+ mapToPayloadProperty('context'),
+ concatMap(context =>
+ this.recurringOrdersService.getRecurringOrders(context).pipe(
+ map(recurringOrders => recurringOrdersApiActions.loadRecurringOrdersSuccess({ recurringOrders, context })),
+ mapErrorToAction(recurringOrdersApiActions.loadRecurringOrdersFail)
+ )
+ )
+ )
+ );
+
+ loadRecurringOrder$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(recurringOrdersActions.loadRecurringOrder),
+ mapToPayloadProperty('recurringOrderId'),
+ concatLatestFrom(() => this.store.pipe(select(selectQueryParam('context')))),
+ switchMap(([recurringOrderId, context]) =>
+ this.recurringOrdersService.getRecurringOrder(recurringOrderId, context).pipe(
+ map(recurringOrder => recurringOrdersApiActions.loadRecurringOrderSuccess({ recurringOrder })),
+ mapErrorToAction(recurringOrdersApiActions.loadRecurringOrderFail)
+ )
+ )
+ )
+ );
+
+ triggerLoadRecurringOrder$ = createEffect(() =>
+ merge(
+ this.store.pipe(select(selectRouteParam('recurringOrderId'))),
+ this.store.pipe(select(selectQueryParam('recurringOrderId')))
+ ).pipe(
+ whenTruthy(),
+ map(recurringOrderId => recurringOrdersActions.loadRecurringOrder({ recurringOrderId }))
+ )
+ );
+
+ updateRecurringOrder$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(recurringOrdersActions.updateRecurringOrder),
+ mapToPayload(),
+ whenTruthy(),
+ concatLatestFrom(payload => this.store.pipe(select(getRecurringOrder(payload.recurringOrderId)))),
+ concatLatestFrom(() => this.store.pipe(select(selectQueryParam('context')))),
+ mergeMap(([[payload, recurringOrder], context]) => {
+ if (payload.active !== recurringOrder.active) {
+ return this.recurringOrdersService
+ .updateRecurringOrder(payload.recurringOrderId, payload.active, context)
+ .pipe(
+ mergeMap(recurringOrder => [recurringOrdersApiActions.updateRecurringOrderSuccess({ recurringOrder })]),
+ mapErrorToAction(recurringOrdersApiActions.updateRecurringOrderFail)
+ );
+ } else {
+ return [recurringOrdersApiActions.updateRecurringOrderSuccess({ recurringOrder })];
+ }
+ })
+ )
+ );
+
+ deleteRecurringOrder$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(recurringOrdersActions.deleteRecurringOrder),
+ mapToPayloadProperty('recurringOrderId'),
+ concatLatestFrom(() => this.store.pipe(select(selectQueryParam('context')))),
+ mergeMap(([recurringOrderId, context]) =>
+ this.recurringOrdersService.deleteRecurringOrder(recurringOrderId, context).pipe(
+ mergeMap(() => [
+ recurringOrdersApiActions.deleteRecurringOrderSuccess({ recurringOrderId }),
+ displaySuccessMessage({
+ message: 'account.recurring_order.delete.confirmation',
+ }),
+ ]),
+ mapErrorToAction(recurringOrdersApiActions.deleteRecurringOrderFail)
+ )
+ )
+ )
+ );
+
+ setRecurringOrderBreadcrumb$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(routerNavigatedAction),
+ switchMap(() =>
+ this.store.pipe(
+ ofUrl(/^\/account\/recurring-orders\/.*/),
+ select(getSelectedRecurringOrder),
+ whenTruthy(),
+ concatLatestFrom(() => this.store.pipe(select(selectQueryParam('context')))),
+ map(([recurringOrder, context]) =>
+ setBreadcrumbData({
+ breadcrumbData: [
+ {
+ key: 'account.recurring_orders.breadcrumb',
+ link: '/account/recurring-orders',
+ linkParams: { context },
+ },
+ {
+ text: `${this.translateService.instant('account.recurring_order.details.breadcrumb')} - ${
+ recurringOrder.documentNo
+ }`,
+ },
+ ],
+ })
+ )
+ )
+ )
+ )
+ );
+}
diff --git a/src/app/core/store/customer/recurring-orders/recurring-orders.reducer.ts b/src/app/core/store/customer/recurring-orders/recurring-orders.reducer.ts
new file mode 100644
index 0000000000..0bb8ca8c16
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/recurring-orders.reducer.ts
@@ -0,0 +1,59 @@
+import { EntityState, createEntityAdapter } from '@ngrx/entity';
+import { createReducer, on } from '@ngrx/store';
+
+import { HttpError } from 'ish-core/models/http-error/http-error.model';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { setErrorOn, setLoadingOn, unsetLoadingAndErrorOn } from 'ish-core/utils/ngrx-creators';
+
+import { recurringOrdersActions, recurringOrdersApiActions } from './recurring-orders.actions';
+
+export const recurringOrdersAdapter = createEntityAdapter();
+
+export interface RecurringOrdersState extends EntityState {
+ loading: boolean;
+ error: HttpError;
+ contexts: { [key: string]: string[] };
+}
+
+export const initialState: RecurringOrdersState = recurringOrdersAdapter.getInitialState({
+ loading: false,
+ error: undefined,
+ contexts: {},
+});
+
+export const recurringOrdersReducer = createReducer(
+ initialState,
+ setLoadingOn(recurringOrdersActions.loadRecurringOrders, recurringOrdersActions.deleteRecurringOrder),
+ setErrorOn(recurringOrdersApiActions.loadRecurringOrdersFail, recurringOrdersApiActions.deleteRecurringOrderFail),
+ unsetLoadingAndErrorOn(
+ recurringOrdersApiActions.loadRecurringOrdersSuccess,
+ recurringOrdersApiActions.deleteRecurringOrderSuccess
+ ),
+ on(recurringOrdersApiActions.loadRecurringOrdersSuccess, (state, action) => {
+ const { recurringOrders } = action.payload;
+ return recurringOrdersAdapter.upsertMany(recurringOrders, {
+ ...state,
+ contexts: {
+ ...state.contexts,
+ [action.payload.context || 'MY']: recurringOrders.map(recurringOrder => recurringOrder.id),
+ },
+ });
+ }),
+ on(
+ recurringOrdersApiActions.loadRecurringOrderSuccess,
+ recurringOrdersApiActions.updateRecurringOrderSuccess,
+ (state, action) => recurringOrdersAdapter.upsertOne(action.payload.recurringOrder, state)
+ ),
+ on(recurringOrdersApiActions.deleteRecurringOrderSuccess, (state, action) => {
+ const { recurringOrderId } = action.payload;
+ return recurringOrdersAdapter.removeOne(recurringOrderId, {
+ ...state,
+ contexts: {
+ ...Object.entries(state.contexts).reduce(
+ (acc, [key, value]) => ({ ...acc, [key]: value.filter(id => id !== recurringOrderId) }),
+ {}
+ ),
+ },
+ });
+ })
+);
diff --git a/src/app/core/store/customer/recurring-orders/recurring-orders.selectors.spec.ts b/src/app/core/store/customer/recurring-orders/recurring-orders.selectors.spec.ts
new file mode 100644
index 0000000000..5f3d669c1f
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/recurring-orders.selectors.spec.ts
@@ -0,0 +1,41 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CoreStoreModule } from 'ish-core/store/core/core-store.module';
+import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module';
+import { StoreWithSnapshots, provideStoreSnapshots } from 'ish-core/utils/dev/ngrx-testing';
+
+import { recurringOrdersActions } from './recurring-orders.actions';
+import { getRecurringOrdersError, getRecurringOrdersLoading } from './recurring-orders.selectors';
+
+describe('Recurring Orders Selectors', () => {
+ let store$: StoreWithSnapshots;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [CoreStoreModule.forTesting(), CustomerStoreModule.forTesting('recurringOrders')],
+ providers: [provideStoreSnapshots()],
+ });
+
+ store$ = TestBed.inject(StoreWithSnapshots);
+ });
+
+ describe('initial state', () => {
+ it('should not be loading when in initial state', () => {
+ expect(getRecurringOrdersLoading(store$.state)).toBeFalse();
+ });
+
+ it('should not have an error when in initial state', () => {
+ expect(getRecurringOrdersError(store$.state)).toBeUndefined();
+ });
+ });
+
+ describe('loadRecurringOrders', () => {
+ beforeEach(() => {
+ store$.dispatch(recurringOrdersActions.loadRecurringOrders({ context: 'MY' }));
+ });
+
+ it('should set loading to true', () => {
+ expect(getRecurringOrdersLoading(store$.state)).toBeTrue();
+ });
+ });
+});
diff --git a/src/app/core/store/customer/recurring-orders/recurring-orders.selectors.ts b/src/app/core/store/customer/recurring-orders/recurring-orders.selectors.ts
new file mode 100644
index 0000000000..02ea024ea2
--- /dev/null
+++ b/src/app/core/store/customer/recurring-orders/recurring-orders.selectors.ts
@@ -0,0 +1,32 @@
+import { createSelector } from '@ngrx/store';
+
+import { selectRouteParam } from 'ish-core/store/core/router';
+import { getCustomerState } from 'ish-core/store/customer/customer-store';
+
+import { initialState, recurringOrdersAdapter } from './recurring-orders.reducer';
+
+const getRecurringOrdersState = createSelector(getCustomerState, state =>
+ state ? state.recurringOrders : initialState
+);
+
+export const getRecurringOrdersLoading = createSelector(getRecurringOrdersState, state => state.loading);
+
+export const getRecurringOrdersError = createSelector(getRecurringOrdersState, state => state.error);
+
+const getRecurringOrdersContexts = createSelector(getRecurringOrdersState, state => state.contexts);
+
+export const { selectEntities, selectAll } = recurringOrdersAdapter.getSelectors(getRecurringOrdersState);
+
+export const getRecurringOrders = (context: string = 'MY') =>
+ createSelector(selectEntities, getRecurringOrdersContexts, (requisitions, contexts) =>
+ contexts[context]?.map(id => requisitions[id])
+ );
+
+export const getSelectedRecurringOrder = createSelector(
+ selectRouteParam('recurringOrderId'),
+ selectEntities,
+ (id, entities) => id && entities[id]
+);
+
+export const getRecurringOrder = (recurringOrderId: string) =>
+ createSelector(selectEntities, entities => recurringOrderId && entities[recurringOrderId]);
diff --git a/src/app/pages/account-order/account-order/account-order.component.html b/src/app/pages/account-order/account-order/account-order.component.html
index af9337c2a2..883eb3c558 100644
--- a/src/app/pages/account-order/account-order/account-order.component.html
+++ b/src/app/pages/account-order/account-order/account-order.component.html
@@ -38,6 +38,13 @@ {{ 'account.orderdetails.heading.default' | translate }}
{{ order.status }}
+
diff --git a/src/app/pages/account-recurring-order/account-recurring-order-page.component.html b/src/app/pages/account-recurring-order/account-recurring-order-page.component.html
new file mode 100644
index 0000000000..b4e00df4c2
--- /dev/null
+++ b/src/app/pages/account-recurring-order/account-recurring-order-page.component.html
@@ -0,0 +1,200 @@
+
+
+
+{{ 'account.recurring_order.heading' | translate }}
+
+
+
+
+
+
{{ 'account.recurring_order.details.inactive-by-system.message' | translate }}
+
{{ recurringOrder.errorCode }}
+
+
+
+
+
+
+
{{ 'account.recurring_order.subtitle' | translate }}
+
+
+
+
+
+ {{ 'account.recurring_order.details.order_number.label' | translate }}
+ {{ recurringOrder.documentNo }}
+
+
+
+ {{ 'account.recurring_order.details.requisition_number.label' | translate }}
+
+ {{
+ recurringOrder.documentNo
+ }}
+ ({{
+ 'account.recurring_order.details.approved.text'
+ | translate
+ : {
+ name: approval.approver.firstName + ' ' + approval.approver.lastName,
+ date: approval.approvalDate | ishDate : 'shortDate'
+ }
+ }}, )
+
+
+
+
+ {{ 'account.recurring_order.details.creation_date.label' | translate }}
+ {{ recurringOrder.creationDate | ishDate }}
+
+
+
+ {{ 'account.recurring_order.details.last_order_date.label' | translate }}
+
+ {{ recurringOrder.lastOrderDate ? (recurringOrder.lastOrderDate | ishDate) : '-' }}
+
+
+
+
+ {{ 'account.recurring_order.details.next_order_date.label' | translate }}
+
+ {{ recurringOrder.nextOrderDate ? (recurringOrder.nextOrderDate | ishDate) : '-' }}
+
+
+
+
+ {{ 'account.recurring_order.details.status.label' | translate }}
+
+ {{
+ 'account.recurring_order.details.expired.text' | translate
+ }}
+
+
+
+
+
+
+
+
+ {{ 'account.recurring_order.details.order_count.label' | translate }}
+ {{ recurringOrder.orderCount }}
+
+
+
+ {{ 'account.recurring_order.details.last_placed_orders.label' | translate }}
+
+
+
+
+
+
+
+
+
+
+ {{ recurringOrder.user.companyName }}
+
+ {{ 'account.recurring_order.details.taxationId.label' | translate }} {{ taxationID }}
+
+ {{ recurringOrder.user.firstName }} {{ recurringOrder.user.lastName }}
+ {{ recurringOrder.user.email }}
+
+
+
+
+ {{ 'account.recurring_order.details.cost_center.label' | translate }}
+ {{ recurringOrder.costCenterId }} {{ recurringOrder.costCenterName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ recurringOrder.payment.displayName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ 'checkout.order_summary.heading' | translate }}
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/account-recurring-order/account-recurring-order-page.component.spec.ts b/src/app/pages/account-recurring-order/account-recurring-order-page.component.spec.ts
new file mode 100644
index 0000000000..c4f4fb81dd
--- /dev/null
+++ b/src/app/pages/account-recurring-order/account-recurring-order-page.component.spec.ts
@@ -0,0 +1,64 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { TranslateModule } from '@ngx-translate/core';
+import { MockComponent, MockDirective, MockPipe } from 'ng-mocks';
+import { of } from 'rxjs';
+import { instance, mock, when } from 'ts-mockito';
+
+import { ServerHtmlDirective } from 'ish-core/directives/server-html.directive';
+import { AccountFacade } from 'ish-core/facades/account.facade';
+import { Customer } from 'ish-core/models/customer/customer.model';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { DatePipe } from 'ish-core/pipes/date.pipe';
+import { AddressComponent } from 'ish-shared/components/address/address/address.component';
+import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component';
+import { BasketShippingMethodComponent } from 'ish-shared/components/basket/basket-shipping-method/basket-shipping-method.component';
+import { InfoBoxComponent } from 'ish-shared/components/common/info-box/info-box.component';
+import { SwitchComponent } from 'ish-shared/components/common/switch/switch.component';
+import { OrderRecurrenceComponent } from 'ish-shared/components/order/order-recurrence/order-recurrence.component';
+
+import { AccountRecurringOrderPageComponent } from './account-recurring-order-page.component';
+
+describe('Account Recurring Order Page Component', () => {
+ let component: AccountRecurringOrderPageComponent;
+ let fixture: ComponentFixture;
+ let element: HTMLElement;
+ let accountFacade: AccountFacade;
+
+ beforeEach(async () => {
+ accountFacade = mock(AccountFacade);
+ when(accountFacade.customer$).thenReturn(of({ customerNo: 'OilCorp' } as Customer));
+ when(accountFacade.selectedRecurringOrder$).thenReturn(
+ of({ id: '4711', user: { companyName: 'company' } } as RecurringOrder)
+ );
+
+ await TestBed.configureTestingModule({
+ declarations: [
+ AccountRecurringOrderPageComponent,
+ MockComponent(AddressComponent),
+ MockComponent(BasketCostSummaryComponent),
+ MockComponent(BasketShippingMethodComponent),
+ MockComponent(FaIconComponent),
+ MockComponent(InfoBoxComponent),
+ MockComponent(OrderRecurrenceComponent),
+ MockComponent(SwitchComponent),
+ MockDirective(ServerHtmlDirective),
+ MockPipe(DatePipe),
+ ],
+ imports: [TranslateModule.forRoot()],
+ providers: [{ provide: AccountFacade, useFactory: () => instance(accountFacade) }],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AccountRecurringOrderPageComponent);
+ component = fixture.componentInstance;
+ element = fixture.nativeElement;
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ expect(element).toBeTruthy();
+ expect(() => fixture.detectChanges()).not.toThrow();
+ });
+});
diff --git a/src/app/pages/account-recurring-order/account-recurring-order-page.component.ts b/src/app/pages/account-recurring-order/account-recurring-order-page.component.ts
new file mode 100644
index 0000000000..7b815e3700
--- /dev/null
+++ b/src/app/pages/account-recurring-order/account-recurring-order-page.component.ts
@@ -0,0 +1,51 @@
+import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Observable, first } from 'rxjs';
+
+import { AccountFacade } from 'ish-core/facades/account.facade';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { whenTruthy } from 'ish-core/utils/operators';
+
+@Component({
+ selector: 'ish-account-recurring-order-page',
+ templateUrl: './account-recurring-order-page.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AccountRecurringOrderPageComponent implements OnInit {
+ recurringOrder$: Observable;
+ private recurringOrder: RecurringOrder;
+ taxationID: string;
+ showErrorCode = false;
+
+ private destroyRef = inject(DestroyRef);
+
+ constructor(private accountFacade: AccountFacade) {}
+
+ ngOnInit() {
+ this.recurringOrder$ = this.accountFacade.selectedRecurringOrder$;
+ this.recurringOrder$.pipe(whenTruthy(), first(), takeUntilDestroyed(this.destroyRef)).subscribe(recurringOrder => {
+ this.recurringOrder = recurringOrder;
+ });
+
+ this.accountFacade.customer$
+ .pipe(whenTruthy(), first(), takeUntilDestroyed(this.destroyRef))
+ .subscribe(customer => {
+ this.taxationID = this.taxationID || customer?.taxationID;
+ });
+ }
+
+ switchActiveStatus(switchStatus: { active: boolean }) {
+ this.accountFacade.setActiveRecurringOrder(this.recurringOrder.id, switchStatus.active);
+ }
+
+ // callback function for ishServerHtml link
+ get activateRecurringOrder() {
+ return () => {
+ this.accountFacade.setActiveRecurringOrder(this.recurringOrder.id, true);
+ };
+ }
+
+ toggleShowErrorCode() {
+ this.showErrorCode = !this.showErrorCode;
+ }
+}
diff --git a/src/app/pages/account-recurring-order/account-recurring-order-page.module.ts b/src/app/pages/account-recurring-order/account-recurring-order-page.module.ts
new file mode 100644
index 0000000000..417af0d88e
--- /dev/null
+++ b/src/app/pages/account-recurring-order/account-recurring-order-page.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { SwitchComponent } from 'ish-shared/components/common/switch/switch.component';
+import { SharedModule } from 'ish-shared/shared.module';
+
+import { AccountRecurringOrderPageComponent } from './account-recurring-order-page.component';
+
+const accountRecurringOrderPageRoutes: Routes = [{ path: '', component: AccountRecurringOrderPageComponent }];
+
+@NgModule({
+ imports: [RouterModule.forChild(accountRecurringOrderPageRoutes), SharedModule, SwitchComponent],
+ declarations: [AccountRecurringOrderPageComponent],
+})
+export class AccountRecurringOrderPageModule {}
diff --git a/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.html b/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.html
new file mode 100644
index 0000000000..43d69ae9bf
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.html
@@ -0,0 +1,28 @@
+{{ 'account.recurring_orders.heading' | translate }}
+
+{{ 'account.recurring_orders.subtitle' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.spec.ts b/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.spec.ts
new file mode 100644
index 0000000000..e6ff0d38ef
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.spec.ts
@@ -0,0 +1,55 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { TranslateModule } from '@ngx-translate/core';
+import { MockComponent } from 'ng-mocks';
+import { of } from 'rxjs';
+import { instance, mock, when } from 'ts-mockito';
+
+import { AuthorizationToggleModule } from 'ish-core/authorization-toggle.module';
+import { AccountFacade } from 'ish-core/facades/account.facade';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
+
+import { AccountRecurringOrdersPageComponent } from './account-recurring-orders-page.component';
+import { RecurringOrderListComponent } from './recurring-order-list/recurring-order-list.component';
+
+describe('Account Recurring Orders Page Component', () => {
+ let component: AccountRecurringOrdersPageComponent;
+ let fixture: ComponentFixture;
+ let element: HTMLElement;
+ let accountFacade: AccountFacade;
+
+ beforeEach(async () => {
+ accountFacade = mock(AccountFacade);
+ when(accountFacade.recurringOrdersContext$).thenReturn(of('MY'));
+ when(accountFacade.recurringOrders$()).thenReturn(of([{ id: '4711' } as RecurringOrder]));
+
+ await TestBed.configureTestingModule({
+ imports: [
+ AuthorizationToggleModule.forTesting('APP_B2B_MANAGE_ORDERS'),
+ NgbNavModule,
+ RouterTestingModule,
+ TranslateModule.forRoot(),
+ ],
+ declarations: [
+ AccountRecurringOrdersPageComponent,
+ MockComponent(ErrorMessageComponent),
+ MockComponent(RecurringOrderListComponent),
+ ],
+ providers: [{ provide: AccountFacade, useFactory: () => instance(accountFacade) }],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AccountRecurringOrdersPageComponent);
+ component = fixture.componentInstance;
+ element = fixture.nativeElement;
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ expect(element).toBeTruthy();
+ expect(() => fixture.detectChanges()).not.toThrow();
+ });
+});
diff --git a/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.ts b/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.ts
new file mode 100644
index 0000000000..a558663216
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/account-recurring-orders-page.component.ts
@@ -0,0 +1,55 @@
+import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, inject } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Observable } from 'rxjs';
+
+import { AccountFacade } from 'ish-core/facades/account.facade';
+import { HttpError } from 'ish-core/models/http-error/http-error.model';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+
+import { RecurringOrderColumnsType } from './recurring-order-list/recurring-order-list.component';
+
+@Component({
+ selector: 'ish-account-recurring-orders-page',
+ templateUrl: './account-recurring-orders-page.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AccountRecurringOrdersPageComponent implements OnInit {
+ recurringOrders$: Observable;
+ recurringOrdersLoading$: Observable;
+ recurringOrdersError$: Observable;
+ columnsToDisplay: RecurringOrderColumnsType[];
+ context: string;
+
+ private destroyRef = inject(DestroyRef);
+
+ constructor(private accountFacade: AccountFacade) {}
+
+ ngOnInit() {
+ this.recurringOrders$ = this.accountFacade.recurringOrders$();
+ this.recurringOrdersLoading$ = this.accountFacade.recurringOrdersLoading$;
+ this.recurringOrdersError$ = this.accountFacade.recurringOrdersError$;
+
+ this.accountFacade.recurringOrdersContext$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(context => {
+ this.context = context || 'MY';
+ context === 'ADMIN'
+ ? (this.columnsToDisplay = [
+ 'recurringOrderNo',
+ 'frequency',
+ 'lastOrderDate',
+ 'nextOrderDate',
+ 'buyer',
+ 'orderTotal',
+ 'actions',
+ ])
+ : (this.columnsToDisplay = [
+ 'recurringOrderNo',
+ 'creationDate',
+ 'frequency',
+ 'lastOrderDate',
+ 'nextOrderDate',
+ 'orderTotal',
+ 'actions',
+ ]);
+ });
+ }
+}
diff --git a/src/app/pages/account-recurring-orders/account-recurring-orders-page.module.ts b/src/app/pages/account-recurring-orders/account-recurring-orders-page.module.ts
new file mode 100644
index 0000000000..eb0dc6a0e5
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/account-recurring-orders-page.module.ts
@@ -0,0 +1,26 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SwitchComponent } from 'ish-shared/components/common/switch/switch.component';
+import { SharedModule } from 'ish-shared/shared.module';
+
+import { AccountRecurringOrdersPageComponent } from './account-recurring-orders-page.component';
+import { RecurringOrderListComponent } from './recurring-order-list/recurring-order-list.component';
+
+const accountRecurringOrdersPageRoutes: Routes = [
+ { path: '', component: AccountRecurringOrdersPageComponent },
+ {
+ path: ':recurringOrderId',
+ loadChildren: () =>
+ import('../account-recurring-order/account-recurring-order-page.module').then(
+ m => m.AccountRecurringOrderPageModule
+ ),
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(accountRecurringOrdersPageRoutes), NgbNavModule, SharedModule, SwitchComponent],
+ declarations: [AccountRecurringOrdersPageComponent, RecurringOrderListComponent],
+})
+export class AccountRecurringOrdersPageModule {}
diff --git a/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.html b/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.html
new file mode 100644
index 0000000000..1f57a2aebf
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.html
@@ -0,0 +1,158 @@
+
+
+
+
+
+ {{ 'account.recurring_orders.table.id_of_order' | translate }}
+
+
+
+ {{ recurringOrder.documentNo }}
+
+
+
+
+
+
+
+ {{ 'account.recurring_orders.table.date_of_order' | translate }}
+
+
+ {{ recurringOrder.creationDate | ishDate }}
+
+
+
+
+
+
+ {{ 'account.recurring_orders.table.order_frequency' | translate }}
+
+
+ {{
+ recurringOrder.expired
+ ? ('account.recurring_orders.expired.text' | translate)
+ : (recurringOrder.recurrence.interval | ishFrequency)
+ }}
+
+
+
+
+
+
+ {{ 'account.recurring_orders.table.last_order_date' | translate }}
+
+
+ {{ recurringOrder.lastOrderDate ? (recurringOrder.lastOrderDate | ishDate) : '-' }}
+
+
+
+
+
+
+ {{ 'account.recurring_orders.table.next_order_date' | translate }}
+
+
+ {{ recurringOrder.nextOrderDate ? (recurringOrder.nextOrderDate | ishDate) : '-' }}
+
+
+
+
+
+
+ {{ 'account.recurring_orders.table.buyer' | translate }}
+
+
+ {{ recurringOrder.user.firstName }} {{ recurringOrder.user.lastName }}
+
+
+
+
+
+
+ {{ 'account.recurring_orders.table.order_total' | translate }}
+
+
+ {{ recurringOrder.totals.total | ishPrice : 'gross' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'account.recurring_orders.no_placed_orders_message' | translate }}
+
+
+
+ {{ 'account.recurring_orders.delete.do_you_really.text' | translate }}
+
diff --git a/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.spec.ts b/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.spec.ts
new file mode 100644
index 0000000000..bdcd55b518
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.spec.ts
@@ -0,0 +1,37 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { MockComponent } from 'ng-mocks';
+import { instance, mock } from 'ts-mockito';
+
+import { AccountFacade } from 'ish-core/facades/account.facade';
+import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component';
+
+import { RecurringOrderListComponent } from './recurring-order-list.component';
+
+describe('Recurring Order List Component', () => {
+ let component: RecurringOrderListComponent;
+ let fixture: ComponentFixture;
+ let element: HTMLElement;
+ let accountFacade: AccountFacade;
+
+ beforeEach(async () => {
+ accountFacade = mock(AccountFacade);
+ await TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [MockComponent(ModalDialogComponent), RecurringOrderListComponent],
+ providers: [{ provide: AccountFacade, useFactory: () => instance(accountFacade) }],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RecurringOrderListComponent);
+ component = fixture.componentInstance;
+ element = fixture.nativeElement;
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ expect(element).toBeTruthy();
+ expect(() => fixture.detectChanges()).not.toThrow();
+ });
+});
diff --git a/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.ts b/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.ts
new file mode 100644
index 0000000000..b05aac71ae
--- /dev/null
+++ b/src/app/pages/account-recurring-orders/recurring-order-list/recurring-order-list.component.ts
@@ -0,0 +1,47 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+
+import { AccountFacade } from 'ish-core/facades/account.facade';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
+import { ModalDialogComponent } from 'ish-shared/components/common/modal-dialog/modal-dialog.component';
+
+export type RecurringOrderColumnsType =
+ | 'recurringOrderNo'
+ | 'creationDate'
+ | 'frequency'
+ | 'lastOrderDate'
+ | 'nextOrderDate'
+ | 'buyer'
+ | 'orderTotal'
+ | 'actions';
+
+@Component({
+ selector: 'ish-recurring-order-list',
+ templateUrl: './recurring-order-list.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RecurringOrderListComponent {
+ @Input() recurringOrders: RecurringOrder[];
+ @Input() columnsToDisplay: RecurringOrderColumnsType[];
+ @Input() context: string;
+
+ constructor(private accountFacade: AccountFacade, private translate: TranslateService) {}
+
+ /** Emits the id of the recurring order to delete. */
+ delete(recurringOrderId: string) {
+ this.accountFacade.deleteRecurringOrder(recurringOrderId);
+ }
+
+ /** Emits id and active state to update the active state for the recurring order */
+ switchActiveStatus(recurringOrder: { active: boolean; id: string }) {
+ this.accountFacade.setActiveRecurringOrder(recurringOrder.id, recurringOrder.active);
+ }
+
+ /** Determine the heading of the delete modal and opens the modal. */
+ openDeleteConfirmationDialog(recurringOrder: RecurringOrder, modal: ModalDialogComponent) {
+ modal.options.titleText = this.translate.instant('account.recurring_order.delete_dialog.header', {
+ 0: recurringOrder.documentNo,
+ });
+ modal.show(recurringOrder.id);
+ }
+}
diff --git a/src/app/pages/account/account-navigation/account-navigation.items.b2c.ts b/src/app/pages/account/account-navigation/account-navigation.items.b2c.ts
index 9f2effa54b..bca7a60e57 100644
--- a/src/app/pages/account/account-navigation/account-navigation.items.b2c.ts
+++ b/src/app/pages/account/account-navigation/account-navigation.items.b2c.ts
@@ -9,6 +9,12 @@ export const navigationItems: NavigationItem[] = [
routerLink: '/account/orders',
notRole: ['APP_B2B_CXML_USER', 'APP_B2B_OCI_USER'],
},
+ {
+ id: 'recurring-orders',
+ localizationKey: 'account.recurring_orders.navigation.link',
+ routerLink: '/account/recurring-orders',
+ serverSetting: 'recurringOrder.enabled',
+ },
{
id: 'wishlists',
localizationKey: 'account.wishlists.link',
diff --git a/src/app/pages/account/account-navigation/account-navigation.items.ts b/src/app/pages/account/account-navigation/account-navigation.items.ts
index 93f0542798..878bccc2e1 100644
--- a/src/app/pages/account/account-navigation/account-navigation.items.ts
+++ b/src/app/pages/account/account-navigation/account-navigation.items.ts
@@ -14,6 +14,12 @@ export const navigationItems: NavigationItem[] = [
routerLink: '/account/orders',
notRole: ['APP_B2B_CXML_USER', 'APP_B2B_OCI_USER'],
},
+ {
+ id: 'recurring-orders',
+ localizationKey: 'account.recurring_orders.navigation.link',
+ routerLink: '/account/recurring-orders',
+ serverSetting: 'recurringOrder.enabled',
+ },
{
id: 'requisitions',
localizationKey: 'account.requisitions.requisitions',
diff --git a/src/app/pages/account/account-page.module.ts b/src/app/pages/account/account-page.module.ts
index c5b545a5a2..3ca6403b0b 100644
--- a/src/app/pages/account/account-page.module.ts
+++ b/src/app/pages/account/account-page.module.ts
@@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { authorizationToggleGuard } from 'ish-core/authorization-toggle.module';
import { featureToggleGuard } from 'ish-core/feature-toggle.module';
+import { serverSettingGuard } from 'ish-core/guards/server-setting.guard';
import { SharedModule } from 'ish-shared/shared.module';
import { AccountOverviewPageModule } from '../account-overview/account-overview-page.module';
@@ -100,6 +101,18 @@ const accountPageRoutes: Routes = [
data: { permission: 'APP_B2B_PURCHASE' },
loadChildren: () => import('requisition-management').then(m => m.RequisitionManagementRoutingModule),
},
+ {
+ path: 'recurring-orders',
+ canActivate: [serverSettingGuard],
+ data: {
+ serverSetting: 'recurringOrder.enabled',
+ breadcrumbData: [{ key: 'account.recurring_orders.breadcrumb' }],
+ },
+ loadChildren: () =>
+ import('../account-recurring-orders/account-recurring-orders-page.module').then(
+ m => m.AccountRecurringOrdersPageModule
+ ),
+ },
],
},
];
diff --git a/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.html b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.html
new file mode 100644
index 0000000000..8f5f531234
--- /dev/null
+++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.html
@@ -0,0 +1,35 @@
+
+
+
+ {{
+ 'order.recurrence.single_order.label' | translate
+ }}
+
+
+
+ {{
+ 'order.recurrence.recurring_order.label' | translate
+ }}
+
+
+
+
+
diff --git a/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.scss b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.scss
new file mode 100644
index 0000000000..b52566de33
--- /dev/null
+++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.scss
@@ -0,0 +1,12 @@
+@import 'variables';
+
+#order-recurrence {
+ padding: ($space-default * 0.5) ($space-default * 1.5);
+ margin: ($space-default * 1) ($space-default * -1.5);
+ font-size: $font-size-corporate;
+ background-color: $white;
+}
+
+#order-recurrence-configuration {
+ height: 100px;
+}
diff --git a/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.spec.ts b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.spec.ts
new file mode 100644
index 0000000000..8fdb5f6766
--- /dev/null
+++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.spec.ts
@@ -0,0 +1,33 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { instance, mock } from 'ts-mockito';
+
+import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
+
+import { BasketOrderRecurrenceEditComponent } from './basket-order-recurrence-edit.component';
+
+describe('Basket Order Recurrence Edit Component', () => {
+ let component: BasketOrderRecurrenceEditComponent;
+ let fixture: ComponentFixture;
+ let element: HTMLElement;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ declarations: [BasketOrderRecurrenceEditComponent],
+ providers: [{ provide: CheckoutFacade, useFactory: () => instance(mock(CheckoutFacade)) }],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BasketOrderRecurrenceEditComponent);
+ component = fixture.componentInstance;
+ element = fixture.nativeElement;
+ });
+
+ it('should be created', () => {
+ expect(component).toBeTruthy();
+ expect(element).toBeTruthy();
+ expect(() => fixture.detectChanges()).not.toThrow();
+ });
+});
diff --git a/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.ts b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.ts
new file mode 100644
index 0000000000..3ccd80aafc
--- /dev/null
+++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.ts
@@ -0,0 +1,253 @@
+/* eslint-disable unicorn/no-null */
+import {
+ ChangeDetectionStrategy,
+ Component,
+ DestroyRef,
+ Input,
+ OnChanges,
+ OnInit,
+ SimpleChanges,
+ inject,
+} from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { UntypedFormGroup } from '@angular/forms';
+import { FormlyFieldConfig } from '@ngx-formly/core';
+import { formatISO, parseISO } from 'date-fns';
+import { isEqual } from 'lodash-es';
+import { debounceTime, distinctUntilChanged, skip } from 'rxjs';
+
+import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
+import { Recurrence } from 'ish-core/models/recurrence/recurrence.model';
+
+interface RecurrenceFormData {
+ period: string;
+ duration: string;
+ startDate: Date;
+ endDate?: Date;
+ repetitions?: number;
+ ending: 'date' | 'repetitions';
+}
+
+@Component({
+ selector: 'ish-basket-order-recurrence-edit',
+ templateUrl: './basket-order-recurrence-edit.component.html',
+ styleUrls: ['./basket-order-recurrence-edit.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class BasketOrderRecurrenceEditComponent implements OnChanges, OnInit {
+ @Input() recurrence: Recurrence;
+
+ // default order recurrence value: weekly recurrence starting today
+ defaultRecurrence: Recurrence = {
+ interval: 'P1W',
+ startDate: formatISO(new Date()),
+ };
+
+ private periodOptions = [
+ { value: 'D', label: `order.recurrence.period.days` },
+ { value: 'W', label: `order.recurrence.period.weeks` },
+ { value: 'M', label: `order.recurrence.period.months` },
+ { value: 'Y', label: `order.recurrence.period.years` },
+ ];
+
+ form = new UntypedFormGroup({});
+ model: RecurrenceFormData;
+ fields: FormlyFieldConfig[] = [
+ {
+ fieldGroupClassName: 'row',
+ fieldGroup: [
+ {
+ key: 'duration',
+ type: 'ish-number-field',
+ className: 'col-12 col-lg-5',
+ props: {
+ label: 'order.recurrence.form.duration.label',
+ labelClass: 'col-md-12',
+ fieldClass: 'col-md-12',
+ min: 1,
+ },
+ },
+ {
+ key: 'period',
+ type: 'ish-select-field',
+ className: 'col-12 col-lg-7',
+ props: {
+ options: this.periodOptions,
+ label: 'order.recurrence.form.period.label',
+ labelClass: 'col-md-12 hidden d-none d-lg-block',
+ fieldClass: 'col-md-12',
+ },
+ },
+ ],
+ },
+ {
+ key: 'startDate',
+ type: 'ish-date-picker-field',
+ props: {
+ label: 'order.recurrence.form.startDate.label',
+ minDays: 0,
+ labelClass: 'col-md-12',
+ fieldClass: 'col-md-12',
+ },
+ },
+ {
+ fieldGroupClassName: 'row',
+ fieldGroup: [
+ {
+ key: 'ending',
+ type: 'ish-radio-field',
+ className: 'col-12 col-md-3',
+ props: {
+ label: 'order.recurrence.form.ending.date.label',
+ value: 'date',
+ labelClass: 'col-md-12 pl-0',
+ fieldClass: 'col-md-12',
+ },
+ },
+ {
+ key: 'endDate',
+ type: 'ish-date-picker-field',
+ className: 'col-12 col-md-9',
+ props: {
+ placeholder: 'mm/dd/yy',
+ labelClass: 'col-md-12',
+ fieldClass: 'col-md-12',
+ },
+ expressions: {
+ 'props.disabled': 'model.ending === "repetitions"',
+ 'props.minDays': field => this.calculateMinimumEndDate(field.model.startDate),
+ 'model.endDate': 'model.repetitions || model.ending === "repetitions" ? null : model.endDate',
+ },
+ },
+ ],
+ },
+ {
+ fieldGroupClassName: 'row',
+ fieldGroup: [
+ {
+ key: 'ending',
+ type: 'ish-radio-field',
+ className: 'col-12 col-md-3 col-lg-5 mr-lg-0',
+ props: {
+ label: 'order.recurrence.form.ending.repetitions.label',
+ value: 'repetitions',
+ labelClass: 'col-md-12 pl-0',
+ fieldClass: 'col-md-12',
+ },
+ },
+ {
+ key: 'repetitions',
+ type: 'ish-number-field',
+ className: 'col-5 col-md-4 col-lg-3 pl-md-3 pl-lg-0 pr-md-0',
+ props: {
+ labelClass: 'col-md-12',
+ fieldClass: 'col-md-12',
+ inputClass: 'testClass',
+ min: 1,
+ },
+ expressions: {
+ 'props.disabled': 'model.ending !== "repetitions"',
+ 'model.repetitions':
+ 'model.endDate || model.ending === "date" ? null : model.repetitions ? model.repetitions : 50',
+ },
+ },
+ {
+ type: 'ish-information-field',
+ className: 'col-7 col-md-5 col-lg-4 pl-0 pt-2 pt-md-0',
+ props: {
+ containerClass: 'p-md-2',
+ localizationKey: 'order.recurrence.form.info.text',
+ },
+ },
+ ],
+ },
+ ];
+
+ private destroyRef = inject(DestroyRef);
+
+ constructor(private checkoutFacade: CheckoutFacade) {}
+
+ ngOnInit(): void {
+ // save changes after form values changed and an update is necessary
+ this.form.valueChanges
+ .pipe(skip(1), debounceTime(1000), distinctUntilChanged(isEqual), takeUntilDestroyed(this.destroyRef))
+ .subscribe(data => {
+ if (this.formDataDifferentToRecurrence(data)) {
+ this.updateOrderRecurrence(this.mapFormDataToRecurrence(data));
+ }
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (!isEqual(changes.recurrence.currentValue, changes.recurrence.previousValue)) {
+ this.model = this.getModel(this.recurrence);
+ }
+ }
+
+ updateOrderRecurrence(updateData: Recurrence | null) {
+ if (updateData) {
+ // update order recurrence with form values (or default value)
+ this.checkoutFacade.updateBasketRecurrence(updateData);
+ } else {
+ // remove order recurrence
+ this.checkoutFacade.updateBasketRecurrence(null);
+ }
+ }
+
+ private getModel(recurrence: Recurrence): RecurrenceFormData {
+ if (!recurrence) {
+ return;
+ }
+ let period = recurrence.interval.slice(-1).toUpperCase();
+ let duration = parseInt(recurrence.interval.slice(1, -1), 10);
+ // convert days to weeks if possible since the API only returns daily, monthly or yearly intervals
+ if (period === 'D' && duration % 7 === 0) {
+ period = 'W';
+ duration = duration / 7;
+ }
+ return {
+ period,
+ duration: duration.toString(),
+ startDate: parseISO(recurrence.startDate),
+ endDate: recurrence.endDate ? parseISO(recurrence.endDate) : undefined,
+ repetitions: recurrence.repetitions,
+ ending: recurrence.repetitions ? 'repetitions' : 'date',
+ };
+ }
+
+ private mapFormDataToRecurrence(data: RecurrenceFormData): Recurrence {
+ if (!data) {
+ return;
+ }
+ return {
+ interval: `P${data.duration}${data.period}`,
+ startDate: formatISO(data.startDate),
+ endDate: data.endDate ? formatISO(data.endDate) : null,
+ repetitions: data.repetitions ? data.repetitions : null,
+ };
+ }
+
+ private formDataDifferentToRecurrence(data: RecurrenceFormData): boolean {
+ if (!data) {
+ return false;
+ }
+ const recurrence = this.mapFormDataToRecurrence(data);
+ if (recurrence.interval.endsWith('W')) {
+ recurrence.interval = `P${parseInt(recurrence.interval.slice(1, -1), 10) * 7}D`;
+ }
+ return (
+ this.recurrence.interval !== recurrence.interval ||
+ this.recurrence.startDate.slice(0, 10) !== recurrence.startDate.slice(0, 10) ||
+ this.recurrence.endDate?.slice(0, 10) !== recurrence.endDate?.slice(0, 10) ||
+ // eslint-disable-next-line eqeqeq
+ this.recurrence.repetitions != recurrence.repetitions
+ );
+ }
+
+ private calculateMinimumEndDate(startDate: string): number {
+ const date1 = new Date(startDate);
+ const date2 = new Date();
+ const difference = date1.getTime() - date2.getTime();
+ return Math.ceil(difference / (1000 * 3600 * 24)) || 1;
+ }
+}
diff --git a/src/app/pages/basket/basket-page.module.ts b/src/app/pages/basket/basket-page.module.ts
index 7076a53d50..b87fce1d35 100644
--- a/src/app/pages/basket/basket-page.module.ts
+++ b/src/app/pages/basket/basket-page.module.ts
@@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from 'ish-shared/shared.module';
+import { BasketOrderRecurrenceEditComponent } from './basket-order-recurrence-edit/basket-order-recurrence-edit.component';
import { BasketPageComponent } from './basket-page.component';
import { ShoppingBasketEmptyComponent } from './shopping-basket-empty/shopping-basket-empty.component';
import { ShoppingBasketPaymentComponent } from './shopping-basket-payment/shopping-basket-payment.component';
@@ -13,6 +14,7 @@ const basketPageRoutes: Routes = [{ path: '', component: BasketPageComponent }];
@NgModule({
imports: [RouterModule.forChild(basketPageRoutes), SharedModule],
declarations: [
+ BasketOrderRecurrenceEditComponent,
BasketPageComponent,
ShoppingBasketComponent,
ShoppingBasketEmptyComponent,
diff --git a/src/app/pages/basket/shopping-basket-payment/shopping-basket-payment.component.html b/src/app/pages/basket/shopping-basket-payment/shopping-basket-payment.component.html
index 36728a80e5..39291b95a6 100644
--- a/src/app/pages/basket/shopping-basket-payment/shopping-basket-payment.component.html
+++ b/src/app/pages/basket/shopping-basket-payment/shopping-basket-payment.component.html
@@ -12,21 +12,22 @@
- {{ 'shopping_cart.payment.or.text' | translate }}
-
-
- {{ paymentMethod.displayName }}
-
-
-
-
-
+
+ {{ 'shopping_cart.payment.or.text' | translate }}
+
+
+ {{ paymentMethod.displayName }}
+
+
+
+
diff --git a/src/app/pages/basket/shopping-basket/shopping-basket.component.html b/src/app/pages/basket/shopping-basket/shopping-basket.component.html
index 516f35f64b..1d40375826 100644
--- a/src/app/pages/basket/shopping-basket/shopping-basket.component.html
+++ b/src/app/pages/basket/shopping-basket/shopping-basket.component.html
@@ -88,6 +88,11 @@ {{ 'checkout.order_details.heading' | translate }}
+
+
{
MockComponent(LineItemListComponent),
MockComponent(ModalDialogLinkComponent),
MockComponent(ShoppingBasketPaymentComponent),
+ MockPipe(ServerSettingPipe),
ShoppingBasketComponent,
],
imports: [
diff --git a/src/app/pages/checkout-address/checkout-address/checkout-address.component.html b/src/app/pages/checkout-address/checkout-address/checkout-address.component.html
index e2633d1363..48ac2e4b6a 100644
--- a/src/app/pages/checkout-address/checkout-address/checkout-address.component.html
+++ b/src/app/pages/checkout-address/checkout-address/checkout-address.component.html
@@ -38,6 +38,8 @@
{{ 'checkout.order_details.heading' | translate }}
+
+
diff --git a/src/app/pages/checkout-address/checkout-address/checkout-address.component.spec.ts b/src/app/pages/checkout-address/checkout-address/checkout-address.component.spec.ts
index 6c195faf65..bd8b9dbc1b 100644
--- a/src/app/pages/checkout-address/checkout-address/checkout-address.component.spec.ts
+++ b/src/app/pages/checkout-address/checkout-address/checkout-address.component.spec.ts
@@ -11,6 +11,7 @@ import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data';
import { BasketCostSummaryComponent } from 'ish-shared/components/basket/basket-cost-summary/basket-cost-summary.component';
import { BasketErrorMessageComponent } from 'ish-shared/components/basket/basket-error-message/basket-error-message.component';
import { BasketItemsSummaryComponent } from 'ish-shared/components/basket/basket-items-summary/basket-items-summary.component';
+import { BasketRecurrenceSummaryComponent } from 'ish-shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component';
import { BasketValidationResultsComponent } from 'ish-shared/components/basket/basket-validation-results/basket-validation-results.component';
import { BasketInvoiceAddressWidgetComponent } from 'ish-shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component';
import { BasketShippingAddressWidgetComponent } from 'ish-shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component';
@@ -33,6 +34,7 @@ describe('Checkout Address Component', () => {
MockComponent(BasketErrorMessageComponent),
MockComponent(BasketInvoiceAddressWidgetComponent),
MockComponent(BasketItemsSummaryComponent),
+ MockComponent(BasketRecurrenceSummaryComponent),
MockComponent(BasketShippingAddressWidgetComponent),
MockComponent(BasketValidationResultsComponent),
MockComponent(ErrorMessageComponent),
diff --git a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html
index 1e9b091c83..0ac088de43 100644
--- a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html
+++ b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.html
@@ -173,6 +173,9 @@
{{ 'checkout.order_details.heading' | translate }}
+
+
+
diff --git a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts
index 5968c9a0c7..852324e54b 100644
--- a/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts
+++ b/src/app/pages/checkout-payment/checkout-payment/checkout-payment.component.spec.ts
@@ -18,6 +18,7 @@ import { BasketErrorMessageComponent } from 'ish-shared/components/basket/basket
import { BasketItemsSummaryComponent } from 'ish-shared/components/basket/basket-items-summary/basket-items-summary.component';
import { BasketPaymentCostInfoComponent } from 'ish-shared/components/basket/basket-payment-cost-info/basket-payment-cost-info.component';
import { BasketPromotionCodeComponent } from 'ish-shared/components/basket/basket-promotion-code/basket-promotion-code.component';
+import { BasketRecurrenceSummaryComponent } from 'ish-shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component';
import { BasketValidationResultsComponent } from 'ish-shared/components/basket/basket-validation-results/basket-validation-results.component';
import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { InfoMessageComponent } from 'ish-shared/components/common/info-message/info-message.component';
@@ -44,6 +45,7 @@ describe('Checkout Payment Component', () => {
MockComponent(BasketItemsSummaryComponent),
MockComponent(BasketPaymentCostInfoComponent),
MockComponent(BasketPromotionCodeComponent),
+ MockComponent(BasketRecurrenceSummaryComponent),
MockComponent(BasketValidationResultsComponent),
MockComponent(ErrorMessageComponent),
MockComponent(FormlyForm),
diff --git a/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.html b/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.html
index fa313ac6e4..11a3d03717 100644
--- a/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.html
+++ b/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.html
@@ -1,7 +1,12 @@
diff --git a/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.ts b/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.ts
index a83dbd8bb0..5b4b18a095 100644
--- a/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.ts
+++ b/src/app/pages/checkout-receipt/checkout-receipt-order/checkout-receipt-order.component.ts
@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Order } from 'ish-core/models/order/order.model';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
@Component({
selector: 'ish-checkout-receipt-order',
@@ -8,5 +9,5 @@ import { Order } from 'ish-core/models/order/order.model';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReceiptOrderComponent {
- @Input({ required: true }) order: Order;
+ @Input({ required: true }) order: Order | RecurringOrder;
}
diff --git a/src/app/pages/checkout-receipt/checkout-receipt-page.component.ts b/src/app/pages/checkout-receipt/checkout-receipt-page.component.ts
index 6de14c533e..a7c927348d 100644
--- a/src/app/pages/checkout-receipt/checkout-receipt-page.component.ts
+++ b/src/app/pages/checkout-receipt/checkout-receipt-page.component.ts
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs';
import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
import { Basket } from 'ish-core/models/basket/basket.model';
import { Order } from 'ish-core/models/order/order.model';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
@Component({
selector: 'ish-checkout-receipt-page',
@@ -11,14 +12,14 @@ import { Order } from 'ish-core/models/order/order.model';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReceiptPageComponent implements OnInit {
- order$: Observable
;
+ order$: Observable;
loading$: Observable;
submittedBasket$: Observable;
constructor(private checkoutFacade: CheckoutFacade) {}
ngOnInit() {
- this.order$ = this.checkoutFacade.selectedOrder$;
+ this.order$ = this.checkoutFacade.submittedOrder$;
this.loading$ = this.checkoutFacade.basketLoading$;
this.submittedBasket$ = this.checkoutFacade.submittedBasket$;
}
diff --git a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html
index 5c187abd30..5ead6ee160 100644
--- a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html
+++ b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html
@@ -8,11 +8,24 @@
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.ts b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.ts
index 5667babff9..531aacc58e 100644
--- a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.ts
+++ b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.ts
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Basket } from 'ish-core/models/basket/basket.model';
import { Order } from 'ish-core/models/order/order.model';
+import { RecurringOrder } from 'ish-core/models/recurring-order/recurring-order.model';
@Component({
selector: 'ish-checkout-receipt',
@@ -9,5 +10,5 @@ import { Order } from 'ish-core/models/order/order.model';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReceiptComponent {
- @Input({ required: true }) order: Order | Basket;
+ @Input({ required: true }) order: Order | RecurringOrder | Basket;
}
diff --git a/src/app/pages/checkout-review/checkout-review/checkout-review.component.html b/src/app/pages/checkout-review/checkout-review/checkout-review.component.html
index a42ca93788..26499b7323 100644
--- a/src/app/pages/checkout-review/checkout-review/checkout-review.component.html
+++ b/src/app/pages/checkout-review/checkout-review/checkout-review.component.html
@@ -1,6 +1,9 @@