import {ComponentMeta} from "../ComponentsMeta"; import React from "react"; import {FieldsMap, FieldType} from "../fields"; import {NumberProps} from "../fields/Number"; import {__, CouponsPlus} from "../../globals"; import {TabsProps} from "../fields/tabs"; import {CardRenderer} from "../CardRenderers"; import {usingOfferOptions} from "./CardHelpers"; import {renderCurrencyValue} from "../fields/helpers"; import {CardItemSecondaryValue} from "../CardItemSecondaryValue"; import classNames from "classnames"; import {iconWithClassName} from "../IconWithClassName"; import {ListItem} from "./ListItem"; import {RowOptions} from "./Fields/RowOptions"; import {ComponentCategory} from "../components"; export enum DiscountScope { FilteredItems = 'filtereditems', CartSubtotal = 'cartsubtotal', } export type DiscountOptions = { type: 'percentage' | 'amount', amount: number, scope: DiscountScope, /** * This is the id of the root testable * * (!) This is exported/imported as a json representation of a NestedTestableNodeData */ /* customFilteredItemsSourceMap: string, // todo: this needs to be exported to json upon save and imported from json upon load */ // this is for the quantity limit: { isEnabled: boolean, // only when type is 'fixed' amount: number, // only when type is 'pattern' pattern: { discounted: number, groupSize: number, }, type: 'fixed' | 'half' | 'pattern' //orderBy: 'lowestprice' | 'highestprice' }, hardLimit: { isEnabled: boolean, amount: number, } } /* & ({ // for nested data customFilteredItemsSourceMap: TestableCompositeDataWithChildren } | { // for state data customFilteredItemsSourceMap: string } | { // for when no custom filtered items customFilteredItemsSourceMap: undefined }) */ export const DiscountMeta: ComponentMeta = { id: 'Discount', type: 'offers', category: ComponentCategory.CartItemDiscounts, priority: 1, description: ({suggestedScope}) => { if(suggestedScope === 'item_discounts') { return __('Select a discount that will be applied to all the filtered items.') } }, icon: iconWithClassName(({className, context}) => { if (context === 'card') { return } return }), fieldsMap: ({options: {scope, type, limit, hardLimit, customFilteredItemsSourceMap}}) => [ [ { id: 'type', renderType: FieldType.Tab, fieldProps: { dimUnselected: true, } as TabsProps, fieldOptions: {} } ], [ new RowOptions({ type: 'featured', colWidth: 'auto' }), { id: 'amount', renderType: FieldType.Number, fieldProps: { hasAmountStructure: false, type: type === 'percentage' ? 'percentage' : 'currency', border: false } as Partial } ], [ { id: 'scope', title: __('Apply to'), renderType: FieldType.Tab, fieldProps: { dimUnselected: true, direction: 'vertical', } as TabsProps, fieldOptions: {} }, ], scope as DiscountOptions['scope'] === DiscountScope.FilteredItems && CouponsPlus.implementations.offers.Discount.extra && [ { id: 'limit.isEnabled', renderType: FieldType.BooleanButton, title: __('Quantity Limit'), fieldOptions: { labels: { // above the button top: [ limit.type as DiscountOptions['limit']['type'] === 'pattern' ? __('The discount will be applied to * matching item(s) of every * matching from all the filtered items using a modular arithmetic algorithm. For example, with this setting of * every *, if a customer adds * matching items to the cart, the cheapest * will be discounted. If they add * matching items, the * cheapest will be discounted. If they add only * matching items, nothing will be discounted because this is designed to work in groups of *. The cheapest items will be discounted. regardless of the order in which they were added to the cart.').replace('*', limit.pattern.discounted.toString()).replace('*', limit.pattern.groupSize.toString()).replace('*', limit.pattern.discounted.toString()).replace('*', limit.pattern.groupSize.toString()).replace('*', limit.pattern.groupSize.toString()).replace('*', limit.pattern.discounted.toString()).replace('*', (limit.pattern.groupSize * 2).toString()).replace('*', (limit.pattern.discounted * 2).toString()).replace('*', (limit.pattern.groupSize - 1).toString()).replace('*', limit.pattern.groupSize) : undefined, limit.type as DiscountOptions['limit']['type'] === 'half' ? '' : undefined, limit.type as DiscountOptions['limit']['type'] === 'half' ? __('The discount will be applied to half (50%) of the items.').replace('*', limit.amount.toString()).replace('*', limit.amount.toString()).replace('*', limit.amount.toString()).replace('*', (limit.amount * 2).toString()) : undefined ].filter(a => a), // bellow the button bottom: [] }, fieldsMap: { true: [ [ { id: 'limit.type', renderType: FieldType.Tab, fieldProps: { size: 'extra-small', dimUnselected: true, } as TabsProps, fieldOptions: {} } ], ...((() => { switch (limit.type as DiscountOptions['limit']['type']) { case 'fixed': return [ [{ id: 'limit.amount', renderType: FieldType.Number, fieldProps: {hasAmountStructure: false} as Partial }] ] case 'pattern': return [ [ { id: 'limit.pattern.discounted', renderType: FieldType.Number, fieldProps: { hasAmountStructure: false, } as Partial, }, { id: '', renderType: FieldType.Custom, fieldProps: { component:
{ __('* of every * items').replace('*', limit.pattern.discounted) .replace('*', limit.pattern.groupSize) } { __('(buy * get *)').replace('*', limit.pattern.groupSize - limit.pattern.discounted) .replace('*', limit.pattern.discounted) }
} }, { id: 'limit.pattern.groupSize', renderType: FieldType.Number, fieldProps: { hasAmountStructure: false, } as Partial }, ] ]; } return [] })()) ].filter(a => { return a.length }) as FieldsMap } } } ], CouponsPlus.implementations.offers.Discount.extra && [ { id: 'hardLimit.isEnabled', renderType: FieldType.BooleanButton, title: __('Maximum Amount'), fieldOptions: { labels: { // above the button top: hardLimit.isEnabled ? [ scope as DiscountOptions['scope'] === 'cartsubtotal' ? __('The maximum discount that can be applied to the cart subtotal.') : undefined, scope as DiscountOptions['scope'] !== 'cartsubtotal' ? __('The maximum discount that can be applied to the cart for this offer.') : undefined, scope as DiscountOptions['scope'] !== 'cartsubtotal' ? __('The discount to apply to the cart will not exceed this amount. This is not a per-item limit.') : undefined, __('This limit applies to this offer only. It is calculated independently of all other offers & discounts from this and other coupons.') /*__('This limit applies only to this specific Discount offer, and is not affected by other offers from this and other coupons in the cart.')*/ ] : [].filter(a => a), // bellow the button bottom: [] }, fieldsMap: { true: [ [ { id: 'hardLimit', // the number field will save the value to hardLimit.amount by default. (see hasAmountStructure,) renderType: FieldType.Number, fieldProps: {type: 'currency'} as Partial } ] ] } } } ], ] } export const DiscountRenderer: CardRenderer = { type: DiscountMeta.id, prefix: "*", // example: __ ('Buy 1 t-shirt, get 1 free, Buy 2 t-shirts, get 1 at 50% off'), icon: DiscountMeta.icon!, AboveComputedValue: () => false, // No above computed value for this card ComputedValue: usingOfferOptions(({type, amount}, {color,}) => { const renderOfferMainValue = (value: string | number, type?: 'normal' | 'number' | 'percentage' | 'amount' | 'fixed-price') => { let off = OFF; return
{type === 'amount' &&
{
{renderCurrencyValue(value)}
} {off}
} {type === 'fixed-price' &&
For{
{renderCurrencyValue(value)}
}
} {!['amount'].includes(type) && value} {type === 'percentage' && % {off}}
; } const renderDiscountValue = () => { if (type === 'percentage') { return renderOfferMainValue(amount, 'percentage'); } if (type === 'amount') { return renderOfferMainValue(amount, 'amount'); } } //const cardData = getCardComponentData('offer', type); return
{false && -}
{false && -} {true && color.text(20) }}>{__('Discount')}} {
{renderDiscountValue()} {/* {interpolateElements(__(' {1} for {2} '), { 1: renderOfferMainValue(quantity), 2: renderDiscountValue(), })} */}
}
}), OutsideComputedValue: usingOfferOptions(({scope, limit, hardLimit}, {color,},) => { const text = { [DiscountScope.FilteredItems]: __('Applied to: [filtered items] '), [DiscountScope.CartSubtotal]: __('Applied to: [cart subtotal]'), }[scope]; const parts = text.split(/\[([^\]]+)\]/); return
    {parts.map((part, index) => { // Every second element (index 1, 3, 5...) will be the captured group if (index % 2 === 1) { return {scope === DiscountScope.FilteredItems && }{part}{scope === DiscountScope.FilteredItems && }; } return part; })} {limit.isEnabled && {__('Limited to: *').replace('*', { ['pattern']: __('* of every * items').replace('*', limit.pattern.discounted).replace('*', limit.pattern.groupSize), ['fixed']: __('* items').replace('*', limit.amount), ['half']: __('1/2 the items'), }[limit.type as DiscountOptions['limit']['type']])} } {hardLimit.isEnabled &&
    {__('Up to * off').split(/(\*)/).map((part, index) => { // Every second element (index 1, 3, 5...) will be the captured asterisk if (index % 2 === 1 && part === '*') { return {renderCurrencyValue(hardLimit.amount)}; } return part; })}
    }
}), }