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..fb7f04d4aa 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.requisitionNo : req.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.ts b/projects/requisition-management/src/app/models/requisition/requisition.mapper.ts index bae189a477..dbc2b74ddd 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, 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/src/app/core/facades/checkout.facade.ts b/src/app/core/facades/checkout.facade.ts index ef4f7b3cd6..fb27760426 100644 --- a/src/app/core/facades/checkout.facade.ts +++ b/src/app/core/facades/checkout.facade.ts @@ -52,6 +52,7 @@ import { updateBasketAddress, updateBasketCostCenter, updateBasketItem, + updateBasketRecurrence, updateBasketShippingMethod, updateConcardisCvcLastUpdated, } from 'ish-core/store/customer/basket'; @@ -143,6 +144,10 @@ export class CheckoutFacade { this.store.dispatch(updateBasketCostCenter({ costCenter })); } + updateBasketRecurrence(recurrence: any) { + this.store.dispatch(updateBasketRecurrence({ recurrence })); + } + updateBasketExternalOrderReference(externalOrderReference: string) { this.store.dispatch(updateBasket({ update: { externalOrderReference } })); } 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/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/services/basket/basket.service.ts b/src/app/core/services/basket/basket.service.ts index 854d549b26..69b6bca268 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'; @@ -31,7 +32,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/store/customer/basket/basket.actions.ts b/src/app/core/store/customer/basket/basket.actions.ts index ab4fbe5c20..91a2713385 100644 --- a/src/app/core/store/customer/basket/basket.actions.ts +++ b/src/app/core/store/customer/basket/basket.actions.ts @@ -68,6 +68,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: any }>() +); + 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 f9d8a5a206..dd73c90cf8 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/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..cafaa2736c --- /dev/null +++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.html @@ -0,0 +1,31 @@ +
+
+ + +
+
+ + +
+
+
+ + +
+
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..a21d6ce632 --- /dev/null +++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.scss @@ -0,0 +1,11 @@ +@import 'variables'; + +#order-recurrence { + padding: ($space-default * 0.5) ($space-default * 1.5); + margin: ($space-default * 1) ($space-default * -1.5); + 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..76c5e7bcd3 --- /dev/null +++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +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({ + declarations: [BasketOrderRecurrenceEditComponent], + }).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..efbf4403bd --- /dev/null +++ b/src/app/pages/basket/basket-order-recurrence-edit/basket-order-recurrence-edit.component.ts @@ -0,0 +1,246 @@ +/* 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'; + +export 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()), + }; + + 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-md-4', + props: { + label: 'Recur every', + labelClass: 'col-md-12', + fieldClass: 'col-md-12', + min: 1, + }, + }, + { + key: 'period', + type: 'ish-select-field', + className: 'col-12 col-md-8', + props: { + options: this.periodOptions, + label: 'Recur every', + labelClass: 'col-md-12 hidden', + fieldClass: 'col-md-12', + }, + }, + ], + }, + { + key: 'startDate', + type: 'ish-date-picker-field', + props: { + label: 'Start from', + 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: 'Until', + value: 'date', + labelClass: 'col-md-12', + 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-4', + props: { + label: 'Until after', + value: 'repetitions', + labelClass: 'col-md-12', + fieldClass: 'col-md-12', + }, + }, + { + key: 'repetitions', + type: 'ish-number-field', + className: 'col-12 col-md-8', + props: { + labelClass: 'col-md-12', + fieldClass: 'col-md-12', + min: 1, + }, + expressions: { + 'props.disabled': 'model.ending !== "repetitions"', + 'model.repetitions': + 'model.endDate || model.ending === "date" ? null : model.repetitions ? model.repetitions : 50', + }, + }, + ], + }, + ]; + + 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) { + console.log('UPDATE ORDER RECURRENCE', updateData); + + 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 467fb825a1..7a6c6665f7 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 { ShoppingBasketComponent } from './shopping-basket/shopping-basket.component'; @@ -11,6 +12,11 @@ const basketPageRoutes: Routes = [{ path: '', component: BasketPageComponent }]; @NgModule({ imports: [RouterModule.forChild(basketPageRoutes), SharedModule], - declarations: [BasketPageComponent, ShoppingBasketComponent, ShoppingBasketEmptyComponent], + declarations: [ + BasketOrderRecurrenceEditComponent, + BasketPageComponent, + ShoppingBasketComponent, + ShoppingBasketEmptyComponent, + ], }) export class BasketPageModule {} 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 71730e379f..319340df27 100644 --- a/src/app/pages/basket/shopping-basket/shopping-basket.component.html +++ b/src/app/pages/basket/shopping-basket/shopping-basket.component.html @@ -89,6 +89,8 @@

{{ 'checkout.order_details.heading' | translate }}

+ + diff --git a/src/app/pages/checkout-shipping/checkout-shipping-page.component.html b/src/app/pages/checkout-shipping/checkout-shipping-page.component.html index 499729f60e..bb1e6a697a 100644 --- a/src/app/pages/checkout-shipping/checkout-shipping-page.component.html +++ b/src/app/pages/checkout-shipping/checkout-shipping-page.component.html @@ -39,7 +39,11 @@

{{ 'checkout.shipping_method.selection.heading' | translate }}

{{ 'checkout.order_details.heading' | translate }}

+ + + +
diff --git a/src/app/shared/components/basket/basket-approval-info/basket-approval-info.component.html b/src/app/shared/components/basket/basket-approval-info/basket-approval-info.component.html index 0d8f287f08..751ea9464d 100644 --- a/src/app/shared/components/basket/basket-approval-info/basket-approval-info.component.html +++ b/src/app/shared/components/basket/basket-approval-info/basket-approval-info.component.html @@ -15,6 +15,7 @@
  • {{ 'approval.details.conditions.order_spend_limit' | translate }}
  • {{ 'approval.details.conditions.budget_limit' | translate }}
  • {{ 'approval.details.conditions.cost_center' | translate }}
  • +
  • {{ 'approval.details.conditions.recurring_order' | translate }}
  • {{ 'approval.details.place_order' | translate }} diff --git a/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.html b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.html new file mode 100644 index 0000000000..5dd0e08063 --- /dev/null +++ b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.html @@ -0,0 +1,5 @@ +

    +
    {{ 'order.recurrence.heading' | translate }}
    + + +
    diff --git a/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.scss b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.scss new file mode 100644 index 0000000000..9945313248 --- /dev/null +++ b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.scss @@ -0,0 +1,5 @@ +@import 'variables'; + +:host ::ng-deep dl { + font-size: $font-size-sm; +} diff --git a/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.spec.ts b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.spec.ts new file mode 100644 index 0000000000..e40b73b908 --- /dev/null +++ b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BasketRecurrenceSummaryComponent } from './basket-recurrence-summary.component'; + +describe('Basket Recurrence Summary Component', () => { + let component: BasketRecurrenceSummaryComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BasketRecurrenceSummaryComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BasketRecurrenceSummaryComponent); + 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/basket/basket-recurrence-summary/basket-recurrence-summary.component.ts b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.ts new file mode 100644 index 0000000000..53806f2822 --- /dev/null +++ b/src/app/shared/components/basket/basket-recurrence-summary/basket-recurrence-summary.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { Recurrence } from 'ish-core/models/recurrence/recurrence.model'; + +@Component({ + selector: 'ish-basket-recurrence-summary', + templateUrl: './basket-recurrence-summary.component.html', + styleUrls: ['./basket-recurrence-summary.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BasketRecurrenceSummaryComponent { + @Input() recurrence: Recurrence; +} diff --git a/src/app/shared/components/order/order-recurrence/order-recurrence.component.html b/src/app/shared/components/order/order-recurrence/order-recurrence.component.html new file mode 100644 index 0000000000..cead80d064 --- /dev/null +++ b/src/app/shared/components/order/order-recurrence/order-recurrence.component.html @@ -0,0 +1,16 @@ + +
    +
    {{ 'order.recurrence.interval.label' | translate }}
    +
    {{ printIntervalDisplayName(recurrence.interval) }}
    +
    {{ 'order.recurrence.start.label' | translate }}
    +
    {{ recurrence.startDate | ishDate }}
    + +
    {{ 'order.recurrence.end.label' | translate }}
    +
    {{ recurrence.endDate | ishDate }}
    +
    + +
    {{ 'order.recurrence.repetitions.label' | translate }}
    +
    after {{ recurrence.repetitions }} orders
    +
    +
    +
    diff --git a/src/app/shared/components/order/order-recurrence/order-recurrence.component.scss b/src/app/shared/components/order/order-recurrence/order-recurrence.component.scss new file mode 100644 index 0000000000..b87ba01d73 --- /dev/null +++ b/src/app/shared/components/order/order-recurrence/order-recurrence.component.scss @@ -0,0 +1,9 @@ +@import 'variables'; + +.dl-horizontal { + margin-bottom: 0; + + dd { + margin-bottom: 0; + } +} diff --git a/src/app/shared/components/order/order-recurrence/order-recurrence.component.spec.ts b/src/app/shared/components/order/order-recurrence/order-recurrence.component.spec.ts new file mode 100644 index 0000000000..cc1369d550 --- /dev/null +++ b/src/app/shared/components/order/order-recurrence/order-recurrence.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OrderRecurrenceComponent } from './order-recurrence.component'; + +describe('Order Recurrence Component', () => { + let component: OrderRecurrenceComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [OrderRecurrenceComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OrderRecurrenceComponent); + 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/order/order-recurrence/order-recurrence.component.ts b/src/app/shared/components/order/order-recurrence/order-recurrence.component.ts new file mode 100644 index 0000000000..bd10d75f4b --- /dev/null +++ b/src/app/shared/components/order/order-recurrence/order-recurrence.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { Recurrence } from 'ish-core/models/recurrence/recurrence.model'; + +@Component({ + selector: 'ish-order-recurrence', + templateUrl: './order-recurrence.component.html', + styleUrls: ['./order-recurrence.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OrderRecurrenceComponent { + @Input() recurrence: Recurrence; + @Input() labelCssClass: string; + @Input() valueCssClass: string; + + printIntervalDisplayName(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 `${duration} Day(s)`; + case 'W': + return `${duration} Week(s)`; + case 'M': + return `${duration} Month(s)`; + case 'Y': + return `${duration} Year(s)`; + default: + return interval; + } + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 54afa72352..f1bfc57f3f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -77,6 +77,7 @@ import { BasketMerchantMessageComponent } from './components/basket/basket-merch import { BasketOrderReferenceComponent } from './components/basket/basket-order-reference/basket-order-reference.component'; import { BasketPromotionCodeComponent } from './components/basket/basket-promotion-code/basket-promotion-code.component'; import { BasketPromotionComponent } from './components/basket/basket-promotion/basket-promotion.component'; +import { BasketRecurrenceSummaryComponent } from './components/basket/basket-recurrence-summary/basket-recurrence-summary.component'; import { BasketShippingMethodComponent } from './components/basket/basket-shipping-method/basket-shipping-method.component'; import { BasketValidationItemsComponent } from './components/basket/basket-validation-items/basket-validation-items.component'; import { BasketValidationProductsComponent } from './components/basket/basket-validation-products/basket-validation-products.component'; @@ -116,6 +117,7 @@ import { IdentityProviderLoginComponent } from './components/login/identity-prov import { LoginFormComponent } from './components/login/login-form/login-form.component'; import { LoginModalComponent } from './components/login/login-modal/login-modal.component'; import { OrderListComponent } from './components/order/order-list/order-list.component'; +import { OrderRecurrenceComponent } from './components/order/order-recurrence/order-recurrence.component'; import { OrderWidgetComponent } from './components/order/order-widget/order-widget.component'; import { ProductAddToBasketComponent } from './components/product/product-add-to-basket/product-add-to-basket.component'; import { ProductAttachmentsComponent } from './components/product/product-attachments/product-attachments.component'; @@ -259,6 +261,7 @@ const exportedComponents = [ BasketItemsSummaryComponent, BasketMerchantMessageComponent, BasketMerchantMessageViewComponent, + BasketRecurrenceSummaryComponent, BasketOrderReferenceComponent, BasketPromotionCodeComponent, BasketPromotionComponent, @@ -286,6 +289,7 @@ const exportedComponents = [ ModalDialogComponent, ModalDialogLinkComponent, OrderListComponent, + OrderRecurrenceComponent, OrderWidgetComponent, ProductAddToBasketComponent, ProductAttachmentsComponent, diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index db6670f692..f0c2951ef9 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -592,9 +592,10 @@ "approval.details.conditions.budget_limit": "The requisition exceeds your budget.", "approval.details.conditions.cost_center": "The requisition is assigned to a cost center.", "approval.details.conditions.order_spend_limit": "The requisition exceeds your spending limit per order.", + "approval.details.conditions.recurring_order": "The requisition places a recurring order.", "approval.details.contacts.heading": "Approver contacts", "approval.details.cost_center_approvers.people_allowed": "The following person is allowed to approve your requisition, being assigned to cost center {{0}}:", - "approval.details.customer_approvers.people_allowed": "The following people are allowed to approve your requisition if the spending limit per order and/or budget limits are exceeded:", + "approval.details.customer_approvers.people_allowed": "The following people are allowed to approve your requisition if the spending limit per order and/or budget limits are exceeded or a recurring order is placed:", "approval.details.heading": "Approval details", "approval.details.place_order": "If you place a requisition requiring approval, an e-mail is sent to all people who are allowed to approve the requisition. You will be notified via e-mail once your requisition has been approved or rejected.", "approval.detailspage.approval.heading": "Approval details", @@ -745,8 +746,11 @@ "checkout.orderReferenceId.success.message": "Your customer order ID has been applied.", "checkout.orderReferenceId.title": "Enter a customer order ID", "checkout.order_details.heading": "Order summary", - "checkout.order_review.heading.text": "Review the details of your order below and make any changes if needed. Click \"Submit Order\" to complete your purchase.", + "checkout.order_review.heading.text": "Review the details of your order below and make any changes if needed. Click \"Submit order\" to complete your purchase.", "checkout.order_review.heading.title": "Review your order information", + "checkout.order_review.recurring.heading.text": "Review the details of your order below and make any changes if needed. Click \"Submit recurring order\" to complete your purchase.", + "checkout.order_review.recurring.heading.title": "Review your recurring order information", + "checkout.order_review.recurring.send.button": "Submit recurring order", "checkout.order_review.send.button": "Submit order", "checkout.order_summary.heading": "Order summary", "checkout.payment.addPayment.link": "Add payment instrument", @@ -824,6 +828,7 @@ "checkout.termsandconditions.details.title": "Terms & conditions", "checkout.update.label": "Edit {{0}}", "checkout.variation.edit.button.label": "Edit", + "checkout.widget.additional-information.heading": "Additional information", "checkout.widget.billing-address.heading": "Invoice address", "checkout.widget.buyer.TaxationID": "Taxation ID:", "checkout.widget.buyer.costcenter": "Cost center", @@ -927,6 +932,15 @@ "navigation.paging.previous_page.label": "Go to previous page", "number.decrease.text": "–", "number.increase.text": "+", + "order.recurrence.end.label": "End", + "order.recurrence.heading": "Recurring order", + "order.recurrence.interval.label": "Recur every", + "order.recurrence.period.days": "Day(s)", + "order.recurrence.period.months": "Month(s)", + "order.recurrence.period.weeks": "Week(s)", + "order.recurrence.period.years": "Year(s)", + "order.recurrence.repetitions.label": "Until", + "order.recurrence.start.label": "Start from", "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/styles/global/global.scss b/src/styles/global/global.scss index a9575a0be3..6eb62589f2 100644 --- a/src/styles/global/global.scss +++ b/src/styles/global/global.scss @@ -264,3 +264,7 @@ img.marketing { margin-bottom: ($space-default * 2); clear: both; } + +.hidden { + visibility: hidden; +} diff --git a/src/styles/pages/checkout/shopping-cart.scss b/src/styles/pages/checkout/shopping-cart.scss index 8dc9fc4a58..2e10ccefd5 100644 --- a/src/styles/pages/checkout/shopping-cart.scss +++ b/src/styles/pages/checkout/shopping-cart.scss @@ -99,7 +99,6 @@ .form-control { padding: 0; - background-color: inherit; } }