diff --git a/docs/src/.vuepress/config.js b/docs/src/.vuepress/config.js index 445f117f85..64109039e3 100755 --- a/docs/src/.vuepress/config.js +++ b/docs/src/.vuepress/config.js @@ -97,6 +97,7 @@ module.exports = { '/lunar/carts', '/lunar/channels', '/lunar/collections', + '/lunar/discounts', '/lunar/currencies', '/lunar/customers', '/lunar/images', diff --git a/docs/src/lunar/discounts.md b/docs/src/lunar/discounts.md new file mode 100644 index 0000000000..07dd051c0a --- /dev/null +++ b/docs/src/lunar/discounts.md @@ -0,0 +1,185 @@ +# Discounts + +[[toc]] + +## Overview + +// ... + +## Discounts + +```php +Lunar\Models\Discount +``` + +|Field|Description|Example| +|:-|:-|:-| +|`id`||| +|`name`|The given name for the discount|| +|`handle`|The unique handle for the discount|| +|`type`|The type of discount|`Lunar\DiscountTypes\Coupon`| +|`data`|JSON|Any data to be used by the type class +|`starts_at`|The datetime the discount starts (required)| +|`ends_at`|The datetime the discount expires, if `NULL` it won't expire| +|`uses`|How many uses the discount has had| +|`max_uses`|The maximum times this discount can be applied storewide| +|`priority`|The order of priority| +|`stop`|Whether this discount will stop others after propagating| +|`created_at`||| +|`updated_at`||| + +### Creating a discount + +```php +Lunar\Models\Discount::create([ + 'name' => '20% Coupon', + 'handle' => '20_coupon', + 'type' => 'Lunar\DiscountTypes\Coupon', + 'data' => [ + 'coupon' => '20OFF', + 'min_prices' => [ + 'USD' => 2000 // $20 + ], + ], + 'starts_at' => '2022-06-17 13:30:55', + 'ends_at' => null, + 'max_uses' => null, +]) +``` + +## Discount Purchasable + +You can relate a purchasable to a discount via this model. Each has a type for whether it's a `condition` or `reward`. + +- `condition` - If your discount requires these purchasable models to be in the cart to activate +- `reward` - Once the conditions are met, discount one of more of these purchasable models. + +```php +Lunar\Models\DiscountPurchasable +``` + +|Field|Description|Example| +|:-|:-|:-| +|`id`||| +|`discount_id`||| +|`purchasable_type`||`Lunar\Models\ProductVariant` +|`type`|`condition` or `reward`| +|`created_at`||| +|`updated_at`||| + +### Relationships + +- Purchasables `discount_purchasables` +- Users - `customer_user` + + +## Usage + +Fetching applied discounts + +```php +use Lunar\Facades\Discounts; + +$appliedDiscounts = Discounts::getApplied(); +``` + +This will return a collection of discounts which are applied to the current cart. + +```php +foreach ($appliedDiscounts as $item) { + // Lunar\Base\DataTransferObjects\CartDiscount + $item->cartLine; // Lunar\Models\CartLine + $item->discount; // Lunar\Models\Discount +} +``` + +Each cart line will also have a `discount` property populated with the model of the applied discount. + +```php +foreach ($cart->lines as $line) { + $line->discount; // Lunar\Models\Discount; +} +``` + +:::tip +These aren't database relationships and will only persist for the lifecycle of the each request. +::: + + +### Adding your own Discount type + + +```php +namespace App\Discounts; + +use Lunar\Base\DataTransferObjects\CartDiscount; +use Lunar\DataTypes\Price; +use Lunar\Facades\Discounts; +use Lunar\Models\CartLine; +use Lunar\Models\Discount; + +class CustomDiscount +{ + protected Discount $discount; + + /** + * Set the data for the discount to user. + * + * @param array $data + * @return self + */ + public function with(Discount $discount): self + { + $this->discount = $discount; + + return $this; + } + + /** + * Return the name of the discount. + * + * @return string + */ + public function getName(): string + { + return 'Custom Discount'; + } + + /** + * Called just before cart totals are calculated. + * + * @return CartLine + */ + public function execute(CartLine $cartLine): CartLine + { + $data = $this->discount->data; + + // Return the unaltered cart line back + if (! $conditionIsMet) { + return $cartLine; + } + + $cartLine->discount = $this->discount; + + $cartLine->discountTotal = new Price( + $cartLine->unitPrice->value * $discountQuantity, + $cartLine->cart->currency, + 1 + ); + + Discounts::addApplied( + new CartDiscount($cartLine, $this->discount) + ); + + return $cartLine; + } +} + +``` + +```php +Discounts::addType( + CustomDiscount::class +); +``` + diff --git a/packages/admin/public/app.css b/packages/admin/public/app.css index 1bb7268177..be6b0d5c78 100644 --- a/packages/admin/public/app.css +++ b/packages/admin/public/app.css @@ -15,4 +15,4 @@ * Copyright (c) 2014, Jason Chen * Copyright (c) 2013, salesforce.com */.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container.ql-disabled .ql-editor ul[data-checked]>li:before{pointer-events:none}.ql-clipboard{height:1px;left:-100000px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{word-wrap:break-word;box-sizing:border-box;height:100%;line-height:1.42;outline:none;overflow-y:auto;padding:12px 15px;-o-tab-size:4;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap}.ql-editor>*{cursor:text}.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6,.ql-editor ol,.ql-editor p,.ql-editor pre,.ql-editor ul{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;margin:0;padding:0}.ql-editor ol,.ql-editor ul{padding-left:1.5em}.ql-editor ol>li,.ql-editor ul>li{list-style-type:none}.ql-editor ul>li:before{content:"\2022"}.ql-editor ul[data-checked=false],.ql-editor ul[data-checked=true]{pointer-events:none}.ql-editor ul[data-checked=false]>li *,.ql-editor ul[data-checked=true]>li *{pointer-events:all}.ql-editor ul[data-checked=false]>li:before,.ql-editor ul[data-checked=true]>li:before{color:#777;cursor:pointer;pointer-events:all}.ql-editor ul[data-checked=true]>li:before{content:"\2611"}.ql-editor ul[data-checked=false]>li:before{content:"\2610"}.ql-editor li:before{display:inline-block;white-space:nowrap;width:1.2em}.ql-editor li:not(.ql-direction-rtl):before{margin-left:-1.5em;margin-right:.3em;text-align:right}.ql-editor li.ql-direction-rtl:before{margin-left:.3em;margin-right:-1.5em}.ql-editor ol li:not(.ql-direction-rtl),.ql-editor ul li:not(.ql-direction-rtl){padding-left:1.5em}.ql-editor ol li.ql-direction-rtl,.ql-editor ul li.ql-direction-rtl{padding-right:1.5em}.ql-editor ol li{counter-increment:list-0;counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li:before{content:counter(list-0,decimal) ". "}.ql-editor ol li.ql-indent-1{counter-increment:list-1}.ql-editor ol li.ql-indent-1:before{content:counter(list-1,lower-alpha) ". "}.ql-editor ol li.ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-2{counter-increment:list-2}.ql-editor ol li.ql-indent-2:before{content:counter(list-2,lower-roman) ". "}.ql-editor ol li.ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-3{counter-increment:list-3}.ql-editor ol li.ql-indent-3:before{content:counter(list-3,decimal) ". "}.ql-editor ol li.ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-4{counter-increment:list-4}.ql-editor ol li.ql-indent-4:before{content:counter(list-4,lower-alpha) ". "}.ql-editor ol li.ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-5{counter-increment:list-5}.ql-editor ol li.ql-indent-5:before{content:counter(list-5,lower-roman) ". "}.ql-editor ol li.ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-6{counter-increment:list-6}.ql-editor ol li.ql-indent-6:before{content:counter(list-6,decimal) ". "}.ql-editor ol li.ql-indent-6{counter-reset:list-7 list-8 list-9}.ql-editor ol li.ql-indent-7{counter-increment:list-7}.ql-editor ol li.ql-indent-7:before{content:counter(list-7,lower-alpha) ". "}.ql-editor ol li.ql-indent-7{counter-reset:list-8 list-9}.ql-editor ol li.ql-indent-8{counter-increment:list-8}.ql-editor ol li.ql-indent-8:before{content:counter(list-8,lower-roman) ". "}.ql-editor ol li.ql-indent-8{counter-reset:list-9}.ql-editor ol li.ql-indent-9{counter-increment:list-9}.ql-editor ol li.ql-indent-9:before{content:counter(list-9,decimal) ". "}.ql-editor .ql-indent-1:not(.ql-direction-rtl){padding-left:3em}.ql-editor li.ql-indent-1:not(.ql-direction-rtl){padding-left:4.5em}.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:3em}.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:4.5em}.ql-editor .ql-indent-2:not(.ql-direction-rtl){padding-left:6em}.ql-editor li.ql-indent-2:not(.ql-direction-rtl){padding-left:7.5em}.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:6em}.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:7.5em}.ql-editor .ql-indent-3:not(.ql-direction-rtl){padding-left:9em}.ql-editor li.ql-indent-3:not(.ql-direction-rtl){padding-left:10.5em}.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:9em}.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:10.5em}.ql-editor .ql-indent-4:not(.ql-direction-rtl){padding-left:12em}.ql-editor li.ql-indent-4:not(.ql-direction-rtl){padding-left:13.5em}.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:12em}.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:13.5em}.ql-editor .ql-indent-5:not(.ql-direction-rtl){padding-left:15em}.ql-editor li.ql-indent-5:not(.ql-direction-rtl){padding-left:16.5em}.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:15em}.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:16.5em}.ql-editor .ql-indent-6:not(.ql-direction-rtl){padding-left:18em}.ql-editor li.ql-indent-6:not(.ql-direction-rtl){padding-left:19.5em}.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:18em}.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:19.5em}.ql-editor .ql-indent-7:not(.ql-direction-rtl){padding-left:21em}.ql-editor li.ql-indent-7:not(.ql-direction-rtl){padding-left:22.5em}.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:21em}.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:22.5em}.ql-editor .ql-indent-8:not(.ql-direction-rtl){padding-left:24em}.ql-editor li.ql-indent-8:not(.ql-direction-rtl){padding-left:25.5em}.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:24em}.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:25.5em}.ql-editor .ql-indent-9:not(.ql-direction-rtl){padding-left:27em}.ql-editor li.ql-indent-9:not(.ql-direction-rtl){padding-left:28.5em}.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:27em}.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:28.5em}.ql-editor .ql-video{display:block;max-width:100%}.ql-editor .ql-video.ql-align-center{margin:0 auto}.ql-editor .ql-video.ql-align-right{margin:0 0 0 auto}.ql-editor .ql-bg-black{background-color:#000}.ql-editor .ql-bg-red{background-color:#e60000}.ql-editor .ql-bg-orange{background-color:#f90}.ql-editor .ql-bg-yellow{background-color:#ff0}.ql-editor .ql-bg-green{background-color:#008a00}.ql-editor .ql-bg-blue{background-color:#06c}.ql-editor .ql-bg-purple{background-color:#93f}.ql-editor .ql-color-white{color:#fff}.ql-editor .ql-color-red{color:#e60000}.ql-editor .ql-color-orange{color:#f90}.ql-editor .ql-color-yellow{color:#ff0}.ql-editor .ql-color-green{color:#008a00}.ql-editor .ql-color-blue{color:#06c}.ql-editor .ql-color-purple{color:#93f}.ql-editor .ql-font-serif{font-family:Georgia,Times New Roman,serif}.ql-editor .ql-font-monospace{font-family:Monaco,Courier New,monospace}.ql-editor .ql-size-small{font-size:.75em}.ql-editor .ql-size-large{font-size:1.5em}.ql-editor .ql-size-huge{font-size:2.5em}.ql-editor .ql-direction-rtl{direction:rtl;text-align:inherit}.ql-editor .ql-align-center{text-align:center}.ql-editor .ql-align-justify{text-align:justify}.ql-editor .ql-align-right{text-align:right}.ql-editor.ql-blank:before{color:rgba(0,0,0,.6);content:attr(data-placeholder);font-style:italic;left:15px;pointer-events:none;position:absolute;right:15px}.ql-snow .ql-toolbar:after,.ql-snow.ql-toolbar:after{clear:both;content:"";display:table}.ql-snow .ql-toolbar button,.ql-snow.ql-toolbar button{background:none;border:none;cursor:pointer;display:inline-block;float:left;height:24px;padding:3px 5px;width:28px}.ql-snow .ql-toolbar button svg,.ql-snow.ql-toolbar button svg{float:left;height:100%}.ql-snow .ql-toolbar button:active:hover,.ql-snow.ql-toolbar button:active:hover{outline:none}.ql-snow .ql-toolbar input.ql-image[type=file],.ql-snow.ql-toolbar input.ql-image[type=file]{display:none}.ql-snow .ql-toolbar .ql-picker-item.ql-selected,.ql-snow .ql-toolbar .ql-picker-item:hover,.ql-snow .ql-toolbar .ql-picker-label.ql-active,.ql-snow .ql-toolbar .ql-picker-label:hover,.ql-snow .ql-toolbar button.ql-active,.ql-snow .ql-toolbar button:focus,.ql-snow .ql-toolbar button:hover,.ql-snow.ql-toolbar .ql-picker-item.ql-selected,.ql-snow.ql-toolbar .ql-picker-item:hover,.ql-snow.ql-toolbar .ql-picker-label.ql-active,.ql-snow.ql-toolbar .ql-picker-label:hover,.ql-snow.ql-toolbar button.ql-active,.ql-snow.ql-toolbar button:focus,.ql-snow.ql-toolbar button:hover{color:#06c}.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:focus .ql-fill,.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover .ql-fill,.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:focus .ql-fill,.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:hover .ql-fill,.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill{fill:#06c}.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow .ql-toolbar button.ql-active .ql-stroke,.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar button:focus .ql-stroke,.ql-snow .ql-toolbar button:focus .ql-stroke-miter,.ql-snow .ql-toolbar button:hover .ql-stroke,.ql-snow .ql-toolbar button:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow.ql-toolbar button.ql-active .ql-stroke,.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar button:focus .ql-stroke,.ql-snow.ql-toolbar button:focus .ql-stroke-miter,.ql-snow.ql-toolbar button:hover .ql-stroke,.ql-snow.ql-toolbar button:hover .ql-stroke-miter{stroke:#06c}@media (pointer:coarse){.ql-snow .ql-toolbar button:hover:not(.ql-active),.ql-snow.ql-toolbar button:hover:not(.ql-active){color:#444}.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill{fill:#444}.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter{stroke:#444}}.ql-snow,.ql-snow *{box-sizing:border-box}.ql-snow .ql-hidden{display:none}.ql-snow .ql-out-bottom,.ql-snow .ql-out-top{visibility:hidden}.ql-snow .ql-tooltip{position:absolute;transform:translateY(10px)}.ql-snow .ql-tooltip a{cursor:pointer;text-decoration:none}.ql-snow .ql-tooltip.ql-flip{transform:translateY(-10px)}.ql-snow .ql-formats{display:inline-block;vertical-align:middle}.ql-snow .ql-formats:after{clear:both;content:"";display:table}.ql-snow .ql-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ql-snow .ql-stroke-miter{fill:none;stroke:#444;stroke-miterlimit:10;stroke-width:2}.ql-snow .ql-fill,.ql-snow .ql-stroke.ql-fill{fill:#444}.ql-snow .ql-empty{fill:none}.ql-snow .ql-even{fill-rule:evenodd}.ql-snow .ql-stroke.ql-thin,.ql-snow .ql-thin{stroke-width:1}.ql-snow .ql-transparent{opacity:.4}.ql-snow .ql-direction svg:last-child{display:none}.ql-snow .ql-direction.ql-active svg:last-child{display:inline}.ql-snow .ql-direction.ql-active svg:first-child{display:none}.ql-snow .ql-editor h1{font-size:2em}.ql-snow .ql-editor h2{font-size:1.5em}.ql-snow .ql-editor h3{font-size:1.17em}.ql-snow .ql-editor h4{font-size:1em}.ql-snow .ql-editor h5{font-size:.83em}.ql-snow .ql-editor h6{font-size:.67em}.ql-snow .ql-editor a{text-decoration:underline}.ql-snow .ql-editor blockquote{border-left:4px solid #ccc;margin-bottom:5px;margin-top:5px;padding-left:16px}.ql-snow .ql-editor code,.ql-snow .ql-editor pre{background-color:#f0f0f0;border-radius:3px}.ql-snow .ql-editor pre{margin-bottom:5px;margin-top:5px;padding:5px 10px;white-space:pre-wrap}.ql-snow .ql-editor code{font-size:85%;padding:2px 4px}.ql-snow .ql-editor pre.ql-syntax{background-color:#23241f;color:#f8f8f2;overflow:visible}.ql-snow .ql-editor img{max-width:100%}.ql-snow .ql-picker{color:#444;display:inline-block;float:left;font-size:14px;font-weight:500;height:24px;position:relative;vertical-align:middle}.ql-snow .ql-picker-label{cursor:pointer;display:inline-block;height:100%;padding-left:8px;padding-right:2px;position:relative;width:100%}.ql-snow .ql-picker-label:before{display:inline-block;line-height:22px}.ql-snow .ql-picker-options{background-color:#fff;display:none;min-width:100%;padding:4px 8px;position:absolute;white-space:nowrap}.ql-snow .ql-picker-options .ql-picker-item{cursor:pointer;display:block;padding-bottom:5px;padding-top:5px}.ql-snow .ql-picker.ql-expanded .ql-picker-label{color:#ccc;z-index:2}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill{fill:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke{stroke:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-options{display:block;margin-top:-1px;top:100%;z-index:1}.ql-snow .ql-color-picker,.ql-snow .ql-icon-picker{width:28px}.ql-snow .ql-color-picker .ql-picker-label,.ql-snow .ql-icon-picker .ql-picker-label{padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-label svg,.ql-snow .ql-icon-picker .ql-picker-label svg{right:4px}.ql-snow .ql-icon-picker .ql-picker-options{padding:4px 0}.ql-snow .ql-icon-picker .ql-picker-item{height:24px;padding:2px 4px;width:24px}.ql-snow .ql-color-picker .ql-picker-options{padding:3px 5px;width:152px}.ql-snow .ql-color-picker .ql-picker-item{border:1px solid transparent;float:left;height:16px;margin:2px;padding:0;width:16px}.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{margin-top:-9px;position:absolute;right:0;top:50%;width:18px}.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=""]):before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=""]):before{content:attr(data-label)}.ql-snow .ql-picker.ql-header{width:98px}.ql-snow .ql-picker.ql-header .ql-picker-item:before,.ql-snow .ql-picker.ql-header .ql-picker-label:before{content:"Normal"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]:before{content:"Heading 1"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]:before{content:"Heading 2"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]:before{content:"Heading 3"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]:before{content:"Heading 4"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]:before{content:"Heading 5"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before,.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]:before{content:"Heading 6"}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]:before{font-size:2em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]:before{font-size:1.5em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]:before{font-size:1.17em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]:before{font-size:1em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]:before{font-size:.83em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]:before{font-size:.67em}.ql-snow .ql-picker.ql-font{width:108px}.ql-snow .ql-picker.ql-font .ql-picker-item:before,.ql-snow .ql-picker.ql-font .ql-picker-label:before{content:"Sans Serif"}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]:before{content:"Serif"}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]:before{content:"Monospace"}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]:before{font-family:Georgia,Times New Roman,serif}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]:before{font-family:Monaco,Courier New,monospace}.ql-snow .ql-picker.ql-size{width:98px}.ql-snow .ql-picker.ql-size .ql-picker-item:before,.ql-snow .ql-picker.ql-size .ql-picker-label:before{content:"Normal"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]:before{content:"Small"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]:before{content:"Large"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]:before{content:"Huge"}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]:before{font-size:10px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]:before{font-size:18px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]:before{font-size:32px}.ql-snow .ql-color-picker.ql-background .ql-picker-item{background-color:#fff}.ql-snow .ql-color-picker.ql-color .ql-picker-item{background-color:#000}.ql-toolbar.ql-snow{border:1px solid #ccc;box-sizing:border-box;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;padding:8px}.ql-toolbar.ql-snow .ql-formats{margin-right:15px}.ql-toolbar.ql-snow .ql-picker-label{border:1px solid transparent}.ql-toolbar.ql-snow .ql-picker-options{border:1px solid transparent;box-shadow:0 2px 8px rgba(0,0,0,.2)}.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label,.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options{border-color:#ccc}.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover{border-color:#000}.ql-toolbar.ql-snow+.ql-container.ql-snow{border-top:0}.ql-snow .ql-tooltip{background-color:#fff;border:1px solid #ccc;box-shadow:0 0 5px #ddd;color:#444;padding:5px 12px;white-space:nowrap}.ql-snow .ql-tooltip:before{content:"Visit URL:";line-height:26px;margin-right:8px}.ql-snow .ql-tooltip input[type=text]{border:1px solid #ccc;display:none;font-size:13px;height:26px;margin:0;padding:3px 5px;width:170px}.ql-snow .ql-tooltip a.ql-preview{display:inline-block;max-width:200px;overflow-x:hidden;text-overflow:ellipsis;vertical-align:top}.ql-snow .ql-tooltip a.ql-action:after{border-right:1px solid #ccc;content:"Edit";margin-left:16px;padding-right:8px}.ql-snow .ql-tooltip a.ql-remove:before{content:"Remove";margin-left:8px}.ql-snow .ql-tooltip a{line-height:26px}.ql-snow .ql-tooltip.ql-editing a.ql-preview,.ql-snow .ql-tooltip.ql-editing a.ql-remove{display:none}.ql-snow .ql-tooltip.ql-editing input[type=text]{display:inline-block}.ql-snow .ql-tooltip.ql-editing a.ql-action:after{border-right:0;content:"Save";padding-right:0}.ql-snow .ql-tooltip[data-mode=link]:before{content:"Enter link:"}.ql-snow .ql-tooltip[data-mode=formula]:before{content:"Enter formula:"}.ql-snow .ql-tooltip[data-mode=video]:before{content:"Enter video:"}.ql-snow a{color:#06c}.ql-container.ql-snow{border:1px solid #ccc} -/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:Nunito,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.form-input,.form-multiselect,.form-select,.form-textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.form-input::-webkit-datetime-edit-fields-wrapper{padding:0}.form-input::-webkit-date-and-time-value{min-height:1.5em}.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}.form-select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem}.form-checkbox,.form-radio,.form-select{color-adjust:exact;-webkit-print-color-adjust:exact}.form-checkbox,.form-radio{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}.form-checkbox{border-radius:0}.form-checkbox:focus,.form-radio:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}.form-checkbox:checked,.form-radio:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:transparent}.form-checkbox:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}.form-checkbox:checked:focus,.form-checkbox:checked:hover,.form-radio:checked:focus,.form-radio:checked:hover{background-color:currentColor;border-color:transparent}.form-checkbox:indeterminate{background-color:currentColor;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:transparent}.form-checkbox:indeterminate:focus,.form-checkbox:indeterminate:hover{background-color:currentColor;border-color:transparent}.menu-link{align-items:center;border-radius:.25rem;display:flex;gap:.5rem;padding:.5rem;position:relative}.menu-link--active{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity));color:rgb(29 78 216/var(--tw-text-opacity))}.dark .menu-link--active{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.menu-link--inactive{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.menu-link--inactive:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity));color:rgb(29 78 216/var(--tw-text-opacity))}.dark .menu-link--inactive{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .menu-link--inactive:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{bottom:0;top:0}.inset-0,.inset-x-0{left:0;right:0}.inset-y-0{bottom:0;top:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.bottom-0{bottom:0}.left-10{left:2.5rem}.left-full{left:100%}.top-6{top:1.5rem}.top-8{top:2rem}.top-2{top:.5rem}.right-2{right:.5rem}.left-5{left:1.25rem}.top-\[2px\]{top:2px}.-left-\[calc\(0\.75rem_-_1px\)\]{left:calc(-.75rem - -1px)}.-left-\[calc\(0\.5rem_-_1px\)\]{left:calc(-.5rem - -1px)}.left-3{left:.75rem}.top-5{top:1.25rem}.-bottom-0\.5{bottom:-.125rem}.-right-1{right:-.25rem}.-bottom-0{bottom:0}.top-full{top:100%}.right-4{right:1rem}.-right-\[13px\]{right:-13px}.bottom-16{bottom:4rem}.bottom-12{bottom:3rem}.top-3{top:.75rem}.left-2{left:.5rem}.z-50{z-index:50}.z-75{z-index:75}.z-40{z-index:40}.z-10{z-index:10}.z-0{z-index:0}.order-3{order:3}.order-2{order:2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-span-2{grid-column:span 2/span 2}.col-span-7{grid-column:span 7/span 7}.col-span-12{grid-column:span 12/span 12}.col-span-9{grid-column:span 9/span 9}.col-span-11{grid-column:span 11/span 11}.col-span-1{grid-column:span 1/span 1}.col-span-5{grid-column:span 5/span 5}.\!m-0{margin:0!important}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-bottom:.25rem;margin-top:.25rem}.-my-8{margin-bottom:-2rem;margin-top:-2rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mt-12{margin-top:3rem}.mt-6{margin-top:1.5rem}.ml-3{margin-left:.75rem}.ml-2{margin-left:.5rem}.-mr-1{margin-right:-.25rem}.mt-2{margin-top:.5rem}.mt-1{margin-top:.25rem}.-mt-10{margin-top:-2.5rem}.ml-4{margin-left:1rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mr-3{margin-right:.75rem}.mr-2{margin-right:.5rem}.-ml-\[5px\]{margin-left:-5px}.ml-5{margin-left:1.25rem}.ml-8{margin-left:2rem}.mt-0\.5{margin-top:.125rem}.mt-0{margin-top:0}.-ml-px{margin-left:-1px}.mr-4{margin-right:1rem}.-mt-2{margin-top:-.5rem}.ml-1{margin-left:.25rem}.-mt-7{margin-top:-1.75rem}.mr-1{margin-right:.25rem}.ml-2\.5{margin-left:.625rem}.-mt-24{margin-top:-6rem}.mb-24{margin-bottom:6rem}.mb-4{margin-bottom:1rem}.ml-7{margin-left:1.75rem}.ml-auto{margin-left:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.aspect-\[16\/9\]{aspect-ratio:16/9}.h-screen{height:100vh}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-4{height:1rem}.h-full{height:100%}.h-7{height:1.75rem}.h-16{height:4rem}.h-24{height:6rem}.h-12{height:3rem}.h-96{height:24rem}.h-80{height:20rem}.h-8{height:2rem}.h-3{height:.75rem}.h-10{height:2.5rem}.h-\[58px\]{height:58px}.h-\[42px\]{height:42px}.max-h-96{max-height:24rem}.max-h-60{max-height:15rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.min-h-screen{min-height:100vh}.w-full{width:100%}.w-32{width:8rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-4{width:1rem}.w-screen{width:100vw}.w-16{width:4rem}.w-12{width:3rem}.w-0{width:0}.w-28{width:7rem}.w-7{width:1.75rem}.w-64{width:16rem}.w-auto{width:auto}.w-8{width:2rem}.w-11{width:2.75rem}.w-72{width:18rem}.w-3{width:.75rem}.w-10{width:2.5rem}.w-\[2px\]{width:2px}.w-0\.5{width:.125rem}.w-px{width:1px}.w-36{width:9rem}.w-20{width:5rem}.w-48{width:12rem}.w-80{width:20rem}.w-1\/3{width:33.333333%}.w-44{width:11rem}.min-w-full{min-width:100%}.min-w-0{min-width:0}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.max-w-2xl{max-width:42rem}.max-w-screen-2xl{max-width:1536px}.max-w-7xl{max-width:80rem}.max-w-\[200px\]{max-width:200px}.\!max-w-4xl{max-width:56rem!important}.max-w-lg{max-width:32rem}.max-w-\[40ch\]{max-width:40ch}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-initial{flex:0 1 auto}.flex-none{flex:none}.flex-shrink-0,.shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.basis-1\/4{flex-basis:25%}.basis-2\/3{flex-basis:66.666667%}.basis-1\/3{flex-basis:33.333333%}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-top-right{transform-origin:top right}.origin-bottom-right{transform-origin:bottom right}.translate-y-2{--tw-translate-y:0.5rem}.translate-y-0,.translate-y-2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.translate-x-5,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x:1.25rem}.-translate-y-px{--tw-translate-y:-1px}.-translate-y-px,.rotate-180{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.-rotate-180{--tw-rotate:-180deg}.-rotate-180,.-rotate-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate:-90deg}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-100,.scale-95{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-150{--tw-scale-x:1.5;--tw-scale-y:1.5;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-not-allowed{cursor:not-allowed}.cursor-grab{cursor:grab}.cursor-default{cursor:default}.cursor-help{cursor:help}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.place-items-start{place-items:start}.place-items-end{place-items:end}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-start{justify-items:start}.justify-items-end{justify-items:end}.justify-items-center{justify-items:center}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-1{gap:.25rem}.gap-8{gap:2rem}.gap-0\.5{gap:.125rem}.gap-0{gap:0}.gap-6{gap:1.5rem}.gap-5{gap:1.25rem}.gap-1\.5{gap:.375rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-y-2>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(2px*var(--tw-divide-y-reverse));border-top-width:calc(2px*(1 - var(--tw-divide-y-reverse)))}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;border-left-width:calc(1px*(1 - var(--tw-divide-x-reverse)));border-right-width:calc(1px*var(--tw-divide-x-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.divide-green-600>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(22 163 74/var(--tw-divide-opacity))}.divide-yellow-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(254 240 138/var(--tw-divide-opacity))}.divide-yellow-600>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(202 138 4/var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.rounded{border-radius:.25rem}.rounded-none{border-radius:0}.rounded-b{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-b-lg{border-bottom-left-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-l-none{border-bottom-left-radius:0;border-top-left-radius:0}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-tl{border-top-left-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-t-0{border-top-width:0}.border-l{border-left-width:1px}.border-dashed{border-style:dashed}.border-hidden{border-style:hidden}.border-none{border-style:none}.border-transparent{border-color:transparent}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-slate-100{--tw-border-opacity:1;border-color:rgb(241 245 249/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity))}.border-red-100{--tw-border-opacity:1;border-color:rgb(254 226 226/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity))}.border-orange-300{--tw-border-opacity:1;border-color:rgb(253 186 116/var(--tw-border-opacity))}.border-indigo-300{--tw-border-opacity:1;border-color:rgb(165 180 252/var(--tw-border-opacity))}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-gray-600\/75{background-color:rgba(75,85,99,.75)}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity))}.bg-red-800{--tw-bg-opacity:1;background-color:rgb(153 27 27/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(243 232 255/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.\!bg-blue-500{--tw-bg-opacity:1!important;background-color:rgb(59 130 246/var(--tw-bg-opacity))!important}.\!bg-purple-500{--tw-bg-opacity:1!important;background-color:rgb(168 85 247/var(--tw-bg-opacity))!important}.\!bg-teal-500{--tw-bg-opacity:1!important;background-color:rgb(20 184 166/var(--tw-bg-opacity))!important}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.bg-white\/75{background-color:hsla(0,0%,100%,.75)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-opacity-5{--tw-bg-opacity:0.05}.object-cover{-o-object-fit:cover;object-fit:cover}.p-4{padding:1rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.p-8{padding:2rem}.p-3{padding:.75rem}.p-12{padding:3rem}.p-6{padding:1.5rem}.p-0\.5{padding:.125rem}.p-0{padding:0}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-0{padding-bottom:0;padding-top:0}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-16{padding-bottom:4rem;padding-top:4rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.py-px{padding-bottom:1px;padding-top:1px}.px-0{padding-left:0;padding-right:0}.px-1{padding-left:.25rem;padding-right:.25rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-10{padding-bottom:2.5rem;padding-top:2.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.pl-3{padding-left:.75rem}.pr-10{padding-right:2.5rem}.pt-6{padding-top:1.5rem}.pl-10{padding-left:2.5rem}.pb-20{padding-bottom:5rem}.pr-0{padding-right:0}.pb-2{padding-bottom:.5rem}.pt-4{padding-top:1rem}.pl-2{padding-left:.5rem}.pr-8{padding-right:2rem}.pr-14{padding-right:3.5rem}.pr-7{padding-right:1.75rem}.pl-7{padding-left:1.75rem}.pr-2{padding-right:.5rem}.pr-12{padding-right:3rem}.pr-3{padding-right:.75rem}.pl-4{padding-left:1rem}.pr-32{padding-right:8rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.pl-8{padding-left:2rem}.pt-\[5px\]{padding-top:5px}.pt-\[1px\]{padding-top:1px}.pb-8{padding-bottom:2rem}.pl-24{padding-left:6rem}.pr-4{padding-right:1rem}.pr-9{padding-right:2.25rem}.pb-24{padding-bottom:6rem}.pl-6{padding-left:1.5rem}.pt-12{padding-top:3rem}.pb-6{padding-bottom:1.5rem}.pb-28{padding-bottom:7rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.not-italic{font-style:normal}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-none{line-height:1}.leading-\[42px\]{line-height:42px}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity))}.text-indigo-700{--tw-text-opacity:1;color:rgb(67 56 202/var(--tw-text-opacity))}.\!text-red-600{--tw-text-opacity:1!important;color:rgb(220 38 38/var(--tw-text-opacity))!important}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity))}.text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.opacity-50{opacity:.5}.opacity-10{opacity:.1}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.shadow,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-blue-500\/25{--tw-shadow-color:rgba(59,130,246,.25);--tw-shadow:var(--tw-shadow-colored)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-4{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-4,.ring-8{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-8{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.\!ring-blue-100{--tw-ring-opacity:1!important;--tw-ring-color:rgb(219 234 254/var(--tw-ring-opacity))!important}.\!ring-purple-100{--tw-ring-opacity:1!important;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity))!important}.\!ring-teal-100{--tw-ring-opacity:1!important;--tw-ring-color:rgb(204 251 241/var(--tw-ring-opacity))!important}.ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur-3xl{--tw-blur:blur(64px)}.blur-3xl,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-75{transition-duration:75ms}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-200{transition-duration:.2s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}p>code{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity));color:rgb(147 51 234/var(--tw-text-opacity));font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.875rem;line-height:1.25rem}[x-cloak]{display:none}.sortable-ghost .sort-item-element{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity));border-color:rgb(147 197 253/var(--tw-border-opacity));color:rgb(37 99 235/var(--tw-text-opacity))}.ql-editor>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.app-sidemenu-expanded [data-ref=layout-sidemenu]{width:16rem}.before\:mx-3:before{content:var(--tw-content);margin-left:.75rem;margin-right:.75rem}.before\:text-gray-200:before{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity));content:var(--tw-content)}.before\:content-\[\'\|\'\]:before{--tw-content:"|";content:var(--tw-content)}.even\:bg-gray-50:nth-child(2n){--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.focus-within\:outline-none:focus-within{outline:2px solid transparent;outline-offset:2px}.focus-within\:ring-2:focus-within{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-within\:ring-indigo-500:focus-within{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus-within\:ring-offset-2:focus-within{--tw-ring-offset-width:2px}.hover\:border-gray-100:hover{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.hover\:border-gray-200:hover{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:border-gray-400:hover{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:bg-blue-50:hover{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.hover\:bg-indigo-500:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-blue-500:hover{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.hover\:bg-yellow-600:hover{--tw-bg-opacity:1;background-color:rgb(202 138 4/var(--tw-bg-opacity))}.hover\:bg-green-50:hover{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-indigo-500:hover{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.hover\:text-purple-900:hover{--tw-text-opacity:1;color:rgb(88 28 135/var(--tw-text-opacity))}.hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:appearance-none:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:border-transparent:focus{border-color:transparent}.focus\:underline:focus{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}.focus\:ring-green-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(34 197 94/var(--tw-ring-opacity))}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68/var(--tw-ring-opacity))}.focus\:ring-gray-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(156 163 175/var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-white:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}.focus\:ring-yellow-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(234 179 8/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus\:ring-offset-gray-100:focus{--tw-ring-offset-color:#f3f4f6}.focus\:ring-offset-gray-50:focus{--tw-ring-offset-color:#f9fafb}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:visible{visibility:visible}.group:hover .group-hover\:text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.group:hover .group-hover\:opacity-0{opacity:0}.group:hover .group-hover\:opacity-100{opacity:1}.peer:checked~.peer-checked\:border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.peer:checked~.peer-checked\:bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.peer:checked~.peer-checked\:text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity))}.dark .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark .dark\:border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark .dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .dark\:shadow-blue-500\/10{--tw-shadow-color:rgba(59,130,246,.1);--tw-shadow:var(--tw-shadow-colored)}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark .dark\:hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:focus\:ring-offset-gray-800:focus{--tw-ring-offset-color:#1f2937}@media (min-width:640px){.sm\:order-2{order:2}.sm\:order-3{order:3}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:mt-0{margin-top:0}.sm\:ml-2{margin-left:.5rem}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:w-full{width:100%}.sm\:w-auto{width:auto}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-md{max-width:28rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-xl{max-width:36rem}.sm\:max-w-2xl{max-width:42rem}.sm\:flex-1{flex:1 1 0%}.sm\:flex-auto{flex:1 1 auto}.sm\:flex-initial{flex:0 1 auto}.sm\:flex-none{flex:none}.sm\:translate-y-0{--tw-translate-y:0px}.sm\:translate-x-2,.sm\:translate-y-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:translate-x-2{--tw-translate-x:0.5rem}.sm\:translate-x-0{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:place-items-start{place-items:start}.sm\:place-items-end{place-items:end}.sm\:place-items-center{place-items:center}.sm\:items-start{align-items:flex-start}.sm\:items-end{align-items:flex-end}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-end{justify-content:flex-end}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:justify-items-start{justify-items:start}.sm\:justify-items-end{justify-items:end}.sm\:justify-items-center{justify-items:center}.sm\:gap-4{gap:1rem}.sm\:overflow-hidden{overflow:hidden}.sm\:overflow-x-hidden{overflow-x:hidden}.sm\:overflow-y-hidden{overflow-y:hidden}.sm\:rounded-lg{border-radius:.5rem}.sm\:rounded-md{border-radius:.375rem}.sm\:border-hidden{border-style:hidden}.sm\:p-6{padding:1.5rem}.sm\:p-1{padding:.25rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-12{padding-bottom:3rem;padding-top:3rem}.sm\:px-8{padding-left:2rem;padding-right:2rem}.sm\:pb-5{padding-bottom:1.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:duration-300{transition-duration:.3s}}@media (min-width:768px){.md\:col-span-6{grid-column:span 6/span 6}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:flex-1{flex:1 1 0%}.md\:flex-auto{flex:1 1 auto}.md\:flex-initial{flex:0 1 auto}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:place-items-start{place-items:start}.md\:place-items-end{place-items:end}.md\:place-items-center{place-items:center}.md\:items-start{align-items:flex-start}.md\:items-end{align-items:flex-end}.md\:items-center{align-items:center}.md\:justify-start{justify-content:flex-start}.md\:justify-end{justify-content:flex-end}.md\:justify-between{justify-content:space-between}.md\:justify-items-start{justify-items:start}.md\:justify-items-end{justify-items:end}.md\:justify-items-center{justify-items:center}.md\:gap-4{gap:1rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.md\:overflow-hidden{overflow:hidden}.md\:overflow-x-hidden{overflow-x:hidden}.md\:overflow-y-hidden{overflow-y:hidden}.md\:border-hidden{border-style:hidden}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}.md\:text-2xl{font-size:1.5rem;line-height:2rem}}@media (min-width:1024px){.lg\:left-auto{left:auto}.lg\:order-last{order:9999}.lg\:order-first{order:-9999}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-\[calc\(100vw_-_16rem\)\]{width:calc(100vw - 16rem)}.lg\:w-\[calc\(100vw_-_5rem\)\]{width:calc(100vw - 5rem)}.lg\:flex-1{flex:1 1 0%}.lg\:flex-auto{flex:1 1 auto}.lg\:flex-initial{flex:0 1 auto}.lg\:flex-none{flex:none}.lg\:flex-shrink-0{flex-shrink:0}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:place-items-start{place-items:start}.lg\:place-items-end{place-items:end}.lg\:place-items-center{place-items:center}.lg\:items-start{align-items:flex-start}.lg\:items-end{align-items:flex-end}.lg\:items-center{align-items:center}.lg\:justify-start{justify-content:flex-start}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:justify-items-start{justify-items:start}.lg\:justify-items-end{justify-items:end}.lg\:justify-items-center{justify-items:center}.lg\:gap-8{gap:2rem}.lg\:overflow-hidden{overflow:hidden}.lg\:overflow-x-hidden{overflow-x:hidden}.lg\:overflow-y-hidden{overflow-y:hidden}.lg\:border-hidden{border-style:hidden}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:order-last{order:9999}.xl\:mt-0{margin-top:0}.xl\:flex{display:flex}.xl\:w-1\/3{width:33.333333%}.xl\:w-2\/3{width:66.666667%}.xl\:flex-row-reverse{flex-direction:row-reverse}.xl\:items-start{align-items:flex-start}.xl\:justify-end{justify-content:flex-end}.xl\:justify-between{justify-content:space-between}.xl\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.xl\:pl-0{padding-left:0}} +/*! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:Nunito,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.form-input,.form-multiselect,.form-select,.form-textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}.form-input:focus,.form-multiselect:focus,.form-select:focus,.form-textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}.form-input::-moz-placeholder,.form-textarea::-moz-placeholder{color:#6b7280;opacity:1}.form-input::placeholder,.form-textarea::placeholder{color:#6b7280;opacity:1}.form-input::-webkit-datetime-edit-fields-wrapper{padding:0}.form-input::-webkit-date-and-time-value{min-height:1.5em}.form-input::-webkit-datetime-edit,.form-input::-webkit-datetime-edit-day-field,.form-input::-webkit-datetime-edit-hour-field,.form-input::-webkit-datetime-edit-meridiem-field,.form-input::-webkit-datetime-edit-millisecond-field,.form-input::-webkit-datetime-edit-minute-field,.form-input::-webkit-datetime-edit-month-field,.form-input::-webkit-datetime-edit-second-field,.form-input::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}.form-select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem}.form-checkbox,.form-radio,.form-select{color-adjust:exact;-webkit-print-color-adjust:exact}.form-checkbox,.form-radio{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}.form-checkbox{border-radius:0}.form-checkbox:focus,.form-radio:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}.form-checkbox:checked,.form-radio:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:transparent}.form-checkbox:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}.form-checkbox:checked:focus,.form-checkbox:checked:hover,.form-radio:checked:focus,.form-radio:checked:hover{background-color:currentColor;border-color:transparent}.form-checkbox:indeterminate{background-color:currentColor;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:transparent}.form-checkbox:indeterminate:focus,.form-checkbox:indeterminate:hover{background-color:currentColor;border-color:transparent}.menu-link{align-items:center;border-radius:.25rem;display:flex;gap:.5rem;padding:.5rem;position:relative}.menu-link--active{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity));color:rgb(29 78 216/var(--tw-text-opacity))}.dark .menu-link--active{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.menu-link--inactive{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.menu-link--inactive:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity));color:rgb(29 78 216/var(--tw-text-opacity))}.dark .menu-link--inactive{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .menu-link--inactive:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{bottom:0;top:0}.inset-0,.inset-x-0{left:0;right:0}.inset-y-0{bottom:0;top:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.bottom-0{bottom:0}.left-10{left:2.5rem}.left-full{left:100%}.top-6{top:1.5rem}.top-8{top:2rem}.top-2{top:.5rem}.right-2{right:.5rem}.left-5{left:1.25rem}.top-\[2px\]{top:2px}.-left-\[calc\(0\.75rem_-_1px\)\]{left:calc(-.75rem - -1px)}.-left-\[calc\(0\.5rem_-_1px\)\]{left:calc(-.5rem - -1px)}.left-3{left:.75rem}.top-5{top:1.25rem}.-bottom-0\.5{bottom:-.125rem}.-right-1{right:-.25rem}.-bottom-0{bottom:0}.top-full{top:100%}.right-4{right:1rem}.-right-\[13px\]{right:-13px}.bottom-16{bottom:4rem}.bottom-12{bottom:3rem}.top-3{top:.75rem}.left-2{left:.5rem}.z-50{z-index:50}.z-75{z-index:75}.z-40{z-index:40}.z-10{z-index:10}.z-0{z-index:0}.order-3{order:3}.order-2{order:2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-span-2{grid-column:span 2/span 2}.col-span-7{grid-column:span 7/span 7}.col-span-12{grid-column:span 12/span 12}.col-span-9{grid-column:span 9/span 9}.col-span-11{grid-column:span 11/span 11}.col-span-1{grid-column:span 1/span 1}.col-span-5{grid-column:span 5/span 5}.\!m-0{margin:0!important}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-bottom:.25rem;margin-top:.25rem}.-my-8{margin-bottom:-2rem;margin-top:-2rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mt-12{margin-top:3rem}.mt-6{margin-top:1.5rem}.ml-3{margin-left:.75rem}.ml-2{margin-left:.5rem}.-mr-1{margin-right:-.25rem}.mt-2{margin-top:.5rem}.mt-1{margin-top:.25rem}.-mt-10{margin-top:-2.5rem}.ml-4{margin-left:1rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.mr-3{margin-right:.75rem}.mr-2{margin-right:.5rem}.-ml-\[5px\]{margin-left:-5px}.ml-5{margin-left:1.25rem}.ml-8{margin-left:2rem}.mt-0\.5{margin-top:.125rem}.mt-0{margin-top:0}.-ml-px{margin-left:-1px}.mr-4{margin-right:1rem}.-mt-2{margin-top:-.5rem}.ml-1{margin-left:.25rem}.-mt-7{margin-top:-1.75rem}.mr-1{margin-right:.25rem}.ml-2\.5{margin-left:.625rem}.-mt-24{margin-top:-6rem}.mb-24{margin-bottom:6rem}.mb-4{margin-bottom:1rem}.ml-7{margin-left:1.75rem}.ml-auto{margin-left:auto}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.aspect-\[16\/9\]{aspect-ratio:16/9}.h-screen{height:100vh}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-4{height:1rem}.h-full{height:100%}.h-7{height:1.75rem}.h-16{height:4rem}.h-24{height:6rem}.h-12{height:3rem}.h-96{height:24rem}.h-80{height:20rem}.h-8{height:2rem}.h-3{height:.75rem}.h-10{height:2.5rem}.h-\[58px\]{height:58px}.h-\[42px\]{height:42px}.max-h-96{max-height:24rem}.max-h-60{max-height:15rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.min-h-screen{min-height:100vh}.w-full{width:100%}.w-32{width:8rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-4{width:1rem}.w-screen{width:100vw}.w-16{width:4rem}.w-12{width:3rem}.w-0{width:0}.w-28{width:7rem}.w-7{width:1.75rem}.w-64{width:16rem}.w-auto{width:auto}.w-8{width:2rem}.w-11{width:2.75rem}.w-72{width:18rem}.w-3{width:.75rem}.w-10{width:2.5rem}.w-\[2px\]{width:2px}.w-0\.5{width:.125rem}.w-px{width:1px}.w-36{width:9rem}.w-20{width:5rem}.w-48{width:12rem}.w-80{width:20rem}.w-1\/3{width:33.333333%}.w-44{width:11rem}.min-w-full{min-width:100%}.min-w-0{min-width:0}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.max-w-2xl{max-width:42rem}.max-w-screen-2xl{max-width:1536px}.max-w-7xl{max-width:80rem}.max-w-\[200px\]{max-width:200px}.\!max-w-4xl{max-width:56rem!important}.max-w-lg{max-width:32rem}.max-w-\[40ch\]{max-width:40ch}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-initial{flex:0 1 auto}.flex-none{flex:none}.flex-shrink-0,.shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.basis-1\/4{flex-basis:25%}.basis-2\/3{flex-basis:66.666667%}.basis-1\/3{flex-basis:33.333333%}.table-auto{table-layout:auto}.border-collapse{border-collapse:collapse}.origin-top-right{transform-origin:top right}.origin-bottom-right{transform-origin:bottom right}.translate-y-2{--tw-translate-y:0.5rem}.translate-y-0,.translate-y-2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y:0px}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x:0px}.translate-x-full{--tw-translate-x:100%}.translate-x-5,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-5{--tw-translate-x:1.25rem}.-translate-y-px{--tw-translate-y:-1px}.-translate-y-px,.rotate-180{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.-rotate-180{--tw-rotate:-180deg}.-rotate-180,.-rotate-90{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate:-90deg}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-100,.scale-95{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-150{--tw-scale-x:1.5;--tw-scale-y:1.5;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-not-allowed{cursor:not-allowed}.cursor-grab{cursor:grab}.cursor-default{cursor:default}.cursor-help{cursor:help}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.place-items-start{place-items:start}.place-items-end{place-items:end}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-items-start{justify-items:start}.justify-items-end{justify-items:end}.justify-items-center{justify-items:center}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-1{gap:.25rem}.gap-8{gap:2rem}.gap-0\.5{gap:.125rem}.gap-0{gap:0}.gap-6{gap:1.5rem}.gap-5{gap:1.25rem}.gap-1\.5{gap:.375rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-y-2>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(2px*var(--tw-divide-y-reverse));border-top-width:calc(2px*(1 - var(--tw-divide-y-reverse)))}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;border-left-width:calc(1px*(1 - var(--tw-divide-x-reverse)));border-right-width:calc(1px*var(--tw-divide-x-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.divide-green-600>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(22 163 74/var(--tw-divide-opacity))}.divide-yellow-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(254 240 138/var(--tw-divide-opacity))}.divide-yellow-600>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(202 138 4/var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.rounded-full{border-radius:9999px}.rounded{border-radius:.25rem}.rounded-none{border-radius:0}.rounded-b{border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-b-lg{border-bottom-left-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-l-none{border-bottom-left-radius:0;border-top-left-radius:0}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-tl{border-top-left-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-t-0{border-top-width:0}.border-l{border-left-width:1px}.border-dashed{border-style:dashed}.border-hidden{border-style:hidden}.border-none{border-style:none}.border-transparent{border-color:transparent}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-red-600{--tw-border-opacity:1;border-color:rgb(220 38 38/var(--tw-border-opacity))}.border-green-600{--tw-border-opacity:1;border-color:rgb(22 163 74/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-slate-100{--tw-border-opacity:1;border-color:rgb(241 245 249/var(--tw-border-opacity))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity))}.border-blue-200{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity))}.border-red-100{--tw-border-opacity:1;border-color:rgb(254 226 226/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(252 165 165/var(--tw-border-opacity))}.border-orange-300{--tw-border-opacity:1;border-color:rgb(253 186 116/var(--tw-border-opacity))}.border-indigo-300{--tw-border-opacity:1;border-color:rgb(165 180 252/var(--tw-border-opacity))}.border-green-300{--tw-border-opacity:1;border-color:rgb(134 239 172/var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgb(239 68 68/var(--tw-border-opacity))}.border-green-500{--tw-border-opacity:1;border-color:rgb(34 197 94/var(--tw-border-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgb(254 252 232/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-gray-600\/75{background-color:rgba(75,85,99,.75)}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-indigo-50{--tw-bg-opacity:1;background-color:rgb(238 242 255/var(--tw-bg-opacity))}.bg-red-800{--tw-bg-opacity:1;background-color:rgb(153 27 27/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-indigo-600{--tw-bg-opacity:1;background-color:rgb(79 70 229/var(--tw-bg-opacity))}.bg-purple-100{--tw-bg-opacity:1;background-color:rgb(243 232 255/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.\!bg-blue-500{--tw-bg-opacity:1!important;background-color:rgb(59 130 246/var(--tw-bg-opacity))!important}.\!bg-purple-500{--tw-bg-opacity:1!important;background-color:rgb(168 85 247/var(--tw-bg-opacity))!important}.\!bg-teal-500{--tw-bg-opacity:1!important;background-color:rgb(20 184 166/var(--tw-bg-opacity))!important}.bg-indigo-100{--tw-bg-opacity:1;background-color:rgb(224 231 255/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.bg-orange-50{--tw-bg-opacity:1;background-color:rgb(255 247 237/var(--tw-bg-opacity))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.bg-white\/75{background-color:hsla(0,0%,100%,.75)}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-opacity-5{--tw-bg-opacity:0.05}.object-cover{-o-object-fit:cover;object-fit:cover}.p-4{padding:1rem}.p-2{padding:.5rem}.p-1{padding:.25rem}.p-8{padding:2rem}.p-3{padding:.75rem}.p-12{padding:3rem}.p-6{padding:1.5rem}.p-0\.5{padding:.125rem}.p-0{padding:0}.py-2{padding-bottom:.5rem;padding-top:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-0{padding-bottom:0;padding-top:0}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-16{padding-bottom:4rem;padding-top:4rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.py-px{padding-bottom:1px;padding-top:1px}.px-0{padding-left:0;padding-right:0}.px-1{padding-left:.25rem;padding-right:.25rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-10{padding-bottom:2.5rem;padding-top:2.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.pl-3{padding-left:.75rem}.pr-10{padding-right:2.5rem}.pt-6{padding-top:1.5rem}.pl-10{padding-left:2.5rem}.pb-20{padding-bottom:5rem}.pr-0{padding-right:0}.pb-2{padding-bottom:.5rem}.pt-4{padding-top:1rem}.pl-2{padding-left:.5rem}.pr-8{padding-right:2rem}.pr-14{padding-right:3.5rem}.pr-7{padding-right:1.75rem}.pl-7{padding-left:1.75rem}.pr-2{padding-right:.5rem}.pr-12{padding-right:3rem}.pr-3{padding-right:.75rem}.pl-4{padding-left:1rem}.pr-32{padding-right:8rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.pl-8{padding-left:2rem}.pt-\[5px\]{padding-top:5px}.pt-\[1px\]{padding-top:1px}.pb-8{padding-bottom:2rem}.pl-24{padding-left:6rem}.pr-4{padding-right:1rem}.pr-9{padding-right:2.25rem}.pb-24{padding-bottom:6rem}.pl-6{padding-left:1.5rem}.pt-12{padding-top:3rem}.pb-6{padding-bottom:1.5rem}.pb-28{padding-bottom:7rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.not-italic{font-style:normal}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-none{line-height:1}.leading-\[42px\]{line-height:42px}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-indigo-500{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-yellow-700{--tw-text-opacity:1;color:rgb(161 98 7/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-blue-700{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgb(79 70 229/var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity))}.text-indigo-700{--tw-text-opacity:1;color:rgb(67 56 202/var(--tw-text-opacity))}.\!text-red-600{--tw-text-opacity:1!important;color:rgb(220 38 38/var(--tw-text-opacity))!important}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity))}.text-yellow-900{--tw-text-opacity:1;color:rgb(113 63 18/var(--tw-text-opacity))}.text-indigo-200{--tw-text-opacity:1;color:rgb(199 210 254/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.opacity-50{opacity:.5}.opacity-10{opacity:.1}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-lg,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.shadow,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-blue-500\/25{--tw-shadow-color:rgba(59,130,246,.25);--tw-shadow:var(--tw-shadow-colored)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-4{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-4,.ring-8{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-8{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.\!ring-blue-100{--tw-ring-opacity:1!important;--tw-ring-color:rgb(219 234 254/var(--tw-ring-opacity))!important}.\!ring-purple-100{--tw-ring-opacity:1!important;--tw-ring-color:rgb(243 232 255/var(--tw-ring-opacity))!important}.\!ring-teal-100{--tw-ring-opacity:1!important;--tw-ring-color:rgb(204 251 241/var(--tw-ring-opacity))!important}.ring-white{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.blur-3xl{--tw-blur:blur(64px)}.blur-3xl,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-100{transition-duration:.1s}.duration-75{transition-duration:75ms}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-200{transition-duration:.2s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}p>code{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity));color:rgb(147 51 234/var(--tw-text-opacity));font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:.875rem;line-height:1.25rem}[x-cloak]{display:none}.sortable-ghost .sort-item-element{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity));border-color:rgb(147 197 253/var(--tw-border-opacity));color:rgb(37 99 235/var(--tw-text-opacity))}.ql-editor>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.app-sidemenu-expanded [data-ref=layout-sidemenu]{width:16rem}.before\:mx-3:before{content:var(--tw-content);margin-left:.75rem;margin-right:.75rem}.before\:text-gray-200:before{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity));content:var(--tw-content)}.before\:content-\[\'\|\'\]:before{--tw-content:"|";content:var(--tw-content)}.even\:bg-gray-50:nth-child(2n){--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.focus-within\:outline-none:focus-within{outline:2px solid transparent;outline-offset:2px}.focus-within\:ring-2:focus-within{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus-within\:ring-indigo-500:focus-within{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus-within\:ring-offset-2:focus-within{--tw-ring-offset-width:2px}.hover\:border-gray-100:hover{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.hover\:border-gray-200:hover{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:border-gray-400:hover{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgb(185 28 28/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.hover\:bg-blue-50:hover{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.hover\:bg-red-50:hover{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity))}.hover\:bg-indigo-500:hover{--tw-bg-opacity:1;background-color:rgb(99 102 241/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-blue-500:hover{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgb(67 56 202/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity))}.hover\:bg-yellow-600:hover{--tw-bg-opacity:1;background-color:rgb(202 138 4/var(--tw-bg-opacity))}.hover\:bg-green-50:hover{--tw-bg-opacity:1;background-color:rgb(240 253 244/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-indigo-500:hover{--tw-text-opacity:1;color:rgb(99 102 241/var(--tw-text-opacity))}.hover\:text-red-500:hover{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.hover\:text-purple-900:hover{--tw-text-opacity:1;color:rgb(88 28 135/var(--tw-text-opacity))}.hover\:text-blue-500:hover{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:text-red-600:hover{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:appearance-none:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:border-transparent:focus{border-color:transparent}.focus\:underline:focus{text-decoration-line:underline}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-1:focus,.focus\:ring-2:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}.focus\:ring-green-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(34 197 94/var(--tw-ring-opacity))}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(239 68 68/var(--tw-ring-opacity))}.focus\:ring-gray-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(156 163 175/var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-white:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 255 255/var(--tw-ring-opacity))}.focus\:ring-yellow-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(234 179 8/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.focus\:ring-offset-gray-100:focus{--tw-ring-offset-color:#f3f4f6}.focus\:ring-offset-gray-50:focus{--tw-ring-offset-color:#f9fafb}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:visible{visibility:visible}.group:hover .group-hover\:text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.group:hover .group-hover\:opacity-0{opacity:0}.group:hover .group-hover\:opacity-100{opacity:1}.peer:checked~.peer-checked\:border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.peer:checked~.peer-checked\:bg-blue-200{--tw-bg-opacity:1;background-color:rgb(191 219 254/var(--tw-bg-opacity))}.peer:checked~.peer-checked\:text-blue-900{--tw-text-opacity:1;color:rgb(30 58 138/var(--tw-text-opacity))}.dark .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark .dark\:border-gray-800{--tw-border-opacity:1;border-color:rgb(31 41 55/var(--tw-border-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark .dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .dark\:shadow-blue-500\/10{--tw-shadow-color:rgba(59,130,246,.1);--tw-shadow:var(--tw-shadow-colored)}.dark .dark\:hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark .dark\:hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:focus\:ring-offset-gray-800:focus{--tw-ring-offset-color:#1f2937}@media (min-width:640px){.sm\:order-2{order:2}.sm\:order-3{order:3}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:mt-0{margin-top:0}.sm\:ml-2{margin-left:.5rem}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:grid{display:grid}.sm\:hidden{display:none}.sm\:w-full{width:100%}.sm\:w-auto{width:auto}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-md{max-width:28rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-xl{max-width:36rem}.sm\:max-w-2xl{max-width:42rem}.sm\:flex-1{flex:1 1 0%}.sm\:flex-auto{flex:1 1 auto}.sm\:flex-initial{flex:0 1 auto}.sm\:flex-none{flex:none}.sm\:translate-y-0{--tw-translate-y:0px}.sm\:translate-x-2,.sm\:translate-y-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:translate-x-2{--tw-translate-x:0.5rem}.sm\:translate-x-0{--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:place-items-start{place-items:start}.sm\:place-items-end{place-items:end}.sm\:place-items-center{place-items:center}.sm\:items-start{align-items:flex-start}.sm\:items-end{align-items:flex-end}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-end{justify-content:flex-end}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:justify-items-start{justify-items:start}.sm\:justify-items-end{justify-items:end}.sm\:justify-items-center{justify-items:center}.sm\:gap-4{gap:1rem}.sm\:overflow-hidden{overflow:hidden}.sm\:overflow-x-hidden{overflow-x:hidden}.sm\:overflow-y-hidden{overflow-y:hidden}.sm\:rounded-lg{border-radius:.5rem}.sm\:rounded-md{border-radius:.375rem}.sm\:border-hidden{border-style:hidden}.sm\:p-6{padding:1.5rem}.sm\:p-1{padding:.25rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-12{padding-bottom:3rem;padding-top:3rem}.sm\:px-8{padding-left:2rem;padding-right:2rem}.sm\:pb-5{padding-bottom:1.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:duration-300{transition-duration:.3s}}@media (min-width:768px){.md\:col-span-6{grid-column:span 6/span 6}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:flex-1{flex:1 1 0%}.md\:flex-auto{flex:1 1 auto}.md\:flex-initial{flex:0 1 auto}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:place-items-start{place-items:start}.md\:place-items-end{place-items:end}.md\:place-items-center{place-items:center}.md\:items-start{align-items:flex-start}.md\:items-end{align-items:flex-end}.md\:items-center{align-items:center}.md\:justify-start{justify-content:flex-start}.md\:justify-end{justify-content:flex-end}.md\:justify-between{justify-content:space-between}.md\:justify-items-start{justify-items:start}.md\:justify-items-end{justify-items:end}.md\:justify-items-center{justify-items:center}.md\:gap-4{gap:1rem}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.md\:overflow-hidden{overflow:hidden}.md\:overflow-x-hidden{overflow-x:hidden}.md\:overflow-y-hidden{overflow-y:hidden}.md\:border-hidden{border-style:hidden}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}.md\:text-2xl{font-size:1.5rem;line-height:2rem}}@media (min-width:1024px){.lg\:left-auto{left:auto}.lg\:order-last{order:9999}.lg\:order-first{order:-9999}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-\[calc\(100vw_-_16rem\)\]{width:calc(100vw - 16rem)}.lg\:w-\[calc\(100vw_-_5rem\)\]{width:calc(100vw - 5rem)}.lg\:flex-1{flex:1 1 0%}.lg\:flex-auto{flex:1 1 auto}.lg\:flex-initial{flex:0 1 auto}.lg\:flex-none{flex:none}.lg\:flex-shrink-0{flex-shrink:0}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:place-items-start{place-items:start}.lg\:place-items-end{place-items:end}.lg\:place-items-center{place-items:center}.lg\:items-start{align-items:flex-start}.lg\:items-end{align-items:flex-end}.lg\:items-center{align-items:center}.lg\:justify-start{justify-content:flex-start}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:justify-items-start{justify-items:start}.lg\:justify-items-end{justify-items:end}.lg\:justify-items-center{justify-items:center}.lg\:gap-8{gap:2rem}.lg\:overflow-hidden{overflow:hidden}.lg\:overflow-x-hidden{overflow-x:hidden}.lg\:overflow-y-hidden{overflow-y:hidden}.lg\:border-hidden{border-style:hidden}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:order-last{order:9999}.xl\:mt-0{margin-top:0}.xl\:flex{display:flex}.xl\:w-1\/3{width:33.333333%}.xl\:w-2\/3{width:66.666667%}.xl\:flex-row-reverse{flex-direction:row-reverse}.xl\:items-start{align-items:flex-start}.xl\:justify-end{justify-content:flex-end}.xl\:justify-between{justify-content:space-between}.xl\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.xl\:pl-0{padding-left:0}} \ No newline at end of file diff --git a/packages/admin/resources/icons/outline/arrows-right-left.svg b/packages/admin/resources/icons/outline/arrows-right-left.svg new file mode 100644 index 0000000000..bbb17a6486 --- /dev/null +++ b/packages/admin/resources/icons/outline/arrows-right-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/admin/resources/icons/solid/arrows-right-left.svg b/packages/admin/resources/icons/solid/arrows-right-left.svg new file mode 100644 index 0000000000..d02cf0952b --- /dev/null +++ b/packages/admin/resources/icons/solid/arrows-right-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/admin/resources/lang/en/auth.php b/packages/admin/resources/lang/en/auth.php index 06948c733a..d16435557e 100644 --- a/packages/admin/resources/lang/en/auth.php +++ b/packages/admin/resources/lang/en/auth.php @@ -20,6 +20,8 @@ 'permissions.catalogue.orders.description' => 'Allow the staff member to manage orders.', 'permissions.catalogue.customers.name' => 'Manage Customers', 'permissions.catalogue.customers.description' => 'Allow the staff member to manage customers.', + 'permissions.discounts.name' => 'Manage Discounts', + 'permissions.discounts.description' => 'Allow the staff member to manage discounts', /** * Reset password. */ diff --git a/packages/admin/resources/lang/en/components.php b/packages/admin/resources/lang/en/components.php index 958c6e51fc..0f95afee95 100644 --- a/packages/admin/resources/lang/en/components.php +++ b/packages/admin/resources/lang/en/components.php @@ -209,4 +209,19 @@ 'activity-log.orders.capture' => 'Payment of :amount on card ending :last_four', 'activity-log.orders.authorized' => 'Authorized of :amount on card ending :last_four', 'activity-log.orders.refund' => 'refund of :amount on card ending :last_four', + /** + * Discounts. + */ + 'discounts.index.title' => 'Discounts', + 'discounts.index.status.pending' => 'Pending', + 'discounts.index.status.active' => 'Active', + 'discounts.index.status.scheduled' => 'Scheduled', + 'discounts.index.status.expired' => 'Expired', + 'discounts.index.create_discount' => 'Create Discount', + 'discounts.create.title' => 'Create Discount', + 'discounts.create_btn' => 'Create Discount', + 'discounts.save_btn' => 'Save Discount', + 'discounts.show.stop.label' => 'Stop other discounts applying after this one', + 'discounts.show.danger_zone.label' => 'Delete discount', + 'discounts.show.danger_zone.instructions' => 'Enter the name of the discount to confirm removal.', ]; diff --git a/packages/admin/resources/lang/en/global.php b/packages/admin/resources/lang/en/global.php index df06144f3f..e441634190 100644 --- a/packages/admin/resources/lang/en/global.php +++ b/packages/admin/resources/lang/en/global.php @@ -118,6 +118,10 @@ 'brand' => 'Brand', 'stock' => 'Stock', 'show_deleted' => 'Show Deleted', + 'starts_at' => 'Starts At', + 'ends_at' => 'Ends At', + 'priority' => 'Priority', + 'stop' => 'Stop', 'preview' => 'Preview', 'delivery_instructions' => 'Delivery Instructions', 'not_provided' => 'Not Provided', diff --git a/packages/admin/resources/lang/en/inputs.php b/packages/admin/resources/lang/en/inputs.php index 8685ee208a..2d10c3a97e 100644 --- a/packages/admin/resources/lang/en/inputs.php +++ b/packages/admin/resources/lang/en/inputs.php @@ -97,9 +97,12 @@ 'text' => 'Text', 'richtext' => 'Richtext', 'transaction.label' => 'Transaction', + 'starts_at.label' => 'Starts at', + 'ends_at.label' => 'Ends at', 'tag.label' => 'Tag', 'active.label' => 'Active', 'priority.label' => 'Priority', 'postcodes.label' => 'Postcodes', 'postcodes.instructions' => 'List each postcode on a new line. Supports wildcards such as NW*', + 'max_uses.label' => 'Max uses', ]; diff --git a/packages/admin/resources/lang/en/menu.php b/packages/admin/resources/lang/en/menu.php index 2e1a44619f..2fbd71a405 100644 --- a/packages/admin/resources/lang/en/menu.php +++ b/packages/admin/resources/lang/en/menu.php @@ -10,6 +10,7 @@ 'sidebar.order-processing' => 'Order Processing', 'sidebar.orders' => 'Orders', 'sidebar.customers' => 'Customers', + 'sidebar.discounts' => 'Discounts', /** * Product side menu. */ diff --git a/packages/admin/resources/lang/en/notifications.php b/packages/admin/resources/lang/en/notifications.php index 36f31cae10..773e1611eb 100644 --- a/packages/admin/resources/lang/en/notifications.php +++ b/packages/admin/resources/lang/en/notifications.php @@ -55,6 +55,8 @@ 'tax_class.deleted' => 'Tax class deleted', 'clipboard.copied' => 'Copied to clipboard', 'clipboard.failed_copy' => 'Unable to copy to clipboard', + 'discount.saved' => 'Discount saved', + 'discount.deleted' => 'Discount deleted', 'tags.updated' => 'Tags updated', 'staff.restored' => 'Staff member restored', 'default_url_protected' => 'Unable to delete the default URL', diff --git a/packages/admin/resources/lang/en/partials.php b/packages/admin/resources/lang/en/partials.php index c06f2e6e67..52cd201fbc 100644 --- a/packages/admin/resources/lang/en/partials.php +++ b/packages/admin/resources/lang/en/partials.php @@ -169,6 +169,7 @@ 'orders.totals.shipping_total' => 'Shipping Total', 'orders.totals.total' => 'Total', 'orders.totals.notes_empty' => 'No notes on this order', + 'orders.totals.discount_total' => 'Discount Total', 'orders.lines.unit_price' => 'Unit Price', 'orders.lines.quantity' => 'Quantity', 'orders.lines.sub_total' => 'Sub Total', diff --git a/packages/admin/resources/lang/en/tables.php b/packages/admin/resources/lang/en/tables.php index 4ae3a3ea0a..c4d88a9451 100644 --- a/packages/admin/resources/lang/en/tables.php +++ b/packages/admin/resources/lang/en/tables.php @@ -46,6 +46,9 @@ 'headings.author' => 'Author', 'headings.products_count' => 'No. Products', 'headings.empty' => 'No Entries Found', + 'headings.starts_at' => 'Starts At', + 'headings.ends_at' => 'Ends At', + 'headings.type' => 'Type', 'headings.tags' => 'Tags', 'headings.new_customer' => 'New Customer', 'headings.new_returning' => 'New/Returning Customers', diff --git a/packages/admin/resources/views/components/input/datepicker.blade.php b/packages/admin/resources/views/components/input/datepicker.blade.php index fcd130fb0a..6e9eadf280 100644 --- a/packages/admin/resources/views/components/input/datepicker.blade.php +++ b/packages/admin/resources/views/components/input/datepicker.blade.php @@ -3,12 +3,21 @@ value: @entangle($attributes->wire('model')), init() { this.$nextTick(() => { - flatpickr($refs.input, {{ json_encode($options) }}) + + passedOptions = {{ json_encode($options) }} + + options = { + altFormat: passedOptions.enableTime ? 'Y-m-d H:i' : 'Y-m-d', + altInput: true, + } + + flatpickr($refs.input, {...options, ...passedOptions}) }) } }" @change="value = $event.target.value" class="flex relative" + wire:ignore > diff --git a/packages/admin/resources/views/livewire/components/collections/collection-tree-select.blade.php b/packages/admin/resources/views/livewire/components/collections/collection-tree-select.blade.php new file mode 100644 index 0000000000..f0e0df001f --- /dev/null +++ b/packages/admin/resources/views/livewire/components/collections/collection-tree-select.blade.php @@ -0,0 +1,36 @@ +
+
+
+ +
+
+ + @foreach($this->collectionGroups as $group) + + @endforeach + +
+
+
+ @if(count($selectedCollections)) + + @endif +
+ @foreach($this->collections as $collectionNode) + @include('adminhub::partials.collections.collection-tree-node', [ + 'node' => $collectionNode, + ]) + @endforeach +
+
+
diff --git a/packages/admin/resources/views/livewire/components/discounts/create.blade.php b/packages/admin/resources/views/livewire/components/discounts/create.blade.php new file mode 100644 index 0000000000..d7f3ae0aa0 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/discounts/create.blade.php @@ -0,0 +1,39 @@ +
+
+

+ @if($discount->id) + {{ $discount->name }} + @else + Create Discount + @endif +

+
+ +
+ @include('adminhub::partials.forms.discount') +
+
+ + + + {{--
+
+
+
+ + + + + + + + +
+ +
+
+
--}} diff --git a/packages/admin/resources/views/livewire/components/discounts/index.blade.php b/packages/admin/resources/views/livewire/components/discounts/index.blade.php new file mode 100644 index 0000000000..e214c3db93 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/discounts/index.blade.php @@ -0,0 +1,16 @@ + +
+
+ + {{ __('adminhub::components.discounts.index.title') }} + + +
+ + {{ __('adminhub::components.discounts.index.create_discount') }} +
+
+ + @livewire('hub.components.discounts.table') +
diff --git a/packages/admin/resources/views/livewire/components/discounts/show.blade.php b/packages/admin/resources/views/livewire/components/discounts/show.blade.php new file mode 100644 index 0000000000..a8e9368d83 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/discounts/show.blade.php @@ -0,0 +1,52 @@ +
+
+ + + + +

+ {{ $discount->name }} +

+
+
+ @include('adminhub::partials.forms.discount') +
+ + {{--
+ +
+ @include('adminhub::partials.forms.discount') +
+ + + +
+
+ {{ __('adminhub::inputs.danger_zone.title') }} +
+
+
+
+ {{ __('adminhub::components.discounts.show.danger_zone.label') }} +

{{ __('adminhub::components.discounts.show.danger_zone.instructions') }}

+
+
+ +
+
+ {{ __('adminhub::global.delete') }} +
+
+
+
+
--}} + +
diff --git a/packages/admin/resources/views/livewire/components/discounts/types/buy-x-get-y.blade.php b/packages/admin/resources/views/livewire/components/discounts/types/buy-x-get-y.blade.php new file mode 100644 index 0000000000..5b7a95a719 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/discounts/types/buy-x-get-y.blade.php @@ -0,0 +1,117 @@ +
+ + +
+
+ Qualify Products +

Select the products required for the discount to apply

+
+
+ @livewire('hub.components.product-search', [ + 'existing' => $this->conditions, + 'ref' => 'discount-conditions', + 'showBtn' => true, + ]) +
+
+ +
+ @if($errors->first('selectedConditions')) + + You must select at least 1 qualifying product. + + @endif + @if(!$this->purchasableConditions->count()) +
+ No products currently selected. +
+ @endif + + @foreach($this->purchasableConditions as $product) +
+ @if($thumbnail = $product->thumbnail) +
+ +
+ @endif +
+ {{ $product->translateAttribute('name') }} +
+
+ +
+
+ @endforeach +
+ +
+ + + +
+ +
+
+ Product Rewards +

Select which products will be discounted if they exist in the cart and the above conditions are met.

+
+
+ @livewire('hub.components.product-search', [ + 'existing' => $this->rewards, + 'ref' => 'discount-rewards', + 'showBtn' => true, + ]) +
+
+ + @if($errors->first('selectedRewards')) + + You must select at least 1 qualifying product. + + @endif + + @if(!$this->purchasableRewards->count()) +
+ No products currently selected +
+ @endif + +
+ @foreach($this->purchasableRewards as $product) +
+ @if($thumbnail = $product->thumbnail) +
+ +
+ @endif +
+ {{ $product->translateAttribute('name') }} +
+
+ +
+
+ @endforeach +
+ + + If one or more items are in the cart, the cheapest item will be discounted. + + +
+ + + + + + + +
+
diff --git a/packages/admin/resources/views/livewire/components/discounts/types/discount.blade.php b/packages/admin/resources/views/livewire/components/discounts/types/discount.blade.php new file mode 100644 index 0000000000..80b9231957 --- /dev/null +++ b/packages/admin/resources/views/livewire/components/discounts/types/discount.blade.php @@ -0,0 +1,47 @@ +
+
+ + +
+ +
!($this->discount->data['fixed_value'] ?? false)])> + @foreach($this->currencies as $currency) + + + + @endforeach +
+ +
$this->discount->data['fixed_value'] ?? false])> +
+ + + +
+
+
diff --git a/packages/admin/resources/views/livewire/pages/discounts/create.blade.php b/packages/admin/resources/views/livewire/pages/discounts/create.blade.php new file mode 100644 index 0000000000..fa92ef8ca6 --- /dev/null +++ b/packages/admin/resources/views/livewire/pages/discounts/create.blade.php @@ -0,0 +1,5 @@ +
+ @livewire('hub.components.discounts.create', [ + 'discount' => $discount, + ]) +
diff --git a/packages/admin/resources/views/livewire/pages/discounts/index.blade.php b/packages/admin/resources/views/livewire/pages/discounts/index.blade.php new file mode 100644 index 0000000000..b9b06a8199 --- /dev/null +++ b/packages/admin/resources/views/livewire/pages/discounts/index.blade.php @@ -0,0 +1,3 @@ +
+ @livewire('hub.components.discounts.index') +
diff --git a/packages/admin/resources/views/livewire/pages/discounts/show.blade.php b/packages/admin/resources/views/livewire/pages/discounts/show.blade.php new file mode 100644 index 0000000000..38c0c0f2b9 --- /dev/null +++ b/packages/admin/resources/views/livewire/pages/discounts/show.blade.php @@ -0,0 +1,5 @@ +
+ @livewire('hub.components.discounts.show', [ + 'discount' => $discount, + ]) +
diff --git a/packages/admin/resources/views/partials/availability.blade.php b/packages/admin/resources/views/partials/availability.blade.php index e81c90c786..c00435d2f2 100644 --- a/packages/admin/resources/views/partials/availability.blade.php +++ b/packages/admin/resources/views/partials/availability.blade.php @@ -48,7 +48,9 @@
- @include('adminhub::partials.availability.customer-groups') + @include('adminhub::partials.availability.customer-groups', [ + 'customerGroupType' => $customerGroupType ?? 'select', + ])
@endif diff --git a/packages/admin/resources/views/partials/availability/customer-groups.blade.php b/packages/admin/resources/views/partials/availability/customer-groups.blade.php index 7a3c20eb95..ae4e225a25 100644 --- a/packages/admin/resources/views/partials/availability/customer-groups.blade.php +++ b/packages/admin/resources/views/partials/availability/customer-groups.blade.php @@ -4,7 +4,9 @@
{{ $group->name }}
- @if($availability['customerGroups'][$group->id]['status'] != 'hidden') + {{-- availability.customerGroups.{{ $group->id }}.enabled --}} + + @if($availability['customerGroups'][$group->id]['status'] != 'hidden' || ($availability['customerGroups'][$group->id]['enabled'] ?? false))
- - - - @if($customerGroups['purchasable'] ?? true) - + @if($customerGroupType == 'toggle') + + @else + + + + @if($customerGroups['purchasable'] ?? true) + + @endif + @endif - +
@endforeach diff --git a/packages/admin/resources/views/partials/collections/collection-tree-node.blade.php b/packages/admin/resources/views/partials/collections/collection-tree-node.blade.php new file mode 100644 index 0000000000..6ad09fe2c3 --- /dev/null +++ b/packages/admin/resources/views/partials/collections/collection-tree-node.blade.php @@ -0,0 +1,28 @@ +
+ + @if($node->children->count()) + @if(in_array($node->id, $selectedCollections)) +
+ + This will apply to all children and descendants of this collection. + +
+ @else +
+ @foreach($node->children as $childNode) + @include('adminhub::partials.collections.collection-tree-node', [ + 'node' => $childNode + ]) + @endforeach +
+ @endif + @endif +
diff --git a/packages/admin/resources/views/partials/forms/discount.blade.php b/packages/admin/resources/views/partials/forms/discount.blade.php new file mode 100644 index 0000000000..1af713cd28 --- /dev/null +++ b/packages/admin/resources/views/partials/forms/discount.blade.php @@ -0,0 +1,96 @@ +
+
+
+
+ @if (!$this->hasChannelAvailability) + + This discount has no availability across channels + + @endif +
+
+ @include('adminhub::partials.forms.discount.basic-information') +
+ +
+
+ + + + + + + + +
+ + +
+
+
+ +
+ @include('adminhub::partials.availability', [ + 'channels' => true, + 'type' => 'discount', + 'customerGroups' => true, + 'customerGroupType' => 'toggle', + ]) +
+ +
+ @include('adminhub::partials.forms.discount.limitations') +
+ +
+ @include('adminhub::partials.forms.discount.conditions') +
+ +
+ @include('adminhub::partials.forms.discount.discount-type') +
+ +
+ +
+
+
+ + + + + +
diff --git a/packages/admin/resources/views/partials/forms/discount/availability.blade.php b/packages/admin/resources/views/partials/forms/discount/availability.blade.php new file mode 100644 index 0000000000..3c8eaab926 --- /dev/null +++ b/packages/admin/resources/views/partials/forms/discount/availability.blade.php @@ -0,0 +1,15 @@ +
+
+
+

+ Availability +

+
+ + @include('adminhub::partials.availability', [ + 'channels' => true, + 'type' => 'discount', + 'customerGroups' => true, + ]) +
+
diff --git a/packages/admin/resources/views/partials/forms/discount/basic-information.blade.php b/packages/admin/resources/views/partials/forms/discount/basic-information.blade.php new file mode 100644 index 0000000000..8072c55e61 --- /dev/null +++ b/packages/admin/resources/views/partials/forms/discount/basic-information.blade.php @@ -0,0 +1,35 @@ +
+
+
+

+ Basic Information +

+
+ +
+
+ + + + + + + +
+ +
+
+ + + +
+ +
+ + + +
+
+
+
+
diff --git a/packages/admin/resources/views/partials/forms/discount/conditions.blade.php b/packages/admin/resources/views/partials/forms/discount/conditions.blade.php new file mode 100644 index 0000000000..e36d94c347 --- /dev/null +++ b/packages/admin/resources/views/partials/forms/discount/conditions.blade.php @@ -0,0 +1,55 @@ +
+
+
+

+ Conditions +

+
+ +
+
+ + + + +
+ + + +
+
+ + +
+
+
+ +
+ + + +
+
+ + +
+
+
diff --git a/packages/admin/resources/views/partials/forms/discount/discount-type.blade.php b/packages/admin/resources/views/partials/forms/discount/discount-type.blade.php new file mode 100644 index 0000000000..4e5ba1ba1d --- /dev/null +++ b/packages/admin/resources/views/partials/forms/discount/discount-type.blade.php @@ -0,0 +1,28 @@ +
+
+
+

+ Discount Type +

+
+ +
+ + + @foreach($this->discountTypes as $discountType) + + @endforeach + + +
+ + @if($discountComponent = $this->getDiscountComponent()) + @livewire($discountComponent->getName(), [ + 'errors' => $errors, + 'discount' => $discount, + ], key('ui_'.$discount->type)) + @endif +
+
diff --git a/packages/admin/resources/views/partials/forms/discount/limitations.blade.php b/packages/admin/resources/views/partials/forms/discount/limitations.blade.php new file mode 100644 index 0000000000..405b1968cb --- /dev/null +++ b/packages/admin/resources/views/partials/forms/discount/limitations.blade.php @@ -0,0 +1,38 @@ +
+
+
+

+ Limitations +

+
+ +
+ + + + + @livewire('hub.components.collections.collection-tree-select', [ + 'selectedCollections' => $selectedCollections, + ]) + + + + +
+ @foreach($this->brands as $brand) + + @endforeach +
+
+
+
+
diff --git a/packages/admin/resources/views/partials/orders/totals.blade.php b/packages/admin/resources/views/partials/orders/totals.blade.php index 339007c97f..d9e1b957f3 100644 --- a/packages/admin/resources/views/partials/orders/totals.blade.php +++ b/packages/admin/resources/views/partials/orders/totals.blade.php @@ -49,6 +49,11 @@ class="mr-2"
{{ $order->sub_total->formatted }}
+
+
{{ __('adminhub::partials.orders.totals.discount_total') }}
+
-{{ $order->discount_total->formatted }}
+
+
{{ __('adminhub::partials.orders.totals.shipping_total') }}
{{ $order->shipping_total->formatted }}
diff --git a/packages/admin/routes/includes/discounts.php b/packages/admin/routes/includes/discounts.php new file mode 100644 index 0000000000..0628458d60 --- /dev/null +++ b/packages/admin/routes/includes/discounts.php @@ -0,0 +1,17 @@ + 'can:catalogue:manage-discounts', +], function () { + Route::get('/', DiscountsIndex::class)->name('hub.discounts.index'); + Route::get('create', DiscountCreate::class)->name('hub.discounts.create'); + Route::get('{discount}', DiscountShow::class)->name('hub.discounts.show'); +}); diff --git a/packages/admin/routes/web.php b/packages/admin/routes/web.php index ca905eb10f..9ed0da22be 100644 --- a/packages/admin/routes/web.php +++ b/packages/admin/routes/web.php @@ -57,6 +57,10 @@ 'prefix' => 'customers', ], __DIR__.'/includes/customers.php'); + Route::group([ + 'prefix' => 'discounts', + ], __DIR__.'/includes/discounts.php'); + Route::group([ 'prefix' => 'brands', ], __DIR__.'/includes/brands.php'); diff --git a/packages/admin/src/AdminHubServiceProvider.php b/packages/admin/src/AdminHubServiceProvider.php index f8e3d4f566..3bd64ce340 100644 --- a/packages/admin/src/AdminHubServiceProvider.php +++ b/packages/admin/src/AdminHubServiceProvider.php @@ -27,12 +27,19 @@ use Lunar\Hub\Http\Livewire\Components\Collections\CollectionGroupsIndex; use Lunar\Hub\Http\Livewire\Components\Collections\CollectionShow; use Lunar\Hub\Http\Livewire\Components\Collections\CollectionTree; +use Lunar\Hub\Http\Livewire\Components\Collections\CollectionTreeSelect; use Lunar\Hub\Http\Livewire\Components\Collections\SideMenu; use Lunar\Hub\Http\Livewire\Components\CollectionSearch; use Lunar\Hub\Http\Livewire\Components\CurrentStaffName; use Lunar\Hub\Http\Livewire\Components\Customers\CustomerShow; use Lunar\Hub\Http\Livewire\Components\Customers\CustomersIndex; use Lunar\Hub\Http\Livewire\Components\Customers\CustomersTable; +use Lunar\Hub\Http\Livewire\Components\Discounts\DiscountCreate; +use Lunar\Hub\Http\Livewire\Components\Discounts\DiscountShow; +use Lunar\Hub\Http\Livewire\Components\Discounts\DiscountsIndex; +use Lunar\Hub\Http\Livewire\Components\Discounts\DiscountsTable; +use Lunar\Hub\Http\Livewire\Components\Discounts\Types\BuyXGetY; +use Lunar\Hub\Http\Livewire\Components\Discounts\Types\Discount as TypesDiscount; use Lunar\Hub\Http\Livewire\Components\Orders\EmailNotification; use Lunar\Hub\Http\Livewire\Components\Orders\OrderCapture; use Lunar\Hub\Http\Livewire\Components\Orders\OrderRefund; @@ -250,6 +257,7 @@ protected function registerLivewireComponents() $this->registerSettingsComponents(); $this->registerOrderComponents(); $this->registerCustomerComponents(); + $this->registerDiscountComponents(); // Blade Components Blade::componentNamespace('Lunar\\Hub\\Views\\Components', 'hub'); @@ -361,6 +369,7 @@ protected function registerCollectionComponents() Livewire::component('hub.components.collections.collection-groups.show', CollectionGroupShow::class); Livewire::component('hub.components.collections.show', CollectionShow::class); Livewire::component('hub.components.collections.collection-tree', CollectionTree::class); + Livewire::component('hub.components.collections.collection-tree-select', CollectionTreeSelect::class); } /** @@ -434,6 +443,17 @@ protected function registerSettingsComponents() Livewire::component('hub.components.settings.taxes.tax-classes.index', TaxClassesIndex::class); } + public function registerDiscountComponents() + { + Livewire::component('hub.components.discounts.index', DiscountsIndex::class); + Livewire::component('hub.components.discounts.show', DiscountShow::class); + Livewire::component('hub.components.discounts.create', DiscountCreate::class); + Livewire::component('hub.components.discounts.table', DiscountsTable::class); + + Livewire::component('lunar.hub.http.livewire.components.discounts.types.discount', TypesDiscount::class); + Livewire::component('lunar.hub.http.livewire.components.discounts.types.buy-x-get-y', BuyXGetY::class); + } + /** * Register our publishables. * diff --git a/packages/admin/src/Auth/Manifest.php b/packages/admin/src/Auth/Manifest.php index 6245d2c6cc..7970a45936 100644 --- a/packages/admin/src/Auth/Manifest.php +++ b/packages/admin/src/Auth/Manifest.php @@ -139,6 +139,11 @@ protected function getBasePermissions(): array 'catalogue:manage-customers', __('adminhub::auth.permissions.catalogue.customers.description') ), + new Permission( + __('adminhub::auth.permissions.discounts.name'), + 'catalogue:manage-discounts', + __('adminhub::auth.permissions.discounts.description') + ), ]; } } diff --git a/packages/admin/src/Editing/DiscountTypes.php b/packages/admin/src/Editing/DiscountTypes.php new file mode 100644 index 0000000000..3f46161be5 --- /dev/null +++ b/packages/admin/src/Editing/DiscountTypes.php @@ -0,0 +1,27 @@ + TypesDiscount::class, + BuyXGetY::class => TypesBuyXGetY::class, + ]; + + public function getComponent($type) + { + $component = $this->mapping[$type] ?? null; + + if (! $component) { + return null; + } + + return app($component); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Collections/CollectionTreeSelect.php b/packages/admin/src/Http/Livewire/Components/Collections/CollectionTreeSelect.php new file mode 100644 index 0000000000..676bc69b0e --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Collections/CollectionTreeSelect.php @@ -0,0 +1,61 @@ +collectionGroupId = $this->collectionGroups->first()?->id; + } + + public function getCollectionGroupsProperty() + { + return CollectionGroup::get(); + } + + public function toggleSelected() + { + $this->showOnlySelected = ! $this->showOnlySelected; + } + + public function getCollectionsProperty() + { + if ($this->showOnlySelected) { + return Collection::whereIn('id', $this->selectedCollections)->get()->toTree(); + } + + if ($this->searchTerm) { + return Collection::search($this->searchTerm)->get(); + } + + return Collection::inGroup($this->collectionGroupId)->get()->toTree(); + } + + public function updatedSelectedCollections($val) + { + $this->emitUp('collectionTreeSelect.updated', $val); + } + + public function render() + { + return view('adminhub::livewire.components.collections.collection-tree-select'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/AbstractDiscount.php b/packages/admin/src/Http/Livewire/Components/Discounts/AbstractDiscount.php new file mode 100644 index 0000000000..45696053d0 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/AbstractDiscount.php @@ -0,0 +1,390 @@ +emit('parentComponentErrorBag', $this->getErrorBag()); + } + + /** + * {@inheritDoc} + */ + protected $listeners = [ + 'discountData.updated' => 'syncDiscountData', + 'discount.conditions' => 'syncConditions', + 'discount.rewards' => 'syncRewards', + 'discount.purchasables' => 'syncPurchasables', + 'collectionTreeSelect.updated' => 'selectCollections', + ]; + + public function mount() + { + $this->currency = Currency::getDefault(); + $this->selectedBrands = $this->discount->brands->pluck('id')->toArray(); + $this->selectedCollections = $this->discount->collections->pluck('id')->toArray(); + + $this->selectedConditions = $this->discount->purchasableConditions() + ->wherePurchasableType(Product::class) + ->pluck('purchasable_id')->values()->toArray(); + + $this->selectedRewards = $this->discount->purchasableRewards() + ->wherePurchasableType(Product::class) + ->pluck('purchasable_id')->values()->toArray(); + + $this->syncAvailability(); + } + + public function syncConditions($conditions) + { + $this->selectedConditions = $conditions; + } + + public function getValidationMessages() + { + return $this->getDiscountComponent()->getValidationMessages(); + } + + /** + * Get the collection attribute data. + * + * @return void + */ + public function getAttributeDataProperty() + { + return $this->discount->attribute_data; + } + + /** + * Set the currency using the provided id. + * + * @param int|string $currencyId + * @return void + */ + public function setCurrency($currencyId) + { + $this->currency = $this->currencies->first(fn ($currency) => $currency->id == $currencyId); + } + + /** + * Return the available discount types. + * + * @return array + */ + public function getDiscountTypesProperty() + { + return Discounts::getTypes(); + } + + /** + * Return the component for the selected discount type. + * + * @return Component + */ + public function getDiscountComponent() + { + return (new DiscountTypes)->getComponent($this->discount->type); + } + + /** + * Sync the discount data with what's provided. + * + * @param array $data + * @return void + */ + public function syncDiscountData(array $data) + { + $this->discount->data = array_merge( + $this->discount->data, + $data + ); + } + + /** + * Select collections given an array of IDs + * + * @param array $ids + * @return void + */ + public function selectCollections(array $ids) + { + $this->selectedCollections = $ids; + } + + public function syncRewards(array $ids) + { + $this->selectedRewards = $ids; + } + + public function syncAvailability() + { + $this->availability = [ + 'channels' => $this->channels->mapWithKeys(function ($channel) { + $discountChannel = $this->discount->channels->first(fn ($assoc) => $assoc->id == $channel->id); + + return [ + $channel->id => [ + 'channel_id' => $channel->id, + 'starts_at' => $discountChannel ? $discountChannel->pivot->starts_at : null, + 'ends_at' => $discountChannel ? $discountChannel->pivot->ends_at : null, + 'enabled' => $discountChannel ? $discountChannel->pivot->enabled : false, + 'scheduling' => false, + ], + ]; + }), + 'customerGroups' => $this->customerGroups->mapWithKeys(function ($group) { + // $productGroup = $this->product->customerGroups->where('id', $group->id)->first(); + + // $pivot = $productGroup->pivot ?? null; + + $pivot = null; + + return [ + $group->id => [ + 'customer_group_id' => $group->id, + 'scheduling' => false, + 'enabled' => false, + 'status' => 'hidden', + 'starts_at' => $pivot?->starts_at ?? null, + 'ends_at' => $pivot?->ends_at ?? null, + ], + ]; + }), + ]; + } + + /** + * Remove the collection by it's index. + * + * @param int|string $index + * @return void + */ + public function removeCollection($index) + { + $this->collections->forget($index); + } + + /** + * Return a list of available countries. + * + * @return Collection + */ + public function getBrandsProperty() + { + return Brand::orderBy('name')->get(); + } + + /** + * Return the category tree. + * + * @return Collection + */ + public function getCollectionTreeProperty() + { + return ModelsCollection::get()->toTree(); + } + + /** + * Save the discount. + * + * @return RedirectResponse + */ + public function save() + { + $redirect = ! $this->discount->id; + + $this->withValidator(function (Validator $validator) { + $validator->after(function ($validator) { + if ($validator->errors()->count()) { + $this->notify( + __('adminhub::validation.generic'), + level: 'error' + ); + } + }); + })->validate(null, $this->getValidationMessages()); + + DB::transaction(function () { + $this->discount->max_uses = $this->discount->max_uses ?: null; + $this->discount->save(); + + $this->discount->brands()->sync( + $this->selectedBrands + ); + + $channels = collect($this->availability['channels'])->mapWithKeys(function ($channel) { + return [ + $channel['channel_id'] => [ + 'starts_at' => ! $channel['enabled'] ? null : $channel['starts_at'], + 'ends_at' => ! $channel['enabled'] ? null : $channel['ends_at'], + 'enabled' => $channel['enabled'], + ], + ]; + }); + + $cgAvailability = collect($this->availability['customerGroups'])->mapWithKeys(function ($group) { + $data = Arr::only($group, ['starts_at', 'ends_at']); + + $data['visible'] = in_array($group['status'], ['purchasable', 'visible']); + $data['enabled'] = $group['enabled']; + + return [ + $group['customer_group_id'] => $data, + ]; + }); + + $this->discount->customerGroups()->sync($cgAvailability); + + $this->discount->channels()->sync($channels); + + $this->discount->collections()->sync( + $this->selectedCollections + ); + }); + + $this->emit('discount.saved', $this->discount->id); + + $this->notify( + __('adminhub::notifications.discount.saved') + ); + + if ($redirect) { + redirect()->route('hub.discounts.show', $this->discount->id); + } + } + + public function getSideMenuProperty() + { + return collect([ + [ + 'title' => __('adminhub::menu.product.basic-information'), + 'id' => 'basic-information', + 'has_errors' => $this->errorBag->hasAny([ + 'discount.name', + 'discount.handle', + 'discount.starts_at', + 'discount.ends_at', + ]), + ], + [ + 'title' => __('adminhub::menu.product.availability'), + 'id' => 'availability', + 'has_errors' => false, + ], + [ + 'title' => 'Limitations', + 'id' => 'limitations', + 'has_errors' => false, + ], + [ + 'title' => 'Conditions', + 'id' => 'conditions', + 'has_errors' => $this->errorBag->hasAny([ + 'minPrices.*.price', + 'discount.max_uses', + ]), + ], + [ + 'title' => 'Discount Type', + 'id' => 'type', + 'has_errors' => $this->errorBag->hasAny(array_merge( + $this->getDiscountComponent()->rules(), + ['selectedConditions', 'selectedRewards'] + )), + ], + ]); + } + + /** + * Render the livewire component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('adminhub::livewire.components.discounts.show') + ->layout('adminhub::layouts.app'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/DiscountCreate.php b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountCreate.php new file mode 100644 index 0000000000..a6ad074533 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountCreate.php @@ -0,0 +1,85 @@ +discount = new Discount([ + 'priority' => 1, + 'type' => DiscountTypesDiscount::class, + 'starts_at' => now()->startOfHour(), + 'data' => [], + ]); + + $this->currency = Currency::getDefault(); + $this->syncAvailability(); + } + + /** + * {@inheritDoc}. + */ + public function rules() + { + $rules = array_merge([ + 'discount.name' => 'required|unique:'.Discount::class.',name', + 'discount.handle' => 'required|unique:'.Discount::class.',handle', + 'discount.stop' => 'nullable', + 'discount.max_uses' => 'nullable|numeric|min:0', + 'discount.priority' => 'required|min:1', + 'discount.starts_at' => 'date', + 'discount.coupon' => 'nullable', + 'discount.ends_at' => 'nullable|date|after:starts_at', + 'discount.type' => 'string|required', + 'discount.data' => 'array', + 'selectedCollections' => 'array', + 'selectedBrands' => 'array', + ], $this->getDiscountComponent()->rules()); + + foreach ($this->currencies as $currency) { + $rules['discount.data.min_prices.'.$currency->code] = 'nullable'; + } + + return $rules; + } + + /** + * Handler for when the discount name is updated. + * + * @param string $val + * @return void + */ + public function updatedDiscountName($val) + { + if (! $this->discount->handle) { + $this->discount->handle = Str::snake(strtolower($val)); + } + } + + /** + * Render the livewire component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('adminhub::livewire.components.discounts.create') + ->layout('adminhub::layouts.app'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/DiscountShow.php b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountShow.php new file mode 100644 index 0000000000..ccda811b6b --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountShow.php @@ -0,0 +1,94 @@ + 'required|unique:'.Discount::class.',name,'.$this->discount->id, + 'discount.handle' => 'required|unique:'.Discount::class.',handle,'.$this->discount->id, + 'discount.stop' => 'nullable', + 'discount.max_uses' => 'nullable|numeric|min:0', + 'discount.priority' => 'required|min:1', + 'discount.starts_at' => 'date', + 'discount.coupon' => 'nullable', + 'discount.ends_at' => 'nullable|date|after:starts_at', + 'discount.type' => 'string|required', + 'discount.data' => 'array', + 'selectedCollections' => 'array', + 'selectedBrands' => 'array', + ], $this->getDiscountComponent()->rules()); + + foreach ($this->currencies as $currency) { + $rules['discount.data.min_prices.'.$currency->code] = 'nullable'; + } + + return $rules; + } + + /** + * Computed property to determine whether the discount can be deleted. + * + * @return bool + */ + public function getCanDeleteProperty() + { + return $this->deleteConfirm === $this->discount->name; + } + + /** + * Delete the discount. + * + * @return Redirector + */ + public function delete() + { + DB::transaction(function () { + $this->discount->purchasables()->delete(); + $this->discount->purchasableConditions()->delete(); + $this->discount->purchasableRewards()->delete(); + $this->discount->collections()->delete(); + $this->discount->delete(); + }); + + $this->emit( + __('adminhub::notifications.discount.deleted') + ); + + return redirect()->route('hub.discounts.index'); + } + + /** + * Render the livewire component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('adminhub::livewire.components.discounts.show') + ->layout('adminhub::layouts.app'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/DiscountsIndex.php b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountsIndex.php new file mode 100644 index 0000000000..3c5effb402 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountsIndex.php @@ -0,0 +1,35 @@ +layout('adminhub::layouts.base'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/DiscountsTable.php b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountsTable.php new file mode 100644 index 0000000000..881a7bd71c --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/DiscountsTable.php @@ -0,0 +1,75 @@ +tableBuilder->baseColumns([ + BadgeColumn::make('status', function ($record) { + $active = $record->starts_at?->isPast() && ! $record->ends_at?->isPast(); + $expired = $record->ends_at?->isPast(); + $future = $record->starts_at?->isFuture(); + + $status = $active ? 'active' : 'pending'; + + if ($expired) { + $status = 'expired'; + } + + if ($future) { + $status = 'scheduled'; + } + + return __('adminhub::components.discounts.index.status.'.$status); + })->states(function ($record) { + return [ + 'info' => $record->starts_at?->isFuture(), + 'pending' => ! $record->starts_at?->isPast(), + 'success' => $record->starts_at?->isPast() && ! $record->ends_at?->isPast(), + 'danger' => $record->ends_at?->isPast(), + ]; + }), + TextColumn::make('name')->heading( + __('adminhub::tables.headings.name') + )->url(function ($record) { + return route('hub.discounts.show', $record->id); + }), + TextColumn::make('type', function ($record) { + return (new $record->type)->getName(); + }), + TextColumn::make('starts_at'), + TextColumn::make('ends_at'), + ]); + } + + /** + * {@inheritDoc} + */ + public function getData() + { + return Discount::paginate($this->perPage); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/Types/AbstractDiscountType.php b/packages/admin/src/Http/Livewire/Components/Discounts/Types/AbstractDiscountType.php new file mode 100644 index 0000000000..e6c6f990cc --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/Types/AbstractDiscountType.php @@ -0,0 +1,58 @@ + 'save', + 'product-search.selected' => 'selectProducts', + ]; + + public function getValidationMessages() + { + return []; + } + + public function save() + { + // .. + } + + public function selectProducts(array $ids, $ref = null) + { + // .. + } + + /** + * {@inheritDoc} + */ + public function parentComponentErrorBag($errorBag) + { + $this->setErrorBag($errorBag); + } + + /** + * Handle when the discount data is updated. + * + * @return void + */ + public function updatedDiscount() + { + $this->emitUp('discountData.updated', $this->discount->data); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/Types/BuyXGetY.php b/packages/admin/src/Http/Livewire/Components/Discounts/Types/BuyXGetY.php new file mode 100644 index 0000000000..63bcbc7735 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/Types/BuyXGetY.php @@ -0,0 +1,236 @@ + 'array', + 'discount.data.min_qty' => 'required', + 'discount.data.reward_qty' => 'required|numeric', + 'discount.data.max_reward_qty' => 'required|numeric', + 'selectedConditions' => 'array|min:1', + 'selectedRewards' => 'array|min:1', + ]; + } + + /** + * {@inheritDoc} + */ + public function mount() + { + parent::mount(); + + $this->conditions = $this->discount->purchasableConditions() + ->wherePurchasableType(Product::class) + ->pluck('purchasable_id')->values(); + + $this->rewards = $this->discount->purchasableRewards() + ->wherePurchasableType(Product::class) + ->pluck('purchasable_id')->values(); + } + + /** + * Return the purchasable condition models. + * + * @return Collection + */ + public function getPurchasableConditionsProperty() + { + return Product::whereIn( + 'id', + $this->conditions + )->get(); + } + + /** + * Return the purchasable reward models. + * + * @return Collection + */ + public function getPurchasableRewardsProperty() + { + return Product::whereIn( + 'id', + $this->rewards + )->get(); + } + + /** + * Handle when the discount data is updated. + * + * @return void + */ + public function updatedDiscountDataMinQty() + { + $this->emitUp('discountData.updated', $this->discount->data); + } + + /** + * Handle when the discount data is updated. + * + * @return void + */ + public function updatedDiscountDataRewardQty() + { + $this->emitUp('discountData.updated', $this->discount->data); + } + + /** + * Remove a condition based on the product id + * + * @param string|int $productId + * @return void + */ + public function removeCondition($productId) + { + $index = $this->conditions->search($productId); + + $conditions = $this->conditions; + + $conditions->forget($index); + + $this->conditions = $conditions; + + $this->emit('discount.conditions', $conditions->toArray()); + } + + /** + * Remove a reward based on the product id + * + * @param string|int $productId + * @return void + */ + public function removeReward($productId) + { + $index = $this->rewards->search($productId); + + $rewards = $this->rewards; + + $rewards->forget($index); + + $this->rewards = $rewards; + } + + /** + * Select products + * + * @param array $ids + * @param string|null $ref + * @return void + */ + public function selectProducts(array $ids, $ref = null) + { + if ($ref == 'discount-conditions') { + $this->conditions = collect($ids); + $this->emit('discount.conditions', $this->conditions->toArray()); + } + + if ($ref == 'discount-rewards') { + $this->rewards = collect($ids); + $this->emit('discount.rewards', $this->rewards->toArray()); + } + } + + public function getValidationMessages() + { + return [ + 'discount.data.min_qty.required' => 'This field is required', + 'discount.data.reward_qty.required' => 'This field is required', + 'discount.data.max_reward_qty.required' => 'This field is required', + ]; + } + + /** + * Save the product discount. + * + * @return void + */ + public function save() + { + DB::transaction(function () { + $conditions = $this->conditions; + + $this->discount->purchasableConditions() + ->whereNotIn('purchasable_id', $conditions) + ->delete(); + + foreach ($conditions as $condition) { + $this->discount->purchasables()->firstOrCreate([ + 'discount_id' => $this->discount->id, + 'type' => 'condition', + 'purchasable_type' => Product::class, + 'purchasable_id' => $condition, + ]); + } + + $rewards = $this->rewards; + + $this->discount->purchasableConditions() + ->whereNotIn('purchasable_id', $conditions) + ->delete(); + + foreach ($rewards as $reward) { + $this->discount->purchasables()->firstOrCreate([ + 'discount_id' => $this->discount->id, + 'type' => 'reward', + 'purchasable_type' => Product::class, + 'purchasable_id' => $reward, + ]); + } + }); + } + + /** + * Return the available currencies. + * + * @return Collection + */ + public function getCurrenciesProperty() + { + return Currency::get(); + } + + /** + * Render the livewire component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('adminhub::livewire.components.discounts.types.buy-x-get-y') + ->layout('adminhub::layouts.base'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/Discounts/Types/Discount.php b/packages/admin/src/Http/Livewire/Components/Discounts/Types/Discount.php new file mode 100644 index 0000000000..3ee0a8b669 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Components/Discounts/Types/Discount.php @@ -0,0 +1,100 @@ + 'array', + 'discount.data.percentage' => 'required_if:discount.data.fixed_value,false|nullable|numeric|min:1', + 'discount.data.fixed_values' => 'array|min:0', + 'discount.data.fixed_value' => 'boolean', + ]; + + foreach ($this->currencies as $currency) { + $rules["discount.data.fixed_values.{$currency->code}"] = 'required_if:discount.data.fixed_value,true|nullable|numeric|min:1'; + } + + return $rules; + } + + public function getValidationMessages() + { + $messages = [ + 'discount.data.percentage.required_if' => 'This field is required', + 'discount.data.percentage.min' => 'Percentage must be at least :min', + 'discount.data.max_reward_qty.required' => 'This field is required', + ]; + + foreach ($this->currencies as $currency) { + $messages["discount.data.fixed_values.{$currency->code}.required_if"] = 'This field is required'; + } + + return $messages; + } + + /** + * {@inheritDoc} + */ + public function mount() + { + parent::mount(); + + if (empty($this->discount->data)) { + $this->discount->data = [ + 'coupon' => null, + 'fixed_value' => false, + ]; + } + } + + /** + * Listen to when the coupon is updated and emit the data change. + * + * @param string $val + * @return void + */ + public function updatedDiscountDataCoupon($val) + { + $data = (array) $this->discount->data; + + $data['coupon'] = strtoupper( + Str::snake( + strtolower($val) + ) + ); + + $this->discount->data = $data; + $this->emitUp('discountData.updated', $this->discount->data); + } + + /** + * Return the available currencies. + * + * @return Collection + */ + public function getCurrenciesProperty() + { + return Currency::get(); + } + + /** + * Render the livewire component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('adminhub::livewire.components.discounts.types.discount') + ->layout('adminhub::layouts.base'); + } +} diff --git a/packages/admin/src/Http/Livewire/Components/ProductSearch.php b/packages/admin/src/Http/Livewire/Components/ProductSearch.php index 546bfadf64..022a2f2a34 100644 --- a/packages/admin/src/Http/Livewire/Components/ProductSearch.php +++ b/packages/admin/src/Http/Livewire/Components/ProductSearch.php @@ -93,7 +93,7 @@ public function getResultsProperty() public function triggerSelect() { - $this->emit('product-search.selected', $this->selected); + $this->emit('product-search.selected', $this->selected, $this->ref); $this->showBrowser = false; } diff --git a/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountCreate.php b/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountCreate.php new file mode 100644 index 0000000000..1d22b72952 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountCreate.php @@ -0,0 +1,34 @@ +discount = new Discount; + } + + /** + * Render the livewire component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('adminhub::livewire.pages.discounts.create') + ->layout('adminhub::layouts.app', [ + 'title' => __('adminhub::components.discounts.create.title'), + ]); + } +} diff --git a/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountShow.php b/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountShow.php new file mode 100644 index 0000000000..3de0632014 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountShow.php @@ -0,0 +1,29 @@ +layout('adminhub::layouts.app', [ + 'title' => 'Discounts', + ]); + } +} diff --git a/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountsIndex.php b/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountsIndex.php new file mode 100644 index 0000000000..1f47e61cb2 --- /dev/null +++ b/packages/admin/src/Http/Livewire/Pages/Discounts/DiscountsIndex.php @@ -0,0 +1,21 @@ +layout('adminhub::layouts.app', [ + 'title' => 'Discounts', + ]); + } +} diff --git a/packages/admin/src/Menu/SidebarMenu.php b/packages/admin/src/Menu/SidebarMenu.php index fa73a0882a..a265b79da5 100644 --- a/packages/admin/src/Menu/SidebarMenu.php +++ b/packages/admin/src/Menu/SidebarMenu.php @@ -67,6 +67,14 @@ protected function addSections() ->icon('pencil'); }); + $slot->addItem(function ($item) { + $item->name( + __('adminhub::menu.sidebar.discounts') + )->handle('hub.discounts') + ->route('hub.discounts.index') + ->icon('ticket'); + }); + $slot->addItem(function ($item) { $item->name( __('adminhub::menu.sidebar.brands') diff --git a/packages/admin/src/Views/Components/Input/Radio.php b/packages/admin/src/Views/Components/Input/Radio.php new file mode 100644 index 0000000000..fe930c5139 --- /dev/null +++ b/packages/admin/src/Views/Components/Input/Radio.php @@ -0,0 +1,44 @@ +on = $on; + $this->disabled = $disabled; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\View\View|\Closure|string + */ + public function render() + { + return view('adminhub::components.input.radio'); + } +} diff --git a/packages/core/config/cart.php b/packages/core/config/cart.php index 6c470969f2..0eff94cd08 100644 --- a/packages/core/config/cart.php +++ b/packages/core/config/cart.php @@ -56,6 +56,7 @@ 'cart' => [ \Lunar\Pipelines\Cart\CalculateLines::class, \Lunar\Pipelines\Cart\ApplyShipping::class, + \Lunar\Pipelines\Cart\ApplyDiscounts::class, \Lunar\Pipelines\Cart\Calculate::class, ], /* diff --git a/packages/core/config/discounts.php b/packages/core/config/discounts.php new file mode 100644 index 0000000000..bcb3863b7f --- /dev/null +++ b/packages/core/config/discounts.php @@ -0,0 +1,14 @@ + \Lunar\Base\Validation\CouponValidator::class, +]; diff --git a/packages/core/database/factories/DiscountFactory.php b/packages/core/database/factories/DiscountFactory.php new file mode 100644 index 0000000000..bc444882f8 --- /dev/null +++ b/packages/core/database/factories/DiscountFactory.php @@ -0,0 +1,25 @@ +faker->unique()->name; + + return [ + 'name' => $name, + 'handle' => Str::snake($name), + 'type' => Coupon::class, + 'starts_at' => now(), + ]; + } +} diff --git a/packages/core/database/factories/DiscountPurchasableFactory.php b/packages/core/database/factories/DiscountPurchasableFactory.php new file mode 100644 index 0000000000..300b68601c --- /dev/null +++ b/packages/core/database/factories/DiscountPurchasableFactory.php @@ -0,0 +1,20 @@ + ProductVariant::factory(), + 'purchasable_type' => ProductVariant::class, + ]; + } +} diff --git a/packages/core/database/migrations/2022_11_18_100000_create_discounts_table.php b/packages/core/database/migrations/2022_11_18_100000_create_discounts_table.php new file mode 100644 index 0000000000..6fa3d3fb01 --- /dev/null +++ b/packages/core/database/migrations/2022_11_18_100000_create_discounts_table.php @@ -0,0 +1,33 @@ +prefix.'discounts', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('handle')->unique(); + $table->string('coupon')->nullable()->unique(); + $table->string('type')->index(); + $table->dateTime('starts_at')->index(); + $table->dateTime('ends_at')->nullable()->index(); + $table->integer('uses')->unsigned()->default(0)->index(); + $table->mediumInteger('max_uses')->unsigned()->nullable(); + $table->mediumInteger('priority')->unsigned()->index()->default(1); + $table->boolean('stop')->default(false)->index(); + $table->string('restriction')->index()->nullable(); + $table->json('data')->nullable(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'discounts'); + } +} diff --git a/packages/core/database/migrations/2022_11_18_100005_create_cart_line_discount_table.php b/packages/core/database/migrations/2022_11_18_100005_create_cart_line_discount_table.php new file mode 100644 index 0000000000..42b7ff5a5b --- /dev/null +++ b/packages/core/database/migrations/2022_11_18_100005_create_cart_line_discount_table.php @@ -0,0 +1,23 @@ +prefix.'cart_line_discount', function (Blueprint $table) { + $table->id(); + $table->foreignId('cart_line_id')->constrained($this->prefix.'carts')->cascadeOnDelete(); + $table->foreignId('discount_id')->constrained($this->prefix.'discounts')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'cart_line_discount'); + } +} diff --git a/packages/core/database/migrations/2022_11_18_100010_create_brand_discount_table.php b/packages/core/database/migrations/2022_11_18_100010_create_brand_discount_table.php new file mode 100644 index 0000000000..02ecfdd56d --- /dev/null +++ b/packages/core/database/migrations/2022_11_18_100010_create_brand_discount_table.php @@ -0,0 +1,23 @@ +prefix.'brand_discount', function (Blueprint $table) { + $table->id(); + $table->foreignId('brand_id')->constrained($this->prefix.'brands')->cascadeOnDelete(); + $table->foreignId('discount_id')->constrained($this->prefix.'discounts')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'brand_discount'); + } +} diff --git a/packages/core/database/migrations/2022_11_18_100015_create_customer_group_discount_table.php b/packages/core/database/migrations/2022_11_18_100015_create_customer_group_discount_table.php new file mode 100644 index 0000000000..eae908e447 --- /dev/null +++ b/packages/core/database/migrations/2022_11_18_100015_create_customer_group_discount_table.php @@ -0,0 +1,25 @@ +prefix.'customer_group_discount', function (Blueprint $table) { + $table->id(); + $table->foreignId('discount_id')->constrained($this->prefix.'discounts'); + $table->foreignId('customer_group_id')->constrained($this->prefix.'customer_groups'); + $table->scheduling(); + $table->boolean('visible')->default(true)->index(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'customer_group_discount'); + } +} diff --git a/packages/core/database/migrations/2022_11_18_100020_create_discount_collections_table.php b/packages/core/database/migrations/2022_11_18_100020_create_discount_collections_table.php new file mode 100644 index 0000000000..e4c606a6af --- /dev/null +++ b/packages/core/database/migrations/2022_11_18_100020_create_discount_collections_table.php @@ -0,0 +1,23 @@ +prefix.'collection_discount', function (Blueprint $table) { + $table->id(); + $table->foreignId('discount_id')->constrained($this->prefix.'discounts')->cascadeOnDelete(); + $table->foreignId('collection_id')->constrained($this->prefix.'collections')->cascadeOnDelete(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'collection_discount'); + } +} diff --git a/packages/core/database/migrations/2022_11_18_100030_create_discount_purchasables_table.php b/packages/core/database/migrations/2022_11_18_100030_create_discount_purchasables_table.php new file mode 100644 index 0000000000..9cb06e6893 --- /dev/null +++ b/packages/core/database/migrations/2022_11_18_100030_create_discount_purchasables_table.php @@ -0,0 +1,24 @@ +prefix.'discount_purchasables', function (Blueprint $table) { + $table->id(); + $table->foreignId('discount_id')->constrained($this->prefix.'discounts')->cascadeOnDelete(); + $table->morphs('purchasable', 'purchasable_idx'); + $table->string('type')->default('condition')->index(); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists($this->prefix.'discount_purchasables'); + } +} diff --git a/packages/core/resources/lang/en/exceptions.php b/packages/core/resources/lang/en/exceptions.php index 05a382a88c..b0fbf561ad 100644 --- a/packages/core/resources/lang/en/exceptions.php +++ b/packages/core/resources/lang/en/exceptions.php @@ -11,4 +11,5 @@ 'missing_currency_price' => 'No price for currency ":currency" exists', 'fieldtype_missing' => 'FieldType ":class" does not exist', 'invalid_fieldtype' => 'Class ":class" does not implement the FieldType interface.', + 'discounts.invalid_type' => 'Collection must only contain ":expected", found ":actual"', ]; diff --git a/packages/core/src/Actions/Carts/CalculateLine.php b/packages/core/src/Actions/Carts/CalculateLine.php index 36bafeec27..a3f8d9eb0a 100644 --- a/packages/core/src/Actions/Carts/CalculateLine.php +++ b/packages/core/src/Actions/Carts/CalculateLine.php @@ -5,7 +5,6 @@ use Illuminate\Support\Collection; use Lunar\Base\Addressable; use Lunar\DataTypes\Price; -use Lunar\Facades\Pricing; use Lunar\Facades\Taxes; use Lunar\Models\CartLine; @@ -28,28 +27,13 @@ public function execute( $cart = $cartLine->cart; $unitQuantity = $purchasable->getUnitQuantity(); - // we check if any cart line modifiers have already specified a unit price in their calculating() method - if (! ($price = $cartLine->unitPrice) instanceof Price) { - $priceResponse = Pricing::currency($cart->currency) - ->qty($cartLine->quantity) - ->currency($cart->currency) - ->customerGroups($customerGroups) - ->for($purchasable) - ->get(); + $cartLine = app(CalculateLineSubtotal::class)->execute($cartLine, $customerGroups); - $price = new Price( - $priceResponse->matched->price->value, - $cart->currency, - $purchasable->getUnitQuantity() - ); + if (! $cartLine->discountTotal) { + $cartLine->discountTotal = new Price(0, $cart->currency, $unitQuantity); } - $unitPrice = (int) round( - (($price->decimal / $purchasable->getUnitQuantity()) - * $cart->currency->factor), - $cart->currency->decimal_places); - - $subTotal = $unitPrice * $cartLine->quantity; + $subTotal = $cartLine->subTotal->value - $cartLine->discountTotal->value; $taxBreakDown = Taxes::setShippingAddress($shippingAddress) ->setBillingAddress($billingAddress) @@ -61,11 +45,8 @@ public function execute( $taxTotal = $taxBreakDown->amounts->sum('price.value'); $cartLine->taxBreakdown = $taxBreakDown; - $cartLine->subTotal = new Price($subTotal, $cart->currency, $unitQuantity); $cartLine->taxAmount = new Price($taxTotal, $cart->currency, $unitQuantity); $cartLine->total = new Price($subTotal + $taxTotal, $cart->currency, $unitQuantity); - $cartLine->unitPrice = new Price($unitPrice, $cart->currency, $unitQuantity); - $cartLine->discountTotal = new Price(0, $cart->currency, $unitQuantity); return $cartLine; } diff --git a/packages/core/src/Actions/Carts/CalculateLineSubtotal.php b/packages/core/src/Actions/Carts/CalculateLineSubtotal.php new file mode 100644 index 0000000000..0942c874b6 --- /dev/null +++ b/packages/core/src/Actions/Carts/CalculateLineSubtotal.php @@ -0,0 +1,71 @@ +purchasable; + $cart = $cartLine->cart; + $unitQuantity = $purchasable->getUnitQuantity(); + + // we check if any cart line modifiers have already specified a unit price in their calculating() method + if (! ($price = $cartLine->unitPrice) instanceof Price) { + $priceResponse = Pricing::currency($cart->currency) + ->qty($cartLine->quantity) + ->currency($cart->currency) + ->customerGroups($customerGroups) + ->for($purchasable) + ->get(); + + $price = new Price( + $priceResponse->matched->price->value, + $cart->currency, + $purchasable->getUnitQuantity() + ); + } + + $unitPrice = (int) round( + (($price->decimal / $purchasable->getUnitQuantity()) + * $cart->currency->factor), + $cart->currency->decimal_places + ); + + $cartLine->subTotal = new Price($unitPrice * $cartLine->quantity, $cart->currency, $unitQuantity); + $cartLine->unitPrice = new Price($unitPrice, $cart->currency, $unitQuantity); + + $pipeline = app(Pipeline::class) + ->through( + $this->getModifiers()->toArray() + ); + + return $pipeline->send($cartLine)->via('subtotalled')->thenReturn(); + } + + /** + * Return the cart line modifiers. + * + * @return \Illuminate\Support\Collection + */ + private function getModifiers() + { + return app(CartLineModifiers::class)->getModifiers(); + } +} diff --git a/packages/core/src/Actions/Carts/CreateOrder.php b/packages/core/src/Actions/Carts/CreateOrder.php index ab7059bab6..09cec4f709 100644 --- a/packages/core/src/Actions/Carts/CreateOrder.php +++ b/packages/core/src/Actions/Carts/CreateOrder.php @@ -45,6 +45,7 @@ public function execute( 'currency_code' => $cart->currency->code, 'exchange_rate' => $cart->currency->exchange_rate, 'compare_currency_code' => Currency::getDefault()?->code, + 'meta' => $cart->meta, ]); $order->update([ diff --git a/packages/core/src/Base/CartLineModifier.php b/packages/core/src/Base/CartLineModifier.php index 0d2a323ef1..d36e181e9a 100644 --- a/packages/core/src/Base/CartLineModifier.php +++ b/packages/core/src/Base/CartLineModifier.php @@ -26,4 +26,14 @@ public function calculated(CartLine $cartLine, Closure $next): CartLine { return $next($cartLine); } + + /** + * Called just after cart sub total is calculated. + * + * @return CartLine + */ + public function subtotalled(CartLine $cartLine, Closure $next): CartLine + { + return $next($cartLine); + } } diff --git a/packages/core/src/Base/DataTransferObjects/CartDiscount.php b/packages/core/src/Base/DataTransferObjects/CartDiscount.php new file mode 100644 index 0000000000..5c6725b715 --- /dev/null +++ b/packages/core/src/Base/DataTransferObjects/CartDiscount.php @@ -0,0 +1,17 @@ +active() + ->where(function ($query) { + $query->whereNull('max_uses') + ->orWhereRaw('uses < max_uses'); + })->where('coupon', '=', strtoupper($coupon))->exists(); + } +} diff --git a/packages/core/src/Base/Validation/CouponValidatorInterface.php b/packages/core/src/Base/Validation/CouponValidatorInterface.php new file mode 100644 index 0000000000..8b657e5383 --- /dev/null +++ b/packages/core/src/Base/Validation/CouponValidatorInterface.php @@ -0,0 +1,14 @@ +discount = $discount; + + return $this; + } +} diff --git a/packages/core/src/DiscountTypes/BuyXGetY.php b/packages/core/src/DiscountTypes/BuyXGetY.php new file mode 100644 index 0000000000..a8763c2ecc --- /dev/null +++ b/packages/core/src/DiscountTypes/BuyXGetY.php @@ -0,0 +1,131 @@ +discount = $discount; + + return $this; + } + + /** + * Return the name of the discount. + * + * @return string + */ + public function getName(): string + { + return 'Buy X Get Y'; + } + + /** + * Return the reward quantity for the discount + * + * @param int $linesQuantity + * @param int $minQty + * @param int $rewardQty + * @param int $maxRewardQty + * @return int + */ + public function getRewardQuantity($linesQuantity, $minQty, $rewardQty, $maxRewardQty = null) + { + $result = ($linesQuantity / $minQty) * $rewardQty; + + if ($maxRewardQty && $result > $maxRewardQty) { + return $maxRewardQty; + } + + return $result; + } + + /** + * Called just before cart totals are calculated. + * + * @return CartLine + */ + public function apply(Cart $cart): Cart + { + $data = $this->discount->data; + + $minQty = $data['min_qty'] ?? null; + $rewardQty = $data['reward_qty'] ?? 1; + $maxRewardQty = $data['max_reward_qty'] ?? null; + + // Get the first condition line where the qty check passes. + $conditions = $cart->lines->reject(function ($line) use ($minQty) { + $match = $this->discount->purchasableConditions->first(function ($item) use ($line) { + return $item->purchasable_type == Product::class && + $item->purchasable_id == $line->purchasable->product->id; + }); + + return ! $match || ($minQty && $line->quantity < $minQty); + }); + + if (! $conditions->count()) { + return $cart; + } + + // How many products are rewarded? + $totalRewardQty = $this->getRewardQuantity( + $conditions->sum('quantity'), + $minQty, + $rewardQty, + $maxRewardQty + ); + + $remainingRewardQty = $totalRewardQty; + + // Get the reward lines and sort by cheapest first. + $rewardLines = $cart->lines->filter(function ($line) { + return $this->discount->purchasableRewards->first(function ($item) use ($line) { + return $item->purchasable_type == Product::class && + $item->purchasable_id == $line->purchasable->product->id; + }); + })->sortBy('subTotal.value'); + + foreach ($rewardLines as $rewardLine) { + if (! $remainingRewardQty) { + continue; + } + + $remainder = $rewardLine->quantity % $remainingRewardQty; + + $qtyToAllocate = ($remainingRewardQty - $remainder) / $rewardLine->quantity; + + $remainingRewardQty -= $qtyToAllocate; + + $subTotal = $rewardLine->subTotal->value; + + $rewardLine->discountTotal = new Price( + $subTotal * $qtyToAllocate, + $cart->currency, + 1 + ); + + if (! $cart->freeItems) { + $cart->freeItems = collect(); + } + + $cart->freeItems->push($rewardLine->purchasable); + } + + return $cart; + } +} diff --git a/packages/core/src/DiscountTypes/Discount.php b/packages/core/src/DiscountTypes/Discount.php new file mode 100644 index 0000000000..112f7644c2 --- /dev/null +++ b/packages/core/src/DiscountTypes/Discount.php @@ -0,0 +1,151 @@ +discount->data; + + $cartCoupon = strtoupper($cart->coupon_code ?? null); + $conditionCoupon = strtoupper($this->discount->coupon ?? null); + + $passes = $cartCoupon && ($cartCoupon === $conditionCoupon); + + $minSpend = $data['min_prices'][$cart->currency->code] ?? null; + + $lines = $this->getEligibleLines($cart); + + if (! $passes || ($minSpend && $minSpend < $lines->sum('subTotal.value'))) { + return $cart; + } + + if ($data['fixed_value']) { + return $this->applyFixedValue( + values: $data['fixed_values'], + cart: $cart, + ); + } + + return $this->applyPercentage( + value: $data['percentage'], + cart: $cart + ); + } + + /** + * Apply fixed value discount + * + * @param array $values + * @param Cart $cart + * @return Cart + */ + private function applyFixedValue(array $values, Cart $cart): Cart + { + $currency = $cart->currency; + + $value = ($values[$currency->code] ?? 0) * 100; + + $lines = $this->getEligibleLines($cart); + + if (! $value || $lines->sum('subTotal.value') < $value) { + return $cart; + } + + $cart->cartDiscountAmount = new Price( + $value, + $currency, + 1 + ); + + if (! $cart->discounts) { + $cart->discounts = collect(); + } + + $cart->discounts->push($this); + + return $cart; + } + + /** + * Return the eligible lines for the discount. + * + * @param Cart $cart + * @return Collection + */ + private function getEligibleLines(Cart $cart) + { + $collectionIds = $this->discount->collections->pluck('id'); + $brandIds = $this->discount->brands->pluck('id'); + + $lines = $cart->lines; + + if ($collectionIds->count()) { + $lines = $lines->filter(function ($line) use ($collectionIds) { + return $line->purchasable->product()->whereHas('collections', function ($query) use ($collectionIds) { + $query->whereIn((new Collection)->getTable().'.id', $collectionIds); + })->exists(); + }); + } + + if ($brandIds->count()) { + $lines = $lines->reject(function ($line) use ($brandIds) { + return ! $brandIds->contains($line->purchasable->product->brand_id); + }); + } + + return $lines; + } + + /** + * Apply the percentage to the cart line. + * + * @param int $value + * @param CartLine $cartLine + * @return CartLine + */ + private function applyPercentage($value, $cart): Cart + { + $lines = $this->getEligibleLines($cart); + + foreach ($lines as $line) { + $subTotal = $line->subTotal->value; + $amount = (int) round($subTotal * ($value / 100)); + + $line->discountTotal = new Price( + $amount, + $cart->currency, + 1 + ); + } + + if (! $cart->discounts) { + $cart->discounts = collect(); + } + + $cart->discounts->push($this); + + return $cart; + } +} diff --git a/packages/core/src/Facades/Discounts.php b/packages/core/src/Facades/Discounts.php new file mode 100644 index 0000000000..7fc4458b01 --- /dev/null +++ b/packages/core/src/Facades/Discounts.php @@ -0,0 +1,17 @@ +app->singleton(PaymentManagerInterface::class, function ($app) { return $app->make(PaymentManager::class); }); + + $this->app->singleton(DiscountManagerInterface::class, function ($app) { + return $app->make(DiscountManager::class); + }); } /** diff --git a/packages/core/src/Managers/DiscountManager.php b/packages/core/src/Managers/DiscountManager.php new file mode 100644 index 0000000000..4043bbf46b --- /dev/null +++ b/packages/core/src/Managers/DiscountManager.php @@ -0,0 +1,223 @@ + + */ + protected ?Collection $channels = null; + + /** + * The current customer groups + * + * @var null|Collection + */ + protected ?Collection $customerGroups = null; + + /** + * The available discounts + * + * @var null|Collection + */ + protected ?Collection $discounts = null; + + /** + * The available discount types + * + * @var array + */ + protected $types = [ + TypesDiscount::class, + BuyXGetY::class, + ]; + + /** + * The applied discounts. + * + * @var Collection + */ + protected Collection $applied; + + /** + * Instantiate the class. + */ + public function __construct() + { + $this->applied = collect(); + $this->channels = collect(); + $this->customerGroups = collect(); + } + + /** + * Set a single channel or a collection. + * + * @param Channel|iterable $channel + * @return self + */ + public function channel(Channel|iterable $channel): self + { + $channels = collect( + ! is_iterable($channel) ? [$channel] : $channel + ); + + if ($nonChannel = $channels->filter(fn ($channel) => ! $channel instanceof Channel)->first()) { + throw new InvalidArgumentException( + __('lunar::exceptions.discounts.invalid_type', [ + 'expected' => Channel::class, + 'actual' => get_class($nonChannel), + + ]) + ); + } + + $this->channels = $channels; + + return $this; + } + + /** + * Set a single customer group or a collection. + * + * @param CustomerGroup|iterable $customerGroups + * @return self + */ + public function customerGroup(CustomerGroup|iterable $customerGroups): self + { + $customerGroups = collect( + ! is_iterable($customerGroups) ? [$customerGroups] : $customerGroups + ); + + if ($nonGroup = $customerGroups->filter(fn ($channel) => ! $channel instanceof CustomerGroup)->first()) { + throw new InvalidArgumentException( + __('lunar::exceptions.discounts.invalid_type', [ + 'expected' => CustomerGroup::class, + 'actual' => get_class($nonGroup), + ]) + ); + } + $this->customerGroups = $customerGroups; + + return $this; + } + + /** + * Return the applied channels. + * + * @return Collection + */ + public function getChannels(): Collection + { + return $this->channels; + } + + /** + * Returns the available discounts. + * + * @return Collection + */ + public function getDiscounts(): Collection + { + if ($this->channels->isEmpty() && $defaultChannel = Channel::getDefault()) { + $this->channel($defaultChannel); + } + + if ($this->customerGroups->isEmpty() && $defaultGroup = CustomerGroup::getDefault()) { + $this->customerGroup($defaultGroup); + } + + return Discount::active()->whereHas('channels', function ($query) { + $joinTable = (new Discount)->channels()->getTable(); + $query->whereIn("{$joinTable}.channel_id", $this->channels->pluck('id')) + ->where("{$joinTable}.enabled", true) + ->whereNotNull("{$joinTable}.starts_at") + ->where("{$joinTable}.starts_at", '<=', now()) + ->where(function ($query) use ($joinTable) { + $query->whereNull("{$joinTable}.ends_at") + ->orWhereDate("{$joinTable}.ends_at", '>', now()); + }); + })->whereHas('customerGroups', function ($query) { + $joinTable = (new Discount)->customerGroups()->getTable(); + + $query->whereIn("{$joinTable}.customer_group_id", $this->customerGroups->pluck('id')) + ->where("{$joinTable}.enabled", true) + ->whereNotNull("{$joinTable}.starts_at") + ->where("{$joinTable}.starts_at", '<=', now()) + ->where(function ($query) use ($joinTable) { + $query->whereNull("{$joinTable}.ends_at") + ->orWhereDate("{$joinTable}.ends_at", '>', now()); + }); + })->orderBy('priority')->get(); + } + + /** + * Return the applied customer groups. + * + * @return Collection + */ + public function getCustomerGroups(): Collection + { + return $this->customerGroups; + } + + public function addType($classname): self + { + $this->types[] = $classname; + + return $this; + } + + public function getTypes(): Collection + { + return collect($this->types)->map(function ($class) { + return app($class); + }); + } + + public function addApplied(CartDiscount $cartDiscount): self + { + $this->applied->push($cartDiscount); + + return $this; + } + + public function getApplied(): Collection + { + return $this->applied; + } + + public function apply(Cart $cart): Cart + { + if (! $this->discounts) { + $this->discounts = $this->getDiscounts(); + } + + foreach ($this->discounts as $discount) { + $cart = $discount->getType()->apply($cart); + } + + return $cart; + } + + public function validateCoupon(string $coupon): bool + { + return app( + config('lunar.discounts.coupon_validator', CouponValidator::class) + )->validate($coupon); + } +} diff --git a/packages/core/src/Models/Cart.php b/packages/core/src/Models/Cart.php index bfff75afbd..db4553dde4 100644 --- a/packages/core/src/Models/Cart.php +++ b/packages/core/src/Models/Cart.php @@ -17,6 +17,7 @@ use Lunar\Actions\Carts\UpdateCartLine; use Lunar\Base\Addressable; use Lunar\Base\BaseModel; +use Lunar\Base\Casts\Address; use Lunar\Base\Purchasable; use Lunar\Base\Traits\CachesProperties; use Lunar\Base\Traits\HasMacros; diff --git a/packages/core/src/Models/CartLine.php b/packages/core/src/Models/CartLine.php index 2864b595f9..1e9216375f 100644 --- a/packages/core/src/Models/CartLine.php +++ b/packages/core/src/Models/CartLine.php @@ -135,6 +135,16 @@ public function taxClass() ); } + public function discounts() + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany( + Discount::class, + "{$prefix}cart_line_discount" + ); + } + /** * Return the polymorphic relation. * diff --git a/packages/core/src/Models/Collection.php b/packages/core/src/Models/Collection.php index efeb8e55c3..81841bd10b 100644 --- a/packages/core/src/Models/Collection.php +++ b/packages/core/src/Models/Collection.php @@ -2,6 +2,7 @@ namespace Lunar\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Arr; @@ -80,6 +81,11 @@ public function group() return $this->belongsTo(CollectionGroup::class, 'collection_group_id'); } + public function scopeInGroup(Builder $builder, $id) + { + return $builder->where('collection_group_id', $id); + } + /** * Return the products relationship. * diff --git a/packages/core/src/Models/Discount.php b/packages/core/src/Models/Discount.php new file mode 100644 index 0000000000..23fddc3f77 --- /dev/null +++ b/packages/core/src/Models/Discount.php @@ -0,0 +1,130 @@ + 'datetime', + 'ends_at' => 'datetime', + 'data' => 'array', + ]; + + /** + * Return a new factory instance for the model. + * + * @return DiscountFactory + */ + protected static function newFactory(): DiscountFactory + { + return DiscountFactory::new(); + } + + /** + * Return the purchasables relationship. + * + * @return HasMany + */ + public function purchasables() + { + return $this->hasMany(DiscountPurchasable::class); + } + + public function purchasableConditions() + { + return $this->hasMany(DiscountPurchasable::class)->whereType('condition'); + } + + public function purchasableRewards() + { + return $this->hasMany(DiscountPurchasable::class)->whereType('reward'); + } + + public function getType() + { + return app($this->type)->with($this); + } + + /** + * Return the collections relationship. + * + * @return HasMany + */ + public function collections() + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany( + Collection::class, + "{$prefix}collection_discount" + )->withTimestamps(); + } + + /** + * Return the customer groups relationship. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function customerGroups(): BelongsToMany + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany( + CustomerGroup::class, + "{$prefix}customer_group_discount" + )->withPivot([ + 'visible', + 'enabled', + 'starts_at', + 'ends_at', + ])->withTimestamps(); + } + + public function brands() + { + $prefix = config('lunar.database.table_prefix'); + + return $this->belongsToMany( + Brand::class, + "{$prefix}brand_discount" + )->withTimestamps(); + } + + /** + * Return the active scope. + * + * @param Builder $query + * @return void + */ + public function scopeActive(Builder $query) + { + return $query->whereNotNull('starts_at') + ->where('starts_at', '<=', now()) + ->where(function ($query) { + $query->whereNull('ends_at') + ->orWhere('ends_at', '>', now()); + }); + } +} diff --git a/packages/core/src/Models/DiscountCollection.php b/packages/core/src/Models/DiscountCollection.php new file mode 100644 index 0000000000..07c37170ae --- /dev/null +++ b/packages/core/src/Models/DiscountCollection.php @@ -0,0 +1,51 @@ +belongsTo(Discount::class); + } + + public function collection() + { + return $this->belongsTo(Collection::class); + } +} diff --git a/packages/core/src/Models/DiscountPurchasable.php b/packages/core/src/Models/DiscountPurchasable.php new file mode 100644 index 0000000000..3302a008f3 --- /dev/null +++ b/packages/core/src/Models/DiscountPurchasable.php @@ -0,0 +1,62 @@ +belongsTo(Discount::class); + } + + /** + * Return the priceable relationship. + * + * @return \Illuminate\Database\Eloquent\Relations\MorphTo + */ + public function purchasable() + { + return $this->morphTo(); + } + + public function scopeCondition(Builder $query) + { + $query->whereType('condition'); + } +} diff --git a/packages/core/src/Pipelines/Cart/ApplyDiscounts.php b/packages/core/src/Pipelines/Cart/ApplyDiscounts.php new file mode 100644 index 0000000000..fcbebd04f6 --- /dev/null +++ b/packages/core/src/Pipelines/Cart/ApplyDiscounts.php @@ -0,0 +1,22 @@ +lines->sum('subTotal.value'); $discountTotal = $cart->lines->sum('discountTotal.value') + $cart->cartDiscountAmount?->value; + $taxTotal = $cart->lines->sum('taxAmount.value'); - $total = $cart->lines->sum('total.value'); + $total = $cart->lines->sum('total.value') - $discountTotal; + $taxBreakDownAmounts = $cart->lines->pluck('taxBreakdown')->pluck('amounts')->flatten(); // Get the shipping address diff --git a/packages/core/tests/Stubs/TestDiscountType.php b/packages/core/tests/Stubs/TestDiscountType.php new file mode 100644 index 0000000000..e8aaad6a14 --- /dev/null +++ b/packages/core/tests/Stubs/TestDiscountType.php @@ -0,0 +1,30 @@ +create([ + 'default' => true, + ]); + $billing = CartAddress::factory()->make([ 'type' => 'billing', 'country_id' => Country::factory(), @@ -187,6 +192,10 @@ public function cannot_create_order_with_incomplete_billing_address() /** @test */ public function can_set_tax_breakdown_correctly() { + CustomerGroup::factory()->create([ + 'default' => true, + ]); + $billing = CartAddress::factory()->make([ 'type' => 'billing', 'country_id' => Country::factory(), diff --git a/packages/core/tests/Unit/Base/Validation/CouponValidatorTest.php b/packages/core/tests/Unit/Base/Validation/CouponValidatorTest.php new file mode 100644 index 0000000000..5937f6ae5f --- /dev/null +++ b/packages/core/tests/Unit/Base/Validation/CouponValidatorTest.php @@ -0,0 +1,129 @@ +create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'coupon' => '10OFF', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 10, + ], + ]); + + $this->assertTrue( + $validator->validate('10OFF') + ); + + $this->assertTrue( + $validator->validate('10off') + ); + + $this->assertTrue( + $validator->validate('10oFf') + ); + + $this->assertFalse( + $validator->validate('20OFF') + ); + } + + /** @test **/ + public function can_validate_based_on_uses() + { + $validator = app(CouponValidator::class); + + $discount = Discount::factory()->create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'uses' => 10, + 'max_uses' => 20, + 'coupon' => '10OFF', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 10, + ], + ]); + + $this->assertTrue( + $validator->validate('10OFF') + ); + + $discount->update([ + 'uses' => 20, + ]); + + $this->assertFalse( + $validator->validate('10OFF') + ); + + $discount->update([ + 'max_uses' => null, + ]); + + $this->assertTrue( + $validator->validate('10OFF') + ); + } + + /** @test */ + public function can_validate_based_on_start_and_end_dates() + { + $validator = app(CouponValidator::class); + + $discount = Discount::factory()->create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'uses' => 0, + 'max_uses' => null, + 'starts_at' => now()->startOfDay(), + 'ends_at' => now()->endOfWeek(), + 'coupon' => '10OFF', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 10, + ], + ]); + + $this->assertTrue( + $validator->validate('10OFF') + ); + + $discount->update([ + 'starts_at' => now()->subWeek(), + 'ends_at' => now()->subWeek()->endOfWeek(), + ]); + + $this->assertFalse( + $validator->validate('10OFF') + ); + + $discount->update([ + 'starts_at' => now()->subWeek(), + 'ends_at' => now()->subWeek()->endOfWeek(), + ]); + + $this->assertFalse( + $validator->validate('10OFF') + ); + } +} diff --git a/packages/core/tests/Unit/DiscountTypes/BuyXGetYTest.php b/packages/core/tests/Unit/DiscountTypes/BuyXGetYTest.php new file mode 100644 index 0000000000..8a576f6d15 --- /dev/null +++ b/packages/core/tests/Unit/DiscountTypes/BuyXGetYTest.php @@ -0,0 +1,286 @@ + 1, + 'minQty' => 1, + 'rewardQty' => 1, + 'expected' => 1, + ], + [ + 'linesQuantity' => 2, + 'minQty' => 1, + 'rewardQty' => 1, + 'expected' => 2, + ], + [ + 'linesQuantity' => 2, + 'minQty' => 2, + 'rewardQty' => 1, + 'expected' => 1, + ], + [ + 'linesQuantity' => 10, + 'minQty' => 10, + 'rewardQty' => 1, + 'expected' => 1, + ], + [ + 'linesQuantity' => 10, + 'minQty' => 1, + 'rewardQty' => 1, + 'expected' => 10, + ], + [ + 'linesQuantity' => 10, + 'minQty' => 1, + 'rewardQty' => 1, + 'maxRewardQty' => 5, + 'expected' => 5, + ], + ]; + + foreach ($checks as $check) { + $this->assertEquals( + $check['expected'], + $driver->getRewardQuantity( + $check['linesQuantity'], + $check['minQty'], + $check['rewardQty'], + $check['maxRewardQty'] ?? null + ) + ); + } + } + + /** @test */ + public function can_discount_eligible_product() + { + $customerGroup = CustomerGroup::factory()->create([ + 'default' => true, + ]); + + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $currency = Currency::factory()->create([ + 'code' => 'GBP', + ]); + + $cart = Cart::factory()->create([ + 'channel_id' => $channel->id, + 'currency_id' => $currency->id, + ]); + + $productA = Product::factory()->create(); + + $productB = Product::factory()->create(); + + $purchasableA = ProductVariant::factory()->create([ + 'product_id' => $productA->id, + ]); + $purchasableB = ProductVariant::factory()->create([ + 'product_id' => $productB->id, + ]); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableA), + 'priceable_id' => $purchasableA->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableA), + 'purchasable_id' => $purchasableA->id, + 'quantity' => 1, + ]); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableB), + 'priceable_id' => $purchasableB->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableB), + 'purchasable_id' => $purchasableB->id, + 'quantity' => 1, + ]); + + $discount = Discount::factory()->create([ + 'type' => BuyXGetY::class, + 'name' => 'Test Product Discount', + 'data' => [ + 'min_qty' => 1, + 'reward_qty' => 1, + ], + ]); + + $discount->purchasableConditions()->create([ + 'purchasable_type' => Product::class, + 'purchasable_id' => $productA->id, + ]); + + $discount->purchasableRewards()->create([ + 'purchasable_type' => Product::class, + 'purchasable_id' => $productB->id, + 'type' => 'reward', + ]); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->subHour(), + ], + ]); + + $cart = $cart->calculate(); + + $purchasableBCartLine = $cart->lines->first(function ($line) use ($purchasableB) { + return $line->purchasable_id == $purchasableB->id; + }); + + $this->assertEquals(1000, $purchasableBCartLine->discountTotal->value); + } + + /** + * @test + * @group thisthis + */ + public function can_discount_eligible_products() + { + $customerGroup = CustomerGroup::factory()->create([ + 'default' => true, + ]); + + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $currency = Currency::factory()->create([ + 'code' => 'GBP', + ]); + + $cart = Cart::factory()->create([ + 'channel_id' => $channel->id, + 'currency_id' => $currency->id, + ]); + + $productA = Product::factory()->create(); + $productB = Product::factory()->create(); + $productC = Product::factory()->create(); + + $purchasableA = ProductVariant::factory()->create([ + 'product_id' => $productA->id, + ]); + $purchasableB = ProductVariant::factory()->create([ + 'product_id' => $productB->id, + ]); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableA), + 'priceable_id' => $purchasableA->id, + ]); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableB), + 'priceable_id' => $purchasableB->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableA), + 'purchasable_id' => $purchasableA->id, + 'quantity' => 1, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableB), + 'purchasable_id' => $purchasableB->id, + 'quantity' => 1, + ]); + + $discount = Discount::factory()->create([ + 'type' => BuyXGetY::class, + 'name' => 'Test Product Discount', + 'data' => [ + 'min_qty' => 1, + 'reward_qty' => 2, + ], + ]); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->subHour(), + ], + ]); + + $discount->purchasableConditions()->create([ + 'purchasable_type' => Product::class, + 'purchasable_id' => $productA->id, + ]); + + $discount->purchasableRewards()->create([ + 'purchasable_type' => Product::class, + 'purchasable_id' => $productB->id, + 'type' => 'reward', + ]); + + $cart = $cart->calculate(); + + $this->assertEquals(1200, $cart->total->value); + $this->assertCount(1, $cart->freeItems); + } +} diff --git a/packages/core/tests/Unit/DiscountTypes/DiscountTest.php b/packages/core/tests/Unit/DiscountTypes/DiscountTest.php new file mode 100644 index 0000000000..aafcfd2207 --- /dev/null +++ b/packages/core/tests/Unit/DiscountTypes/DiscountTest.php @@ -0,0 +1,275 @@ +create([ + 'default' => true, + ]); + + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $currency = Currency::factory()->create([ + 'code' => 'GBP', + ]); + + $cart = Cart::factory()->create([ + 'channel_id' => $channel->id, + 'currency_id' => $currency->id, + 'coupon_code' => '10OFF', + ]); + + $brandA = Brand::factory()->create([ + 'name' => 'Brand A', + ]); + + $brandB = Brand::factory()->create([ + 'name' => 'Brand B', + ]); + + $productA = Product::factory()->create([ + 'brand_id' => $brandA->id, + ]); + + $productB = Product::factory()->create([ + 'brand_id' => $brandB->id, + ]); + + $purchasableA = ProductVariant::factory()->create([ + 'product_id' => $productA->id, + ]); + $purchasableB = ProductVariant::factory()->create([ + 'product_id' => $productB->id, + ]); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableA), + 'priceable_id' => $purchasableA->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableA), + 'purchasable_id' => $purchasableA->id, + 'quantity' => 1, + ]); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableB), + 'priceable_id' => $purchasableB->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableB), + 'purchasable_id' => $purchasableB->id, + 'quantity' => 1, + ]); + + $discount = Discount::factory()->create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'coupon' => '10OFF', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 10, + ], + ]); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->subHour(), + ], + ]); + + $discount->brands()->sync([$brandA->id]); + + $cart = $cart->calculate(); + + $this->assertEquals(100, $cart->discountTotal->value); + $this->assertEquals(2100, $cart->total->value); + } + + /** + * @test + * @group thisdiscount + */ + public function can_apply_fixed_amount_discount() + { + $currency = Currency::factory()->create([ + 'code' => 'GBP', + ]); + + $customerGroup = CustomerGroup::factory()->create([ + 'default' => true, + ]); + + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + 'channel_id' => $channel->id, + 'coupon_code' => '10OFF', + ]); + + $purchasableA = ProductVariant::factory()->create(); + + Price::factory()->create([ + 'price' => 1000, // £10 + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasableA), + 'priceable_id' => $purchasableA->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasableA), + 'purchasable_id' => $purchasableA->id, + 'quantity' => 2, + ]); + + $discount = Discount::factory()->create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'coupon' => '10OFF', + 'data' => [ + 'fixed_value' => true, + 'fixed_values' => [ + 'GBP' => 10, + ], + ], + ]); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->subHour(), + ], + ]); + + $cart = $cart->calculate(); + + $this->assertEquals(1000, $cart->discountTotal->value); + $this->assertEquals(1400, $cart->total->value); + $this->assertEquals(400, $cart->taxTotal->value); + $this->assertCount(1, $cart->discounts); + } + + /** @test */ + public function can_apply_percentage_discount() + { + $customerGroup = CustomerGroup::factory()->create([ + 'default' => true, + ]); + + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $currency = Currency::factory()->create([ + 'code' => 'GBP', + ]); + + $cart = Cart::factory()->create([ + 'channel_id' => $channel->id, + 'currency_id' => $currency->id, + 'coupon_code' => '10PERCENTOFF', + ]); + + $purchasable = ProductVariant::factory()->create(); + + Price::factory()->create([ + 'price' => 1000, + 'tier' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => get_class($purchasable), + 'priceable_id' => $purchasable->id, + ]); + + $cart->lines()->create([ + 'purchasable_type' => get_class($purchasable), + 'purchasable_id' => $purchasable->id, + 'quantity' => 1, + ]); + + $discount = Discount::factory()->create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'coupon' => '10PERCENTOFF', + 'data' => [ + 'percentage' => 10, + 'fixed_value' => false, + ], + ]); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->subHour(), + ], + ]); + + $this->assertNull($cart->total); + $this->assertNull($cart->taxTotal); + $this->assertNull($cart->subTotal); + + $cart = $cart->calculate(); + + $this->assertEquals(100, $cart->discountTotal->value); + $this->assertEquals(200, $cart->taxTotal->value); + $this->assertEquals(1100, $cart->total->value); + } +} diff --git a/packages/core/tests/Unit/Managers/DiscountManagerTest.php b/packages/core/tests/Unit/Managers/DiscountManagerTest.php new file mode 100644 index 0000000000..65b7ac34ee --- /dev/null +++ b/packages/core/tests/Unit/Managers/DiscountManagerTest.php @@ -0,0 +1,315 @@ +assertInstanceOf(DiscountManager::class, $manager); + } + + /** @test */ + public function can_set_channel() + { + $manager = app(DiscountManagerInterface::class); + + $channel = Channel::factory()->create(); + + $this->assertCount(0, $manager->getChannels()); + + $manager->channel($channel); + + $this->assertCount(1, $manager->getChannels()); + + $channels = Channel::factory(2)->create(); + + $manager->channel($channels); + + $this->assertCount(2, $manager->getChannels()); + + $this->expectException(InvalidArgumentException::class); + + $manager->channel(Product::factory(2)->create()); + } + + /** @test */ + public function can_set_customer_group() + { + $manager = app(DiscountManagerInterface::class); + + $customerGroup = CustomerGroup::factory()->create(); + + $this->assertCount(0, $manager->getCustomerGroups()); + + $manager->customerGroup($customerGroup); + + $this->assertCount(1, $manager->getCustomerGroups()); + + $customerGroups = CustomerGroup::factory(2)->create(); + + $manager->customerGroup($customerGroups); + + $this->assertCount(2, $manager->getCustomerGroups()); + + $this->expectException(InvalidArgumentException::class); + + $manager->channel(Product::factory(2)->create()); + } + + /** @test */ + public function can_restrict_discounts_to_channel() + { + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $channelTwo = Channel::factory()->create([ + 'default' => false, + ]); + + $customerGroup = CustomerGroup::factory()->create([ + 'default' => true, + ]); + + $discount = Discount::factory()->create(); + + $manager = app(DiscountManagerInterface::class); + + $this->assertEmpty($manager->getDiscounts()); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'visible' => true, + 'starts_at' => now(), + ], + ]); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + $channelTwo->id => [ + 'enabled' => false, + 'starts_at' => now(), + ], + ]); + + $this->assertCount(1, $manager->getDiscounts()); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->addHour(), + ], + $channelTwo->id => [ + 'enabled' => false, + 'starts_at' => now(), + ], + ]); + + $this->assertEmpty($manager->getDiscounts()); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now()->subDay(), + 'ends_at' => now(), + ], + $channelTwo->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $this->assertEmpty($manager->getDiscounts()); + + $manager->channel($channelTwo); + + $this->assertCount(1, $manager->getDiscounts()); + } + + /** @test */ + public function can_restrict_discounts_to_customer_group() + { + $channel = Channel::factory()->create([ + 'default' => true, + ]); + + $customerGroup = CustomerGroup::factory()->create([ + 'default' => true, + ]); + + $customerGroupTwo = CustomerGroup::factory()->create([ + 'default' => false, + ]); + + $discount = Discount::factory()->create(); + + $discount->channels()->sync([ + $channel->id => [ + 'enabled' => true, + 'starts_at' => now(), + ], + ]); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'visible' => true, + 'starts_at' => now(), + ], + ]); + + $manager = app(DiscountManagerInterface::class); + + $this->assertCount(1, $manager->getDiscounts()); + + $discount->customerGroups()->sync([ + $channel->id => [ + 'enabled' => false, + 'starts_at' => now(), + ], + ]); + + $this->assertEmpty($manager->getDiscounts()); + + $discount->customerGroups()->sync([ + $customerGroup->id => [ + 'enabled' => true, + 'visible' => true, + 'starts_at' => now(), + ], + $customerGroupTwo->id => [ + 'enabled' => true, + 'visible' => false, + 'starts_at' => null, + ], + ]); + + $manager->customerGroup($customerGroupTwo); + + $this->assertEmpty($manager->getDiscounts()); + } + + /** + * @test + */ + public function can_fetch_discount_types() + { + $manager = app(DiscountManagerInterface::class); + + $this->assertInstanceOf(Collection::class, $manager->getTypes()); + } + + /** + * @test + */ + public function can_fetch_applied_discounts() + { + $manager = app(DiscountManagerInterface::class); + + $this->assertInstanceOf(Collection::class, $manager->getApplied()); + $this->assertCount(0, $manager->getApplied()); + } + + /** + * @test + */ + public function can_add_applied_discount() + { + $manager = app(DiscountManagerInterface::class); + + $this->assertInstanceOf(Collection::class, $manager->getApplied()); + + $this->assertCount(0, $manager->getApplied()); + + ProductVariant::factory()->create(); + + $discount = Discount::factory()->create(); + $cartLine = CartLine::factory()->create(); + + $discount = new CartDiscount( + model: $cartLine, + discount: $discount + ); + + $manager->addApplied($discount); + + $this->assertCount(1, $manager->getApplied()); + } + + /** + * @test + */ + public function can_add_new_types() + { + $manager = app(DiscountManagerInterface::class); + + $testType = $manager->getTypes()->first(function ($type) { + return get_class($type) == TestDiscountType::class; + }); + + $this->assertNull($testType); + + $manager->addType(TestDiscountType::class); + + $testType = $manager->getTypes()->first(function ($type) { + return get_class($type) == TestDiscountType::class; + }); + + $this->assertInstanceOf(TestDiscountType::class, $testType); + } + + /** @test */ + public function can_validate_coupons() + { + $manager = app(DiscountManagerInterface::class); + + Discount::factory()->create([ + 'type' => DiscountTypesDiscount::class, + 'name' => 'Test Coupon', + 'coupon' => '10OFF', + 'data' => [ + 'fixed_value' => false, + 'percentage' => 10, + ], + ]); + + $this->assertTrue( + $manager->validateCoupon('10OFF') + ); + + $this->assertFalse( + $manager->validateCoupon('20OFF') + ); + } +} diff --git a/utils/livewire-tables/resources/views/columns/badge.blade.php b/utils/livewire-tables/resources/views/columns/badge.blade.php index f781acba24..e834410c34 100644 --- a/utils/livewire-tables/resources/views/columns/badge.blade.php +++ b/utils/livewire-tables/resources/views/columns/badge.blade.php @@ -1,6 +1,7 @@
!@empty($info), 'lt-text-green-600 lt-bg-green-50' => !@empty($success), 'lt-text-blue-600 lt-bg-blue-50' => !@empty($info), 'lt-text-yellow-600 lt-bg-yellow-50' => !@empty(