From 098723589100027a3402288a8ea8752f52d73276 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Tue, 15 Sep 2026 08:48:25 +0100 Subject: [PATCH 1/9] spec: product bundles add-on (0085) Co-Authored-By: Claude Fable 5.1 --- TODO.md | 1 + specs/0085-product-bundles.md | 478 ++++++++++++++++++++++++++++++++++ specs/README.md | 1 + 3 files changed, 480 insertions(+) create mode 100644 specs/0085-product-bundles.md diff --git a/TODO.md b/TODO.md index 8a81b47be2..adaa023754 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,7 @@ Items tagged _(judgement)_ are genuine line-calls worth revisiting. - Bulk order operations — goal-oriented bulk actions on the orders table (spec 0026) - Order print templates — Print dropdown of selectable PDF templates, ships an Advice Note (spec 0027) - Cart totals caching in the database — additive performance optimisation +- Product bundles — `lunarphp/bundles`: sell a set of variants as one product with derived stock and component order lines (spec 0085) - Add Boost guidelines to packages ## Ideas diff --git a/specs/0085-product-bundles.md b/specs/0085-product-bundles.md new file mode 100644 index 0000000000..0ddddccd33 --- /dev/null +++ b/specs/0085-product-bundles.md @@ -0,0 +1,478 @@ +# 0085 — Product bundles (`lunarphp/bundles`) + +- Status: accepted +- Author: Glenn Jacobs +- Created: 2026-09-15 +- TODO item: Product bundles — sell a set of variants as one product with derived stock and component fulfilment +- Target branch: `2.x` (monorepo) and `next` (docs) + +## Problem + +Lunar has no way to sell a kit. A merchant who wants "Camera starter kit: body + bag + SD card" at one price has two poor options today: + +- Create a normal product for the kit. Stock is then tracked on the kit SKU independently of its parts, so the kit sells out (or oversells) without regard to the components, and the warehouse pick list shows "starter kit x1" with no parts to pick. +- Use a `BuyXGetY` discount. The customer has to add every part separately, there is no single "kit" product to list, merchandise or search, and the saving is a promotion, not a price. + +The core was designed with this in mind. `Purchasable` and `TracksStock` are separate contracts, and `TracksStock`'s docblock names "a bundle" as a motivating custom purchasable. Spec 0030 lists bundles as a downstream `Purchasable`. Nothing has been built on that foundation. + +Concretely, the seams a bundle needs today are these: + +- Stock: `ProductVariant::getTotalInventory()` and `canBeFulfilledAtQuantity()` read the variant's own rollup columns. There is no way for an add-on to answer them from other variants' stock. +- Order lines: `CreateOrderLines`, `CleanUpOrderLines` and `MapDiscountBreakdown` identify a line by purchasable plus a `meta` diff. There is no notion of a line that belongs to another line, so nothing can add component lines to an order without those stages deleting or mis-mapping them. +- Fulfilment and stock commitment both walk `order_lines` by purchasable (`SyncsTrackedStock`, `SyncStockCommitment`, `FulfilmentLine`). A bundle sold as one line never commits or picks its parts. +- Pricing: `PricingManager::get()` throws `MissingCurrencyPriceException` before the `lunar.pricing.pipelines` stages run, so a purchasable whose price is derived from other purchasables has no runtime hook. + +## Proposal + +Three deliverables, in order: + +1. **Two small, generic seams in `lunarphp/core`**: component order lines (`order_lines.parent_line_id`) and a resolver seam behind `ProductVariant`'s inventory answers. Both usable by any add-on that sells composites (gift sets, subscriptions with a starter kit, made-to-order assemblies). +2. **A new package `lunarphp/bundles`** (`packages/bundles`, namespace `Lunar\Bundles`) that makes any product variant a bundle of other variants, with fixed or component-derived pricing, stock derived from components, and component lines on the order so fulfilment and stock commitment work unchanged. It ships an Inertia panel extension. +3. **A docs PR** to `lunarphp/docs`. + +### Design position: a bundle is a product + +A bundle is a normal `Product` with normal `ProductVariant`s. The package attaches a component list to a **variant** (the sellable unit) through its own tables. Nothing about the product, its media, attributes, URLs, collections, channels, customer groups, search indexing, tax class, discount targeting, `public_id`, cache tags or the panel product editor changes. + +That decision is what keeps the package small. The alternative, a standalone `Bundle` purchasable, is rejected in "Alternatives considered": the core has more than a dozen sites that assume a cart or order line's purchasable is a `ProductVariant`, the storefront lists products, and the panel's product editing components are not on the add-on surface. + +Two consequences follow, and they are the only behaviour the package has to add: + +- **The bundle variant's own stock is ignored.** Availability is derived from the components through the new inventory seam. +- **At order creation the bundle line gains component lines.** Fulfilment, stock commitment, returns and location allocation then see the parts, because they already work from order lines. + +Terminology used throughout: the **bundle variant** is the `ProductVariant` that carries the components; a **component** is a `(product_variant, quantity)` pair inside it; a **group** is an optional choice set inside a configurable bundle; a **selection** is the customer's resolved list of components for one cart line. + +### Part 1: seams in `lunarphp/core` + +#### 1.1 Component order lines + +`order_lines` gains a nullable self-referencing `parent_line_id` (folded into the baseline migration while v2 is in alpha): + +```php +$table->foreignId('parent_line_id')->nullable()->index() + ->constrained($this->prefix.'order_lines')->cascadeOnDelete(); +``` + +`OrderLine` gains: + +```php +public function parent(): BelongsTo; // ?OrderLine +public function components(): HasMany; // OrderLine, ordered by id +public function isComponent(): bool; // parent_line_id !== null +public function scopeTopLevel(Builder $query): void; // whereNull('parent_line_id') +``` + +A component line is a real order line: it has a purchasable, `type`, `requires_shipping`, `requires_fulfilment`, `identifier` and `quantity`, so every consumer that already walks order lines to fulfil, allocate, ship, return or commit stock keeps working. What it does not carry is money: `unit_price`, `sub_total`, `discount_total`, `tax_total` and `total` are `0` and `tax_breakdown` is empty. The parent line is the customer's contract and holds the price, the tax and the refundable amount. Component lines carry `meta.allocated_total` (see 2.6) for reporting only. + +Core stages that identify order lines by purchasable plus `meta` skip component lines, because a component line is never the image of a cart line: + +- `CreateOrderLines` matches against `$order->lines->whereNull('parent_line_id')`. +- `CleanUpOrderLines` iterates `$order->productLines->whereNull('parent_line_id')`. Component lines are removed with their parent through the cascade. +- `MapDiscountBreakdown` maps `$order->lines->whereNull('parent_line_id')`. + +`EnsureInitialFulfilment` and the fulfilment methods are untouched: they already work from `fulfillableLines()`, and a parent line is stamped `requires_fulfilment = false` (2.6), so only the components are claimed. + +Refunds are parent-only. `RefundLine` continues to point at any order line, but `OrderLine::refundableQuantity()` returns `0` for a component line, and the panel refund composer lists top-level lines only. + +Panel (`lunarphp/panel`, first party): + +- `OrderShowController` builds `otherLines` and `refundableLines` from top-level lines. +- Fulfilment line rows (`FulfilmentLineRow.vue`) render a "part of {parent description}" hint when the order line is a component, so the pick list reads "Camera body x1 — part of Camera starter kit". +- The non-fulfillable lines card shows the parent line with its components nested beneath it, quantities only. + +Storefront guidance (docs): order history renders `$order->lines()->topLevel()->with('components')` and nests components under their parent. + +#### 1.2 Inventory resolver seam + +`ProductVariant::getTotalInventory()` and `canBeFulfilledAtQuantity()` delegate to an action so an add-on can answer them from somewhere other than the variant's own rollup columns: + +```php +namespace Lunar\Core\Contracts\Actions\Products; + +interface ResolvesInventory +{ + public function execute(ProductVariant $variant): VariantInventory; +} +``` + +```php +namespace Lunar\Core\DataObjects; + +final readonly class VariantInventory +{ + public function __construct( + public int $available, // units that can be sold now + public bool $unlimited, // selling continues regardless of $available + ) {} + + public function allows(int $quantity): bool + { + return $this->unlimited || $quantity <= $this->available; + } +} +``` + +The default implementation, `Actions/Products/ResolveInventory`, moves the existing `match ($this->selling_policy)` logic out of the model unchanged. `ProductVariant` becomes: + +```php +public function getTotalInventory(): int +{ + return app(ResolvesInventory::class)->execute($this)->available; +} + +public function canBeFulfilledAtQuantity(int $quantity): bool +{ + return app(ResolvesInventory::class)->execute($this)->allows($quantity); +} +``` + +Registered in `ActionServiceProvider::$actions`. Spec 0082 (selling-policy rework) changes the formula inside `ResolveInventory`, not the seam, so the two specs compose. `ShippingOption` and custom purchasables are unaffected. + +The bundles package decorates the binding (`$this->app->extend(ResolvesInventory::class, ...)`): a bundle variant's inventory is computed from its components; every other variant falls through to the inner action. + +### Part 2: `lunarphp/bundles` + +#### 2.1 Package skeleton + +``` +packages/bundles/ + composer.json name lunarphp/bundles, requires lunarphp/core; lunarphp/panel is suggested + config/bundles.php merged as lunar.bundles + database/migrations/ three tables, all extending Lunar\Core\Database\Migration + database/factories/ BundleFactory, BundleGroupFactory, BundleComponentFactory + resources/lang/{16 locales}/bundles.php + resources/js/ panel add-on (Vue), built like packages/panel-addon-example + build/ compiled panel bundle, tracked (CI commits it) + src/ + BundlesServiceProvider.php + Contracts/Actions/{DefinesBundle,SyncsBundleComponents,SyncsBundleGroups,RepricesBundle, + ResolvesBundleSelection,DeletesBundle}.php + Actions/{DefineBundle,SyncBundleComponents,SyncBundleGroups,RepriceBundle, + ResolveBundleSelection,DeleteBundle,ResolveBundleInventory}.php + Models/{Bundle,BundleGroup,BundleComponent}.php + Enums/BundlePricing.php + ValueObjects/{BundleSelection,SelectedComponent}.php + Modifiers/BundleSelectionPricing.php CartLineModifier + Validation/CartLine/BundleSelection.php add_to_cart / update_cart_line validator + Pipelines/Order/Creation/CreateBundleComponentLines.php + Listeners/{RepriceOnComponentPriceChange,RepriceOnComponentInvalidation,InvalidateContainingBundles}.php + Events/BundleInvalidated.php + Console/RepriceBundlesCommand.php + Exceptions/{InvalidBundleSelection,BundleNesting}.php + Panel/{BundlesSectionExtension.php, Http/Controllers/*, Http/Requests/*, Tables/BundleColumn.php, + Tables/BundlesOnlyFilter.php, Tables/ProductsTableExtension.php} +``` + +`composer.json` follows `search-relevance`: `type: library`, psr-4 for `Lunar\Bundles\` and `Lunar\Bundles\Database\Factories\`, `extra.lunar.name = "Bundles"`, `extra.laravel.providers`, `suggest: lunarphp/panel`. Monorepo wiring: root `composer.json` (psr-4 x2, autoload-dev, `extra.lunar.name`, providers, `replace`), `phpunit.xml` testsuite `bundles`, `.github/workflows/tests.yml` matrix plus a `panel-js` build step, `split_packages.yml`, `build_panel_assets.yml`, root `package.json` workspace. `monorepo-builder.php` and `phpstan.neon.dist` need nothing. + +#### 2.2 Database + +**`{prefix}bundles`** — one row per bundle variant. + +| Column | Type | Notes | +|---|---|---| +| id | bigint PK | | +| public_id | ulid unique | | +| product_variant_id | FK product_variants, unique, cascadeOnDelete | the bundle variant | +| pricing | string(16) | `fixed` or `components` (`BundlePricing` enum) | +| discount_percentage | decimal(5,2) null | only meaningful with `components`; `null` means no saving | +| timestamps | | | + +**`{prefix}bundle_groups`** — a choice set inside a configurable bundle. + +| Column | Type | Notes | +|---|---|---| +| id | bigint PK | | +| public_id | ulid unique | referenced from cart line meta | +| bundle_id | FK bundles, cascadeOnDelete | | +| name | json | translated | +| min_selections | smallint default 1 | `0` makes the group optional | +| max_selections | smallint default 1 | `1` is "pick one" | +| position | smallint default 0 | | +| timestamps | | | + +**`{prefix}bundle_components`** — a variant inside a bundle, fixed or offered in a group. + +| Column | Type | Notes | +|---|---|---| +| id | bigint PK | | +| public_id | ulid unique | referenced from cart line meta | +| bundle_id | FK bundles, cascadeOnDelete | | +| bundle_group_id | FK bundle_groups null, cascadeOnDelete | `null` means always included | +| product_variant_id | FK product_variants, cascadeOnDelete | the component | +| quantity | smallint default 1 | units per bundle unit | +| default | boolean default false | pre-selected inside a group; drives repricing (2.5) | +| position | smallint default 0 | | +| timestamps | | | +| unique (bundle_id, bundle_group_id, product_variant_id) | | | + +A bundle with no groups is a **fixed bundle**. A bundle with one or more groups is a **configurable bundle**. Fixed and grouped components can coexist ("always includes the body; choose a lens"). + +Invariants, enforced by the actions and by a `BundleNesting` exception: + +- A component variant is never itself a bundle variant (no nesting). +- A component variant is never the bundle variant. +- A bundle has at least one component, and no more than `lunar.bundles.max_components` (default 25). +- `min_selections <= max_selections <= number of components in the group`. + +Deleting a component variant cascades to its `bundle_components` row; the panel "Included in bundles" card (2.8) is the merchant's warning before they do that. + +#### 2.3 Models and the variant relation + +`Bundle`, `BundleGroup`, `BundleComponent` extend `Lunar\Core\Models\Base` with `HasPublicId`, `HasMacros` and `LogsActivity`; `BundleGroup` adds `HasTranslations`; `BundleComponent` and `BundleGroup` use `InvalidatesRelatedCache` with `cacheInvalidationTargets()` returning `[$this->bundle]`; `Bundle` uses `InvalidatesCache` with `cacheInvalidationTargets()` returning `[$this->variant->product]` and a `BundleInvalidated` event. `ModelManifest::addDirectory(__DIR__.'/Models')` registers all three (morph keys `bundle`, `bundle_group`, `bundle_component`). + +```php +class Bundle extends Base +{ + public function variant(): BelongsTo; // ProductVariant + public function groups(): HasMany; // ordered by position + public function components(): HasMany; // ordered by position, with group + public function fixedComponents(): HasMany; // whereNull('bundle_group_id') + public function isConfigurable(): bool; + + // Verbs, one-line delegating to the action contracts (spec 0029) + public function syncComponents(array $components): self; + public function syncGroups(array $groups): self; + public function reprice(): self; + public function resolveSelection(array $meta = []): BundleSelection; +} +``` + +The package adds the relation the other way with the precedent `table-rate-shipping` uses: + +```php +ProductVariant::resolveRelationUsing('bundle', fn (ProductVariant $variant) => $variant->hasOne(Bundle::class)); +Product::resolveRelationUsing('bundles', fn (Product $product) => $product->hasManyThrough(Bundle::class, ProductVariant::class)); +``` + +`$variant->bundle` is `null` for an ordinary variant. That is the whole "is this a bundle" test; there is no flag on the variant. + +#### 2.4 Actions + +Every action implements a contract in `Contracts/Actions/`, exposes `execute()`, injects its collaborators, and is bound in `BundlesServiceProvider::register()`. + +| Action | Signature | Notes | +|---|---|---| +| `DefineBundle` | `execute(ProductVariant $variant, BundlePricing $pricing, ?float $discountPercentage = null): Bundle` | Creates or updates the `bundles` row. Throws `BundleNesting` if `$variant` is already a component elsewhere. Reprices. | +| `SyncBundleComponents` | `execute(Bundle $bundle, array $components): Bundle` | `$components` is `list`. Upserts and deletes to match. Validates the invariants. Reprices. | +| `SyncBundleGroups` | `execute(Bundle $bundle, array $groups): Bundle` | `list, min_selections: int, max_selections: int, position?: int}>`. Deleting a group deletes its components. Reprices. | +| `RepriceBundle` | `execute(Bundle $bundle): Bundle` | Materialises price rows (2.5). No-op for `fixed`. | +| `ResolveBundleSelection` | `execute(Bundle $bundle, array $meta = []): BundleSelection` | Turns cart line meta into a validated component list (2.6). Throws `InvalidBundleSelection`. | +| `DeleteBundle` | `execute(Bundle $bundle): void` | Removes the bundle definition. The variant becomes an ordinary variant again; its materialised prices are left in place. | +| `ResolveBundleInventory` | `execute(ProductVariant $variant): VariantInventory` | Decorator over core `ResolvesInventory` (2.7). | + +#### 2.5 Pricing + +`BundlePricing::Fixed`: the merchant prices the bundle variant like any other variant. The package does nothing. + +`BundlePricing::Components`: the bundle's price is the quantity-weighted sum of its components' prices, less `discount_percentage`. `PricingManager::get()` throws before its pipeline runs when a purchasable has no price row in the requested currency, so a derived price cannot be computed at read time. Instead `RepriceBundle` **materialises real `prices` rows** on the bundle variant, and the package keeps them current: + +- One base row per currency in which every included component has a base price. A currency any component lacks gets no row, so the bundle is unsellable in that currency and the panel card says which component is missing a price. +- One customer-group row per (currency, customer group) where at least one component has a group price; each component contributes its group price if it has one, otherwise its base price. +- Quantity price breaks on components are ignored. Bundle pricing is per bundle unit. +- `price` is the sum less `discount_percentage`, computed through `PriceCalculator::percentage()` so rounding follows `Currency::decimal_places`. `list_price` is the undiscounted sum when a discount applies, so storefronts show the saving with the machinery they already use for `list_price`. +- For a configurable bundle the rows reflect the **default selection**: fixed components plus each group's `default` components. That is the "from" price on a listing. + +Rows are written through the core `Price` model so cache invalidation and search reindexing fire as for any price change. Repricing is triggered by `DefineBundle`, `SyncBundleComponents`, `SyncBundleGroups`, and by two listeners: `RepriceOnComponentPriceChange` (a `Price` saved or deleted whose priceable is a component variant) and `RepriceOnComponentInvalidation` (`ProductInvalidated` for a product with a variant used as a component). `lunar:bundles:reprice` recomputes every `components` bundle, for backfill and after currency or customer-group changes. + +Because the rows are ordinary price rows, the panel pricing editor shows them and lets the merchant edit them. The bundle card states that prices in `components` mode are derived and overwritten on reprice. A merchant who wants to hand-tune switches to `fixed`. + +A configurable bundle in `components` mode whose selection differs from the default is priced at cart time by `Modifiers/BundleSelectionPricing`, a `CartLineModifier` whose `calculating()` sets `$cartLine->unitPrice` and `unitPriceInclTax` from the resolved selection. `CalculateLineSubtotal` already honours a unit price set in `calculating()` and skips `GetUnitPrice` for that line. The modifier mirrors `GetUnitPrice`'s currency, quantity and customer-group handling by calling `Pricing::for($component)` per component, and applies the same `discount_percentage`. Fixed bundles and `fixed`-priced bundles never enter the modifier. + +Tax: the parent line is taxed at the bundle variant's tax class, like any variant. A bundle whose components have different tax rates is taxed at one rate. Mixed-supply apportionment is out of scope (see Open questions). + +Discounts: a discount targeting the bundle product or a collection it sits in applies to the bundle line as normal. A discount targeting a component product does **not** apply to bundles containing it; discounts see only the bundle cart line. + +#### 2.6 Cart and checkout + +A bundle is added to the cart as its variant, the same call as any product: + +```php +CartSession::add($bundleVariant, 1); // fixed bundle +CartSession::add($bundleVariant, 1, ['bundle' => [ + 'selections' => [ + '01J...GROUP-PUBLIC-ID' => ['01J...COMPONENT-PUBLIC-ID'], + ], +]]); // configurable bundle +``` + +Cart line identity is purchasable plus a `meta` diff (`GetExistingCartLine`), so two different selections of the same configurable bundle correctly become two lines, and re-adding the same selection increments quantity. + +`ResolveBundleSelection` is the single place meta is interpreted: + +- Fixed components are always included. +- For each group, `meta.bundle.selections[group public_id]` must list between `min_selections` and `max_selections` component public ids belonging to that group. A group absent from meta with `min_selections = 0` contributes nothing; absent with `min_selections > 0` falls back to the group's `default` components when they satisfy the minimum, otherwise the selection is invalid. +- Unknown groups or components, duplicates, and components from another bundle are invalid. +- Returns a `BundleSelection` value object: `Collection` where `SelectedComponent` is `(ProductVariant $variant, int $quantity, ?BundleGroup $group)`, plus `isDefault(): bool`. + +`Validation/CartLine/BundleSelection` is appended to `lunar.cart.validators.add_to_cart` and `update_cart_line` by the service provider (the same config-append pattern `table-rate-shipping` uses for modifiers) and rejects an invalid selection with a validation message. For a bundle line it also checks every selected component with `canBeFulfilledAtQuantity(component.quantity * line.quantity)`, so the cart-time stock check sees the actual selection rather than the default one the inventory seam reports (2.7). + +`Pipelines/Order/Creation/CreateBundleComponentLines` is appended to `lunar.orders.pipelines.creation` after `MapDiscountBreakdown`. For every top-level order line whose purchasable is a bundle variant it: + +1. Sets `type = 'bundle'` and `requires_fulfilment = false` on the parent line. `requires_shipping` keeps the variant's value so shipping is still required at the order level. `meta.bundle` is left as the cart line wrote it. +2. Deletes the parent's existing component lines (the stage is idempotent for `orderIdToUpdate` re-runs). +3. Resolves the selection from the parent's `meta` and creates one component line per `SelectedComponent`: `parent_line_id`, `purchasable` = the component variant, `type`, `requires_shipping`, `requires_fulfilment`, `description`, `option`, `identifier`, `unit_quantity` snapshotted from the component, `quantity = component.quantity * parent.quantity`, every money column `0`, empty `tax_breakdown`. +4. Writes `meta.allocated_total` on each component line: the parent's `total` distributed across components in proportion to their current unit prices with `PriceCalculator::distribute()`, which is exact and drift-free. This is for revenue-per-SKU reporting only and never feeds a total. + +Because component lines exist before `OrderPlaced` fires, the existing listeners do the rest with no bundle awareness: `EnsureInitialFulfilment` claims the components into fulfilments, `SyncStockForOrder` commits component stock through `SyncStockCommitment`'s existing `order_lines` predicate, `AllocateStockForFulfilment` allocates per location, and shipping, returning or cancelling a fulfilment moves component stock. The bundle variant itself is never committed: its parent line is `requires_fulfilment = false`, so `SyncsTrackedStock` skips it. + +Spec 0083 (cart-line fulfilment method), if it lands, applies per line: the stage copies the parent's `fulfilment_method` to each component line, and `FulfilmentMethod::supports()` is answered per component. A mixed physical and digital bundle therefore fulfils correctly without the bundle variant having to answer for both. + +The order-line snapshot is authoritative (spec 0045). The component lines are the record of what was sold; the `Bundle` row can change afterwards without touching placed orders. + +#### 2.7 Stock + +`ResolveBundleInventory` decorates core `ResolvesInventory`: + +```php +public function execute(ProductVariant $variant): VariantInventory +{ + $bundle = $variant->bundle; // eager-loaded where the caller can; the relation is cheap otherwise + + if (! $bundle) { + return $this->inner->execute($variant); + } + + // Fixed components plus, per group, the best-stocked option: "available" means + // at least one valid configuration can be sold. + $candidates = $this->candidates($bundle); + + return new VariantInventory( + available: $candidates->min(fn ($c) => intdiv($this->inner->execute($c->variant)->available, $c->quantity)), + unlimited: $candidates->every(fn ($c) => $this->inner->execute($c->variant)->unlimited), + ); +} +``` + +So `$bundleVariant->getTotalInventory()` reports how many complete bundles can be sold, `CartLineStock` enforces it without change, spec 0082's order-creation re-validation enforces it at placement, and a storefront stock badge on a bundle is right without knowing bundles exist. The bundle variant's own `stock_*` columns are ignored and stay at zero; the panel inventory card on a bundle variant still shows them (see Open questions). + +For configurable bundles the seam answers for the best case, and `Validation/CartLine/BundleSelection` (2.6) answers exactly for the chosen selection. + +`ProductVariant implements TracksStock` already; `syncStockCommitment()` on a bundle variant recomputes from the (absent) order lines pointing at it and stays at zero. No override is needed. + +#### 2.8 Panel + +`Panel/BundlesSectionExtension extends SectionExtension` with `extends(): 'catalog'`, reusing `CatalogSection::PRODUCTS_PERMISSION`. Bundles are products, so there is no new nav item, no new permission, no new search source and no draftable: the product's own page is where a bundle is edited, and product search already finds it. + +Slots, all lazily loading their data through the extension's JSON routes so ordinary products pay nothing: + +| Zone | Component | Purpose | +|---|---|---| +| `products.edit:variants:after` | `bundles::BundleCard` | Single-variant product: the full bundle editor inline. Multi-variant product: a summary per variant linking to the variant page. | +| `products.variants.edit:main:after` | `bundles::BundleCard` | The bundle editor for that variant. | +| `products.edit:sidebar:after` | `bundles::IncludedInBundlesCard` | Bundles that include any of this product's variants, each linking to its product. Hidden when empty. | + +The bundle editor: a "Sell as a bundle" toggle (creates or deletes the definition), pricing mode and discount percentage, the fixed component list (variant picker, quantity, reorder, remove), and, in a later slice, groups with their options, min/max selections and defaults. It saves through its own routes immediately, the way media and URL sub-resources save on the product page, rather than through the edit draft; the product's own drafted fields are untouched. + +Variant picking reuses the exported `TargetPickerDialog` and `TargetChipList` against a new endpoint, `GET bundles/search-variants?q=`, returning `id, public_id, sku, product name, option, thumbnail, is_bundle` and excluding bundle variants so nesting is impossible from the UI as well as the actions. + +Routes (inside `Route::middleware('can:'.CatalogSection::PRODUCTS_PERMISSION)`), all named `panel.bundles.*`: + +``` +GET bundles/search-variants +GET bundles/variants/{variant} summary JSON for the cards +PUT bundles/variants/{variant} define (pricing, discount) or create +DELETE bundles/variants/{variant} remove the definition +PUT bundles/variants/{variant}/components sync +PUT bundles/variants/{variant}/groups sync +GET bundles/products/{product}/included-in JSON for the sidebar card +``` + +Table extension on `products.index`: a `Bundle` badge column (hidden by default) and a "Bundles only" filter. + +Order view: covered by the first-party changes in 1.1. The extension adds nothing. + +The card needs a main-column card wrapper; `Section.vue` is internal to the panel today. Slice 6 exports it from `ui.ts` (and `index.js`, `dist/ui.d.ts`) so add-ons stop reimplementing it. + +Translations: `resources/lang/{16 locales}/bundles.php`, served through `langNamespaces(): ['bundles']`. Vue strings use vue-i18n `{placeholder}` syntax. + +Filament admin (`lunarphp/admin`): none in this spec. Filament users get the core behaviour and the storefront surface; a relation manager is a follow-on. + +#### 2.9 Configuration (`lunar.bundles`) + +```php +return [ + 'max_components' => 25, // per bundle, groups included +]; +``` + +Values only. Every class seam is a container binding. + +#### 2.10 Storefront surface (headless) + +```php +$variant->bundle; // ?Bundle +$variant->bundle?->components; // with ->variant, ->group +$variant->bundle?->groups; // with ->components +$variant->pricing()->get(); // works: the rows are real (components mode) or the merchant's (fixed) +$variant->getTotalInventory(); // derived from components +$variant->bundle?->resolveSelection($meta); // BundleSelection for a proposed selection; throws InvalidBundleSelection +CartSession::add($variant, $qty, ['bundle' => ['selections' => [...]]]); +$order->lines()->topLevel()->with('components.purchasable'); +``` + +Search: bundles are products, so they are indexed and found as products. The package adds nothing to the search index. + +Cache: editing a bundle's components or groups invalidates the bundle's product through `InvalidatesRelatedCache`. A change to a component product (`ProductInvalidated`) is fanned out by `Listeners/InvalidateContainingBundles` to every bundle product that includes one of its variants, so a storefront page cached against the bundle product refreshes when a component's price or stock changes. + +## Alternatives considered + +- **A standalone `Bundle` purchasable model.** Cleanest package boundary, but the core assumes cart and order line purchasables are variants in at least these places: `CartLine::taxClass()` (a `HasOneThrough` on `purchasable_type`), `CartLineQuantity` (reads `min_quantity` and `quantity_increment` as raw properties), `CartLineAvailability` (short-circuits non-variants to `isPurchasable()`), `config('lunar.cart.eager_load')` and `CalculateLines::loadMissing()` (variant relation paths under `preventLazyLoading`), `TargetsCartLines` and `BuyXGetY` (dereference `$line->purchasable->product` unguarded), and `ValueObjects\Cart\FreeItem` (typed `ProductVariant`). Storefronts list products; a separate model would be invisible in collections, brand pages and search unless every storefront learned about it. The panel's media, attribute, URL, availability and pricing editors are not exported to add-ons, so a bundle edit page would reimplement the product page. Rejected. +- **One order line with the components snapshotted in `meta`.** No schema change, but `SyncStockCommitment` counts `order_lines` by variant morph and would see zero commitment for components; JSON-path queries to fix that differ across MySQL, Postgres and SQLite; fulfilment lines would point at the bundle with no parts to pick; per-component location allocation and returns would need a parallel implementation. Rejected. +- **Component lines carrying allocated prices and no parent line.** Totals stay right and Shopify's order model looks like this, but the customer's contract ("a kit at this price") is spread across lines, refunds become per-component, discount breakdown mapping breaks (component lines are not the image of a cart line), and a standalone line for the same variant in the same cart collides on identity. The allocation is kept, as `meta.allocated_total` on the components, for reporting. Rejected as the primary shape. +- **Runtime derived pricing through `lunar.pricing.pipelines`.** `PricingManager::get()` throws `MissingCurrencyPriceException` before its pipeline runs, so the bundle variant would still need placeholder rows; and Scout price sorting, the panel price column and any storefront listing read price rows, not the manager. Materialised rows make the derived price real everywhere. Rejected. +- **Cart validators instead of the inventory seam.** A `BundleStock` validator on add and update covers the cart, but a storefront stock badge, the panel and spec 0082's placement re-validation would each need their own bundle awareness. One seam behind the two contract methods makes every caller right. The validator survives only for the selection-specific check. +- **Do nothing; use `BuyXGetY`.** It is a cart-time promotion over independent lines, not a sellable composite: no single product to list, no derived stock, no kit price. Rejected. + +## Migration impact + +- **Database**: `order_lines.parent_line_id` is added to the core baseline migration (v2 is in alpha; no change migration). Three new tables from `bundles`, prefixed through `Migration::$prefix`, types chosen for MySQL 8, Postgres and SQLite. +- **Public contract surface**: `OrderLine` gains `parent()`, `components()`, `isComponent()` and the `topLevel` scope; `ProductVariant::getTotalInventory()` and `canBeFulfilledAtQuantity()` keep their signatures and delegate to the new `ResolvesInventory` action contract; `VariantInventory` is a new DTO. Three order-creation stages skip component lines. Non-breaking. Code that iterates `$order->lines` for display sees component lines with zero money columns and should switch to `topLevel()`; the docs and `WHATS-NEW` call this out. +- **Upgrade path for v1.x**: none. v1 has no bundles; `lunarphp/upgrade` needs no rule. The `topLevel()` guidance is documented for anyone porting a v1 storefront. +- **Translation / locale impact**: new `bundles` lang namespace, all 16 locales shipped with translated values, per the monorepo rule. +- **Filament / admin impact**: none. Component-line nesting in the Filament order view is a follow-on. +- **Panel**: first-party changes in `OrderShowController`, `FulfilmentLineRow.vue`, the refund composer and the non-fulfillable lines card; `Section` exported from `ui.ts`. +- **Queues**: repricing runs synchronously on the triggering event. A store with many bundles per component can move it to the queue in a follow-on. + +## Docs PR (`lunarphp/docs`) + +1. **`2.x/addons/bundles.mdx`** (Add-ons, General): what a bundle is and is not; installation; fixed and configurable bundles; pricing modes and how materialised prices behave; stock derivation; how the order records components and why; the storefront calls above with a worked add-to-cart for a configurable bundle; the reprice command. +2. **`2.x/reference/orders.mdx`**: "Component lines" section — `parent_line_id`, `topLevel()`, `components()`, what money a component line carries, `meta.allocated_total`. +3. **`2.x/reference/inventory.mdx`**: the `ResolvesInventory` seam and `VariantInventory`. +4. **`2.x/admin/bundles.mdx`** (Admin Panel tab): the bundle card, the included-in card, the products table badge and filter, what the inventory card means on a bundle variant. +5. **`2.x/guides/cart.mdx`**: a short "Selling bundles" subsection. +6. **`logs/flight-plan.mdx`**: entry for the feature. + +## Open questions + +1. **Mixed-rate tax.** The parent line is taxed at the bundle variant's tax class. Apportioning across components at their own rates (a UK "mixed supply") is a real requirement for some merchants. Proposed: out of scope here, tracked as a follow-on that could use `meta.allocated_total` as the apportionment base. Owner: Glenn. +2. **Inventory card on a bundle variant.** The first-party inventory card shows the bundle variant's own (meaningless) stock. Options: leave it and let the bundle card explain; add a `products.variants.edit:inventory` zone the extension can replace; or have the card read `getTotalInventory()`. Proposed: leave it in this spec, add the zone in a panel follow-on. Owner: Glenn. +3. **`fixed` pricing with an automatic saving display.** In `fixed` mode the merchant sets `list_price` by hand. Should the package offer "show the component sum as the list price" as a toggle? Proposed: no; `components` mode with a percentage covers the common case. Owner: Glenn. +4. **Per-component refunds.** Refunds are parent-only. A merchant refunding one damaged part of a kit refunds an amount against the parent line. Confirm this is acceptable for v1. Owner: Glenn. +5. **Repricing on the queue.** Synchronous repricing is simplest and correct; a component variant used in hundreds of bundles makes a price save slow. Decide the threshold, or ship a `lunar.bundles.reprice_queue` connection value from the start. Owner: Glenn. + +## References + +- `packages/core/src/Contracts/TracksStock.php` — names a bundle as a motivating custom purchasable +- [[0030-fulfillable-order-lines]] — `type` is display-only; fulfilment keys off `requires_fulfilment` +- [[0038-inventory-fundamentals]] — commitment recomputed from the order book; `SyncsTrackedStock` +- [[0045-optional-purchasables-and-shipping-de-morph]] — the order-line snapshot is authoritative +- [[0046-public-id-external-addressing]] — group and component public ids in cart meta +- [[0057-panel-products-section]] and [[0067-panel-fulfilment-centric-order-view]] — the zones and order view this extends +- [[0082-selling-policy-rework]] (draft) — changes `ResolveInventory`'s formula, not the seam +- [[0083-cart-line-fulfilment-method]] (draft) — per-line fulfilment method copied to component lines +- [[0084-search-relevance]] — the add-on packaging this package copies +- `packages/panel-addon-example` — the panel add-on pattern + +## Implementation plan + +- [ ] Slice 1 — core: `order_lines.parent_line_id`, `OrderLine` relations and `topLevel` scope, creation stages skip component lines, parent-only `refundableQuantity()`, tests +- [ ] Slice 2 — core: `ResolvesInventory` contract, `VariantInventory`, `ResolveInventory` action, `ProductVariant` delegation, `ActionServiceProvider` binding, tests +- [ ] Slice 3 — `bundles`: skeleton and monorepo wiring, migrations, models, factories, `ProductVariant::bundle` relation, define/sync/delete actions and verbs, invariants, cache invalidation, headless tests +- [ ] Slice 4 — `bundles`: `ResolveBundleInventory` decorator, `ResolveBundleSelection`, `BundleSelection` validator, `CreateBundleComponentLines`, end-to-end cart → order → fulfilment → stock tests including mixed physical and digital +- [ ] Slice 5 — `bundles`: `RepriceBundle`, materialised price rows, listeners, `BundleSelectionPricing` modifier, `lunar:bundles:reprice`, tests +- [ ] Slice 6 — panel (first party): component-line nesting in the order view and refund composer, `Section` exported from `ui.ts` +- [ ] Slice 7 — `bundles` panel: section extension, routes, `BundleCard` for fixed bundles, variant search endpoint, `IncludedInBundlesCard`, products table badge and filter, 16 locales, panel tests, JS build and CI +- [ ] Slice 8 — `bundles` panel: configurable groups editor +- [ ] Slice 9 — demo data: two bundles in the catalogue generator; docs PR (`lunarphp/docs`) diff --git a/specs/README.md b/specs/README.md index 9ca366bf1e..d532f54a68 100644 --- a/specs/README.md +++ b/specs/README.md @@ -93,3 +93,4 @@ Each spec carries a `Status:` line in its frontmatter / header: | 0072 | Panel Discounts section | accepted | | 0073 | Split `AmountOff` into `PercentageOff` and `FixedAmountOff` | implemented | | 0074 | Panel global search (command palette) | implemented | +| 0085 | Product bundles (`lunarphp/bundles`) | accepted | From 2688ee983aba65f1fedcf68f7e18bc67f036a056 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Tue, 15 Sep 2026 08:51:40 +0100 Subject: [PATCH 2/9] feat(core): component order lines and the ResolvesInventory seam (spec 0085, slices 1-2) Co-Authored-By: Claude Fable 5.1 --- ..._01_01_000047_create_order_lines_table.php | 1 + packages/core/src/ActionServiceProvider.php | 2 + .../src/Actions/Products/ResolveInventory.php | 25 ++++++++++ .../Actions/Products/ResolvesInventory.php | 18 ++++++++ .../core/src/DataObjects/VariantInventory.php | 22 +++++++++ packages/core/src/Models/OrderLine.php | 36 ++++++++++++++- packages/core/src/Models/ProductVariant.php | 10 ++-- .../core/src/Observers/OrderLineObserver.php | 9 ++++ .../Order/Creation/CleanUpOrderLines.php | 3 +- .../Order/Creation/CreateOrderLines.php | 2 +- .../Order/Creation/MapDiscountBreakdown.php | 2 +- .../components/orders/FulfilmentLineRow.vue | 21 ++++++++- .../resources/js/components/orders/types.ts | 10 ++++ packages/panel/resources/lang/ar/orders.php | 1 + packages/panel/resources/lang/bg/orders.php | 1 + packages/panel/resources/lang/de/orders.php | 1 + packages/panel/resources/lang/en/orders.php | 1 + packages/panel/resources/lang/es/orders.php | 1 + packages/panel/resources/lang/fa/orders.php | 1 + packages/panel/resources/lang/fr/orders.php | 1 + packages/panel/resources/lang/hr/orders.php | 1 + packages/panel/resources/lang/hu/orders.php | 1 + packages/panel/resources/lang/mn/orders.php | 1 + packages/panel/resources/lang/nl/orders.php | 1 + packages/panel/resources/lang/pl/orders.php | 1 + .../panel/resources/lang/pt_BR/orders.php | 1 + packages/panel/resources/lang/ro/orders.php | 1 + packages/panel/resources/lang/tr/orders.php | 1 + packages/panel/resources/lang/vi/orders.php | 1 + .../Orders/OrderShowController.php | 11 +++++ tests/core/Unit/Models/OrderLineTest.php | 37 +++++++++++++++ .../ProductVariantSellingPolicyTest.php | 20 ++++++++ .../Order/Creation/CleanUpOrderLinesTest.php | 46 +++++++++++++++++++ 33 files changed, 278 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/Actions/Products/ResolveInventory.php create mode 100644 packages/core/src/Contracts/Actions/Products/ResolvesInventory.php create mode 100644 packages/core/src/DataObjects/VariantInventory.php diff --git a/packages/core/database/migrations/2026_01_01_000047_create_order_lines_table.php b/packages/core/database/migrations/2026_01_01_000047_create_order_lines_table.php index 22e395c35d..f4330f3f68 100644 --- a/packages/core/database/migrations/2026_01_01_000047_create_order_lines_table.php +++ b/packages/core/database/migrations/2026_01_01_000047_create_order_lines_table.php @@ -12,6 +12,7 @@ public function up(): void $table->bigIncrements('id'); $table->ulid('public_id')->unique(); $table->foreignId('order_id')->constrained($this->prefix.'orders'); + $table->foreignId('parent_line_id')->nullable()->index()->constrained($this->prefix.'order_lines')->cascadeOnDelete()->comment('Set on a component line that belongs to another line (a bundle part); never priced'); $table->nullableMorphs('purchasable'); $table->string('type')->index(); $table->boolean('requires_shipping')->default(false)->index()->comment('Needs physical delivery; stamped from the purchasable isShippable()'); diff --git a/packages/core/src/ActionServiceProvider.php b/packages/core/src/ActionServiceProvider.php index b3959d4164..3b8b9189d8 100644 --- a/packages/core/src/ActionServiceProvider.php +++ b/packages/core/src/ActionServiceProvider.php @@ -109,6 +109,7 @@ use Lunar\Core\Actions\Products\RecordStockMovement; use Lunar\Core\Actions\Products\ReleaseReservation; use Lunar\Core\Actions\Products\ReserveStock; +use Lunar\Core\Actions\Products\ResolveInventory; use Lunar\Core\Actions\Products\SyncStockCommitment; use Lunar\Core\Actions\Products\UpdateProduct; use Lunar\Core\Actions\Products\UpdateProductStatus; @@ -243,6 +244,7 @@ class ActionServiceProvider extends ServiceProvider Contracts\Products\RecomputesStockRollup::class => RecomputeStockRollup::class, Contracts\Products\RecordsStockMovement::class => RecordStockMovement::class, Contracts\Products\ReleasesReservation::class => ReleaseReservation::class, + Contracts\Products\ResolvesInventory::class => ResolveInventory::class, Contracts\Products\ReservesStock::class => ReserveStock::class, Contracts\Products\SyncsStockCommitment::class => SyncStockCommitment::class, Contracts\Products\MapsVariantsToProductOptions::class => MapVariantsToProductOptions::class, diff --git a/packages/core/src/Actions/Products/ResolveInventory.php b/packages/core/src/Actions/Products/ResolveInventory.php new file mode 100644 index 0000000000..f11154b015 --- /dev/null +++ b/packages/core/src/Actions/Products/ResolveInventory.php @@ -0,0 +1,25 @@ +selling_policy) { + SellingPolicy::Always, SellingPolicy::InStock => (int) $variant->stock_available, + SellingPolicy::InStockOrOnBackorder => (int) $variant->stock_available + (int) $variant->backorder, + }, + unlimited: $variant->selling_policy === SellingPolicy::Always, + ); + } +} diff --git a/packages/core/src/Contracts/Actions/Products/ResolvesInventory.php b/packages/core/src/Contracts/Actions/Products/ResolvesInventory.php new file mode 100644 index 0000000000..1dfda5313e --- /dev/null +++ b/packages/core/src/Contracts/Actions/Products/ResolvesInventory.php @@ -0,0 +1,18 @@ +unlimited || $quantity <= $this->available; + } +} diff --git a/packages/core/src/Models/OrderLine.php b/packages/core/src/Models/OrderLine.php index 43c284edfb..3b347f162f 100644 --- a/packages/core/src/Models/OrderLine.php +++ b/packages/core/src/Models/OrderLine.php @@ -22,6 +22,7 @@ * @property int $id * @property string $public_id * @property int $order_id + * @property ?int $parent_line_id * @property string $purchasable_type * @property int $purchasable_id * @property string $type @@ -100,6 +101,34 @@ public function order(): BelongsTo return $this->belongsTo(Order::class); } + /** + * The line this one is a component of. Component lines are the parts of a + * composite purchasable (a bundle) recorded so fulfilment and stock see them; + * the parent holds the money. + */ + public function parent(): BelongsTo + { + return $this->belongsTo(OrderLine::class, 'parent_line_id'); + } + + public function components(): HasMany + { + return $this->hasMany(OrderLine::class, 'parent_line_id')->orderBy('id'); + } + + public function isComponent(): bool + { + return $this->parent_line_id !== null; + } + + /** + * Limit the query to lines that are not components of another line. + */ + public function scopeTopLevel(Builder $query): Builder + { + return $query->whereNull('parent_line_id'); + } + public function fulfilmentLines(): HasMany { return $this->hasMany(FulfilmentLine::class); @@ -112,10 +141,15 @@ public function refundLines(): HasMany /** * Quantity still available to refund — the line's quantity minus - * whatever refund_lines have already claimed against it. + * whatever refund_lines have already claimed against it. Component lines + * carry no money, so they are refunded through their parent. */ public function refundableQuantity(): int { + if ($this->isComponent()) { + return 0; + } + return max(0, $this->quantity - $this->refunded_quantity); } diff --git a/packages/core/src/Models/ProductVariant.php b/packages/core/src/Models/ProductVariant.php index 1cb9692f14..ff13fb8d38 100644 --- a/packages/core/src/Models/ProductVariant.php +++ b/packages/core/src/Models/ProductVariant.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; +use Lunar\Core\Contracts\Actions\Products\ResolvesInventory; use Lunar\Core\Contracts\HasThumbnailImage; use Lunar\Core\Contracts\Purchasable; use Lunar\Core\Contracts\TracksStock; @@ -260,8 +261,7 @@ public function getThumbnail(): ?Media public function canBeFulfilledAtQuantity(int $quantity): bool { - return $this->selling_policy === SellingPolicy::Always - || $quantity <= $this->getTotalInventory(); + return app(ResolvesInventory::class)->execute($this)->allows($quantity); } public function scopeEnabled(Builder $query): Builder @@ -291,11 +291,7 @@ public function hasOrderHistory(): bool public function getTotalInventory(): int { - return match ($this->selling_policy) { - SellingPolicy::Always => $this->stock_available, - SellingPolicy::InStock => $this->stock_available, - SellingPolicy::InStockOrOnBackorder => $this->stock_available + $this->backorder, - }; + return app(ResolvesInventory::class)->execute($this)->available; } public function getThumbnailImage(): string diff --git a/packages/core/src/Observers/OrderLineObserver.php b/packages/core/src/Observers/OrderLineObserver.php index 295fd954fc..add0ad3617 100644 --- a/packages/core/src/Observers/OrderLineObserver.php +++ b/packages/core/src/Observers/OrderLineObserver.php @@ -37,6 +37,15 @@ public function updating(OrderLine $orderLine): void } } + /** + * Component lines go with their parent; done here rather than left to the + * database cascade so model events fire and SQLite test runs match. + */ + public function deleting(OrderLine $orderLine): void + { + $orderLine->components()->get()->each->delete(); + } + /** * Ensure the order line references a purchasable model. */ diff --git a/packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php b/packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php index 5944639644..c87a95df7d 100644 --- a/packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php +++ b/packages/core/src/Pipelines/Order/Creation/CleanUpOrderLines.php @@ -20,7 +20,8 @@ public function handle(Order $order, Closure $next): mixed return $this->signature($line->purchasable_id, $line->purchasable_type, (array) $line->meta, $line->quantity); })->toArray(); - $order->productLines->each(function ($orderLine) use ($cartSignatures) { + // Component lines are never the image of a cart line; they go with their parent. + $order->productLines->whereNull('parent_line_id')->each(function ($orderLine) use ($cartSignatures) { $sig = $this->signature( $orderLine->purchasable_id, $orderLine->purchasable_type, diff --git a/packages/core/src/Pipelines/Order/Creation/CreateOrderLines.php b/packages/core/src/Pipelines/Order/Creation/CreateOrderLines.php index 8cc7036c33..e1a50aa6b0 100644 --- a/packages/core/src/Pipelines/Order/Creation/CreateOrderLines.php +++ b/packages/core/src/Pipelines/Order/Creation/CreateOrderLines.php @@ -26,7 +26,7 @@ public function handle(Order $order, Closure $next): mixed foreach ($cart->lines as $cartLine) { /** @var OrderLine $orderLine */ - $orderLine = $order->lines->first(function ($line) use ($cartLine) { + $orderLine = $order->lines->whereNull('parent_line_id')->first(function ($line) use ($cartLine) { $diff = Arr::diff($line->meta, $cartLine->meta); return empty($diff->new) && diff --git a/packages/core/src/Pipelines/Order/Creation/MapDiscountBreakdown.php b/packages/core/src/Pipelines/Order/Creation/MapDiscountBreakdown.php index dfe6223c2b..02be4c14b8 100644 --- a/packages/core/src/Pipelines/Order/Creation/MapDiscountBreakdown.php +++ b/packages/core/src/Pipelines/Order/Creation/MapDiscountBreakdown.php @@ -18,7 +18,7 @@ public function handle(Order $order, Closure $next): mixed $cartLinesMappedToOrderLines = []; - foreach ($order->lines as $orderLine) { + foreach ($order->lines->whereNull('parent_line_id') as $orderLine) { $cartLine = $cart->lines->first(function ($cartLine) use ($orderLine) { $diff = Arr::diff($cartLine->meta, $orderLine->meta); diff --git a/packages/panel/resources/js/components/orders/FulfilmentLineRow.vue b/packages/panel/resources/js/components/orders/FulfilmentLineRow.vue index 71baa2b99a..ffb066493f 100644 --- a/packages/panel/resources/js/components/orders/FulfilmentLineRow.vue +++ b/packages/panel/resources/js/components/orders/FulfilmentLineRow.vue @@ -39,13 +39,30 @@ const detailRows = (): { label: string; value: string | null; highlight?: boolea · {{ line.identifier }} +
+ {{ t('orders.line_part_of', { parent: line.part_of }) }} +
- + + {{ line.quantity }} + + {{ line.quantity }} @ {{ line.unit_price }} -
+
    +
  • + + {{ component.description }} + {{ component.option }} + {{ component.identifier }} + + x{{ component.quantity }} +
  • +
+ +
'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'جزء من {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/bg/orders.php b/packages/panel/resources/lang/bg/orders.php index 0fb23677ed..583ca6bd06 100644 --- a/packages/panel/resources/lang/bg/orders.php +++ b/packages/panel/resources/lang/bg/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Част от {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/de/orders.php b/packages/panel/resources/lang/de/orders.php index 0fb23677ed..0f22818701 100644 --- a/packages/panel/resources/lang/de/orders.php +++ b/packages/panel/resources/lang/de/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Teil von {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/en/orders.php b/packages/panel/resources/lang/en/orders.php index 0fb23677ed..1cdf7a6cb8 100644 --- a/packages/panel/resources/lang/en/orders.php +++ b/packages/panel/resources/lang/en/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Part of {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/es/orders.php b/packages/panel/resources/lang/es/orders.php index 0fb23677ed..729f1accd3 100644 --- a/packages/panel/resources/lang/es/orders.php +++ b/packages/panel/resources/lang/es/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Parte de {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/fa/orders.php b/packages/panel/resources/lang/fa/orders.php index 0fb23677ed..ba4e28cf12 100644 --- a/packages/panel/resources/lang/fa/orders.php +++ b/packages/panel/resources/lang/fa/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'بخشی از {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/fr/orders.php b/packages/panel/resources/lang/fr/orders.php index 0fb23677ed..76469e9bd5 100644 --- a/packages/panel/resources/lang/fr/orders.php +++ b/packages/panel/resources/lang/fr/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Fait partie de {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/hr/orders.php b/packages/panel/resources/lang/hr/orders.php index 0fb23677ed..78df46a3fc 100644 --- a/packages/panel/resources/lang/hr/orders.php +++ b/packages/panel/resources/lang/hr/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Dio od {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/hu/orders.php b/packages/panel/resources/lang/hu/orders.php index 0fb23677ed..978eb4a5de 100644 --- a/packages/panel/resources/lang/hu/orders.php +++ b/packages/panel/resources/lang/hu/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => '{parent} része', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/mn/orders.php b/packages/panel/resources/lang/mn/orders.php index 0fb23677ed..83a98cd0b6 100644 --- a/packages/panel/resources/lang/mn/orders.php +++ b/packages/panel/resources/lang/mn/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => '{parent}-н хэсэг', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/nl/orders.php b/packages/panel/resources/lang/nl/orders.php index 0fb23677ed..171ce6b5ef 100644 --- a/packages/panel/resources/lang/nl/orders.php +++ b/packages/panel/resources/lang/nl/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Onderdeel van {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/pl/orders.php b/packages/panel/resources/lang/pl/orders.php index 0fb23677ed..17d49d944f 100644 --- a/packages/panel/resources/lang/pl/orders.php +++ b/packages/panel/resources/lang/pl/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Część {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/pt_BR/orders.php b/packages/panel/resources/lang/pt_BR/orders.php index 0fb23677ed..729f1accd3 100644 --- a/packages/panel/resources/lang/pt_BR/orders.php +++ b/packages/panel/resources/lang/pt_BR/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Parte de {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/ro/orders.php b/packages/panel/resources/lang/ro/orders.php index 0fb23677ed..8beb2af67a 100644 --- a/packages/panel/resources/lang/ro/orders.php +++ b/packages/panel/resources/lang/ro/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Parte din {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/tr/orders.php b/packages/panel/resources/lang/tr/orders.php index 0fb23677ed..7dc7431789 100644 --- a/packages/panel/resources/lang/tr/orders.php +++ b/packages/panel/resources/lang/tr/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => '{parent} parçası', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/resources/lang/vi/orders.php b/packages/panel/resources/lang/vi/orders.php index 0fb23677ed..43f020c6a0 100644 --- a/packages/panel/resources/lang/vi/orders.php +++ b/packages/panel/resources/lang/vi/orders.php @@ -239,6 +239,7 @@ 'line_sub_total' => 'Subtotal', 'line_discount' => 'Discount', 'line_total' => 'Total', + 'line_part_of' => 'Thuộc {parent}', 'flash_fulfilment_shipped' => 'Fulfilment marked as shipped.', 'flash_fulfilment_fulfilled' => 'Fulfilment marked as fulfilled.', diff --git a/packages/panel/src/Http/Controllers/Orders/OrderShowController.php b/packages/panel/src/Http/Controllers/Orders/OrderShowController.php index 497bbf906f..9eecd8bb47 100644 --- a/packages/panel/src/Http/Controllers/Orders/OrderShowController.php +++ b/packages/panel/src/Http/Controllers/Orders/OrderShowController.php @@ -43,6 +43,8 @@ public function show(Order $order): Response { $order->load([ 'lines.purchasable', + 'lines.parent', + 'lines.components', 'lines.fulfilmentLines', 'fulfilments.lines.orderLine.purchasable', 'fulfilments.trackings', @@ -355,6 +357,15 @@ protected function line(?OrderLine $line, int $quantity, callable $money): array ])->values()->all(), 'total' => $money($line->total), 'notes' => $line->notes, + // Component lines (a bundle's parts) carry no money; the parent holds it. + 'part_of' => $line->parent?->description, + 'components' => $line->components->map(fn (OrderLine $component) => [ + 'id' => $component->id, + 'description' => $component->description, + 'option' => $component->option, + 'identifier' => $component->identifier, + 'quantity' => $component->quantity, + ])->values()->all(), ]; } diff --git a/tests/core/Unit/Models/OrderLineTest.php b/tests/core/Unit/Models/OrderLineTest.php index a0889e5173..7524a16a40 100644 --- a/tests/core/Unit/Models/OrderLineTest.php +++ b/tests/core/Unit/Models/OrderLineTest.php @@ -261,3 +261,40 @@ expect(OrderLine::withoutFulfilment()->pluck('id')) ->not->toContain($line->id); }); + +test('a component line belongs to its parent and is never refundable', function () { + $order = Order::factory()->create(); + $variant = ProductVariant::factory()->create(); + $part = ProductVariant::factory()->create(); + + $parent = OrderLine::factory()->create([ + 'order_id' => $order->id, + 'purchasable_type' => $variant->getMorphClass(), + 'purchasable_id' => $variant->id, + 'quantity' => 2, + ]); + + $component = OrderLine::factory()->create([ + 'order_id' => $order->id, + 'parent_line_id' => $parent->id, + 'purchasable_type' => $part->getMorphClass(), + 'purchasable_id' => $part->id, + 'quantity' => 4, + 'unit_price' => 0, + 'sub_total' => 0, + 'total' => 0, + 'tax_total' => 0, + ]); + + expect($component->isComponent())->toBeTrue() + ->and($parent->isComponent())->toBeFalse() + ->and($component->parent->is($parent))->toBeTrue() + ->and($parent->components->pluck('id')->all())->toBe([$component->id]) + ->and($component->refundableQuantity())->toBe(0) + ->and($parent->refundableQuantity())->toBe(2) + ->and($order->lines()->topLevel()->pluck('id')->all())->toBe([$parent->id]); + + $parent->delete(); + + expect(OrderLine::query()->whereKey($component->id)->exists())->toBeFalse(); +}); diff --git a/tests/core/Unit/Models/ProductVariantSellingPolicyTest.php b/tests/core/Unit/Models/ProductVariantSellingPolicyTest.php index 9b5322baf4..a4e2c2b59f 100644 --- a/tests/core/Unit/Models/ProductVariantSellingPolicyTest.php +++ b/tests/core/Unit/Models/ProductVariantSellingPolicyTest.php @@ -1,6 +1,8 @@ refresh()->canBeFulfilledAtQuantity(6))->toBeFalse(); expect($variant->refresh()->canBeFulfilledAtQuantity(5))->toBeTrue(); }); + +test('inventory answers come from the ResolvesInventory seam', function () { + $variant = ProductVariant::factory()->inStock(1)->create([ + 'selling_policy' => SellingPolicy::InStock, + ]); + + app()->bind(ResolvesInventory::class, fn () => new class implements ResolvesInventory + { + public function execute(ProductVariant $variant): VariantInventory + { + return new VariantInventory(available: 7, unlimited: false); + } + }); + + expect($variant->refresh()->getTotalInventory())->toBe(7) + ->and($variant->canBeFulfilledAtQuantity(7))->toBeTrue() + ->and($variant->canBeFulfilledAtQuantity(8))->toBeFalse(); +}); diff --git a/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php b/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php index a1cd2e5a10..1230c58682 100644 --- a/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php +++ b/tests/core/Unit/Pipelines/Order/Creation/CleanUpOrderLinesTest.php @@ -223,3 +223,49 @@ 'meta' => json_encode(['bar' => 'baz']), ]); }); + +test('component lines survive clean up with their parent', function () { + $currency = Currency::factory()->create(); + + $cart = Cart::factory()->create([ + 'currency_id' => $currency->id, + ]); + + $order = Order::factory()->create([ + 'cart_id' => $cart->id, + ]); + + $bundleVariant = ProductVariant::factory()->create(); + $componentVariant = ProductVariant::factory()->create(); + + $cart->lines()->create([ + 'purchasable_type' => $bundleVariant->getMorphClass(), + 'purchasable_id' => $bundleVariant->id, + 'quantity' => 1, + ]); + + $parent = OrderLine::factory()->create([ + 'order_id' => $order->id, + 'purchasable_id' => $bundleVariant->id, + 'purchasable_type' => $bundleVariant->getMorphClass(), + 'quantity' => 1, + ]); + + // Not the image of any cart line, but it belongs to one that is. + $component = OrderLine::factory()->create([ + 'order_id' => $order->id, + 'parent_line_id' => $parent->id, + 'purchasable_id' => $componentVariant->id, + 'purchasable_type' => $componentVariant->getMorphClass(), + 'quantity' => 3, + 'unit_price' => 0, + 'sub_total' => 0, + 'total' => 0, + 'tax_total' => 0, + ]); + + app(CleanUpOrderLines::class)->handle($order, fn ($order) => $order); + + assertDatabaseHas((new OrderLine)->getTable(), ['id' => $parent->id]); + assertDatabaseHas((new OrderLine)->getTable(), ['id' => $component->id]); +}); From a07f27598aed686a2d5c6b06c46af331edb18019 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Tue, 15 Sep 2026 08:55:16 +0100 Subject: [PATCH 3/9] feat(panel): nest component order lines and export Section to add-ons (spec 0085, slice 6) Co-Authored-By: Claude Fable 5.1 --- packages/panel/CLAUDE.md | 2 +- .../panel/public/build/assets/app-C3gs_Or0.js | 272 ----------------- .../public/build/assets/app-DrcNYi_n.css | 1 - .../public/build/assets/app-XBH6bAy4.css | 1 + .../panel/public/build/assets/app-gwoY7wNM.js | 277 ++++++++++++++++++ packages/panel/public/build/manifest.json | 6 +- .../orders/FulfilmentLineRow.test.ts | 36 +++ packages/panel/resources/js/ui.ts | 1 + .../panel/resources/panel-package/index.js | 6 + .../Orders/OrderShowController.php | 2 + specs/0085-product-bundles.md | 6 +- tests/panel/Feature/Orders/OrderShowTest.php | 48 +++ 12 files changed, 378 insertions(+), 280 deletions(-) delete mode 100644 packages/panel/public/build/assets/app-C3gs_Or0.js delete mode 100644 packages/panel/public/build/assets/app-DrcNYi_n.css create mode 100644 packages/panel/public/build/assets/app-XBH6bAy4.css create mode 100644 packages/panel/public/build/assets/app-gwoY7wNM.js diff --git a/packages/panel/CLAUDE.md b/packages/panel/CLAUDE.md index 3709455b81..d9411e8212 100644 --- a/packages/panel/CLAUDE.md +++ b/packages/panel/CLAUDE.md @@ -32,7 +32,7 @@ public page-building surface to them: `app.ts` publishes the components on `@lunarphp/panel` import to that global. **`resources/js/ui.ts` is the source of truth for what add-ons can import** — the barrel there lists the whole set (layout, DataTable, FilterDropdown, KpiCard, form inputs, overlays, `Button`, `Icon`, …). -Internal machinery (`NavBody`, `PanelSlot`, `RowActions`, `PageActions`, `Section`, …) +Internal machinery (`NavBody`, `PanelSlot`, `RowActions`, `PageActions`, …) is deliberately not exported. The `PanelLayout` shell is applied to add-on pages automatically (the resolver sets it as their persistent layout via the `default` entry in the layout registry), so an add-on page only builds its own `PageHeader` + diff --git a/packages/panel/public/build/assets/app-C3gs_Or0.js b/packages/panel/public/build/assets/app-C3gs_Or0.js deleted file mode 100644 index ff81bdd4a7..0000000000 --- a/packages/panel/public/build/assets/app-C3gs_Or0.js +++ /dev/null @@ -1,272 +0,0 @@ -var lP=Object.defineProperty;var e_=e=>{throw TypeError(e)};var iP=(e,t,n)=>t in e?lP(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var Re=(e,t,n)=>iP(e,typeof t!="symbol"?t+"":t,n),aP=(e,t,n)=>t.has(e)||e_("Cannot "+n);var Ha=(e,t,n)=>(aP(e,t,"read from private field"),n?n.call(e):t.get(e)),za=(e,t,n)=>t.has(e)?e_("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n);/* empty css *//** -* @vue/shared v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/function nm(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const bt={},Hi=[],qr=()=>{},Sw=()=>!1,Pc=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),rm=e=>e.startsWith("onUpdate:"),Nt=Object.assign,Ay=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},uP=Object.prototype.hasOwnProperty,Ot=(e,t)=>uP.call(e,t),Ke=Array.isArray,Jo=e=>Pa(e)==="[object Map]",Es=e=>Pa(e)==="[object Set]",t_=e=>Pa(e)==="[object Date]",cP=e=>Pa(e)==="[object RegExp]",nt=e=>typeof e=="function",jt=e=>typeof e=="string",wr=e=>typeof e=="symbol",Pt=e=>e!==null&&typeof e=="object",Py=e=>(Pt(e)||nt(e))&&nt(e.then)&&nt(e.catch),Ew=Object.prototype.toString,Pa=e=>Ew.call(e),dP=e=>Pa(e).slice(8,-1),sm=e=>Pa(e)==="[object Object]",om=e=>jt(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ul=nm(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),lm=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},fP=/-\w/g,fn=lm(e=>e.replace(fP,t=>t.slice(1).toUpperCase())),pP=/\B([A-Z])/g,dr=lm(e=>e.replace(pP,"-$1").toLowerCase()),Mc=lm(e=>e.charAt(0).toUpperCase()+e.slice(1)),zi=lm(e=>e?`on${Mc(e)}`:""),On=(e,t)=>!Object.is(e,t),Ki=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},im=e=>{const t=parseFloat(e);return isNaN(t)?e:t},vf=e=>{const t=jt(e)?Number(e):NaN;return isNaN(t)?e:t};let n_;const am=()=>n_||(n_=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}),mP="Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol",hP=nm(mP);function kt(e){if(Ke(e)){const t={};for(let n=0;n{if(n){const r=n.split(vP);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function pe(e){let t="";if(jt(e))t=e;else if(Ke(e))for(let n=0;nKr(n,t))}const Aw=e=>!!(e&&e.__v_isRef===!0),m=e=>jt(e)?e:e==null?"":Ke(e)||Pt(e)&&(e.toString===Ew||!nt(e.toString))?Aw(e)?m(e.value):JSON.stringify(e,Pw,2):String(e),Pw=(e,t)=>Aw(t)?Pw(e,t.value):Jo(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,s],o)=>(n[yh(r,o)+" =>"]=s,n),{})}:Es(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>yh(n))}:wr(t)?yh(t):Pt(t)&&!Ke(t)&&!sm(t)?String(t):t,yh=(e,t="")=>{var n;return wr(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};function wP(e){return e==null?"initial":typeof e=="string"?e===""?" ":e:String(e)}/** -* @vue/reactivity v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/let Cn;class My{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&Cn&&(Cn.active?(this.parent=Cn,this.index=(Cn.scopes||(Cn.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes){const r=this.scopes.slice();for(t=0,n=r.length;t0&&--this._on===0){if(Cn===this)Cn=this.prevScope;else{let t=Cn;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,r;for(n=0,r=this.effects.length;n0)return;if(wu){let t=wu;for(wu=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;ku;){let t=ku;for(ku=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(r){e||(e=r)}t=n}}if(e)throw e}function Vw(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Iw(e){let t,n=e.depsTail,r=n;for(;r;){const s=r.prevDep;r.version===-1?(r===n&&(n=s),Iy(r),$P(r)):t=r,r.dep.activeLink=r.prevActiveLink,r.prevActiveLink=void 0,r=s}e.deps=t,e.depsTail=n}function zg(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Rw(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Rw(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Gu)||(e.globalVersion=Gu,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!zg(e))))return;e.flags|=2;const t=e.dep,n=Kt,r=es;Kt=e,es=!0;try{Vw(e);const s=e.fn(e._value);(t.version===0||On(s,e._value))&&(e.flags|=128,e._value=s,t.version++)}catch(s){throw t.version++,s}finally{Kt=n,es=r,Iw(e),e.flags&=-3}}function Iy(e,t=!1){const{dep:n,prevSub:r,nextSub:s}=e;if(r&&(r.nextSub=s,e.prevSub=void 0),s&&(s.prevSub=r,e.nextSub=void 0),n.subs===e&&(n.subs=r,!r&&n.computed)){n.computed.flags&=-5;for(let o=n.computed.deps;o;o=o.nextDep)Iy(o,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function $P(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}function CP(e,t){e.effect instanceof Wu&&(e=e.effect.fn);const n=new Wu(e);t&&Nt(n,t);try{n.run()}catch(s){throw n.stop(),s}const r=n.run.bind(n);return r.effect=n,r}function SP(e){e.effect.stop()}let es=!0;const Nw=[];function ao(){Nw.push(es),es=!1}function uo(){const e=Nw.pop();es=e===void 0?!0:e}function s_(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Kt;Kt=void 0;try{t()}finally{Kt=n}}}let Gu=0,EP=class{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}};class dm{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Kt||!es||Kt===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Kt)n=this.activeLink=new EP(Kt,this),Kt.deps?(n.prevDep=Kt.depsTail,Kt.depsTail.nextDep=n,Kt.depsTail=n):Kt.deps=Kt.depsTail=n,Lw(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const r=n.nextDep;r.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=r),n.prevDep=Kt.depsTail,n.nextDep=void 0,Kt.depsTail.nextDep=n,Kt.depsTail=n,Kt.deps===n&&(Kt.deps=r)}return n}trigger(t){this.version++,Gu++,this.notify(t)}notify(t){Dy();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{Vy()}}}function Lw(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let r=t.deps;r;r=r.nextDep)Lw(r)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const yf=new WeakMap,jl=Symbol(""),Kg=Symbol(""),Ju=Symbol("");function qn(e,t,n){if(es&&Kt){let r=yf.get(e);r||yf.set(e,r=new Map);let s=r.get(n);s||(r.set(n,s=new dm),s.map=r,s.key=n),s.track()}}function Hs(e,t,n,r,s,o){const l=yf.get(e);if(!l){Gu++;return}const a=u=>{u&&u.trigger()};if(Dy(),t==="clear")l.forEach(a);else{const u=Ke(e),c=u&&om(n);if(u&&n==="length"){const d=Number(r);l.forEach((f,p)=>{(p==="length"||p===Ju||!wr(p)&&p>=d)&&a(f)})}else switch((n!==void 0||l.has(void 0))&&a(l.get(n)),c&&a(l.get(Ju)),t){case"add":u?c&&a(l.get("length")):(a(l.get(jl)),Jo(e)&&a(l.get(Kg)));break;case"delete":u||(a(l.get(jl)),Jo(e)&&a(l.get(Kg)));break;case"set":Jo(e)&&a(l.get(jl));break}}Vy()}function OP(e,t){const n=yf.get(e);return n&&n.get(t)}function _i(e){const t=$t(e);return t===e?t:(qn(t,"iterate",Ju),mr(e)?t:t.map(rs))}function fm(e){return qn(e=$t(e),"iterate",Ju),e}function ks(e,t){return Os(e)?oa(Cs(e)?rs(t):t):rs(t)}const TP={__proto__:null,[Symbol.iterator](){return _h(this,Symbol.iterator,e=>ks(this,e))},concat(...e){return _i(this).concat(...e.map(t=>Ke(t)?_i(t):t))},entries(){return _h(this,"entries",e=>(e[1]=ks(this,e[1]),e))},every(e,t){return Ds(this,"every",e,t,void 0,arguments)},filter(e,t){return Ds(this,"filter",e,t,n=>n.map(r=>ks(this,r)),arguments)},find(e,t){return Ds(this,"find",e,t,n=>ks(this,n),arguments)},findIndex(e,t){return Ds(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ds(this,"findLast",e,t,n=>ks(this,n),arguments)},findLastIndex(e,t){return Ds(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ds(this,"forEach",e,t,void 0,arguments)},includes(...e){return xh(this,"includes",e)},indexOf(...e){return xh(this,"indexOf",e)},join(e){return _i(this).join(e)},lastIndexOf(...e){return xh(this,"lastIndexOf",e)},map(e,t){return Ds(this,"map",e,t,void 0,arguments)},pop(){return Ka(this,"pop")},push(...e){return Ka(this,"push",e)},reduce(e,...t){return o_(this,"reduce",e,t)},reduceRight(e,...t){return o_(this,"reduceRight",e,t)},shift(){return Ka(this,"shift")},some(e,t){return Ds(this,"some",e,t,void 0,arguments)},splice(...e){return Ka(this,"splice",e)},toReversed(){return _i(this).toReversed()},toSorted(e){return _i(this).toSorted(e)},toSpliced(...e){return _i(this).toSpliced(...e)},unshift(...e){return Ka(this,"unshift",e)},values(){return _h(this,"values",e=>ks(this,e))}};function _h(e,t,n){const r=fm(e),s=r[t]();return r!==e&&!mr(e)&&(s._next=s.next,s.next=()=>{const o=s._next();return o.done||(o.value=n(o.value)),o}),s}const AP=Array.prototype;function Ds(e,t,n,r,s,o){const l=fm(e),a=l!==e&&!mr(e),u=l[t];if(u!==AP[t]){const f=u.apply(e,o);return a?rs(f):f}let c=n;l!==e&&(a?c=function(f,p){return n.call(this,ks(e,f),p,e)}:n.length>2&&(c=function(f,p){return n.call(this,f,p,e)}));const d=u.call(l,c,r);return a&&s?s(d):d}function o_(e,t,n,r){const s=fm(e),o=s!==e&&!mr(e);let l=n,a=!1;s!==e&&(o?(a=r.length===0,l=function(c,d,f){return a&&(a=!1,c=ks(e,c)),n.call(this,c,ks(e,d),f,e)}):n.length>3&&(l=function(c,d,f){return n.call(this,c,d,f,e)}));const u=s[t](l,...r);return a?ks(e,u):u}function xh(e,t,n){const r=$t(e);qn(r,"iterate",Ju);const s=r[t](...n);return(s===-1||s===!1)&&Vc(n[0])?(n[0]=$t(n[0]),r[t](...n)):s}function Ka(e,t,n=[]){ao(),Dy();const r=$t(e)[t].apply(e,n);return Vy(),uo(),r}const PP=nm("__proto__,__v_isRef,__isVue"),Fw=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(wr));function MP(e){wr(e)||(e=String(e));const t=$t(this);return qn(t,"has",e),t.hasOwnProperty(e)}class Bw{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,r){if(n==="__v_skip")return t.__v_skip;const s=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!s;if(n==="__v_isReadonly")return s;if(n==="__v_isShallow")return o;if(n==="__v_raw")return r===(s?o?Kw:zw:o?Hw:jw).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(r)?t:void 0;const l=Ke(t);if(!s){let u;if(l&&(u=TP[n]))return u;if(n==="hasOwnProperty")return MP}const a=Reflect.get(t,n,Et(t)?t:r);if((wr(n)?Fw.has(n):PP(n))||(s||qn(t,"get",n),o))return a;if(Et(a)){const u=l&&om(n)?a:a.value;return s&&Pt(u)?bf(u):u}return Pt(a)?s?bf(a):rt(a):a}}class qw extends Bw{constructor(t=!1){super(!1,t)}set(t,n,r,s){let o=t[n];const l=Ke(t)&&om(n);if(!this._isShallow){const c=Os(o);if(!mr(r)&&!Os(r)&&(o=$t(o),r=$t(r)),!l&&Et(o)&&!Et(r))return c||(o.value=r),!0}const a=l?Number(n)e,dd=e=>Reflect.getPrototypeOf(e);function NP(e,t,n){return function(...r){const s=this.__v_raw,o=$t(s),l=Jo(o),a=e==="entries"||e===Symbol.iterator&&l,u=e==="keys"&&l,c=s[e](...r),d=n?Wg:t?oa:rs;return!t&&qn(o,"iterate",u?Kg:jl),Nt(Object.create(c),{next(){const{value:f,done:p}=c.next();return p?{value:f,done:p}:{value:a?[d(f[0]),d(f[1])]:d(f),done:p}}})}}function fd(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function LP(e,t){const n={get(s){const o=this.__v_raw,l=$t(o),a=$t(s);e||(On(s,a)&&qn(l,"get",s),qn(l,"get",a));const{has:u}=dd(l),c=t?Wg:e?oa:rs;if(u.call(l,s))return c(o.get(s));if(u.call(l,a))return c(o.get(a));o!==l&&o.get(s)},get size(){const s=this.__v_raw;return!e&&qn($t(s),"iterate",jl),s.size},has(s){const o=this.__v_raw,l=$t(o),a=$t(s);return e||(On(s,a)&&qn(l,"has",s),qn(l,"has",a)),s===a?o.has(s):o.has(s)||o.has(a)},forEach(s,o){const l=this,a=l.__v_raw,u=$t(a),c=t?Wg:e?oa:rs;return!e&&qn(u,"iterate",jl),a.forEach((d,f)=>s.call(o,c(d),c(f),l))}};return Nt(n,e?{add:fd("add"),set:fd("set"),delete:fd("delete"),clear:fd("clear")}:{add(s){const o=$t(this),l=dd(o),a=$t(s),u=!t&&!mr(s)&&!Os(s)?a:s;return l.has.call(o,u)||On(s,u)&&l.has.call(o,s)||On(a,u)&&l.has.call(o,a)||(o.add(u),Hs(o,"add",u,u)),this},set(s,o){!t&&!mr(o)&&!Os(o)&&(o=$t(o));const l=$t(this),{has:a,get:u}=dd(l);let c=a.call(l,s);c||(s=$t(s),c=a.call(l,s));const d=u.call(l,s);return l.set(s,o),c?On(o,d)&&Hs(l,"set",s,o):Hs(l,"add",s,o),this},delete(s){const o=$t(this),{has:l,get:a}=dd(o);let u=l.call(o,s);u||(s=$t(s),u=l.call(o,s)),a&&a.call(o,s);const c=o.delete(s);return u&&Hs(o,"delete",s,void 0),c},clear(){const s=$t(this),o=s.size!==0,l=s.clear();return o&&Hs(s,"clear",void 0,void 0),l}}),["keys","values","entries",Symbol.iterator].forEach(s=>{n[s]=NP(s,e,t)}),n}function pm(e,t){const n=LP(e,t);return(r,s,o)=>s==="__v_isReactive"?!e:s==="__v_isReadonly"?e:s==="__v_raw"?r:Reflect.get(Ot(n,s)&&s in r?n:r,s,o)}const FP={get:pm(!1,!1)},BP={get:pm(!1,!0)},qP={get:pm(!0,!1)},UP={get:pm(!0,!0)},jw=new WeakMap,Hw=new WeakMap,zw=new WeakMap,Kw=new WeakMap;function jP(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function rt(e){return Os(e)?e:mm(e,!1,DP,FP,jw)}function Ww(e){return mm(e,!1,IP,BP,Hw)}function bf(e){return mm(e,!0,VP,qP,zw)}function Ro(e){return mm(e,!0,RP,UP,Kw)}function mm(e,t,n,r,s){if(!Pt(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const o=s.get(e);if(o)return o;const l=jP(dP(e));if(l===0)return e;const a=new Proxy(e,l===2?r:n);return s.set(e,a),a}function Cs(e){return Os(e)?Cs(e.__v_raw):!!(e&&e.__v_isReactive)}function Os(e){return!!(e&&e.__v_isReadonly)}function mr(e){return!!(e&&e.__v_isShallow)}function Vc(e){return e?!!e.__v_raw:!1}function $t(e){const t=e&&e.__v_raw;return t?$t(t):e}function sa(e){return!Ot(e,"__v_skip")&&Object.isExtensible(e)&&Ow(e,"__v_skip",!0),e}const rs=e=>Pt(e)?rt(e):e,oa=e=>Pt(e)?bf(e):e;function Et(e){return e?e.__v_isRef===!0:!1}function U(e){return Gw(e,!1)}function yo(e){return Gw(e,!0)}function Gw(e,t){return Et(e)?e:new HP(e,t)}class HP{constructor(t,n){this.dep=new dm,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:$t(t),this._value=n?t:rs(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,r=this.__v_isShallow||mr(t)||Os(t);t=r?t:$t(t),On(t,n)&&(this._rawValue=t,this._value=r?t:rs(t),this.dep.trigger())}}function Jw(e){e.dep&&e.dep.trigger()}function i(e){return Et(e)?e.value:e}function Ln(e){return nt(e)?e():i(e)}const zP={get:(e,t,n)=>t==="__v_raw"?e:i(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const s=e[t];return Et(s)&&!Et(n)?(s.value=n,!0):Reflect.set(e,t,n,r)}};function Ry(e){return Cs(e)?e:new Proxy(e,zP)}class KP{constructor(t){this.__v_isRef=!0,this._value=void 0;const n=this.dep=new dm,{get:r,set:s}=t(n.track.bind(n),n.trigger.bind(n));this._get=r,this._set=s}get value(){return this._value=this._get()}set value(t){this._set(t)}}function hm(e){return new KP(e)}function ln(e){const t=Ke(e)?new Array(e.length):{};for(const n in e)t[n]=Xw(e,n);return t}class WP{constructor(t,n,r){this._object=t,this._defaultValue=r,this.__v_isRef=!0,this._value=void 0,this._key=wr(n)?n:String(n),this._raw=$t(t);let s=!0,o=t;if(!Ke(t)||wr(this._key)||!om(this._key))do s=!Vc(o)||mr(o);while(s&&(o=o.__v_raw));this._shallow=s}get value(){let t=this._object[this._key];return this._shallow&&(t=i(t)),this._value=t===void 0?this._defaultValue:t}set value(t){if(this._shallow&&Et(this._raw[this._key])){const n=this._object[this._key];if(Et(n)){n.value=t;return}}this._object[this._key]=t}get dep(){return OP(this._raw,this._key)}}class GP{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Yw(e,t,n){return Et(e)?e:nt(e)?new GP(e):Pt(e)&&arguments.length>1?Xw(e,t,n):U(e)}function Xw(e,t,n){return new WP(e,t,n)}class JP{constructor(t,n,r){this.fn=t,this.setter=n,this._value=void 0,this.dep=new dm(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Gu-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=r}notify(){if(this.flags|=16,!(this.flags&8)&&Kt!==this)return Dw(this,!0),!0}get value(){const t=this.dep.track();return Rw(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function YP(e,t,n=!1){let r,s;return nt(e)?r=e:(r=e.get,s=e.set),new JP(r,s,n)}const XP={GET:"get",HAS:"has",ITERATE:"iterate"},QP={SET:"set",ADD:"add",DELETE:"delete",CLEAR:"clear"},pd={},_f=new WeakMap;let No;function ZP(){return No}function Qw(e,t=!1,n=No){if(n){let r=_f.get(n);r||_f.set(n,r=[]),r.push(e)}}function e5(e,t,n=bt){const{immediate:r,deep:s,once:o,scheduler:l,augmentJob:a,call:u}=n,c=E=>s?E:mr(E)||s===!1||s===0?zs(E,1):zs(E);let d,f,p,h,b=!1,x=!1;if(Et(e)?(f=()=>e.value,b=mr(e)):Cs(e)?(f=()=>c(e),b=!0):Ke(e)?(x=!0,b=e.some(E=>Cs(E)||mr(E)),f=()=>e.map(E=>{if(Et(E))return E.value;if(Cs(E))return c(E);if(nt(E))return u?u(E,2):E()})):nt(e)?t?f=u?()=>u(e,2):e:f=()=>{if(p){ao();try{p()}finally{uo()}}const E=No;No=d;try{return u?u(e,3,[h]):e(h)}finally{No=E}}:f=qr,t&&s){const E=f,T=s===!0?1/0:s;f=()=>zs(E(),T)}const w=Ma(),k=()=>{d.stop(),w&&w.active&&Ay(w.effects,d)};if(o&&t){const E=t;t=(...T)=>{const O=E(...T);return k(),O}}let C=x?new Array(e.length).fill(pd):pd;const S=E=>{if(!(!(d.flags&1)||!d.dirty&&!E))if(t){const T=d.run();if(E||s||b||(x?T.some((O,P)=>On(O,C[P])):On(T,C))){p&&p();const O=No;No=d;try{const P=[T,C===pd?void 0:x&&C[0]===pd?[]:C,h];C=T,u?u(t,3,P):t(...P)}finally{No=O}}}else d.run()};return a&&a(S),d=new Wu(f),d.scheduler=l?()=>l(S,!1):S,h=E=>Qw(E,!1,d),p=d.onStop=()=>{const E=_f.get(d);if(E){if(u)u(E,4);else for(const T of E)T();_f.delete(d)}},t?r?S(!0):C=d.run():l?l(S.bind(null,!0),!0):d.run(),k.pause=d.pause.bind(d),k.resume=d.resume.bind(d),k.stop=k,k}function zs(e,t=1/0,n){if(t<=0||!Pt(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,Et(e))zs(e.value,t,n);else if(Ke(e))for(let r=0;r{zs(r,t,n)});else if(sm(e)){for(const r in e)zs(e[r],t,n);for(const r of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,r)&&zs(e[r],t,n)}return e}/** -* @vue/runtime-core v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/const Zw=[];function t5(e){Zw.push(e)}function n5(){Zw.pop()}function r5(e,t){}const s5={SETUP_FUNCTION:0,0:"SETUP_FUNCTION",RENDER_FUNCTION:1,1:"RENDER_FUNCTION",NATIVE_EVENT_HANDLER:5,5:"NATIVE_EVENT_HANDLER",COMPONENT_EVENT_HANDLER:6,6:"COMPONENT_EVENT_HANDLER",VNODE_HOOK:7,7:"VNODE_HOOK",DIRECTIVE_HOOK:8,8:"DIRECTIVE_HOOK",TRANSITION_HOOK:9,9:"TRANSITION_HOOK",APP_ERROR_HANDLER:10,10:"APP_ERROR_HANDLER",APP_WARN_HANDLER:11,11:"APP_WARN_HANDLER",FUNCTION_REF:12,12:"FUNCTION_REF",ASYNC_COMPONENT_LOADER:13,13:"ASYNC_COMPONENT_LOADER",SCHEDULER:14,14:"SCHEDULER",COMPONENT_UPDATE:15,15:"COMPONENT_UPDATE",APP_UNMOUNT_CLEANUP:16,16:"APP_UNMOUNT_CLEANUP"},o5={sp:"serverPrefetch hook",bc:"beforeCreate hook",c:"created hook",bm:"beforeMount hook",m:"mounted hook",bu:"beforeUpdate hook",u:"updated",bum:"beforeUnmount hook",um:"unmounted hook",a:"activated hook",da:"deactivated hook",ec:"errorCaptured hook",rtc:"renderTracked hook",rtg:"renderTriggered hook",0:"setup function",1:"render function",2:"watcher getter",3:"watcher callback",4:"watcher cleanup function",5:"native event handler",6:"component event handler",7:"vnode hook",8:"directive hook",9:"transition hook",10:"app errorHandler",11:"app warnHandler",12:"ref function",13:"async component loader",14:"scheduler flush",15:"component update",16:"app unmount cleanup function"};function Da(e,t,n,r){try{return r?e(...r):e()}catch(s){fi(s,t,n)}}function $r(e,t,n,r){if(nt(e)){const s=Da(e,t,n,r);return s&&Py(s)&&s.catch(o=>{fi(o,t,n)}),s}if(Ke(e)){const s=[];for(let o=0;o>>1,s=Xn[r],o=Xu(s);o=Xu(n)?Xn.push(e):Xn.splice(i5(t),0,e),e.flags|=1,t2()}}function t2(){xf||(xf=e2.then(n2))}function Yu(e){if(!Ke(e))Lo&&e.id===-1?Lo.splice(Ai+1,0,e):e.flags&1||(Wi.push(e),e.flags|=1);else for(let t=0;tXu(n)-Xu(r));if(Wi.length=0,Lo){for(let n=0;ne.id==null?e.flags&2?-1:1/0:e.id;function n2(e){try{for(ms=0;msPi.emit(s,...o)),md=[]):typeof window<"u"&&window.HTMLElement&&!((r=(n=window.navigator)==null?void 0:n.userAgent)!=null&&r.includes("jsdom"))?((t.__VUE_DEVTOOLS_HOOK_REPLAY__=t.__VUE_DEVTOOLS_HOOK_REPLAY__||[]).push(o=>{r2(o,t)}),setTimeout(()=>{Pi||(t.__VUE_DEVTOOLS_HOOK_REPLAY__=null,md=[])},3e3)):md=[]}let Rn=null,gm=null;function Qu(e){const t=Rn;return Rn=e,gm=e&&e.type.__scopeId||null,t}function a5(e){gm=e}function u5(){gm=null}const c5=e=>_;function _(e,t=Rn,n){if(!t||e._n)return e;const r=(...s)=>{r._d&&tc(-1);const o=Qu(t),l=ro.length;let a;try{a=e(...s)}finally{for(let u=ro.length;u>l;u--)Cm();Qu(o),r._d&&tc(1)}return a};return r._n=!0,r._c=!0,r._d=!0,r}function jn(e,t){if(Rn===null)return e;const n=Fc(Rn),r=e.dirs||(e.dirs=[]);for(let s=0;s1)return n&&nt(t)?t.call(r&&r.proxy):t}}function d5(){return!!(zt()||Hl)}const s2=Symbol.for("v-scx"),o2=()=>eo(s2);function Cr(e,t){return Rc(e,null,t)}function Ly(e,t){return Rc(e,null,{flush:"post"})}function Fy(e,t){return Rc(e,null,{flush:"sync"})}function ye(e,t,n){return Rc(e,t,n)}function Rc(e,t,n=bt){const{immediate:r,deep:s,flush:o,once:l}=n,a=Nt({},n),u=t&&r||!t&&o!=="post";let c;if(Zl){if(o==="sync"){const h=o2();c=h.__watcherHandles||(h.__watcherHandles=[])}else if(!u){const h=()=>{};return h.stop=qr,h.resume=qr,h.pause=qr,h}}const d=In;a.call=(h,b,x)=>$r(h,d,b,x);let f=!1;o==="post"?a.scheduler=h=>{bn(h,d&&d.suspense)}:o!=="sync"&&(f=!0,a.scheduler=(h,b)=>{b?h():Ny(h)}),a.augmentJob=h=>{t&&(h.flags|=4),f&&(h.flags|=2,d&&(h.id=d.uid,h.i=d))};const p=e5(e,t,a);return Zl&&(c?c.push(p):u&&p()),p}function f5(e,t,n){const r=this.proxy,s=jt(e)?e.includes(".")?l2(r,e):()=>r[e]:e.bind(r,r);let o;nt(t)?o=t:(o=t.handler,n=t);const l=Va(this),a=Rc(s,o.bind(r),n);return l(),a}function l2(e,t){const n=t.split(".");return()=>{let r=e;for(let s=0;se.__isTeleport,Pl=e=>e&&(e.disabled||e.disabled===""),p5=e=>e&&(e.defer||e.defer===""),i_=e=>typeof SVGElement<"u"&&e instanceof SVGElement,a_=e=>typeof MathMLElement=="function"&&e instanceof MathMLElement,Gg=(e,t)=>{const n=e&&e.to;return jt(n)?t?t(n):null:n},m5={name:"Teleport",__isTeleport:!0,process(e,t,n,r,s,o,l,a,u,c){const{mc:d,pc:f,pbc:p,o:{insert:h,querySelector:b,createText:x,createComment:w,parentNode:k}}=c,C=Pl(t.props);let{dynamicChildren:S}=t;const E=(P,I,M)=>{P.shapeFlag&16&&d(P.children,I,M,s,o,l,a,u)},T=(P=t)=>{const I=Pl(P.props),M=P.target=Gg(P.props,b),D=Jg(M,P,x,h);M&&(l!=="svg"&&i_(M)?l="svg":l!=="mathml"&&a_(M)&&(l="mathml"),s&&s.isCE&&(s.ce._teleportTargets||(s.ce._teleportTargets=new Set)).add(M),I||(E(P,M,D),cu(P,!1)))},O=P=>{const I=()=>{if(Po.get(P)===I){if(Po.delete(P),Pl(P.props)){const M=k(P.el)||n;E(P,M,P.anchor),cu(P,!0)}T(P)}};Po.set(P,I),bn(I,o)};if(e==null){const P=t.el=x(""),I=t.anchor=x("");if(h(P,n,r),h(I,n,r),p5(t.props)||o&&o.pendingBranch){O(t);return}C&&(E(t,n,I),cu(t,!0)),T()}else{t.el=e.el;const P=t.anchor=e.anchor,I=Po.get(e);if(I){I.flags|=8,Po.delete(e),O(t);return}t.targetStart=e.targetStart;const M=t.target=e.target,D=t.targetAnchor=e.targetAnchor,L=Pl(e.props),F=L?n:M,N=L?P:D;if(l==="svg"||i_(M)?l="svg":(l==="mathml"||a_(M))&&(l="mathml"),S?(p(e.dynamicChildren,S,F,s,o,l,a),e1(e,t,!0)):u||f(e,t,F,N,s,o,l,a,!1),C)L?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):hd(t,n,P,c,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const j=Gg(t.props,b);j&&(t.target=j,hd(t,j,null,c,0))}else L&&hd(t,M,D,c,1);cu(t,C)}},remove(e,t,n,{um:r,o:{remove:s}},o){const{shapeFlag:l,children:a,anchor:u,targetStart:c,targetAnchor:d,target:f,props:p}=e,h=Pl(p),b=o||!h,x=Po.get(e);if(x&&(x.flags|=8,Po.delete(e)),f&&(s(c),s(d)),o&&s(u),!x&&(h||f)&&l&16)for(let w=0;w{e.isMounted=!0}),Kn(()=>{e.isUnmounting=!0}),e}const Or=[Function,Array],Uy={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Or,onEnter:Or,onAfterEnter:Or,onEnterCancelled:Or,onBeforeLeave:Or,onLeave:Or,onAfterLeave:Or,onLeaveCancelled:Or,onBeforeAppear:Or,onAppear:Or,onAfterAppear:Or,onAppearCancelled:Or},a2=e=>{const t=e.subTree;return t.component?a2(t.component):t},g5={name:"BaseTransition",props:Uy,setup(e,{slots:t}){const n=zt(),r=qy();return()=>{const s=t.default&&ym(t.default(),!0),o=s&&s.length?u2(s):n.subTree?R():void 0;if(!o)return;const l=$t(e),{mode:a}=l;if(r.isLeaving)return kh(o);const u=wf(o);if(!u)return kh(o);let c=la(u,l,r,n,f=>c=f);u.type!==dn&&co(u,c);let d=n.subTree&&wf(n.subTree);if(d&&d.type!==dn&&!Yr(d,u)&&a2(n).type!==dn){let f=la(d,l,r,n);if(co(d,f),a==="out-in"&&u.type!==dn)return r.isLeaving=!0,f.afterLeave=()=>{r.isLeaving=!1,n.job.flags&8||n.update(),delete f.afterLeave,d=void 0},kh(o);a==="in-out"&&u.type!==dn?f.delayLeave=(p,h,b)=>{const x=d2(r,d);x[String(d.key)]=d,p[Dr]=()=>{h(),p[Dr]=void 0,delete c.delayedLeave,d=void 0},c.delayedLeave=()=>{b(),delete c.delayedLeave,d=void 0}}:d=void 0}else d&&(d=void 0);return o}}};function u2(e){let t=e[0];if(e.length>1){for(const n of e)if(n.type!==dn){t=n;break}}return t}const c2=g5;function d2(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function la(e,t,n,r,s){const{appear:o,mode:l,persisted:a=!1,onBeforeEnter:u,onEnter:c,onAfterEnter:d,onEnterCancelled:f,onBeforeLeave:p,onLeave:h,onAfterLeave:b,onLeaveCancelled:x,onBeforeAppear:w,onAppear:k,onAfterAppear:C,onAppearCancelled:S}=t,E=String(e.key),T=d2(n,e),O=(M,D)=>{M&&$r(M,r,9,D)},P=(M,D)=>{const L=D[1];O(M,D),Ke(M)?M.every(F=>F.length<=1)&&L():M.length<=1&&L()},I={mode:l,persisted:a,beforeEnter(M){let D=u;if(!n.isMounted)if(o)D=w||u;else return;M[Dr]&&M[Dr](!0);const L=T[E];L&&Yr(e,L)&&L.el[Dr]&&L.el[Dr](),O(D,[M])},enter(M){if(T[E]===e)return;let D=c,L=d,F=f;if(!n.isMounted)if(o)D=k||c,L=C||d,F=S||f;else return;let N=!1;M[Wa]=q=>{N||(N=!0,q?O(F,[M]):O(L,[M]),I.delayedLeave&&I.delayedLeave(),M[Wa]=void 0)};const j=M[Wa].bind(null,!1);D?P(D,[M,j]):j()},leave(M,D){const L=String(e.key);if(M[Wa]&&M[Wa](!0),n.isUnmounting)return D();O(p,[M]);let F=!1;M[Dr]=j=>{F||(F=!0,D(),j?O(x,[M]):O(b,[M]),M[Dr]=void 0,T[L]===e&&delete T[L])};const N=M[Dr].bind(null,!1);T[L]=e,h?P(h,[M,N]):N()},clone(M){const D=la(M,t,n,r,s);return s&&s(D),D}};return I}function kh(e){if(Nc(e))return e=ss(e),e.children=null,e}function wf(e){if(!Nc(e))return vm(e.type)&&e.children?u2(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(t&16)return n[0];if(t&32&&nt(n.default))return n.default()}}function co(e,t){if(e.shapeFlag&6&&e.component){e.transition=t;const n=e.component.subTree;co(vm(n.type)&&wf(n)||n,t)}else e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function ym(e,t=!1,n){let r=[],s=0;for(let o=0;o1)for(let o=0;on.value,set:o=>n.value=o})}return n}function u_(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))&&!n.configurable)}const $f=new WeakMap;function Gi(e,t,n,r,s=!1){if(Ke(e)){e.forEach((x,w)=>Gi(x,t&&(Ke(t)?t[w]:t),n,r,s));return}if(to(r)&&!s){r.shapeFlag&512&&r.type.__asyncResolved&&r.component.subTree.component&&Gi(e,t,n,r.component.subTree);return}const o=r.shapeFlag&4?Fc(r.component):r.el,l=s?null:o,{i:a,r:u}=e,c=t&&t.r,d=a.refs===bt?a.refs={}:a.refs,f=a.setupState,p=$t(f),h=f===bt?Sw:x=>u_(d,x)?!1:Ot(p,x),b=(x,w)=>!(w&&u_(d,w));if(c!=null&&c!==u){if(c_(t),jt(c))d[c]=null,h(c)&&(f[c]=null);else if(Et(c)){const x=t;b(c,x.k)&&(c.value=null),x.k&&(d[x.k]=null)}}if(nt(u))Da(u,a,12,[l,d]);else{const x=jt(u),w=Et(u);if(x||w){const k=()=>{if(e.f){const C=x?h(u)?f[u]:d[u]:b()||!e.k?u.value:d[e.k];if(s)Ke(C)&&Ay(C,o);else if(Ke(C))C.includes(o)||C.push(o);else if(x)d[u]=[o],h(u)&&(f[u]=d[u]);else{const S=[o];b(u,e.k)&&(u.value=S),e.k&&(d[e.k]=S)}}else x?(d[u]=l,h(u)&&(f[u]=l)):w&&(b(u,e.k)&&(u.value=l),e.k&&(d[e.k]=l))};if(l){const C=()=>{k(),$f.delete(e)};C.id=-1,$f.set(e,C),bn(C,n)}else c_(e),k()}}}function c_(e){const t=$f.get(e);t&&(t.flags|=8,$f.delete(e))}let d_=!1;const xi=()=>{d_||(console.error("Hydration completed but contains mismatches."),d_=!0)},y5=e=>e.namespaceURI.includes("svg")&&e.tagName!=="foreignObject",b5=e=>e.namespaceURI.includes("MathML"),gd=e=>{if(e.nodeType===1){if(y5(e))return"svg";if(b5(e))return"mathml"}},qi=e=>e.nodeType===8;function _5(e){const{mt:t,p:n,o:{patchProp:r,createText:s,nextSibling:o,parentNode:l,remove:a,insert:u,createComment:c}}=e,d=(S,E)=>{if(!E.hasChildNodes()){n(null,S,E),kf(),E._vnode=S;return}f(E.firstChild,S,null,null,null),kf(),E._vnode=S},f=(S,E,T,O,P,I=!1)=>{I=I||!!E.dynamicChildren;const M=qi(S)&&S.data==="[",D=()=>x(S,E,T,O,P,M),{type:L,ref:F,shapeFlag:N,patchFlag:j}=E;let q=S.nodeType;E.el=S,j===-2&&(I=!1,E.dynamicChildren=null);let W=null;switch(L){case no:q!==3?E.children===""?(u(E.el=s(""),l(S),S),W=S):W=D():(S.data!==E.children&&(xi(),S.data=E.children),W=o(S));break;case dn:C(S)?(W=o(S),k(E.el=S.content.firstChild,S,T)):q!==8||M?W=D():W=o(S);break;case Yo:if(M&&(S=o(S),q=S.nodeType),q===1||q===3){W=S;const X=!E.children.length;for(let ee=0;ee{I=I||!!E.dynamicChildren;const{type:M,dynamicProps:D,props:L,patchFlag:F,shapeFlag:N,dirs:j,transition:q}=E,W=M==="input"||M==="option",X=!!D;if(W||X||F!==-1){j&&_s(E,null,T,"created");let ee=!1;if(C(S)){ee=L2(null,q)&&T&&T.vnode.props&&T.vnode.props.appear;const fe=S.content.firstChild;if(ee){const le=fe.getAttribute("class");le&&(fe.$cls=le),q.beforeEnter(fe)}k(fe,S,T),E.el=S=fe}if(N&16&&!(L&&(L.innerHTML||L.textContent))){let fe=h(S.firstChild,E,S,T,O,P,I);for(fe&&!Xd(S,1)&&xi();fe;){const le=fe;fe=fe.nextSibling,a(le)}}else if(N&8){let fe=E.children;fe[0]===` -`&&(S.tagName==="PRE"||S.tagName==="TEXTAREA")&&(fe=fe.slice(1));const{textContent:le}=S;le!==fe&&le!==fe.replace(/\r\n|\r/g,` -`)&&(Xd(S,0)||xi(),S.textContent=E.children)}if(L){if(W||X||!I||F&48){const fe=S.tagName.includes("-"),le=S.namespaceURI.includes("svg")?"svg":S.namespaceURI.includes("MathML")?"mathml":void 0;for(const re in L)if(W&&(re.endsWith("value")||re==="indeterminate")||Pc(re)&&!Ul(re)||re[0]==="."||fe&&!Ul(re)||D&&D.includes(re)){if(k5(S,re,L[re]))continue;r(S,re,null,L[re],le,T)}}else if(L.onClick)r(S,"onClick",null,L.onClick,void 0,T);else if(F&4&&Cs(L.style))for(const fe in L.style)L.style[fe]}let ce;(ce=L&&L.onVnodeBeforeMount)&&lr(ce,T,E),j&&_s(E,null,T,"beforeMount"),((ce=L&&L.onVnodeMounted)||j||ee)&&U2(()=>{ce&&lr(ce,T,E),ee&&q.enter(S),j&&_s(E,null,T,"mounted")},O)}return S.nextSibling},h=(S,E,T,O,P,I,M)=>{M=M||!!E.dynamicChildren;const D=E.children,L=D.length;let F=!1;for(let N=0;N{const{slotScopeIds:M}=E;M&&(P=P?P.concat(M):M);const D=l(S),L=h(o(S),E,D,T,O,P,I);return L&&qi(L)&&L.data==="]"?o(E.anchor=L):(xi(),u(E.anchor=c("]"),D,L),L)},x=(S,E,T,O,P,I)=>{if($5(S,E)||xi(),E.el=null,I){const L=w(S);for(;;){const F=o(S);if(F&&F!==L)a(F);else break}}const M=o(S),D=l(S);return a(S),n(null,E,D,M,T,O,gd(D),P),T&&(T.vnode.el=E.el,$m(T,E.el)),M},w=(S,E="[",T="]")=>{let O=0;for(;S;)if(S=o(S),S&&qi(S)&&(S.data===E&&O++,S.data===T)){if(O===0)return o(S);O--}return S},k=(S,E,T)=>{const O=E.parentNode;O&&O.replaceChild(S,E);let P=T;for(;P;)P.vnode.el===E&&(P.vnode.el=P.subTree.el=S),P=P.parent},C=S=>S.nodeType===1&&S.tagName==="TEMPLATE";return[d,f]}const x5=new Set(["src","srcset","href","poster"]);function k5(e,t,n){return x5.has(t)?e.getAttribute(t)===(n==null?null:`${n}`):!1}const Cf="data-allow-mismatch",w5={0:"text",1:"children",2:"class",3:"style",4:"attribute"};function Xd(e,t){if(t===0||t===1)for(;e&&!e.hasAttribute(Cf);)e=e.parentElement;return Hy(e&&e.getAttribute(Cf),t)}function Hy(e,t){if(e==null)return!1;if(e==="")return!0;{const n=e.split(",");return t===0&&n.includes("children")?!0:n.includes(w5[t])}}function $5(e,t){return Xd(e.parentElement,1)||C5(e)||S5(t)}function C5(e){return e.nodeType===1&&Hy(e.getAttribute(Cf),1)}function S5({props:e}){const t=e&&e[Cf];return typeof t=="string"&&Hy(t,1)}const E5=am().requestIdleCallback||(e=>setTimeout(e,1)),O5=am().cancelIdleCallback||(e=>clearTimeout(e)),T5=(e=1e4)=>t=>{const n=E5(t,{timeout:e});return()=>O5(n)};function A5(e){const{top:t,left:n,bottom:r,right:s}=e.getBoundingClientRect(),{innerHeight:o,innerWidth:l}=window;return(t>0&&t0&&r0&&n0&&s(t,n)=>{const r=new IntersectionObserver(s=>{for(const o of s)if(o.isIntersecting){r.disconnect(),t();break}},e);return n(s=>{if(s instanceof Element){if(A5(s))return t(),r.disconnect(),!1;r.observe(s)}}),()=>r.disconnect()},M5=e=>t=>{if(e){const n=matchMedia(e);if(n.matches)t();else return n.addEventListener("change",t,{once:!0}),()=>n.removeEventListener("change",t)}},D5=(e=[])=>(t,n)=>{jt(e)&&(e=[e]);let r=!1;const s=l=>{r||(r=!0,o(),t(),l.target.dispatchEvent(new l.constructor(l.type,l)))},o=()=>{n(l=>{for(const a of e)l.removeEventListener(a,s)})};return n(l=>{for(const a of e)l.addEventListener(a,s,{once:!0})}),o};function V5(e,t){if(qi(e)&&e.data==="["){let n=1,r=e.nextSibling;for(;r;){if(r.nodeType===1){if(t(r)===!1)break}else if(qi(r))if(r.data==="]"){if(--n===0)break}else r.data==="["&&n++;r=r.nextSibling}}else t(e)}const to=e=>!!e.type.__asyncLoader;function I5(e){nt(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:s=200,hydrate:o,timeout:l,suspensible:a=!0,onError:u}=e;let c=null,d,f=0;const p=()=>(f++,c=null,h()),h=()=>{let b;return c||(b=c=t().catch(x=>{if(x=x instanceof Error?x:new Error(String(x)),u)return new Promise((w,k)=>{u(x,()=>w(p()),()=>k(x),f+1)});throw x}).then(x=>b!==c&&c?c:(x&&(x.__esModule||x[Symbol.toStringTag]==="Module")&&(x=x.default),d=x,x)))};return K({name:"AsyncComponentWrapper",__asyncLoader:h,__asyncHydrate(b,x,w){const k=b.isConnected;let C=!1;(x.bu||(x.bu=[])).push(()=>C=!0);const S=()=>{C||!b.parentNode||k&&!b.isConnected||w()},E=o?()=>{const T=o(S,O=>V5(b,O));T&&(x.bum||(x.bum=[])).push(T)}:S;d?E():h().then(()=>!x.isUnmounted&&E())},get __asyncResolved(){return d},setup(){const b=In;if(jy(b),d)return()=>vd(d,b);const x=T=>{c=null,fi(T,b,13,!r)};if(a&&b.suspense||Zl)return h().then(T=>()=>vd(T,b)).catch(T=>(x(T),()=>r?v(r,{error:T}):null));const w=U(!1),k=U(),C=U(!!s);let S,E;return nn(()=>{S!=null&&clearTimeout(S),E!=null&&clearTimeout(E)}),s&&(E=setTimeout(()=>{b.isUnmounted||(C.value=!1)},s)),l!=null&&(S=setTimeout(()=>{if(!b.isUnmounted&&!w.value&&!k.value){const T=new Error(`Async component timed out after ${l}ms.`);x(T),k.value=T}},l)),h().then(()=>{b.isUnmounted||(w.value=!0,b.parent&&Nc(b.parent.vnode)&&b.parent.update())}).catch(T=>{if(b.isUnmounted){c=null;return}x(T),k.value=T}),()=>{if(w.value&&d)return vd(d,b);if(k.value&&r)return v(r,{error:k.value});if(n&&!C.value)return vd(n,b)}}})}function vd(e,t){const{ref:n,props:r,children:s,ce:o}=t.vnode,l=v(e,r,s);return l.ref=n,l.ce=o,delete t.vnode.ce,l}const Nc=e=>e.type.__isKeepAlive,R5={name:"KeepAlive",__isKeepAlive:!0,props:{include:[String,RegExp,Array],exclude:[String,RegExp,Array],max:[String,Number]},setup(e,{slots:t}){const n=zt(),r=n.ctx;if(!r.renderer)return()=>{const C=t.default&&t.default();return C&&C.length===1?C[0]:C};const s=new Map,o=new Set;let l=null;const a=n.suspense,{renderer:{p:u,m:c,um:d,o:{createElement:f}}}=r,p=f("div");r.activate=(C,S,E,T,O)=>{const P=C.component;c(C,S,E,0,a),u(P.vnode,C,S,E,P,a,T,C.slotScopeIds,O),bn(()=>{P.isDeactivated=!1,P.a&&Ki(P.a);const I=C.props&&C.props.onVnodeMounted;I&&lr(I,P.parent,C)},a)},r.deactivate=C=>{const S=C.component;Ef(S.m),Ef(S.a),c(C,p,null,1,a),bn(()=>{S.da&&Ki(S.da);const E=C.props&&C.props.onVnodeUnmounted;E&&lr(E,S.parent,C),S.isDeactivated=!0},a)};function h(C){wh(C),d(C,n,a,!0)}function b(C){s.forEach((S,E)=>{const T=sv(to(S)?S.type.__asyncResolved||{}:S.type);T&&!C(T)&&x(E)})}function x(C){const S=s.get(C);S&&(!l||!Yr(S,l))?h(S):l&&wh(l),s.delete(C),o.delete(C)}ye(()=>[e.include,e.exclude],([C,S])=>{C&&b(E=>du(C,E)),S&&b(E=>!du(S,E))},{flush:"post",deep:!0});let w=null;const k=()=>{w!=null&&(Of(n.subTree.type)?bn(()=>{const C=yd(n.subTree);C.component&&s.set(w,C)},n.subTree.suspense):s.set(w,yd(n.subTree)))};return mt(k),Lc(k),Kn(()=>{s.forEach(C=>{const{subTree:S,suspense:E}=n,T=yd(S);if(C.type===T.type&&C.key===T.key){wh(T);const O=T.component.da;O&&bn(O,E);return}h(C)})}),()=>{if(w=null,!t.default)return l=null;const C=t.default(),S=C[0];if(C.length>1)return l=null,C;if(!fo(S)||!(S.shapeFlag&4)&&!(S.shapeFlag&128))return l=null,S;let E=yd(S);if(E.type===dn)return l=null,E;const T=E.type,O=sv(to(E)?E.type.__asyncResolved||{}:T),{include:P,exclude:I,max:M}=e;if(P&&(!O||!du(P,O))||I&&O&&du(I,O))return E.shapeFlag&=-257,l=E,S;const D=E.key==null?T:E.key,L=s.get(D);return E.el&&(E=ss(E),S.shapeFlag&128&&(S.ssContent=E)),w=D,L?(E.el=L.el,E.component=L.component,E.transition&&co(E,E.transition),E.shapeFlag|=512,o.delete(D),o.add(D)):(o.add(D),M&&o.size>parseInt(M,10)&&x(o.values().next().value)),E.shapeFlag|=256,l=E,Of(S.type)?S:E}}},N5=R5;function du(e,t){return Ke(e)?e.some(n=>du(n,t)):jt(e)?e.split(",").includes(t):cP(e)?(e.lastIndex=0,e.test(t)):!1}function f2(e,t){m2(e,"a",t)}function p2(e,t){m2(e,"da",t)}function m2(e,t,n=In){const r=e.__wdc||(e.__wdc=()=>{let s=n;for(;s;){if(s.isDeactivated)return;s=s.parent}return e()});if(bm(t,r,n),n){let s=n.parent;for(;s&&s.parent;)Nc(s.parent.vnode)&&L5(r,t,n,s),s=s.parent}}function L5(e,t,n,r){const s=bm(t,e,r,!0);nn(()=>{Ay(r[t],s)},n)}function wh(e){e.shapeFlag&=-257,e.shapeFlag&=-513}function yd(e){return e.shapeFlag&128?e.ssContent:e}function bm(e,t,n=In,r=!1){if(n){const s=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...l)=>{ao();const a=Va(n),u=$r(t,n,e,l);return a(),uo(),u});return r?s.unshift(o):s.push(o),o}}const bo=e=>(t,n=In)=>{(!Zl||e==="sp")&&bm(e,(...r)=>t(...r),n)},h2=bo("bm"),mt=bo("m"),zy=bo("bu"),Lc=bo("u"),Kn=bo("bum"),nn=bo("um"),g2=bo("sp"),v2=bo("rtg"),y2=bo("rtc");function b2(e,t=In){bm("ec",e,t)}const Ky="components",F5="directives";function Wy(e,t){return Gy(Ky,e,!0,t)||e}const _2=Symbol.for("v-ndc");function Pn(e){return jt(e)?Gy(Ky,e,!1)||e:e||_2}function B5(e){return Gy(F5,e)}function Gy(e,t,n=!0,r=!1){const s=Rn||In;if(s){const o=s.type;if(e===Ky){const a=sv(o,!1);if(a&&(a===t||a===fn(t)||a===Mc(fn(t))))return o}const l=f_(s[e]||o[e],t)||f_(s.appContext[e],t);return!l&&r?o:l}}function f_(e,t){return e&&(e[t]||e[fn(t)]||e[Mc(fn(t))])}function ne(e,t,n,r){let s;const o=n&&n[r],l=Ke(e);if(l||jt(e)){const a=l&&Cs(e);let u=!1,c=!1;a&&(u=!mr(e),c=Os(e),e=fm(e)),s=new Array(e.length);for(let d=0,f=e.length;dt(a,u,void 0,o&&o[u]));else{const a=Object.keys(e);s=new Array(a.length);for(let u=0,c=a.length;u{const o=r.fn(...s);return o&&(o.key=r.key),o}:r.fn)}return e}function he(e,t,n,r,s,o){if(n==null&&(n={}),Rn.ce||Rn.parent&&to(Rn.parent)&&Rn.parent.ce){const c=o!=null&&n.key==null?Nt({},n,{key:o}):n,d=Object.keys(c).length>0;return t!=="default"&&(c.name=t),y(),B(H,null,[v("slot",c,r&&r())],d?-2:64)}let l=e[t];l&&l._c&&(l._d=!1);const a=ro.length;y();let u;try{const c=l&&Jy(l(n)),d=n.key||o||c&&c.key;u=B(H,{key:(d&&!wr(d)?d:`_${t}`)+(!c&&r?"_fb":"")},c||(r?r():[]),c&&e._===1?64:-2)}catch(c){for(let d=ro.length;d>a;d--)Cm();throw c}finally{l&&l._c&&(l._d=!0)}return!s&&u.scopeId&&(u.slotScopeIds=[u.scopeId+"-s"]),u}function Jy(e){return e.some(t=>fo(t)?!(t.type===dn||t.type===H&&!Jy(t.children)):!0)?e:null}function _m(e,t){const n={};for(const r in e)n[t&&/[A-Z]/.test(r)?`on:${r}`:zi(r)]=e[r];return n}const Yg=e=>e?K2(e)?Fc(e):Yg(e.parent):null,$u=Nt(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Yg(e.parent),$root:e=>Yg(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Xy(e),$forceUpdate:e=>e.f||(e.f=()=>{Ny(e.update)}),$nextTick:e=>e.n||(e.n=ot.bind(e.proxy)),$watch:e=>f5.bind(e)}),$h=(e,t)=>e!==bt&&!e.__isScriptSetup&&Ot(e,t),Xg={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:r,data:s,props:o,accessCache:l,type:a,appContext:u}=e;if(t[0]!=="$"){const p=l[t];if(p!==void 0)switch(p){case 1:return r[t];case 2:return s[t];case 4:return n[t];case 3:return o[t]}else{if($h(r,t))return l[t]=1,r[t];if(s!==bt&&Ot(s,t))return l[t]=2,s[t];if(Ot(o,t))return l[t]=3,o[t];if(n!==bt&&Ot(n,t))return l[t]=4,n[t];Qg&&(l[t]=0)}}const c=$u[t];let d,f;if(c)return t==="$attrs"&&qn(e.attrs,"get",""),c(e);if((d=a.__cssModules)&&(d=d[t]))return d;if(n!==bt&&Ot(n,t))return l[t]=4,n[t];if(f=u.config.globalProperties,Ot(f,t))return f[t]},set({_:e},t,n){const{data:r,setupState:s,ctx:o}=e;return $h(s,t)?(s[t]=n,!0):r!==bt&&Ot(r,t)?(r[t]=n,!0):Ot(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:s,props:o,type:l}},a){let u;return!!(n[a]||e!==bt&&a[0]!=="$"&&Ot(e,a)||$h(t,a)||Ot(o,a)||Ot(r,a)||Ot($u,a)||Ot(s.config.globalProperties,a)||(u=l.__cssModules)&&u[a])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:Ot(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}},q5=Nt({},Xg,{get(e,t){if(t!==Symbol.unscopables)return Xg.get(e,t,e)},has(e,t){return t[0]!=="_"&&!hP(t)}});function U5(){return null}function j5(){return null}function H5(e){}function z5(e){}function K5(){return null}function W5(){}function G5(e,t){return null}function xm(){return x2().slots}function km(){return x2().attrs}function x2(e){const t=zt();return t.setupContext||(t.setupContext=J2(t))}function Zu(e){return Ke(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}function Yy(e,t){const n=Zu(e);for(const r in t){if(r.startsWith("__skip"))continue;let s=n[r];s?Ke(s)||nt(s)?s=n[r]={type:s,default:t[r]}:s.default=t[r]:s===null&&(s=n[r]={default:t[r]}),s&&t[`__skip_${r}`]&&(s.skipFactory=!0)}return n}function J5(e,t){return!e||!t?e||t:Ke(e)&&Ke(t)?e.concat(t):Nt({},Zu(e),Zu(t))}function Y5(e,t){const n={};for(const r in e)t.includes(r)||Object.defineProperty(n,r,{enumerable:!0,get:()=>e[r]});return n}function X5(e){const t=zt(),n=Zl;let r=e();nc(),n&&Xo(!1);const s=()=>{Va(t),n&&Xo(!0)},o=()=>{zt()!==t&&t.scope.off(),nc(),n&&Xo(!1)};return Py(r)&&(r=r.catch(l=>{throw s(),Promise.resolve().then(()=>Promise.resolve().then(o)),l})),[r,()=>{s(),Promise.resolve().then(o)}]}let Qg=!0;function Q5(e){const t=Xy(e),n=e.proxy,r=e.ctx;Qg=!1,t.beforeCreate&&p_(t.beforeCreate,e,"bc");const{data:s,computed:o,methods:l,watch:a,provide:u,inject:c,created:d,beforeMount:f,mounted:p,beforeUpdate:h,updated:b,activated:x,deactivated:w,beforeDestroy:k,beforeUnmount:C,destroyed:S,unmounted:E,render:T,renderTracked:O,renderTriggered:P,errorCaptured:I,serverPrefetch:M,expose:D,inheritAttrs:L,components:F,directives:N,filters:j}=t;if(c&&Z5(c,r,null),l)for(const X in l){const ee=l[X];nt(ee)&&(r[X]=ee.bind(n))}if(s){const X=s.call(n,n);Pt(X)&&(e.data=rt(X))}if(Qg=!0,o)for(const X in o){const ee=o[X],ce=nt(ee)?ee.bind(n,n):nt(ee.get)?ee.get.bind(n,n):qr,fe=!nt(ee)&&nt(ee.set)?ee.set.bind(n):qr,le=V({get:ce,set:fe});Object.defineProperty(r,X,{enumerable:!0,configurable:!0,get:()=>le.value,set:re=>le.value=re})}if(a)for(const X in a)k2(a[X],r,n,X);if(u){const X=nt(u)?u.call(n):u;Reflect.ownKeys(X).forEach(ee=>{Ic(ee,X[ee])})}d&&p_(d,e,"c");function W(X,ee){Ke(ee)?ee.forEach(ce=>X(ce.bind(n))):ee&&X(ee.bind(n))}if(W(h2,f),W(mt,p),W(zy,h),W(Lc,b),W(f2,x),W(p2,w),W(b2,I),W(y2,O),W(v2,P),W(Kn,C),W(nn,E),W(g2,M),Ke(D))if(D.length){const X=e.exposed||(e.exposed={});D.forEach(ee=>{Object.defineProperty(X,ee,{get:()=>n[ee],set:ce=>n[ee]=ce,enumerable:!0})})}else e.exposed||(e.exposed={});T&&e.render===qr&&(e.render=T),L!=null&&(e.inheritAttrs=L),F&&(e.components=F),N&&(e.directives=N),M&&jy(e)}function Z5(e,t,n=qr){Ke(e)&&(e=Zg(e));for(const r in e){const s=e[r];let o;Pt(s)?"default"in s?o=eo(s.from||r,s.default,!0):o=eo(s.from||r):o=eo(s),Et(o)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>o.value,set:l=>o.value=l}):t[r]=o}}function p_(e,t,n){$r(Ke(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function k2(e,t,n,r){let s=r.includes(".")?l2(n,r):()=>n[r];if(jt(e)){const o=t[e];nt(o)&&ye(s,o)}else if(nt(e))ye(s,e.bind(n));else if(Pt(e))if(Ke(e))e.forEach(o=>k2(o,t,n,r));else{const o=nt(e.handler)?e.handler.bind(n):t[e.handler];nt(o)&&ye(s,o,e)}}function Xy(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:s,optionsCache:o,config:{optionMergeStrategies:l}}=e.appContext,a=o.get(t);let u;return a?u=a:!s.length&&!n&&!r?u=t:(u={},s.length&&s.forEach(c=>Sf(u,c,l,!0)),Sf(u,t,l)),Pt(t)&&o.set(t,u),u}function Sf(e,t,n,r=!1){const{mixins:s,extends:o}=t;o&&Sf(e,o,n,!0),s&&s.forEach(l=>Sf(e,l,n,!0));for(const l in t)if(!(r&&l==="expose")){const a=e8[l]||n&&n[l];e[l]=a?a(e[l],t[l]):t[l]}return e}const e8={data:m_,props:h_,emits:h_,methods:fu,computed:fu,beforeCreate:Gn,created:Gn,beforeMount:Gn,mounted:Gn,beforeUpdate:Gn,updated:Gn,beforeDestroy:Gn,beforeUnmount:Gn,destroyed:Gn,unmounted:Gn,activated:Gn,deactivated:Gn,errorCaptured:Gn,serverPrefetch:Gn,components:fu,directives:fu,watch:n8,provide:m_,inject:t8};function m_(e,t){return t?e?function(){return Nt(nt(e)?e.call(this,this):e,nt(t)?t.call(this,this):t)}:t:e}function t8(e,t){return fu(Zg(e),Zg(t))}function Zg(e){if(Ke(e)){const t={};for(let n=0;n{let d,f=bt,p;return Fy(()=>{const h=e[s];On(d,h)&&(d=h,c())}),{get(){return u(),n.get?n.get(d):d},set(h){const b=n.set?n.set(h):h;if(!On(b,d)&&!(f!==bt&&On(h,f)))return;const x=r.vnode.props,w=!!(x&&(t in x||s in x||o in x)&&(`onUpdate:${t}`in x||`onUpdate:${s}`in x||`onUpdate:${o}`in x));w||(d=h,c()),r.emit(`update:${t}`,b),On(h,f)&&(On(h,b)&&!On(b,p)||w&&f!==bt&&!On(b,d))&&c(),f=h,p=b}}});return a[Symbol.iterator]=()=>{let u=0;return{next(){return u<2?{value:u++?l||bt:a,done:!1}:{done:!0}}}},a}const $2=(e,t)=>t==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${fn(t)}Modifiers`]||e[`${dr(t)}Modifiers`];function l8(e,t,...n){if(e.isUnmounted)return;const r=e.vnode.props||bt;let s=n;const o=t.startsWith("update:"),l=o&&$2(r,t.slice(7));l&&(l.trim&&(s=n.map(d=>jt(d)?d.trim():d)),l.number&&(s=s.map(im)));let a,u=r[a=zi(t)]||r[a=zi(fn(t))];!u&&o&&(u=r[a=zi(dr(t))]),u&&$r(u,e,6,s);const c=r[a+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[a])return;e.emitted[a]=!0,$r(c,e,6,s)}}const i8=new WeakMap;function C2(e,t,n=!1){const r=n?i8:t.emitsCache,s=r.get(e);if(s!==void 0)return s;const o=e.emits;let l={},a=!1;if(!nt(e)){const u=c=>{const d=C2(c,t,!0);d&&(a=!0,Nt(l,d))};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}return!o&&!a?(Pt(e)&&r.set(e,null),null):(Ke(o)?o.forEach(u=>l[u]=null):Nt(l,o),Pt(e)&&r.set(e,l),l)}function wm(e,t){return!e||!Pc(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),Ot(e,t[0].toLowerCase()+t.slice(1))||Ot(e,dr(t))||Ot(e,t))}function Qd(e){const{type:t,vnode:n,proxy:r,withProxy:s,propsOptions:[o],slots:l,attrs:a,emit:u,render:c,renderCache:d,props:f,data:p,setupState:h,ctx:b,inheritAttrs:x}=e,w=Qu(e);let k,C;try{if(n.shapeFlag&4){const E=s||r,T=E;k=ir(c.call(T,E,d,f,h,p,b)),C=a}else{const E=t;k=ir(E.length>1?E(f,{attrs:a,slots:l,emit:u}):E(f,null)),C=t.props?a:u8(a)}}catch(E){ro.length=0,fi(E,e,1),k=v(dn)}let S=k;if(C&&x!==!1){const E=Object.keys(C),{shapeFlag:T}=S;E.length&&T&7&&(o&&E.some(rm)&&(C=c8(C,o)),S=ss(S,C,!1,!0))}if(n.dirs&&(S=ss(S,null,!1,!0),S.dirs=S.dirs?S.dirs.concat(n.dirs):n.dirs),n.transition){const E=vm(S.type)&&wf(S)||S;co(E,n.transition)}return k=S,Qu(w),k}function a8(e,t=!0){let n;for(let r=0;r{let t;for(const n in e)(n==="class"||n==="style"||Pc(n))&&((t||(t={}))[n]=e[n]);return t},c8=(e,t)=>{const n={};for(const r in e)(!rm(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function d8(e,t,n){const{props:r,children:s,component:o}=e,{props:l,children:a,patchFlag:u}=t,c=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&u>=0){if(u&1024)return!0;if(u&16)return r?g_(r,l,c):!!l;if(u&8){const d=t.dynamicProps;for(let f=0;fObject.create(E2),T2=e=>Object.getPrototypeOf(e)===E2;function f8(e,t,n,r=!1){const s={},o=O2();e.propsDefaults=Object.create(null),A2(e,t,s,o);for(const l in e.propsOptions[0])l in s||(s[l]=void 0);n?e.props=r?s:Ww(s):e.type.props?e.props=s:e.props=o,e.attrs=o}function p8(e,t,n,r){const{props:s,attrs:o,vnode:{patchFlag:l}}=e,a=$t(s),[u]=e.propsOptions;let c=!1;if((r||l>0)&&!(l&16)){if(l&8){const d=e.vnode.dynamicProps;for(let f=0;f{u=!0;const[p,h]=P2(f,t,!0);Nt(l,p),h&&a.push(...h)};!n&&t.mixins.length&&t.mixins.forEach(d),e.extends&&d(e.extends),e.mixins&&e.mixins.forEach(d)}if(!o&&!u)return Pt(e)&&r.set(e,Hi),Hi;if(Ke(o))for(let d=0;de==="_"||e==="_ctx"||e==="$stable",Zy=e=>Ke(e)?e.map(ir):[ir(e)],h8=(e,t,n)=>{if(t._n)return t;const r=_((...s)=>Zy(t(...s)),n);return r._c=!1,r},M2=(e,t,n)=>{const r=e._ctx;for(const s in e){if(Qy(s))continue;const o=e[s];if(nt(o))t[s]=h8(s,o,r);else if(o!=null){const l=Zy(o);t[s]=()=>l}}},D2=(e,t)=>{const n=Zy(t);e.slots.default=()=>n},V2=(e,t,n)=>{for(const r in t)(n||!Qy(r))&&(e[r]=t[r])},g8=(e,t,n)=>{const r=e.slots=O2();if(e.vnode.shapeFlag&32){const s=t._;s?(V2(r,t,n),n&&Ow(r,"_",s,!0)):M2(t,r)}else t&&D2(e,t)},v8=(e,t,n)=>{const{vnode:r,slots:s}=e;let o=!0,l=bt;if(r.shapeFlag&32){const a=t._;a?n&&a===1?o=!1:V2(s,t,n):(o=!t.$stable,M2(t,s)),l=t}else t&&(D2(e,t),l={default:1});if(o)for(const a in s)!Qy(a)&&l[a]==null&&delete s[a]},bn=U2;function I2(e){return N2(e)}function R2(e){return N2(e,_5)}function N2(e,t){const n=am();n.__VUE__=!0;const{insert:r,remove:s,patchProp:o,createElement:l,createText:a,createComment:u,setText:c,setElementText:d,parentNode:f,nextSibling:p,setScopeId:h=qr,insertStaticContent:b}=e,x=(G,se,_e,we=null,Oe=null,z=null,J=void 0,ae=null,ve=!!se.dynamicChildren)=>{if(G===se)return;G&&!Yr(G,se)&&(we=Le(G),re(G,Oe,z,!0),G=null),se.patchFlag===-2&&(ve=!1,se.dynamicChildren=null);const{type:$e,ref:Pe,shapeFlag:oe}=se;switch($e){case no:w(G,se,_e,we);break;case dn:k(G,se,_e,we);break;case Yo:G==null&&C(se,_e,we,J);break;case H:F(G,se,_e,we,Oe,z,J,ae,ve);break;default:oe&1?T(G,se,_e,we,Oe,z,J,ae,ve):oe&6?N(G,se,_e,we,Oe,z,J,ae,ve):(oe&64||oe&128)&&$e.process(G,se,_e,we,Oe,z,J,ae,ve,ue)}Pe!=null&&Oe?Gi(Pe,G&&G.ref,z,se||G,!se):Pe==null&&G&&G.ref!=null&&Gi(G.ref,null,z,G,!0)},w=(G,se,_e,we)=>{if(G==null)r(se.el=a(se.children),_e,we);else{const Oe=se.el=G.el;se.children!==G.children&&c(Oe,se.children)}},k=(G,se,_e,we)=>{G==null?r(se.el=u(se.children||""),_e,we):se.el=G.el},C=(G,se,_e,we)=>{[G.el,G.anchor]=b(G.children,se,_e,we,G.el,G.anchor)},S=({el:G,anchor:se},_e,we)=>{let Oe;for(;G&&G!==se;)Oe=p(G),r(G,_e,we),G=Oe;r(se,_e,we)},E=({el:G,anchor:se})=>{let _e;for(;G&&G!==se;)_e=p(G),s(G),G=_e;s(se)},T=(G,se,_e,we,Oe,z,J,ae,ve)=>{if(se.type==="svg"?J="svg":se.type==="math"&&(J="mathml"),G==null)O(se,_e,we,Oe,z,J,ae,ve);else{const $e=G.el&&G.el._isVueCE?G.el:null;try{$e&&$e._beginPatch(),M(G,se,Oe,z,J,ae,ve)}finally{$e&&$e._endPatch()}}},O=(G,se,_e,we,Oe,z,J,ae)=>{let ve,$e;const{props:Pe,shapeFlag:oe,transition:be,dirs:Fe}=G;if(ve=G.el=l(G.type,z,Pe&&Pe.is,Pe),oe&8?d(ve,G.children):oe&16&&I(G.children,ve,null,we,Oe,Ch(G,z),J,ae),Fe&&_s(G,null,we,"created"),P(ve,G,G.scopeId,J,we),Pe){for(const ht in Pe)ht!=="value"&&!Ul(ht)&&o(ve,ht,null,Pe[ht],z,we);"value"in Pe&&o(ve,"value",null,Pe.value,z),($e=Pe.onVnodeBeforeMount)&&lr($e,we,G)}Fe&&_s(G,null,we,"beforeMount");const Xe=L2(Oe,be);Xe&&be.beforeEnter(ve),r(ve,se,_e),(($e=Pe&&Pe.onVnodeMounted)||Xe||Fe)&&bn(()=>{try{$e&&lr($e,we,G),Xe&&be.enter(ve),Fe&&_s(G,null,we,"mounted")}finally{}},Oe)},P=(G,se,_e,we,Oe)=>{if(_e&&h(G,_e),we)for(let z=0;z{for(let $e=ve;$e{const ae=se.el=G.el;let{patchFlag:ve,dynamicChildren:$e,dirs:Pe}=se;ve|=G.patchFlag&16;const oe=G.props||bt,be=se.props||bt;let Fe;if(_e&&$l(_e,!1),(Fe=be.onVnodeBeforeUpdate)&&lr(Fe,_e,se,G),Pe&&_s(se,G,_e,"beforeUpdate"),_e&&$l(_e,!0),$e&&(!G.dynamicChildren||G.dynamicChildren.length!==$e.length)&&(ve=0,J=!1,$e=null),(oe.innerHTML&&be.innerHTML==null||oe.textContent&&be.textContent==null)&&d(ae,""),$e?D(G.dynamicChildren,$e,ae,_e,we,Ch(se,Oe),z):J||ee(G,se,ae,null,_e,we,Ch(se,Oe),z,!1),ve>0){if(ve&16)L(ae,oe,be,_e,Oe);else if(ve&2&&oe.class!==be.class&&o(ae,"class",null,be.class,Oe),ve&4&&o(ae,"style",oe.style,be.style,Oe),ve&8){const Xe=se.dynamicProps;for(let ht=0;ht{Fe&&lr(Fe,_e,se,G),Pe&&_s(se,G,_e,"updated")},we)},D=(G,se,_e,we,Oe,z,J)=>{for(let ae=0;ae{if(se!==_e){if(se!==bt)for(const z in se)!Ul(z)&&!(z in _e)&&o(G,z,se[z],null,Oe,we);for(const z in _e){if(Ul(z))continue;const J=_e[z],ae=se[z];J!==ae&&z!=="value"&&o(G,z,ae,J,Oe,we)}"value"in _e&&o(G,"value",se.value,_e.value,Oe)}},F=(G,se,_e,we,Oe,z,J,ae,ve)=>{const $e=se.el=G?G.el:a(""),Pe=se.anchor=G?G.anchor:a("");let{patchFlag:oe,dynamicChildren:be,slotScopeIds:Fe}=se;Fe&&(ae=ae?ae.concat(Fe):Fe),G==null?(r($e,_e,we),r(Pe,_e,we),I(se.children||[],_e,Pe,Oe,z,J,ae,ve)):oe>0&&oe&64&&be&&G.dynamicChildren&&G.dynamicChildren.length===be.length?(D(G.dynamicChildren,be,_e,Oe,z,J,ae),(se.key!=null||Oe&&se===Oe.subTree)&&e1(G,se,!0)):ee(G,se,_e,Pe,Oe,z,J,ae,ve)},N=(G,se,_e,we,Oe,z,J,ae,ve)=>{se.slotScopeIds=ae,G==null?se.shapeFlag&512?Oe.ctx.activate(se,_e,we,J,ve):j(se,_e,we,Oe,z,J,ve):q(G,se,ve)},j=(G,se,_e,we,Oe,z,J)=>{const ae=G.component=z2(G,we,Oe);if(Nc(G)&&(ae.ctx.renderer=ue),W2(ae,!1,J),ae.asyncDep){if(Oe&&Oe.registerDep(ae,W,J),!G.el){const ve=ae.subTree=v(dn);k(null,ve,se,_e),G.placeholder=ve.el}}else W(ae,G,se,_e,Oe,z,J)},q=(G,se,_e)=>{const we=se.component=G.component;if(d8(G,se,_e))if(we.asyncDep&&!we.asyncResolved){X(we,se,_e);return}else we.next=se,we.update();else se.el=G.el,we.vnode=se},W=(G,se,_e,we,Oe,z,J)=>{const ae=()=>{if(G.isMounted){let{next:oe,bu:be,u:Fe,parent:Xe,vnode:ht}=G;{const Ae=F2(G);if(Ae){oe&&(oe.el=ht.el,X(G,oe,J)),Ae.asyncDep.then(()=>{bn(()=>{G.isUnmounted||$e()},Oe)});return}}let it=oe,_t;$l(G,!1),oe?(oe.el=ht.el,X(G,oe,J)):oe=ht,be&&Ki(be),(_t=oe.props&&oe.props.onVnodeBeforeUpdate)&&lr(_t,Xe,oe,ht),$l(G,!0);const Zt=Qd(G),Ce=G.subTree;G.subTree=Zt,x(Ce,Zt,f(Ce.el),Le(Ce),G,Oe,z),oe.el=Zt.el,it===null&&$m(G,Zt.el),Fe&&bn(Fe,Oe),(_t=oe.props&&oe.props.onVnodeUpdated)&&bn(()=>lr(_t,Xe,oe,ht),Oe)}else{let oe;const{el:be,props:Fe}=se,{bm:Xe,m:ht,parent:it,root:_t,type:Zt}=G,Ce=to(se);if($l(G,!1),Xe&&Ki(Xe),!Ce&&(oe=Fe&&Fe.onVnodeBeforeMount)&&lr(oe,it,se),$l(G,!0),be&&ge){const Ae=()=>{G.subTree=Qd(G),ge(be,G.subTree,G,Oe,null)};Ce&&Zt.__asyncHydrate?Zt.__asyncHydrate(be,G,Ae):Ae()}else{_t.ce&&_t.ce._hasShadowRoot()&&_t.ce._injectChildStyle(Zt,G.parent?G.parent.type:void 0);const Ae=G.subTree=Qd(G);x(null,Ae,_e,we,G,Oe,z),se.el=Ae.el}if(ht&&bn(ht,Oe),!Ce&&(oe=Fe&&Fe.onVnodeMounted)){const Ae=se;bn(()=>lr(oe,it,Ae),Oe)}(se.shapeFlag&256||it&&to(it.vnode)&&it.vnode.shapeFlag&256)&&G.a&&bn(G.a,Oe),G.isMounted=!0,se=_e=we=null}};G.scope.on();const ve=G.effect=new Wu(ae);G.scope.off();const $e=G.update=ve.run.bind(ve),Pe=G.job=ve.runIfDirty.bind(ve);Pe.i=G,Pe.id=G.uid,ve.scheduler=()=>Ny(Pe),$l(G,!0),$e()},X=(G,se,_e)=>{se.component=G;const we=G.vnode.props;G.vnode=se,G.next=null,p8(G,se.props,we,_e),v8(G,se.children,_e),ao(),l_(G),uo()},ee=(G,se,_e,we,Oe,z,J,ae,ve=!1)=>{const $e=G&&G.children,Pe=G?G.shapeFlag:0,oe=se.children,{patchFlag:be,shapeFlag:Fe}=se;if(be>0){if(be&128){fe($e,oe,_e,we,Oe,z,J,ae,ve);return}else if(be&256){ce($e,oe,_e,we,Oe,z,J,ae,ve);return}}Fe&8?(Pe&16&&Se($e,Oe,z),oe!==$e&&d(_e,oe)):Pe&16?Fe&16?fe($e,oe,_e,we,Oe,z,J,ae,ve):Se($e,Oe,z,!0):(Pe&8&&d(_e,""),Fe&16&&I(oe,_e,we,Oe,z,J,ae,ve))},ce=(G,se,_e,we,Oe,z,J,ae,ve)=>{G=G||Hi,se=se||Hi;const $e=G.length,Pe=se.length,oe=Math.min($e,Pe);let be;for(be=0;bePe?Se(G,Oe,z,!0,!1,oe):I(se,_e,we,Oe,z,J,ae,ve,oe)},fe=(G,se,_e,we,Oe,z,J,ae,ve)=>{let $e=0;const Pe=se.length;let oe=G.length-1,be=Pe-1;for(;$e<=oe&&$e<=be;){const Fe=G[$e],Xe=se[$e]=ve?js(se[$e]):ir(se[$e]);if(Yr(Fe,Xe))x(Fe,Xe,_e,null,Oe,z,J,ae,ve);else break;$e++}for(;$e<=oe&&$e<=be;){const Fe=G[oe],Xe=se[be]=ve?js(se[be]):ir(se[be]);if(Yr(Fe,Xe))x(Fe,Xe,_e,null,Oe,z,J,ae,ve);else break;oe--,be--}if($e>oe){if($e<=be){const Fe=be+1,Xe=Febe)for(;$e<=oe;)re(G[$e],Oe,z,!0),$e++;else{const Fe=$e,Xe=$e,ht=new Map;for($e=Xe;$e<=be;$e++){const yr=se[$e]=ve?js(se[$e]):ir(se[$e]);yr.key!=null&&ht.set(yr.key,$e)}let it,_t=0;const Zt=be-Xe+1;let Ce=!1,Ae=0;const Ie=new Array(Zt);for($e=0;$e=Zt){re(yr,Oe,z,!0);continue}let us;if(yr.key!=null)us=ht.get(yr.key);else for(it=Xe;it<=be;it++)if(Ie[it-Xe]===0&&Yr(yr,se[it])){us=it;break}us===void 0?re(yr,Oe,z,!0):(Ie[us-Xe]=$e+1,us>=Ae?Ae=us:Ce=!0,x(yr,se[us],_e,null,Oe,z,J,ae,ve),_t++)}const Co=Ce?y8(Ie):Hi;for(it=Co.length-1,$e=Zt-1;$e>=0;$e--){const yr=Xe+$e,us=se[yr],Qb=se[yr+1],Zb=yr+1{const{el:z,type:J,transition:ae,children:ve,shapeFlag:$e}=G;if($e&6){le(G.component.subTree,se,_e,we);return}if($e&128){G.suspense.move(se,_e,we);return}if($e&64){J.move(G,se,_e,ue);return}if(J===H){r(z,se,_e);for(let oe=0;oeae.enter(z),Oe));else{const{leave:oe,delayLeave:be,afterLeave:Fe}=ae,Xe=()=>{G.ctx.isUnmounted?s(z):r(z,se,_e)},ht=()=>{const it=z._isLeaving||!!z[Dr];z._isLeaving&&z[Dr](!0),ae.persisted&&!it?Xe():oe(z,()=>{Xe(),Fe&&Fe()})};be?be(z,Xe,ht):ht()}else r(z,se,_e)},re=(G,se,_e,we=!1,Oe=!1)=>{const{type:z,props:J,ref:ae,children:ve,dynamicChildren:$e,shapeFlag:Pe,patchFlag:oe,dirs:be,cacheIndex:Fe,memo:Xe}=G;if(oe===-2&&(Oe=!1),ae!=null&&(ao(),Gi(ae,null,_e,G,!0),uo()),Fe!=null&&(se.renderCache[Fe]=void 0),Pe&256){se.ctx.deactivate(G);return}const ht=Pe&1&&be,it=!to(G);let _t;if(it&&(_t=J&&J.onVnodeBeforeUnmount)&&lr(_t,se,G),Pe&6)De(G.component,_e,we);else{if(Pe&128){G.suspense.unmount(_e,we);return}ht&&_s(G,null,se,"beforeUnmount"),Pe&64?G.type.remove(G,se,_e,ue,we):$e&&!$e.hasOnce&&(z!==H||oe>0&&oe&64)?Se($e,se,_e,!1,!0):(z===H&&oe&384||!Oe&&Pe&16)&&Se(ve,se,_e),we&&Q(G)}const Zt=Xe!=null&&Fe==null;(it&&(_t=J&&J.onVnodeUnmounted)||ht||Zt)&&bn(()=>{_t&&lr(_t,se,G),ht&&_s(G,null,se,"unmounted"),Zt&&(G.el=null)},_e)},Q=G=>{const{type:se,el:_e,anchor:we,transition:Oe}=G;if(se===H){Ee(_e,we);return}if(se===Yo){E(G);return}const z=()=>{s(_e),Oe&&!Oe.persisted&&Oe.afterLeave&&Oe.afterLeave()};if(G.shapeFlag&1&&Oe&&!Oe.persisted){const{leave:J,delayLeave:ae}=Oe,ve=()=>J(_e,z);ae?ae(G.el,z,ve):ve()}else z()},Ee=(G,se)=>{let _e;for(;G!==se;)_e=p(G),s(G),G=_e;s(se)},De=(G,se,_e)=>{const{bum:we,scope:Oe,job:z,subTree:J,um:ae,m:ve,a:$e}=G;Ef(ve),Ef($e),we&&Ki(we),Oe.stop(),z&&(z.flags|=8,re(J,G,se,_e)),ae&&bn(ae,se),bn(()=>{G.isUnmounted=!0},se)},Se=(G,se,_e,we=!1,Oe=!1,z=0)=>{for(let J=z;J{if(G.shapeFlag&6)return Le(G.component.subTree);if(G.shapeFlag&128)return G.suspense.next();const se=p(G.anchor||G.el),_e=se&&se[i2];return _e?p(_e):se};let Be=!1;const me=(G,se,_e)=>{let we;G==null?se._vnode&&(re(se._vnode,null,null,!0),we=se._vnode.component):x(se._vnode||null,G,se,null,null,null,_e),se._vnode=G,Be||(Be=!0,l_(we),kf(),Be=!1)},ue={p:x,um:re,m:le,r:Q,mt:j,mc:I,pc:ee,pbc:D,n:Le,o:e};let te,ge;return t&&([te,ge]=t(ue)),{render:me,hydrate:te,createApp:s8(me,te)}}function Ch({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function $l({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function L2(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function e1(e,t,n=!1){const r=e.children,s=t.children;if(Ke(r)&&Ke(s))for(let o=0;o>1,e[n[a]]0&&(t[r]=n[o-1]),n[o]=r)}}for(o=n.length,l=n[o-1];o-- >0;)n[o]=l,l=t[l];return n}function F2(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:F2(t)}function Ef(e){if(e)for(let t=0;te.__isSuspense;let tv=0;const b8={name:"Suspense",__isSuspense:!0,process(e,t,n,r,s,o,l,a,u,c){if(e==null)x8(t,n,r,s,o,l,a,u,c);else{if(o&&o.deps>0&&!e.suspense.isInFallback){t.suspense=e.suspense,t.suspense.vnode=t,t.el=e.el;return}k8(e,t,n,r,s,l,a,u,c)}},hydrate:w8,normalize:$8},_8=b8;function ec(e,t){const n=e.props&&e.props[t];nt(n)&&n()}function x8(e,t,n,r,s,o,l,a,u){const{p:c,o:{createElement:d}}=u,f=d("div"),p=e.suspense=q2(e,s,r,t,f,n,o,l,a,u);c(null,p.pendingBranch=e.ssContent,f,null,r,p,o,l),p.deps>0?(ec(e,"onPending"),ec(e,"onFallback"),c(null,e.ssFallback,t,n,r,null,o,l),Ji(p,e.ssFallback)):p.resolve(!1,!0)}function k8(e,t,n,r,s,o,l,a,{p:u,um:c,o:{createElement:d}}){const f=t.suspense=e.suspense;f.vnode=t,t.el=e.el;const p=t.ssContent,h=t.ssFallback,{activeBranch:b,pendingBranch:x,isInFallback:w,isHydrating:k}=f;if(x)f.pendingBranch=p,Yr(x,p)?(u(x,p,f.hiddenContainer,null,s,f,o,l,a),f.deps<=0?f.resolve():w&&!k&&!f.isFallbackMountPending&&(u(b,h,n,r,s,null,o,l,a),Ji(f,h))):(f.pendingId=tv++,k?(f.isHydrating=!1,f.activeBranch=x):c(x,s,f),f.deps=0,f.effects.length=0,f.hiddenContainer=d("div"),w?(u(null,p,f.hiddenContainer,null,s,f,o,l,a),f.deps<=0?f.resolve():f.isFallbackMountPending||(u(b,h,n,r,s,null,o,l,a),Ji(f,h))):b&&Yr(b,p)?(u(b,p,n,r,s,f,o,l,a),f.resolve(!0)):(u(null,p,f.hiddenContainer,null,s,f,o,l,a),f.deps<=0&&f.resolve()));else if(b&&Yr(b,p))u(b,p,n,r,s,f,o,l,a),Ji(f,p);else if(ec(t,"onPending"),f.pendingBranch=p,p.shapeFlag&512?f.pendingId=p.component.suspenseId:f.pendingId=tv++,u(null,p,f.hiddenContainer,null,s,f,o,l,a),f.deps<=0)f.resolve();else{const{timeout:C,pendingId:S}=f;C>0?setTimeout(()=>{f.pendingId===S&&f.fallback(h)},C):C===0&&f.fallback(h)}}function q2(e,t,n,r,s,o,l,a,u,c,d=!1){const{p:f,m:p,um:h,n:b,o:{parentNode:x,remove:w}}=c;let k;const C=C8(e);C&&t&&t.pendingBranch&&(k=t.pendingId,t.deps++);const S=e.props?vf(e.props.timeout):void 0,E=o,T={vnode:e,parent:t,parentComponent:n,namespace:l,container:r,hiddenContainer:s,deps:0,pendingId:tv++,timeout:typeof S=="number"?S:-1,activeBranch:null,isFallbackMountPending:!1,pendingBranch:null,isInFallback:!d,isHydrating:d,isUnmounted:!1,effects:[],resolve(O=!1,P=!1){const{vnode:I,activeBranch:M,pendingBranch:D,pendingId:L,effects:F,parentComponent:N,container:j,isInFallback:q}=T;let W=!1;if(T.isHydrating)T.isHydrating=!1;else if(!O){W=M&&D.transition&&D.transition.mode==="out-in";let ce=!1;W&&(M.transition.afterLeave=()=>{L===T.pendingId&&(p(D,j,o===E&&!ce?b(M):o,0),Yu(F),q&&I.ssFallback&&(I.ssFallback.el=null))}),M&&!T.isFallbackMountPending&&(x(M.el)===j&&(o=b(M),ce=!0),h(M,N,T,!0),!W&&q&&I.ssFallback&&bn(()=>I.ssFallback.el=null,T)),W||p(D,j,o,0)}T.isFallbackMountPending=!1,Ji(T,D),T.pendingBranch=null,T.isInFallback=!1;let X=T.parent,ee=!1;for(;X;){if(X.pendingBranch){for(let ce=0;ce{if(T.isFallbackMountPending=!1,!T.isInFallback)return;const q=T.vnode.ssFallback;f(null,q,D,F,M,null,L,a,u),Ji(T,q)},j=O.transition&&O.transition.mode==="out-in";j&&(T.isFallbackMountPending=!0,I.transition.afterLeave=N),T.isInFallback=!0,h(I,M,null,!0),j||N()},move(O,P,I){T.activeBranch&&p(T.activeBranch,O,P,I),T.container=O},next(){return T.activeBranch&&b(T.activeBranch)},registerDep(O,P,I){const M=!!T.pendingBranch;M&&T.deps++;const D=O.vnode.el;O.asyncDep.catch(L=>{fi(L,O,0)}).then(L=>{if(O.isUnmounted||T.isUnmounted||T.pendingId!==O.suspenseId)return;nc(),O.asyncResolved=!0;const{vnode:F}=O;nv(O,L,!1),D&&(F.el=D);const N=!D&&O.subTree.el;P(O,F,x(D||O.subTree.el),D?null:b(O.subTree),T,l,I),N&&(F.placeholder=null,w(N)),$m(O,F.el),M&&--T.deps===0&&T.resolve()})},unmount(O,P){T.isUnmounted=!0,T.activeBranch&&h(T.activeBranch,n,O,P),T.pendingBranch&&h(T.pendingBranch,n,O,P)}};return T}function w8(e,t,n,r,s,o,l,a,u){const c=t.suspense=q2(t,r,n,e.parentNode,document.createElement("div"),null,s,o,l,a,!0),d=u(e,c.pendingBranch=t.ssContent,n,c,o,l);return c.deps===0&&c.resolve(!1,!0),d}function $8(e){const{shapeFlag:t,children:n}=e,r=t&32;e.ssContent=y_(r?n.default:n),e.ssFallback=r?y_(n.fallback):v(dn)}function y_(e){let t;if(nt(e)){const n=Ql&&e._c;n&&(e._d=!1,y()),e=e(),n&&(e._d=!0,t=Un,Cm())}return Ke(e)&&(e=a8(e)),e=ir(e),t&&!e.dynamicChildren&&(e.dynamicChildren=t.filter(n=>n!==e)),e}function U2(e,t){t&&t.pendingBranch?Ke(e)?t.effects.push(...e):t.effects.push(e):Yu(e)}function Ji(e,t){e.activeBranch=t;const{vnode:n,parentComponent:r}=e;let s=t.el;for(;!s&&t.component;)t=t.component.subTree,s=t.el;n.el=s,r&&r.subTree===n&&(r.vnode.el=s,$m(r,s))}function C8(e){const t=e.props&&e.props.suspensible;return t!=null&&t!==!1}const H=Symbol.for("v-fgt"),no=Symbol.for("v-txt"),dn=Symbol.for("v-cmt"),Yo=Symbol.for("v-stc"),ro=[];let Un=null;function y(e=!1){ro.push(Un=e?null:[])}function Cm(){ro.pop(),Un=ro[ro.length-1]||null}let Ql=1;function tc(e,t=!1){Ql+=e,e<0&&Un&&t&&(Un.hasOnce=!0)}function j2(e){return e.dynamicChildren=Ql>0?Un||Hi:null,Cm(),Ql>0&&Un&&Un.push(e),e}function $(e,t,n,r,s,o){return j2(g(e,t,n,r,s,o,!0))}function B(e,t,n,r,s){return j2(v(e,t,n,r,s,!0))}function fo(e){return e?e.__v_isVNode===!0:!1}function Yr(e,t){return e.type===t.type&&e.key===t.key}function S8(e){}const H2=({key:e})=>e??null,Zd=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?jt(e)||Et(e)||nt(e)?{i:Rn,r:e,k:t,f:!!n}:e:null);function g(e,t=null,n=null,r=0,s=null,o=e===H?0:1,l=!1,a=!1){const u={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&H2(t),ref:t&&Zd(t),scopeId:gm,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:r,dynamicProps:s,dynamicChildren:null,appContext:null,ctx:Rn};return a?(Tf(u,n),o&128&&e.normalize(u)):n&&(u.shapeFlag|=jt(n)?8:16),Ql>0&&!l&&Un&&(u.patchFlag>0||o&6)&&u.patchFlag!==32&&Un.push(u),u}const v=E8;function E8(e,t=null,n=null,r=0,s=null,o=!1){if((!e||e===_2)&&(e=dn),fo(e)){const a=ss(e,t,!0);return n&&Tf(a,n),Ql>0&&!o&&Un&&(a.shapeFlag&6?Un[Un.indexOf(e)]=a:Un.push(a)),a.patchFlag=-2,a}if(I8(e)&&(e=e.__vccOpts),t){t=ct(t);let{class:a,style:u}=t;a&&!jt(a)&&(t.class=pe(a)),Pt(u)&&(Vc(u)&&!Ke(u)&&(u=Nt({},u)),t.style=kt(u))}const l=jt(e)?1:Of(e)?128:vm(e)?64:Pt(e)?4:nt(e)?2:0;return g(e,t,n,r,s,l,o,!0)}function ct(e){return e?Vc(e)||T2(e)?Nt({},e):e:null}function ss(e,t,n=!1,r=!1){const{props:s,ref:o,patchFlag:l,children:a,transition:u}=e,c=t?je(s||{},t):s,d={__v_isVNode:!0,__v_skip:!0,type:e.type,props:c,key:c&&H2(c),ref:t&&t.ref?n&&o?Ke(o)?o.concat(Zd(t)):[o,Zd(t)]:Zd(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:a,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==H?l===-1?16:l|16:l,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:u,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&ss(e.ssContent),ssFallback:e.ssFallback&&ss(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return u&&r&&co(d,u.clone(d)),d}function A(e=" ",t=0){return v(no,null,e,t)}function O8(e,t){const n=v(Yo,null,e);return n.staticCount=t,n}function R(e="",t=!1){return t?(y(),B(dn,null,e)):v(dn,null,e)}function ir(e){return e==null||typeof e=="boolean"?v(dn):Ke(e)?v(H,null,e.slice()):fo(e)?js(e):v(no,null,String(e))}function js(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:ss(e)}function Tf(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(Ke(t))n=16;else if(typeof t=="object")if(r&65){const s=t.default;s&&(s._c&&(s._d=!1),Tf(e,s()),s._c&&(s._d=!0));return}else{n=32;const s=t._;!s&&!T2(t)?t._ctx=Rn:s===3&&Rn&&(Rn.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(nt(t)){if(r&65){Tf(e,{default:t});return}t={default:t,_ctx:Rn},n=32}else t=String(t),r&64?(n=16,t=[A(t)]):n=8;e.children=t,e.shapeFlag|=n}function je(...e){const t={};for(let n=0;nIn||Rn;let Af,Xo;{const e=am(),t=(n,r)=>{let s;return(s=e[n])||(s=e[n]=[]),s.push(r),o=>{s.length>1?s.forEach(l=>l(o)):s[0](o)}};Af=t("__VUE_INSTANCE_SETTERS__",n=>In=n),Xo=t("__VUE_SSR_SETTERS__",n=>Zl=n)}const Va=e=>{const t=In;return Af(e),e.scope.on(),()=>{e.scope.off(),Af(t)}},nc=()=>{In&&In.scope.off(),Af(null)};function K2(e){return e.vnode.shapeFlag&4}let Zl=!1;function W2(e,t=!1,n=!1){t&&Xo(t);const{props:r,children:s}=e.vnode,o=K2(e);f8(e,r,o,t),g8(e,s,n||t);const l=o?P8(e,t):void 0;return t&&Xo(!1),l}function P8(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,Xg);const{setup:r}=n;if(r){ao();const s=e.setupContext=r.length>1?J2(e):null,o=Va(e),l=Da(r,e,0,[e.props,s]),a=Py(l);if(uo(),o(),(a||e.sp)&&!to(e)&&jy(e),a){if(l.then(nc,nc),t)return l.then(u=>{Xo(!0);try{nv(e,u,t)}finally{Xo(!1)}}).catch(u=>{fi(u,e,0)});e.asyncDep=l}else nv(e,l,t)}else G2(e,t)}function nv(e,t,n){nt(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:Pt(t)&&(e.setupState=Ry(t)),G2(e,n)}let Pf,rv;function M8(e){Pf=e,rv=t=>{t.render._rc&&(t.withProxy=new Proxy(t.ctx,q5))}}const D8=()=>!Pf;function G2(e,t,n){const r=e.type;if(!e.render){if(!t&&Pf&&!r.render){const s=r.template||Xy(e).template;if(s){const{isCustomElement:o,compilerOptions:l}=e.appContext.config,{delimiters:a,compilerOptions:u}=r,c=Nt(Nt({isCustomElement:o,delimiters:a},l),u);r.render=Pf(s,c)}}e.render=r.render||qr,rv&&rv(e)}{const s=Va(e);ao();try{Q5(e)}finally{uo(),s()}}}const V8={get(e,t){return qn(e,"get",""),e[t]}};function J2(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,V8),slots:e.slots,emit:e.emit,expose:t}}function Fc(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Ry(sa(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in $u)return $u[n](e)},has(t,n){return n in t||n in $u}})):e.proxy}function sv(e,t=!0){return nt(e)?e.displayName||e.name:e.name||t&&e.__name}function I8(e){return nt(e)&&"__vccOpts"in e}const V=(e,t)=>YP(e,t,Zl);function Rt(e,t,n){try{tc(-1);const r=arguments.length;return r===2?Pt(t)&&!Ke(t)?fo(t)?v(e,null,[t]):v(e,t):v(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&fo(n)&&(n=[n]),v(e,t,n))}finally{tc(1)}}function R8(){}function Sm(e,t,n,r){const s=n[r];if(s&&Y2(s,e))return s;const o=t();return o.memo=e.slice(),o.cacheIndex=r,n[r]=o}function Y2(e,t){const n=e.memo;if(n.length!=t.length)return!1;for(let r=0;r0&&Un&&Un.push(e),!0}const X2="3.5.42",N8=qr,L8=o5,F8=Pi,B8=r2,q8={createComponentInstance:z2,setupComponent:W2,renderComponentRoot:Qd,setCurrentRenderingInstance:Qu,isVNode:fo,normalizeVNode:ir,getComponentPublicInstance:Fc,ensureValidVNode:Jy,pushWarningContext:t5,popWarningContext:n5},U8=q8,j8=null,H8=null,z8=null;/** -* @vue/runtime-dom v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/let ov;const b_=typeof window<"u"&&window.trustedTypes;if(b_)try{ov=b_.createPolicy("vue",{createHTML:e=>e})}catch{}const Q2=ov?e=>ov.createHTML(e):e=>e,K8="http://www.w3.org/2000/svg",W8="http://www.w3.org/1998/Math/MathML",qs=typeof document<"u"?document:null,__=qs&&qs.createElement("template"),Z2={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const s=t==="svg"?qs.createElementNS(K8,e):t==="mathml"?qs.createElementNS(W8,e):n?qs.createElement(e,{is:n}):qs.createElement(e);return e==="select"&&r&&r.multiple!=null&&s.setAttribute("multiple",r.multiple),s},createText:e=>qs.createTextNode(e),createComment:e=>qs.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>qs.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,s,o){const l=n?n.previousSibling:t.lastChild;if(s&&(s===o||s.nextSibling))for(;t.insertBefore(s.cloneNode(!0),n),!(s===o||!(s=s.nextSibling)););else{__.innerHTML=Q2(r==="svg"?`${e}`:r==="mathml"?`${e}`:e);const a=__.content;if(r==="svg"||r==="mathml"){const u=a.firstChild;for(;u.firstChild;)a.appendChild(u.firstChild);a.removeChild(u)}t.insertBefore(a,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},So="transition",Ga="animation",ia=Symbol("_vtc"),e$={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},t$=Nt({},Uy,e$),G8=e=>(e.displayName="Transition",e.props=t$,e),J8=G8((e,{slots:t})=>Rt(c2,n$(e),t)),Cl=(e,t=[])=>{Ke(e)?e.forEach(n=>n(...t)):e&&e(...t)},x_=e=>e?Ke(e)?e.some(t=>t.length>1):e.length>1:!1;function n$(e){const t={};for(const F in e)F in e$||(t[F]=e[F]);if(e.css===!1)return t;const{name:n="v",type:r,duration:s,enterFromClass:o=`${n}-enter-from`,enterActiveClass:l=`${n}-enter-active`,enterToClass:a=`${n}-enter-to`,appearFromClass:u=o,appearActiveClass:c=l,appearToClass:d=a,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:h=`${n}-leave-to`}=e,b=Y8(s),x=b&&b[0],w=b&&b[1],{onBeforeEnter:k,onEnter:C,onEnterCancelled:S,onLeave:E,onLeaveCancelled:T,onBeforeAppear:O=k,onAppear:P=C,onAppearCancelled:I=S}=t,M=(F,N,j,q)=>{F._enterCancelled=q,Mo(F,N?d:a),Mo(F,N?c:l),j&&j()},D=(F,N)=>{F._isLeaving=!1,Mo(F,f),Mo(F,h),Mo(F,p),N&&N()},L=F=>(N,j)=>{const q=F?P:C,W=()=>M(N,F,j);Cl(q,[N,W]),k_(()=>{Mo(N,F?u:o),ps(N,F?d:a),x_(q)||w_(N,r,x,W)})};return Nt(t,{onBeforeEnter(F){Cl(k,[F]),ps(F,o),ps(F,l)},onBeforeAppear(F){Cl(O,[F]),ps(F,u),ps(F,c)},onEnter:L(!1),onAppear:L(!0),onLeave(F,N){F._isLeaving=!0;const j=()=>D(F,N);ps(F,f),F._enterCancelled?(ps(F,p),lv(F)):(lv(F),ps(F,p)),k_(()=>{F._isLeaving&&(Mo(F,f),ps(F,h),x_(E)||w_(F,r,w,j))}),Cl(E,[F,j])},onEnterCancelled(F){M(F,!1,void 0,!0),Cl(S,[F])},onAppearCancelled(F){M(F,!0,void 0,!0),Cl(I,[F])},onLeaveCancelled(F){D(F),Cl(T,[F])}})}function Y8(e){if(e==null)return null;if(Pt(e))return[Sh(e.enter),Sh(e.leave)];{const t=Sh(e);return[t,t]}}function Sh(e){return vf(e)}function ps(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e[ia]||(e[ia]=new Set)).add(t)}function Mo(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const n=e[ia];n&&(n.delete(t),n.size||(e[ia]=void 0))}function k_(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let X8=0;function w_(e,t,n,r){const s=e._endId=++X8,o=()=>{s===e._endId&&r()};if(n!=null)return setTimeout(o,n);const{type:l,timeout:a,propCount:u}=r$(e,t);if(!l)return r();const c=l+"end";let d=0;const f=()=>{e.removeEventListener(c,p),o()},p=h=>{h.target===e&&++d>=u&&f()};setTimeout(()=>{d(n[b]||"").split(", "),s=r(`${So}Delay`),o=r(`${So}Duration`),l=$_(s,o),a=r(`${Ga}Delay`),u=r(`${Ga}Duration`),c=$_(a,u);let d=null,f=0,p=0;t===So?l>0&&(d=So,f=l,p=o.length):t===Ga?c>0&&(d=Ga,f=c,p=u.length):(f=Math.max(l,c),d=f>0?l>c?So:Ga:null,p=d?d===So?o.length:u.length:0);const h=d===So&&/\b(?:transform|all)(?:,|$)/.test(r(`${So}Property`).toString());return{type:d,timeout:f,propCount:p,hasTransform:h}}function $_(e,t){for(;e.lengthC_(n)+C_(e[r])))}function C_(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function lv(e){return(e?e.ownerDocument:document).body.offsetHeight}function Q8(e,t,n){const r=e[ia];r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Mf=Symbol("_vod"),t1=Symbol("_vsh"),sl={name:"show",beforeMount(e,{value:t},{transition:n}){e[Mf]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):Ja(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),Ja(e,!0),r.enter(e)):r.leave(e,()=>{Ja(e,!1)}):Ja(e,t))},beforeUnmount(e,{value:t}){Ja(e,t)}};function Ja(e,t){e.style.display=t?e[Mf]:"none",e[t1]=!t}function Z8(){sl.getSSRProps=({value:e})=>{if(!e)return{style:{display:"none"}}}}const s$=Symbol("");function e3(e){const t=zt();if(!t)return;const n=t.ut=(s=e(t.proxy))=>{Array.from(document.querySelectorAll(`[data-v-owner="${t.uid}"]`)).forEach(o=>Df(o,s))},r=()=>{const s=e(t.proxy);t.ce?Df(t.ce,s):iv(t.subTree,s),n(s)};zy(()=>{Yu(r)}),mt(()=>{ye(r,qr,{flush:"post"});const s=new MutationObserver(r);s.observe(t.subTree.el.parentNode,{childList:!0}),nn(()=>s.disconnect())})}function iv(e,t){if(e.shapeFlag&128){const n=e.suspense;e=n.activeBranch,n.pendingBranch&&!n.isHydrating&&n.effects.push(()=>{iv(n.activeBranch,t)})}for(;e.component;)e=e.component.subTree;if(e.shapeFlag&1&&e.el)Df(e.el,t);else if(e.type===H)e.children.forEach(n=>iv(n,t));else if(e.type===Yo){let{el:n,anchor:r}=e;for(;n&&(Df(n,t),n!==r);)n=n.nextSibling}}function Df(e,t){if(e.nodeType===1){const n=e.style;let r="";for(const s in t){const o=wP(t[s]);n.setProperty(`--${s}`,o),r+=`--${s}: ${o};`}n[s$]=r}}const t3=/(?:^|;)\s*display\s*:/;function n3(e,t,n){const r=e.style,s=jt(n);let o=!1;if(n&&!s){if(t)if(jt(t))for(const l of t.split(";")){const a=l.slice(0,l.indexOf(":")).trim();n[a]==null&&pu(r,a,"")}else for(const l in t)n[l]==null&&pu(r,l,"");for(const l in n){l==="display"&&(o=!0);const a=n[l];a!=null?s3(e,l,!jt(t)&&t?t[l]:void 0,a)||pu(r,l,a):pu(r,l,"")}}else if(s){if(t!==n){const l=r[s$];l&&(n+=";"+l),r.cssText=n,o=t3.test(n)}}else t&&e.removeAttribute("style");Mf in e&&(e[Mf]=o?r.display:"",e[t1]&&(r.display="none"))}const bd=/\s*!important$/;function pu(e,t,n){if(Ke(n))n.forEach(r=>pu(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))bd.test(n)?e.setProperty(t,n.replace(bd,""),"important"):e.setProperty(t,n);else{const r=r3(e,t);bd.test(n)?e.setProperty(dr(r),n.replace(bd,""),"important"):e[r]=n}}const S_=["Webkit","Moz","ms"],Eh={};function r3(e,t){const n=Eh[t];if(n)return n;let r=fn(t);if(r!=="filter"&&r in e)return Eh[t]=r;r=Mc(r);for(let s=0;sOh||(c3.then(()=>Oh=0),Oh=Date.now());function f3(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;const s=n.value;if(Ke(s)){const o=r.stopImmediatePropagation;r.stopImmediatePropagation=()=>{o.call(r),r._stopped=!0};const l=s.slice(),a=[r];for(let u=0;ue.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,o$=(e,t,n,r,s,o)=>{const l=s==="svg";t==="class"?Q8(e,r,l):t==="style"?n3(e,n,r):Pc(t)?rm(t)||l3(e,t,n,r,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):p3(e,t,r,l))?(T_(e,t,r),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&O_(e,t,r,l,o,t!=="value")):e._isVueCE&&(m3(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!jt(r)))?T_(e,fn(t),r,o,t):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),O_(e,t,r,l))};function p3(e,t,n,r){if(r)return!!(t==="innerHTML"||t==="textContent"||t in e&&P_(t)&&nt(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const s=e.tagName;if(s==="IMG"||s==="VIDEO"||s==="CANVAS"||s==="SOURCE")return!1}return P_(t)&&jt(n)?!1:t in e}function m3(e,t){const n=e._def.props;if(!n)return!1;const r=fn(t);return Array.isArray(n)?n.some(s=>fn(s)===r):Object.keys(n).some(s=>fn(s)===r)}const M_={};function l$(e,t,n){let r=K(e,t);sm(r)&&(r=Nt({},r,t));class s extends Em{constructor(l){super(r,l,n)}}return s.def=r,s}const h3=((e,t)=>l$(e,t,If)),g3=typeof HTMLElement<"u"?HTMLElement:class{};class Em extends g3{constructor(t,n={},r=sc){super(),this._def=t,this._props=n,this._createApp=r,this._isVueCE=!0,this._instance=null,this._app=null,this._nonce=this._def.nonce,this._connected=!1,this._resolved=!1,this._patching=!1,this._dirty=!1,this._numberProps=null,this._styleChildren=new WeakSet,this._styleAnchors=new WeakMap,this._ob=null,this.shadowRoot&&r!==sc?this._root=this.shadowRoot:t.shadowRoot!==!1?(this.attachShadow(Nt({},t.shadowRootOptions,{mode:"open"})),this._root=this.shadowRoot):this._root=this}connectedCallback(){if(!this.isConnected)return;!this.shadowRoot&&!this._resolved&&this._parseSlots(),this._connected=!0;let t=this;for(;t=t&&(t.assignedSlot||t.parentNode||t.host);)if(t instanceof Em){this._parent=t;break}this._instance||(this._resolved?this._mount(this._def):t&&t._pendingResolve?this._pendingResolve=t._pendingResolve.then(()=>{if(this._pendingResolve=void 0,this.isConnected)return this._resolveDef()}):this._resolveDef())}_setParent(t=this._parent){t&&(this._instance.parent=t._instance,this._inheritParentContext(t))}_inheritParentContext(t=this._parent){t&&this._app&&Object.setPrototypeOf(this._app._context.provides,t._instance.provides)}disconnectedCallback(){this._connected=!1,ot(()=>{this._connected||(this._ob&&(this._ob.disconnect(),this._ob=null),this._app&&this._app.unmount(),this._instance&&(this._instance.ce=void 0),this._app=this._instance=null,this._teleportTargets&&(this._teleportTargets.clear(),this._teleportTargets=void 0))})}_processMutations(t){for(const n of t)this._setAttr(n.attributeName)}_resolveDef(){if(this._pendingResolve)return this._pendingResolve;for(let r=0;r{this._resolved=!0,this._pendingResolve=void 0;const{props:o,styles:l}=r;let a;if(o&&!Ke(o))for(const u in o){const c=o[u];(c===Number||c&&c.type===Number)&&(u in this._props&&(this._props[u]=vf(this._props[u])),(a||(a=Object.create(null)))[fn(u)]=!0)}this._numberProps=a,this._resolveProps(r),this.shadowRoot&&this._applyStyles(l),this._mount(r)},n=this._def.__asyncLoader;if(n)return this._pendingResolve=n().then(r=>{r.configureApp=this._def.configureApp,t(this._def=r,!0)}),this._pendingResolve;t(this._def)}_mount(t){this._app=this._createApp(t),this._inheritParentContext(),t.configureApp&&t.configureApp(this._app),this._app._ceVNode=this._createVNode(),this._app.mount(this._root);const n=this._instance&&this._instance.exposed;if(n)for(const r in n)Ot(this,r)||Object.defineProperty(this,r,{get:()=>i(n[r])})}_resolveProps(t){const{props:n}=t,r=Ke(n)?n:Object.keys(n||{});for(const s of Object.keys(this))s[0]!=="_"&&r.includes(s)&&this._setProp(s,this[s]);for(const s of r.map(fn))Object.defineProperty(this,s,{get(){return this._getProp(s)},set(o){this._setProp(s,o,!0,!this._patching)}})}_setAttr(t){if(t.startsWith("data-v-"))return;const n=this.hasAttribute(t);let r=n?this.getAttribute(t):M_;const s=fn(t);n&&this._numberProps&&this._numberProps[s]&&(r=vf(r)),this._setProp(s,r,!1,!0)}_getProp(t){return this._props[t]}_setProp(t,n,r=!0,s=!1){if(n!==this._props[t]&&(this._dirty=!0,n===M_?delete this._props[t]:(this._props[t]=n,t==="key"&&this._app&&(this._app._ceVNode.key=n)),s&&this._instance&&this._update(),r)){const o=this._ob;o&&(this._processMutations(o.takeRecords()),o.disconnect()),n===!0?this.setAttribute(dr(t),""):typeof n=="string"||typeof n=="number"?this.setAttribute(dr(t),n+""):n||this.removeAttribute(dr(t)),o&&o.observe(this,{attributes:!0})}}_update(){const t=this._createVNode();this._app&&(t.appContext=this._app._context),y$(t,this._root)}_createVNode(){const t={};this.shadowRoot||(t.onVnodeMounted=t.onVnodeUpdated=this._renderSlots.bind(this));const n=v(this._def,Nt(t,this._props));return this._instance||(n.ce=r=>{this._instance=r,r.ce=this,r.isCE=!0;const s=(o,l)=>{this.dispatchEvent(new CustomEvent(o,sm(l[0])?Nt({detail:l},l[0]):{detail:l}))};r.emit=(o,...l)=>{s(o,l),dr(o)!==o&&s(dr(o),l)},this._setParent()}),n}_applyStyles(t,n,r){if(!t)return;if(n){if(n===this._def||this._styleChildren.has(n))return;this._styleChildren.add(n)}const s=this._nonce,o=this.shadowRoot,l=r?this._getStyleAnchor(r)||this._getStyleAnchor(this._def):this._getRootStyleInsertionAnchor(o);let a=null;for(let u=t.length-1;u>=0;u--){const c=document.createElement("style");s&&c.setAttribute("nonce",s),c.textContent=t[u],o.insertBefore(c,a||l),a=c,u===0&&(r||this._styleAnchors.set(this._def,c),n&&this._styleAnchors.set(n,c))}}_getStyleAnchor(t){if(!t)return null;const n=this._styleAnchors.get(t);return n&&n.parentNode===this.shadowRoot?n:(n&&this._styleAnchors.delete(t),null)}_getRootStyleInsertionAnchor(t){for(let n=0;n(delete e.props.mode,e),_3=b3({name:"TransitionGroup",props:Nt({},t$,{tag:String,moveClass:String}),setup(e,{slots:t}){const n=zt(),r=qy();let s,o;return Lc(()=>{if(!s.length)return;const l=e.moveClass||`${e.name||"v"}-move`;if(!$3(s[0].el,n.vnode.el,l)){s=[];return}s.forEach(x3),s.forEach(k3);const a=s.filter(w3);lv(n.vnode.el),a.forEach(u=>{const c=u.el,d=c.style;ps(c,l),d.transform=d.webkitTransform=d.transitionDuration="";const f=c[Vf]=p=>{p&&p.target!==c||(!p||p.propertyName.endsWith("transform"))&&(c.removeEventListener("transitionend",f),c[Vf]=null,Mo(c,l))};c.addEventListener("transitionend",f)}),s=[]}),()=>{const l=$t(e),a=n$(l);let u=l.tag||H;if(s=[],o)for(let c=0;c{a.split(/\s+/).forEach(u=>u&&r.classList.remove(u))}),n.split(/\s+/).forEach(a=>a&&r.classList.add(a)),r.style.display="none";const o=t.nodeType===1?t:t.parentNode;o.appendChild(r);const{hasTransform:l}=r$(r);return o.removeChild(r),l}const ol=e=>{const t=e.props["onUpdate:modelValue"]||!1;return Ke(t)?n=>Ki(t,n):t};function C3(e){e.target.composing=!0}function V_(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const _r=Symbol("_assign"),_d=Symbol("_initialValue");function Th(e,t,n){return t&&(e=e.trim()),n&&(e=im(e)),e}const po={created(e,{modifiers:{lazy:t,trim:n,number:r}},s){e.parentNode&&(e.type==="text"?e[_d]=e.defaultValue.replace(/[\r\n]/g,""):e.type==="textarea"&&(e[_d]=e.defaultValue.replace(/\r\n?/g,` -`))),e[_r]=ol(s);const o=r||s.props&&s.props.type==="number";Ks(e,t?"change":"input",l=>{l.target.composing||e[_r](Th(e.value,n,o))}),(n||o)&&Ks(e,"change",()=>{e.value=Th(e.value,n,o)}),t||(Ks(e,"compositionstart",C3),Ks(e,"compositionend",V_),Ks(e,"change",V_))},mounted(e,{value:t,modifiers:{trim:n,number:r}}){const s=t??"",o=e[_d];delete e[_d],o!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==o?e[_r](Th(e.value,n,r)):e.value=s},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:s,number:o}},l){if(e[_r]=ol(l),e.composing)return;const a=(o||e.type==="number")&&!/^0\d/.test(e.value)?im(e.value):e.value,u=t??"";if(a===u)return;const c=e.getRootNode();(c instanceof Document||c instanceof ShadowRoot)&&c.activeElement===e&&e.type!=="range"&&(r&&t===n||s&&e.value.trim()===u)||(e.value=u)}},n1={deep:!0,created(e,t,n){e[_r]=ol(n),Ks(e,"change",()=>{const r=e._modelValue,s=aa(e),o=e.checked,l=e[_r];if(Ke(r)){const a=um(r,s),u=a!==-1;if(o&&!u)l(r.concat(s));else if(!o&&u){const c=[...r];c.splice(a,1),l(c)}}else if(Es(r)){const a=new Set(r);o?a.add(s):a.delete(s),l(a)}else l(f$(e,o))})},mounted:I_,beforeUpdate(e,t,n){e[_r]=ol(n),I_(e,t,n)}};function I_(e,{value:t,oldValue:n},r){e._modelValue=t;let s;if(Ke(t))s=um(t,r.props.value)>-1;else if(Es(t))s=t.has(r.props.value);else{if(t===n)return;s=Kr(t,f$(e,!0))}e.checked!==s&&(e.checked=s)}const Om={created(e,{value:t},n){e.checked=Kr(t,n.props.value),e[_r]=ol(n),Ks(e,"change",()=>{e[_r](aa(e))})},beforeUpdate(e,{value:t,oldValue:n},r){e[_r]=ol(r),t!==n&&(e.checked=Kr(t,r.props.value))}},rc={deep:!0,created(e,{value:t,modifiers:{number:n}},r){e._modelValue=t,Ks(e,"change",()=>{const s=Array.prototype.filter.call(e.options,u=>u.selected).map(u=>n?im(aa(u)):aa(u)),o=e.multiple,l=o?Es(e._modelValue)?new Set(s):s:s[0],a=e._pendingValue=[o,o?Ke(l)?s.slice():s:l];try{e[_r](l)}finally{ot(()=>{e._pendingValue===a&&(e._pendingValue=void 0)})}}),e[_r]=ol(r)},mounted(e,{value:t}){R_(e,t)},beforeUpdate(e,{value:t},n){e._modelValue=t,e[_r]=ol(n)},updated(e,{value:t}){const n=e._pendingValue;e._pendingValue=void 0,(!n||n[0]!==e.multiple||!S3(t,n[1],n[0]))&&R_(e,t)}};function S3(e,t,n){if(!n||Ke(e))return Kr(e,t);if(Es(e)){if(e.size!==t.length)return!1;for(const r of t)if(!e.has(r))return!1;return!0}return!1}function R_(e,t){const n=e.multiple,r=Ke(t);if(!(n&&!r&&!Es(t))){for(let s=0,o=e.options.length;sString(c)===String(a)):l.selected=um(t,a)>-1}else l.selected=t.has(a);else if(Kr(aa(l),t)){e.selectedIndex!==s&&(e.selectedIndex=s);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function aa(e){return"_value"in e?e._value:e.value}function f$(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const p$={created(e,t,n){xd(e,t,n,null,"created")},mounted(e,t,n){xd(e,t,n,null,"mounted")},beforeUpdate(e,t,n,r){xd(e,t,n,r,"beforeUpdate")},updated(e,t,n,r){xd(e,t,n,r,"updated")}};function m$(e,t){switch(e){case"SELECT":return rc;case"TEXTAREA":return po;default:switch(t){case"checkbox":return n1;case"radio":return Om;default:return po}}}function xd(e,t,n,r,s){const l=m$(e.tagName,n.props&&n.props.type)[s];l&&l(e,t,n,r)}function E3(){po.getSSRProps=({value:e})=>({value:e}),Om.getSSRProps=({value:e},t)=>{if(t.props&&Kr(t.props.value,e))return{checked:!0}},n1.getSSRProps=({value:e},t)=>{if(Ke(e)){if(t.props&&um(e,t.props.value)>-1)return{checked:!0}}else if(Es(e)){if(t.props&&e.has(t.props.value))return{checked:!0}}else if(e)return{checked:!0}},p$.getSSRProps=(e,t)=>{if(typeof t.type!="string")return;const n=m$(t.type.toUpperCase(),t.props&&t.props.type);if(n.getSSRProps)return n.getSSRProps(e,t)}}const O3=["ctrl","shift","alt","meta"],T3={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>O3.some(n=>e[`${n}Key`]&&!t.includes(n))},Ne=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=((s,...o)=>{for(let l=0;l{const n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=(s=>{if(!("key"in s))return;const o=dr(s.key);if(t.some(l=>l===o||A3[l]===o))return e(s)}))},h$=Nt({patchProp:o$},Z2);let Cu,N_=!1;function g$(){return Cu||(Cu=I2(h$))}function v$(){return Cu=N_?Cu:R2(h$),N_=!0,Cu}const y$=((...e)=>{g$().render(...e)}),P3=((...e)=>{v$().hydrate(...e)}),sc=((...e)=>{const t=g$().createApp(...e),{mount:n}=t;return t.mount=r=>{const s=_$(r);if(!s)return;const o=t._component;!nt(o)&&!o.render&&!o.template&&(o.template=s.innerHTML),s.nodeType===1&&(s.textContent="");const l=n(s,!1,b$(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),l},t}),If=((...e)=>{const t=v$().createApp(...e),{mount:n}=t;return t.mount=r=>{const s=_$(r);if(s)return n(s,!0,b$(s))},t});function b$(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function _$(e){return jt(e)?document.querySelector(e):e}let L_=!1;const M3=()=>{L_||(L_=!0,E3(),Z8())};/** -* vue v3.5.42 -* (c) 2018-present Yuxi (Evan) You and Vue contributors -* @license MIT -**/const D3=()=>{},V3=Object.freeze(Object.defineProperty({__proto__:null,BaseTransition:c2,BaseTransitionPropsValidators:Uy,Comment:dn,DeprecationTypes:z8,EffectScope:My,ErrorCodes:s5,ErrorTypeStrings:L8,Fragment:H,KeepAlive:N5,ReactiveEffect:Wu,Static:Yo,Suspense:_8,Teleport:By,Text:no,TrackOpTypes:XP,Transition:J8,TransitionGroup:c$,TriggerOpTypes:QP,VueElement:Em,assertNumber:r5,callWithAsyncErrorHandling:$r,callWithErrorHandling:Da,camelize:fn,capitalize:Mc,cloneVNode:ss,compatUtils:H8,compile:D3,computed:V,createApp:sc,createBlock:B,createCommentVNode:R,createElementBlock:$,createElementVNode:g,createHydrationRenderer:R2,createPropsRestProxy:Y5,createRenderer:I2,createSSRApp:If,createSlots:pl,createStaticVNode:O8,createTextVNode:A,createVNode:v,customRef:hm,defineAsyncComponent:I5,defineComponent:K,defineCustomElement:l$,defineEmits:j5,defineExpose:H5,defineModel:W5,defineOptions:z5,defineProps:U5,defineSSRCustomElement:h3,defineSlots:K5,devtools:F8,effect:CP,effectScope:cm,getCurrentInstance:zt,getCurrentScope:Ma,getCurrentWatcher:ZP,getTransitionRawChildren:ym,guardReactiveProps:ct,h:Rt,handleError:fi,hasInjectionContext:d5,hydrate:P3,hydrateOnIdle:T5,hydrateOnInteraction:D5,hydrateOnMediaQuery:M5,hydrateOnVisible:P5,initCustomFormatter:R8,initDirectivesForSSR:M3,inject:eo,isMemoSame:Y2,isProxy:Vc,isReactive:Cs,isReadonly:Os,isRef:Et,isRuntimeOnly:D8,isShallow:mr,isVNode:fo,markRaw:sa,mergeDefaults:Yy,mergeModels:J5,mergeProps:je,nextTick:ot,nodeOps:Z2,normalizeClass:pe,normalizeProps:lt,normalizeStyle:kt,onActivated:f2,onBeforeMount:h2,onBeforeUnmount:Kn,onBeforeUpdate:zy,onDeactivated:p2,onErrorCaptured:b2,onMounted:mt,onRenderTracked:y2,onRenderTriggered:v2,onScopeDispose:Dc,onServerPrefetch:g2,onUnmounted:nn,onUpdated:Lc,onWatcherCleanup:Qw,openBlock:y,patchProp:o$,popScopeId:u5,provide:Ic,proxyRefs:Ry,pushScopeId:a5,queuePostFlushCb:Yu,reactive:rt,readonly:bf,ref:U,registerRuntimeCompiler:M8,render:y$,renderList:ne,renderSlot:he,resolveComponent:Wy,resolveDirective:B5,resolveDynamicComponent:Pn,resolveFilter:j8,resolveTransitionHooks:la,setBlockTracking:tc,setDevtoolsHook:B8,setTransitionHooks:co,shallowReactive:Ww,shallowReadonly:Ro,shallowRef:yo,ssrContextKey:s2,ssrUtils:U8,stop:SP,toDisplayString:m,toHandlerKey:zi,toHandlers:_m,toRaw:$t,toRef:Yw,toRefs:ln,toValue:Ln,transformVNodeArgs:S8,triggerRef:Jw,unref:i,useAttrs:km,useCssModule:y3,useCssVars:e3,useHost:i$,useId:Yd,useModel:o8,useSSRContext:o2,useShadowRoot:v3,useSlots:xm,useTemplateRef:v5,useTransitionState:qy,vModelCheckbox:n1,vModelDynamic:p$,vModelRadio:Om,vModelSelect:rc,vModelText:po,vShow:sl,version:X2,warn:N8,watch:ye,watchEffect:Cr,watchPostEffect:Ly,watchSyncEffect:Fy,withAsyncContext:X5,withCtx:_,withDefaults:G5,withDirectives:jn,withKeys:Wt,withMemo:Sm,withModifiers:Ne,withScopeId:c5},Symbol.toStringTag,{value:"Module"})),F_=typeof globalThis=="object"&&globalThis||typeof window=="object"&&window||typeof self=="object"&&self||typeof global=="object"&&global||(function(){return this})();function I3(e,t,{signal:n,edges:r}={}){let s,o=null;const l=r!=null&&r.includes("leading"),a=r==null||r.includes("trailing"),u=()=>{o!==null&&(e.apply(s,o),s=void 0,o=null)},c=()=>{a&&u(),h()};let d=null;const f=()=>{d!=null&&clearTimeout(d),d=setTimeout(()=>{d=null,c()},t)},p=()=>{d!==null&&(clearTimeout(d),d=null)},h=()=>{p(),s=void 0,o=null},b=()=>{u()},x=function(...w){if(n!=null&&n.aborted)return;s=this,o=w;const k=d==null;f(),l&&k&&u()};return x.schedule=f,x.cancel=h,x.flush=b,n==null||n.addEventListener("abort",h,{once:!0}),x}function x$(){}function r1(e){return e==null||typeof e!="object"&&typeof e!="function"}function s1(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}function R3(e){if(r1(e))return e;if(Array.isArray(e)||s1(e)||e instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&e instanceof SharedArrayBuffer)return e.slice(0);const t=Object.getPrototypeOf(e);if(t==null)return Object.assign(Object.create(t),e);const n=t.constructor;if(e instanceof Date||e instanceof Map||e instanceof Set)return new n(e);if(e instanceof RegExp){const r=new n(e);return r.lastIndex=e.lastIndex,r}if(e instanceof DataView)return new n(e.buffer.slice(0));if(e instanceof Error){let r;return e instanceof AggregateError?r=new n(e.errors,e.message,{cause:e.cause}):r=new n(e.message,{cause:e.cause}),r.stack=e.stack,Object.assign(r,e),r}return typeof File<"u"&&e instanceof File?new n([e],e.name,{type:e.type,lastModified:e.lastModified}):typeof e=="object"?Object.assign(Object.create(t),e):e}function Rf(e){return typeof F_.Buffer<"u"&&F_.Buffer.isBuffer(e)}function Nf(e){return Object.getOwnPropertySymbols(e).filter(t=>Object.prototype.propertyIsEnumerable.call(e,t))}function oc(e){return e==null?e===void 0?"[object Undefined]":"[object Null]":Object.prototype.toString.call(e)}const k$="[object RegExp]",o1="[object String]",l1="[object Number]",i1="[object Boolean]",w$="[object Arguments]",$$="[object Symbol]",C$="[object Date]",S$="[object Map]",E$="[object Set]",O$="[object Array]",N3="[object Function]",T$="[object ArrayBuffer]",ef="[object Object]",L3="[object Error]",A$="[object DataView]",P$="[object Uint8Array]",M$="[object Uint8ClampedArray]",D$="[object Uint16Array]",V$="[object Uint32Array]",F3="[object BigUint64Array]",I$="[object Int8Array]",R$="[object Int16Array]",N$="[object Int32Array]",B3="[object BigInt64Array]",L$="[object Float32Array]",F$="[object Float64Array]";function q3(e,t){return Vl(e,void 0,e,new Map,t)}function Vl(e,t,n,r=new Map,s=void 0){const o=s==null?void 0:s(e,t,n,r);if(o!==void 0)return o;if(r1(e))return e;if(r.has(e))return r.get(e);if(Array.isArray(e)){const l=new Array(e.length);r.set(e,l);for(let a=0;amu(f,h,void 0,e,t,n,r));if(p===-1)return!1;c.splice(p,1)}return!0}case O$:case P$:case M$:case D$:case V$:case F3:case I$:case R$:case N$:case B3:case L$:case F$:if(Rf(e)!==Rf(t)||e.length!==t.length)return!1;for(let u=0;u=0}const z3={"&":"&","<":"<",">":">",'"':""","'":"'"};function K3(e){return e.replace(/[&<>"']/g,t=>z3[t])}function W3(e){return e!=null&&typeof e!="function"&&H3(e.length)}function q$(e){return typeof e=="symbol"||e instanceof Symbol}function U$(e){return e==null?"":j$(e)}function j$(e){if(typeof e=="string")return e;if(Array.isArray(e))return e.map(j$).join(",");if(q$(e))return e.toString();const t=e+"";return t==="0"&&Object.is(Number(e),-0)?"-0":t}function Tm(e){var t;return typeof e=="string"||typeof e=="symbol"?e:Object.is((t=e==null?void 0:e.valueOf)==null?void 0:t.call(e),-0)?"-0":String(e)}function Am(e){if(Array.isArray(e))return e.map(Tm);if(typeof e=="symbol")return[e];e=U$(e);const t=[],n=e.length;if(n===0)return t;let r=0,s="",o="",l=!1,a=!1;const u=/^-?\d+(?:\.\d+)?$/;for(e.charCodeAt(0)===46&&t.push("");r{if(typeof e=="object"){if(oc(e)==="[object Object]"&&typeof e.constructor!="function"){const l={};return o.set(e,l),Jr(l,e,s,o),l}switch(Object.prototype.toString.call(e)){case l1:case o1:case i1:{const l=new e.constructor(e==null?void 0:e.valueOf());return Jr(l,e),l}case w$:{const l={};return Jr(l,e),l.length=e.length,l[Symbol.iterator]=e[Symbol.iterator],l}default:return}}})}function U_(e){return Y3(e)}function av(e){return e!==null&&typeof e=="object"&&oc(e)==="[object Arguments]"}const X3=/^(?:0|[1-9]\d*)$/;function z$(e,t=Number.MAX_SAFE_INTEGER){switch(typeof e){case"number":return Number.isInteger(e)&&e>=0&&e{const r=e[t];(!(Object.hasOwn(e,t)&&B$(r,n))||n===void 0&&!(t in e))&&(e[t]=n)};function rM(e){return e==="__proto__"||e==="constructor"||e==="prototype"}function sM(e,t,n,r){if(e==null&&!q_(e))return e;let s;tM(t,e)?s=[t]:Array.isArray(t)?s=t:s=Am(t);const o=n(qt(e,s));let l=e;for(let a=0;an,()=>{})}function oM(e,t=0,n={}){typeof n!="object"&&(n={});const{leading:r=!1,trailing:s=!0,maxWait:o}=n,l=Array(2);r&&(l[0]="leading"),s&&(l[1]="trailing");let a,u=null;const c=I3(function(...p){a=e.apply(this,p),u=null},t,{edges:l}),d=function(...p){return o!=null&&(u===null&&(u=Date.now()),Date.now()-u>=o)?((r||s)&&(a=e.apply(this,p)),u=Date.now(),c.cancel(),c.schedule(),a):(c.apply(this,p),a)},f=()=>(c.flush(),a);return d.cancel=c.cancel,d.flush=f,d}function lM(e,...t){const n=t.slice(0,-1),r=t[t.length-1];let s=e;for(let o=0;otypeof File<"u"&&e instanceof File||e instanceof Blob||typeof FileList<"u"&&e instanceof FileList&&e.length>0,Pm=e=>e instanceof FormData?!0:a1(e)||typeof e=="object"&&e!==null&&Object.values(e).some(t=>Pm(t));let Bf=class extends Error{constructor(n){super(`HTTP error ${n.status}`);Re(this,"response");this.name="HttpResponseError",this.response=n}},G$=class extends Error{constructor(t="Request was cancelled"){super(t),this.name="HttpCancelledError"}},iM=class extends Error{constructor(t="Network error"){super(t),this.name="HttpNetworkError"}};function aM(e){const t=new URLSearchParams;return Object.entries(e).forEach(([n,r])=>{r!=null&&(Array.isArray(r)?r.forEach(s=>t.append(`${n}[]`,String(s))):typeof r=="object"?t.append(n,JSON.stringify(r)):t.append(n,String(r)))}),t.toString()}function uM(e,t,n){if(t&&!e.startsWith("http://")&&!e.startsWith("https://")&&(e=t.replace(/\/$/,"")+"/"+e.replace(/^\//,"")),n&&Object.keys(n).length>0){const r=aM(n);r&&(e+=(e.includes("?")?"&":"?")+r)}return e}function cM(){var e,t,n,r;return typeof window>"u"?null:((r=(n=(t=(e=window.axios)==null?void 0:e.defaults)==null?void 0:t.headers)==null?void 0:n.common)==null?void 0:r["X-Requested-With"])??null}function J$(e,t=new FormData,n=null){for(const r in e)Object.prototype.hasOwnProperty.call(e,r)&&Y$(t,n?`${n}[${r}]`:r,e[r]);return t}function Y$(e,t,n){if(Array.isArray(n))return n.forEach((r,s)=>Y$(e,`${t}[${s}]`,r));if(n instanceof Date)return e.append(t,n.toISOString());if(typeof File<"u"&&n instanceof File)return e.append(t,n,n.name);if(n instanceof Blob)return e.append(t,n);if(typeof n=="boolean")return e.append(t,n?"1":"0");if(typeof n=="string")return e.append(t,n);if(typeof n=="number")return e.append(t,`${n}`);if(n==null)return e.append(t,"");J$(n,e,t)}function dM(e,t){var n;if(e!=null)return e instanceof FormData?e:typeof e=="object"&&Pm(e)?J$(e):typeof e=="object"||(n=t["Content-Type"])!=null&&n.includes("application/json")?JSON.stringify(e):String(e)}function fM(e){const t={};return e.forEach((n,r)=>{t[r.toLowerCase()]=n}),t}function pM(e={}){let t=e.xsrfCookieName??"XSRF-TOKEN",n=e.xsrfHeaderName??"X-XSRF-TOKEN";function r(){if(typeof document>"u")return null;const s=document.cookie.match(new RegExp("(^|;\\s*)"+t+"=([^;]*)"));return s?decodeURIComponent(s[2]):null}return{setXsrfCookieName(s){t=s},setXsrfHeaderName(s){n=s},async request(s){const o=uM(s.url,s.baseURL,s.params),l=s.method.toUpperCase(),a={},u=cM();u&&(a["X-Requested-With"]=u),s.data!==void 0&&!["GET","DELETE"].includes(l)&&!(s.data instanceof FormData)&&!Pm(s.data)&&(a["Content-Type"]="application/json"),s.headers&&Object.entries(s.headers).forEach(([b,x])=>{x!==void 0&&(a[b]=String(x))});const c=r();c&&!["GET","HEAD","OPTIONS"].includes(l)&&(a[n]=c);let d=s.signal,f;const p=s.timeout??3e4;if(p>0&&!d){const b=new AbortController;d=b.signal,f=setTimeout(()=>b.abort(),p)}const h=["GET","DELETE"].includes(l)?void 0:dM(s.data,a);h instanceof FormData&&delete a["Content-Type"];try{const b=await fetch(o,{method:l,headers:a,body:h,signal:d,credentials:s.credentials??"same-origin"});f&&clearTimeout(f);let x;const w=b.headers.get("content-type");w!=null&&w.includes("application/json")?x=await b.json():x=await b.text();const k={status:b.status,data:x,headers:fM(b.headers)};if(!b.ok)throw new Bf(k);return k}catch(b){throw f&&clearTimeout(f),b instanceof Bf?b:b instanceof DOMException&&b.name==="AbortError"?new G$:b instanceof TypeError?new iM(b.message):b}}}}const cv=pM();let u1=cv,c1,X$,Q$="same-origin",Z$=e=>`${e.method}:${e.baseURL??c1??""}${e.url}`,eC=e=>e.status===204&&e.headers["precognition-success"]==="true";const qf={},Yn={get:(e,t={},n={})=>Xa(Ya("get",e,t,n)),post:(e,t={},n={})=>Xa(Ya("post",e,t,n)),patch:(e,t={},n={})=>Xa(Ya("patch",e,t,n)),put:(e,t={},n={})=>Xa(Ya("put",e,t,n)),delete:(e,t={},n={})=>Xa(Ya("delete",e,t,n)),useHttpClient(e){return u1=e,Yn},withBaseURL(e){return c1=e,Yn},withTimeout(e){return X$=e,Yn},withCredentials(e){return Q$=typeof e=="string"?e:e?"include":"omit",Yn},fingerprintRequestsUsing(e){return Z$=e===null?()=>null:e,Yn},determineSuccessUsing(e){return eC=e,Yn},withXsrfCookieName(e){return cv.setXsrfCookieName(e),Yn},withXsrfHeaderName(e){return cv.setXsrfHeaderName(e),Yn}},Ya=(e,t,n,r)=>({url:t,method:e,...r,...["get","delete"].includes(e)?{params:Ff({},n,r==null?void 0:r.params)}:{data:Ff({},n,r==null?void 0:r.data)}}),Xa=(e={})=>{const t=[mM,gM,vM].reduce((n,r)=>r(n),e);return(t.onBefore??(()=>!0))()===!1?Promise.resolve(null):((t.onStart??(()=>null))(),u1.request({method:t.method,url:t.url,baseURL:t.baseURL??c1,data:t.data,params:t.params,headers:t.headers,signal:t.signal,timeout:t.timeout,credentials:Q$}).then(async n=>{t.precognitive&&j_(n);const r=n.status;let s=n;return t.precognitive&&t.onPrecognitionSuccess&&eC(n)&&(s=await Promise.resolve(t.onPrecognitionSuccess(n)??s)),t.onSuccess&&hM(r)&&(s=await Promise.resolve(t.onSuccess(s)??s)),(H_(t,r)??(l=>l))(s)??s},n=>{if(yM(n))return Promise.reject(n);const r=n;return t.precognitive&&j_(r.response),(H_(t,r.response.status)??((o,l)=>Promise.reject(l)))(r.response,r)}).finally(t.onFinish??(()=>null)))},mM=e=>{const t=e.only??e.validate;return{...e,timeout:e.timeout??X$,precognitive:e.precognitive!==!1,fingerprint:typeof e.fingerprint>"u"?Z$(e,u1):e.fingerprint,headers:{...e.headers,Accept:"application/json","Content-Type":bM(e),...e.precognitive!==!1?{Precognition:!0}:{},...t?{"Precognition-Validate-Only":Array.from(t).join()}:{}}}},hM=e=>e>=200&&e<300,gM=e=>{var t;return typeof e.fingerprint!="string"||((t=qf[e.fingerprint])==null||t.abort(),delete qf[e.fingerprint]),e},vM=e=>typeof e.fingerprint!="string"||e.signal||!e.precognitive?e:(qf[e.fingerprint]=new AbortController,{...e,signal:qf[e.fingerprint].signal}),j_=e=>{var t;if(((t=e.headers)==null?void 0:t.precognition)!=="true")throw Error("Did not receive a Precognition response. Ensure you have the Precognition middleware in place for the route.")},yM=e=>{var t;return!(e instanceof Bf)||typeof((t=e.response)==null?void 0:t.status)!="number"},H_=(e,t)=>({401:e.onUnauthorized,403:e.onForbidden,404:e.onNotFound,409:e.onConflict,422:e.onValidationError,423:e.onLocked})[t],bM=e=>{var t,n,r;return((t=e.headers)==null?void 0:t["Content-Type"])??((n=e.headers)==null?void 0:n["Content-type"])??((r=e.headers)==null?void 0:r["content-type"])??(Pm(e.data)?"multipart/form-data":"application/json")},_M=(e,t)=>{if(!e.includes("*"))return[e];const n=e.split(".");let r=[""];for(const s of n)if(s==="*"){const o=[];for(const l of r){const a=l?qt(t,l):t;if(Array.isArray(a))for(let u=0;uo?`${o}.${s}`:s);return r},xM=(e,t)=>t.includes("*")?new RegExp("^"+t.replace(/\./g,"\\.").replace(/\*/g,"[^.]+")+"$").test(e):e===t,z_=(e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>!t.some(r=>xM(n,r)))),kM=(e,t={})=>{const n={errorsChanged:[],touchedChanged:[],validatingChanged:[],validatedChanged:[]};let r=!1,s=!1;const o=L=>L!==s?(s=L,n.validatingChanged):[];let l=[];const a=L=>{const F=[...new Set(L)];return l.length!==F.length||!F.every(N=>l.includes(N))?(l=F,n.validatedChanged):[]},u=()=>l.filter(L=>typeof f[L]>"u");let c=[];const d=L=>{const F=[...new Set(L)];return c.length!==F.length||!F.every(N=>c.includes(N))?(c=F,n.touchedChanged):[]};let f={};const p=L=>{const F=wM(L);return ts(f,F)?[]:(f=F,n.errorsChanged)},h=L=>{const F={...f};return delete F[Eu(L)],p(F)},b=()=>Object.keys(f).length>0;let x=1500;const w=L=>{x=L,O.cancel(),O=T()};let k=t,C=null,S=[],E=null;const T=()=>oM(L=>{e({get:(F,N={},j={})=>Yn.get(F,M(N),P(j,L,N)),post:(F,N={},j={})=>Yn.post(F,M(N),P(j,L,N)),patch:(F,N={},j={})=>Yn.patch(F,M(N),P(j,L,N)),put:(F,N={},j={})=>Yn.put(F,M(N),P(j,L,N)),delete:(F,N={},j={})=>Yn.delete(F,M(N),P(j,L,N))}).catch(F=>{var N;return F instanceof G$||F instanceof Bf&&((N=F.response)==null?void 0:N.status)===422?null:Promise.reject(F)})},x,{leading:!0,trailing:!0});let O=T();const P=(L,F,N={})=>{const j={...L,...F},q=Array.from(j.only??j.validate??c);return{...F,...Ff({},L,F),only:q,timeout:j.timeout??5e3,onValidationError:(W,X)=>([...a([...l,...q]),...p(Ff(z_({...f},q),W.data.errors))].forEach(ee=>ee()),j.onValidationError?j.onValidationError(W,X):Promise.reject(X)),onSuccess:W=>(a([...l,...q]).forEach(X=>X()),j.onSuccess?j.onSuccess(W):W),onPrecognitionSuccess:W=>([...a([...l,...q]),...p(z_({...f},q))].forEach(X=>X()),j.onPrecognitionSuccess?j.onPrecognitionSuccess(W):W),onBefore:()=>{const W=c.some(ce=>ce.includes("*")),X=W?[...new Set(c.flatMap(ce=>_M(ce,N)))]:c;return j.onBeforeValidation&&j.onBeforeValidation({data:N,touched:X},{data:k,touched:S})===!1||(j.onBefore||(()=>!0))()===!1?!1:(W&&d(X).forEach(ce=>ce()),E=c,C=N,!0)},onStart:()=>{o(!0).forEach(W=>W()),(j.onStart??(()=>null))()},onFinish:()=>{o(!1).forEach(W=>W()),S=E,k=C,E=C=null,(j.onFinish??(()=>null))()}}},I=(L,F,N)=>{if(typeof L>"u"){const j=Array.from((N==null?void 0:N.only)??(N==null?void 0:N.validate)??[]);d([...c,...j]).forEach(q=>q()),O(N??{});return}if(a1(F)&&!r){console.warn('Precognition file validation is not active. Call the "validateFiles" function on your form to enable it.');return}L=Eu(L),(L.includes("*")||qt(k,L)!==F)&&(d([L,...c]).forEach(j=>j()),O(N??{}))},M=L=>r===!1?dv(L):L,D={touched:()=>c,validate(L,F,N){return typeof L=="object"&&!("target"in L)&&(N=L,L=F=void 0),I(L,F,N),D},touch(L){const F=Array.isArray(L)?L:[Eu(L)];return d([...c,...F]).forEach(N=>N()),D},validating:()=>s,valid:u,errors:()=>f,hasErrors:b,setErrors(L){return p(L).forEach(F=>F()),D},forgetError(L){return h(L).forEach(F=>F()),D},defaults(L){return t=L,k=L,D},reset(...L){if(L.length===0)d([]).forEach(F=>F());else{const F=[...c];L.forEach(N=>{F.includes(N)&&F.splice(F.indexOf(N),1),fr(k,N,qt(t,N))}),d(F).forEach(N=>N())}return D},setTimeout(L){return w(L),D},on(L,F){return n[L].push(F),D},validateFiles(){return r=!0,D},withoutFileValidation(){return r=!1,D}};return D},tC=e=>Object.keys(e).reduce((t,n)=>({...t,[n]:Array.isArray(e[n])?e[n][0]:e[n]}),{}),wM=e=>Object.keys(e).reduce((t,n)=>({...t,[n]:typeof e[n]=="string"?[e[n]]:e[n]}),{}),Eu=e=>typeof e!="string"?e.target.name:e,dv=e=>{const t={...e};return Object.keys(t).forEach(n=>{const r=t[n];if(r!==null){if(a1(r)){delete t[n];return}if(Array.isArray(r)){t[n]=Object.values(dv({...r}));return}if(typeof r=="object"){t[n]=dv(t[n]);return}}}),t};var $M=class{constructor(e){Re(this,"config",{});Re(this,"defaults");this.defaults=e}extend(e){return e&&(this.defaults={...this.defaults,...e}),this}replace(e){this.config=e}get(e){return K$(this.config,e)?qt(this.config,e):qt(this.defaults,e)}set(e,t){typeof e=="string"?fr(this.config,e,t):Object.entries(e).forEach(([n,r])=>{fr(this.config,n,r)})}},ei=new $M({form:{recentlySuccessfulDuration:2e3,forceIndicesArrayFormatInFormData:!0,withAllErrors:!1},prefetch:{cacheFor:3e4,hoverDelay:75}});function lc(e,t){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>e.apply(this,r),t)}}function rr(e,t){return document.dispatchEvent(new CustomEvent(`inertia:${e}`,t))}var K_=e=>rr("before",{cancelable:!0,detail:{visit:e}}),CM=(e,{page:t,visitId:n}={})=>rr("error",{detail:{errors:e,page:t,visitId:n}}),SM=e=>rr("networkError",{cancelable:!0,detail:{error:e}}),EM=e=>rr("finish",{detail:{visit:e}}),W_=e=>rr("httpException",{cancelable:!0,detail:{response:e}}),OM=e=>rr("beforeUpdate",{detail:{page:e}}),Uf=(e,{cached:t=!1,visitId:n}={})=>rr("navigate",{detail:{page:e,cached:t,visitId:n}}),TM=(e,{replace:t,visitId:n})=>rr("clientVisit",{detail:{page:e,replace:t,visitId:n}}),AM=e=>rr("progress",{detail:{progress:e}}),PM=e=>rr("start",{detail:{visit:e}}),MM=(e,{visitId:t}={})=>rr("success",{detail:{page:e,visitId:t}}),DM=(e,t)=>rr("prefetched",{detail:{fetchedAt:Date.now(),response:e,visit:t}}),VM=e=>rr("prefetching",{detail:{visit:e}}),jf=e=>rr("flash",{detail:{flash:e}}),IM=(e,t)=>rr("location",{cancelable:!0,detail:{url:e,versionChange:t}}),Hg,ar=(Hg=class{static set(e,t){typeof window<"u"&&window.sessionStorage.setItem(e,JSON.stringify(t))}static get(e){if(typeof window<"u")return JSON.parse(window.sessionStorage.getItem(e)||"null")}static merge(e,t){const n=this.get(e);n===null?this.set(e,t):this.set(e,{...n,...t})}static remove(e){typeof window<"u"&&window.sessionStorage.removeItem(e)}static removeNested(e,t){const n=this.get(e);n!==null&&(delete n[t],this.set(e,n))}static exists(e){try{return this.get(e)!==null}catch{return!1}}static clear(){typeof window<"u"&&window.sessionStorage.clear()}},Re(Hg,"locationVisitKey","inertiaLocationVisit"),Hg),RM=async e=>{if(typeof window>"u")throw new Error("Unable to encrypt history");const t=nC(),n=await rC(),r=await UM(n);if(!r)throw new Error("Unable to encrypt history");return await LM(t,r,e)},ua={key:"historyKey",iv:"historyIv"},NM=async e=>{const t=nC(),n=await rC();if(!n)throw new Error("Unable to decrypt history");return await FM(t,n,e)},LM=async(e,t,n)=>{if(typeof window>"u")throw new Error("Unable to encrypt history");if(typeof window.crypto.subtle>"u")return console.warn("Encryption is not supported in this environment. SSL is required."),Promise.resolve(n);const r=new TextEncoder,s=JSON.stringify(n),o=new Uint8Array(s.length*3),l=r.encodeInto(s,o);return window.crypto.subtle.encrypt({name:"AES-GCM",iv:e},t,o.subarray(0,l.written))},FM=async(e,t,n)=>{if(typeof window.crypto.subtle>"u")return console.warn("Decryption is not supported in this environment. SSL is required."),Promise.resolve(n);const r=await window.crypto.subtle.decrypt({name:"AES-GCM",iv:e},t,n);return JSON.parse(new TextDecoder().decode(r))},nC=()=>{const e=ar.get(ua.iv);if(e)return new Uint8Array(e);const t=window.crypto.getRandomValues(new Uint8Array(12));return ar.set(ua.iv,Array.from(t)),t},BM=async()=>typeof window.crypto.subtle>"u"?(console.warn("Encryption is not supported in this environment. SSL is required."),Promise.resolve(null)):window.crypto.subtle.generateKey({name:"AES-GCM",length:256},!0,["encrypt","decrypt"]),qM=async e=>{if(typeof window.crypto.subtle>"u")return console.warn("Encryption is not supported in this environment. SSL is required."),Promise.resolve();const t=await window.crypto.subtle.exportKey("raw",e);ar.set(ua.key,Array.from(new Uint8Array(t)))},UM=async e=>{if(e)return e;const t=await BM();return t?(await qM(t),t):null},rC=async()=>{const e=ar.get(ua.key);return e?await window.crypto.subtle.importKey("raw",new Uint8Array(e),{name:"AES-GCM",length:256},!0,["encrypt","decrypt"]):null},G_=e=>{const t={};for(const n of Object.keys(e))e[n]!==void 0&&(t[n]=e[n]);return t},sC=(e,t,n)=>{if(e===t)return!0;for(const r in e)if(!n.includes(r)&&e[r]!==t[r]&&!jM(e[r],t[r]))return!1;for(const r in t)if(!n.includes(r)&&!(r in e))return!1;return!0},jM=(e,t)=>{switch(typeof e){case"object":return sC(e,t,[]);case"function":return e.toString()===t.toString();default:return e===t}},HM=(e,t,n)=>{const r=Am(t);if(r.length===0)return e;const s=(o,l)=>{if(l===r.length)return n;const a=r[l],u=Array.isArray(o)?[...o]:o&&typeof o=="object"?{...o}:/^(?:0|[1-9]\d*)$/.test(a)?[]:{};return u[a]=s(o==null?void 0:o[a],l+1),u};return s(e,0)},zM={ms:1,s:1e3,m:1e3*60,h:1e3*60*60,d:1e3*60*60*24},J_=e=>{if(typeof e=="number")return e;for(const[t,n]of Object.entries(zM))if(e.endsWith(t))return parseFloat(e)*n;return parseInt(e)},KM=class{constructor(){Re(this,"cached",[]);Re(this,"inFlightRequests",[]);Re(this,"removalTimers",[]);Re(this,"currentUseId",null)}add(e,t,{cacheFor:n,cacheTags:r}){if(this.findInFlight(e))return Promise.resolve();const o=this.findCached(e);if(!e.fresh&&o&&o.staleTimestamp>Date.now())return Promise.resolve();const[l,a]=this.extractStaleValues(n),u=new Promise((c,d)=>{t({...e,onCancel:()=>{this.remove(e),e.onCancel(),d()},onError:f=>{this.remove(e),e.onError(f),d()},onPrefetching(f){e.onPrefetching(f)},onPrefetched(f,p){e.onPrefetched(f,p)},onPrefetchResponse(f){c(f)},onPrefetchError(f){hs.removeFromInFlight(e),d(f)}})}).then(c=>{this.remove(e);const d=c.getPageResponse();Ve.mergeOncePropsIntoResponse(d),this.cached.push({params:{...e},staleTimestamp:Date.now()+l,expiresAt:Date.now()+a,response:u,singleUse:a===0,timestamp:Date.now(),inFlight:!1,tags:Array.isArray(r)?r:[r]});const f=this.getShortestOncePropTtl(d);return this.scheduleForRemoval(e,f?Math.min(a,f):a),this.removeFromInFlight(e),c.handlePrefetch(),c});return this.inFlightRequests.push({params:{...e},response:u,staleTimestamp:null,inFlight:!0}),u}removeAll(){this.cached=[],this.removalTimers.forEach(e=>{clearTimeout(e.timer)}),this.removalTimers=[]}removeByTags(e){this.cached=this.cached.filter(t=>!t.tags.some(n=>e.includes(n)))}remove(e){this.cached=this.cached.filter(t=>!this.paramsAreEqual(t.params,e)),this.clearTimer(e)}removeFromInFlight(e){this.inFlightRequests=this.inFlightRequests.filter(t=>!this.paramsAreEqual(t.params,e))}extractStaleValues(e){const[t,n]=this.cacheForToStaleAndExpires(e);return[J_(t),J_(n)]}cacheForToStaleAndExpires(e){if(!Array.isArray(e))return[e,e];switch(e.length){case 0:return[0,0];case 1:return[e[0],e[0]];default:return[e[0],e[1]]}}clearTimer(e){const t=this.removalTimers.find(n=>this.paramsAreEqual(n.params,e));t&&(clearTimeout(t.timer),this.removalTimers=this.removalTimers.filter(n=>n!==t))}scheduleForRemoval(e,t){if(!(typeof window>"u")&&(this.clearTimer(e),t>0)){const n=window.setTimeout(()=>this.remove(e),t);this.removalTimers.push({params:e,timer:n})}}get(e){return this.findCached(e)||this.findInFlight(e)}use(e,t){const n=`${t.url.pathname}-${Date.now()}-${Math.random().toString(36).substring(7)}`;this.currentUseId=n;const r={...t,cached:!0};return e.response.then(s=>{if(this.currentUseId===n)return s.mergeParams({...r,onPrefetched:()=>{}}),this.removeSingleUseItems(t),s.handle()})}removeSingleUseItems(e){this.cached=this.cached.filter(t=>this.paramsAreEqual(t.params,e)?!t.singleUse:!0)}findCached(e){return this.cached.find(t=>this.paramsAreEqual(t.params,e))||null}findInFlight(e){return this.inFlightRequests.find(t=>this.paramsAreEqual(t.params,e))||null}withoutPurposePrefetchHeader(e){const t=Tt(e);return t.headers.Purpose==="prefetch"&&delete t.headers.Purpose,t}paramsAreEqual(e,t){return sC(this.withoutPurposePrefetchHeader(e),this.withoutPurposePrefetchHeader(t),["id","showProgress","replace","prefetch","preserveScroll","preserveState","onBefore","onBeforeUpdate","onStart","onProgress","onFinish","onCancel","onSuccess","onError","onFlash","onPrefetched","onCancelToken","onPrefetching","async","viewTransition","optimistic","component","pageProps","cached"])}updateCachedOncePropsFromCurrentPage(){this.cached.forEach(e=>{e.response.then(t=>{const n=t.getPageResponse();Ve.mergeOncePropsIntoResponse(n,{force:!0});for(const[l,a]of Object.entries(n.deferredProps??{})){const u=a.filter(c=>qt(n.props,c)===void 0);u.length>0?n.deferredProps[l]=u:delete n.deferredProps[l]}const r=this.getShortestOncePropTtl(n);if(r===null)return;const s=e.expiresAt-Date.now(),o=Math.min(s,r);o>0?this.scheduleForRemoval(e.params,o):this.remove(e.params)})})}getShortestOncePropTtl(e){const t=Object.values(e.onceProps??{}).map(n=>n.expiresAt).filter(n=>!!n);return t.length===0?null:Math.min(...t)-Date.now()}},hs=new KM,Mh=e=>{if(e.offsetParent===null)return!1;const t=e.getBoundingClientRect(),n=t.top=0,r=t.left=0;return n&&r},WM=e=>{const t=l=>{const a=window.getComputedStyle(l);return a.overflowY==="scroll"?!0:a.overflowY!=="auto"?!1:["visible","clip"].includes(a.overflowX)?!0:r(a.maxHeight,l.style.height)||s(l,"height")},n=l=>{const a=window.getComputedStyle(l);return a.overflowX==="scroll"?!0:a.overflowX!=="auto"?!1:["visible","clip"].includes(a.overflowY)?!0:r(a.maxWidth,l.style.width)||s(l,"width")},r=(l,a)=>!!(l&&l!=="none"&&l!=="0px"||a&&a!=="auto"&&a!=="0"),s=(l,a)=>{const u=l.parentElement;if(!u)return!1;const c=window.getComputedStyle(u);if(["flex","inline-flex"].includes(c.display)){const d=["column","column-reverse"].includes(c.flexDirection);return a==="height"?d:!d}return["grid","inline-grid"].includes(c.display)};let o=e==null?void 0:e.parentElement;for(;o;){const l=t(o)||n(o);if(window.getComputedStyle(o).display!=="contents"&&l)return o;o=o.parentElement}return null},oC=(e,t)=>{if(!t)return e.filter(o=>Mh(o));const n=e.indexOf(t),r=[],s=[];for(let o=n;o>=0;o--){const l=e[o];if(Mh(l))r.push(l);else break}for(let o=n+1;o{window.requestAnimationFrame(()=>{t>1?Ou(e,t-1):e()})},GM=e=>{if(typeof window>"u")return null;const t=document.querySelector(`script[data-page="${e}"][type="application/json"]`);return t!=null&&t.textContent?JSON.parse(t.textContent):null},hu=typeof window>"u",JM=!hu&&/Firefox/i.test(window.navigator.userAgent),ur=class{static save(){xt.saveScrollPositions(this.getScrollRegions())}static getScrollRegions(){return Array.from(this.regions()).map(e=>({top:e.scrollTop,left:e.scrollLeft}))}static regions(){return document.querySelectorAll("[scroll-region]")}static scrollToTop(){if(JM&&getComputedStyle(document.documentElement).scrollBehavior==="smooth")return Ou(()=>window.scrollTo(0,0),2);window.scrollTo(0,0)}static reset(){(hu?null:window.location.hash)||this.scrollToTop(),this.regions().forEach(t=>{typeof t.scrollTo=="function"?t.scrollTo(0,0):(t.scrollTop=0,t.scrollLeft=0)}),this.save(),this.scrollToAnchor()}static scrollToAnchor(){const e=hu?null:window.location.hash;e&&setTimeout(()=>{const t=document.getElementById(e.slice(1));t?t.scrollIntoView():this.scrollToTop()})}static restore(e){hu||window.requestAnimationFrame(()=>{this.restoreDocument(),this.restoreScrollRegions(e)})}static restoreScrollRegions(e){hu||this.regions().forEach((t,n)=>{const r=e[n];r&&(typeof t.scrollTo=="function"?t.scrollTo(r.left,r.top):(t.scrollTop=r.top,t.scrollLeft=r.left))})}static restoreDocument(){const e=xt.getDocumentScrollPosition();window.scrollTo(e.left,e.top)}static onScroll(e){const t=e.target;typeof t.hasAttribute=="function"&&t.hasAttribute("scroll-region")&&this.save()}static onWindowScroll(){xt.saveDocumentScrollPosition({top:window.scrollY,left:window.scrollX})}},d1=e=>typeof File<"u"&&e instanceof File||e instanceof Blob||typeof FileList<"u"&&e instanceof FileList&&e.length>0;function Hf(e){return d1(e)||e instanceof FormData&&Array.from(e.values()).some(t=>Hf(t))||typeof e=="object"&&e!==null&&Object.values(e).some(t=>Hf(t))}var fv=e=>e instanceof FormData;function f1(e,t=new FormData,n=null,r="brackets"){e=e||{};for(const s in e)Object.prototype.hasOwnProperty.call(e,s)&&iC(t,lC(n,s,"indices"),e[s],r);return t}function lC(e,t,n){return e?n==="brackets"?`${e}[]`:`${e}[${t}]`:t}function iC(e,t,n,r){if(Array.isArray(n))return Array.from(n.keys()).forEach(s=>iC(e,lC(t,s.toString(),r),n[s],r));if(n instanceof Date)return e.append(t,n.toISOString());if(n instanceof File)return e.append(t,n,n.name);if(n instanceof Blob)return e.append(t,n);if(typeof n=="boolean")return e.append(t,n?"1":"0");if(typeof n=="string")return e.append(t,n);if(typeof n=="number")return e.append(t,`${n}`);if(n==null)return e.append(t,"");f1(n,e,t,r)}function YM(e){return/\[\d+\]/.test(decodeURIComponent(e.search))}function XM(e){if(!e||e==="?")return{};const t={};return e.replace(/^\?/,"").split("&").filter(Boolean).forEach(n=>{const[r,s]=ZM(n);eD(t,Y_(r),Y_(s))}),t}function QM(e,t){const n=[];return pv(e,"",n,t),n.length?"?"+n.join("&"):""}function ZM(e){const t=e.indexOf("=");return t===-1?[e,""]:[e.substring(0,t),e.substring(t+1)]}function Y_(e){return decodeURIComponent(e.replace(/\+/g," "))}function eD(e,t,n){const r=tD(t);if(r.some(l=>l==="__proto__"))return;let s=e;for(;r.length>1;){const l=r.shift(),a=r[0]==="";(typeof s[l]!="object"||s[l]===null)&&(s[l]=a?[]:{}),s=s[l]}const o=r.shift();o===""&&Array.isArray(s)?s.push(n):s[o]=n}function tD(e){const t=[],n=e.split("[")[0];n&&t.push(n);let r;const s=/\[([^\]]*)\]/g;for(;(r=s.exec(e))!==null;)t.push(r[1]);return t}function pv(e,t,n,r){if(e!==void 0){if(e===null){n.push(`${t}=`);return}if(Array.isArray(e)){e.forEach((s,o)=>{const l=r==="indices"?`${t}[${o}]`:`${t}[]`;pv(s,l,n,r)});return}if(typeof e=="object"){Object.keys(e).forEach(s=>{pv(e[s],t?`${t}[${s}]`:s,n,r)});return}n.push(`${t}=${encodeURIComponent(String(e))}`)}}function Lr(e){return new URL(e.toString(),typeof window>"u"?void 0:window.location.toString())}var nD=(e,t,n,r,s)=>{let o=typeof e=="string"?Lr(e):e;if((Hf(t)||r)&&!fv(t)&&(ei.get("form.forceIndicesArrayFormatInFormData")&&(s="indices"),t=f1(t,new FormData,null,s)),fv(t))return[o,t];const[l,a]=Bc(n,o,t,s);return[Lr(l),a]};function Bc(e,t,n,r="brackets"){const s=e==="get"&&!fv(n)&&Object.keys(n).length>0,o=uC(t.toString()),l=o||t.toString().startsWith("/")||t.toString()==="",a=!l&&!t.toString().startsWith("#")&&!t.toString().startsWith("?"),u=/^[.]{1,2}([/]|$)/.test(t.toString()),c=t.toString().includes("?")||s,d=t.toString().includes("#"),f=new URL(t.toString(),typeof window>"u"?"http://localhost":window.location.toString());if(s){const p=YM(f)?"indices":r;f.search=QM({...XM(f.search),...n},p)}return[[o?`${f.protocol}//${f.host}`:"",l?f.pathname:"",a?f.pathname.substring(u?0:1):"",c?f.search:"",d?f.hash:""].join(""),s?{}:n]}function zf(e){return e=new URL(e.href),e.hash="",e}var X_=(e,t)=>{e.hash&&!t.hash&&zf(e).href===t.href&&(t.hash=e.hash)},Kf=(e,t)=>zf(e).href===zf(t).href,mv=(e,t)=>e.origin===t.origin&&e.pathname===t.pathname;function Qr(e){return e!==null&&typeof e=="object"&&e!==void 0&&"url"in e&&"method"in e}function aC(e){return e.component?typeof e.component!="string"?(console.error(`The "component" property on the URL method pair received multiple components (${Object.keys(e.component).join(", ")}), but only a single component string is supported for instant visits. Use the withComponent() method to specify which component to use.`),null):e.component:null}function uC(e){return/^([a-z][a-z0-9+.-]*:)?\/\/[^/]/i.test(e)}function rD(e,t){const n=typeof e=="string"?Lr(e):e;return t?`${n.protocol}//${n.host}${n.pathname}${n.search}${n.hash}`:`${n.pathname}${n.search}${n.hash}`}var sD=class{constructor(){Re(this,"page");Re(this,"swapComponent");Re(this,"resolveComponent");Re(this,"onFlashCallback");Re(this,"componentId",{});Re(this,"listeners",[]);Re(this,"isFirstPageLoad",!0);Re(this,"cleared",!1);Re(this,"pendingDeferredProps",null);Re(this,"historyQuotaExceeded",!1);Re(this,"optimisticBaseline",{});Re(this,"pendingOptimistics",[]);Re(this,"optimisticCounter",0)}init({initialPage:e,swapComponent:t,resolveComponent:n,onFlash:r}){return this.page={...e,flash:e.flash??{},rescuedProps:e.rescuedProps??[]},this.swapComponent=t,this.resolveComponent=n,this.onFlashCallback=r,ws.on("historyQuotaExceeded",()=>{this.historyQuotaExceeded=!0}),this}set(e,{replace:t=!1,preserveScroll:n=!1,preserveState:r=!1,viewTransition:s=!1,cached:o=!1,initialRender:l=!1,visitId:a}={}){Object.keys(e.deferredProps||{}).length&&(this.pendingDeferredProps={deferredProps:e.deferredProps,component:e.component,url:e.url},e.initialDeferredProps===void 0&&(e.initialDeferredProps=e.deferredProps)),this.componentId={};const u=this.componentId;return e.clearHistory&&xt.clear(),this.resolve(e.component,e).then(c=>{if(u!==this.componentId)return;e.rememberedState??(e.rememberedState={});const d=typeof window>"u",f=d?new URL(e.url):window.location,p=!d&&n?ur.getScrollRegions():[];t=t||Kf(Lr(e.url),f);const h={...e,flash:{}};return new Promise(b=>t?xt.replaceState(h,b):xt.pushState(h,b)).then(()=>{const b=!this.isTheSame(e);if(!b&&Object.keys(e.props.errors||{}).length>0&&(s=!1),this.page=e,this.cleared=!1,this.hasOnceProps()&&hs.updateCachedOncePropsFromCurrentPage(),b&&this.fireEventsFor("newComponent"),this.isFirstPageLoad&&this.fireEventsFor("firstLoad"),this.isFirstPageLoad=!1,this.historyQuotaExceeded){this.historyQuotaExceeded=!1;return}return this.swap({component:c,page:e,preserveState:r,viewTransition:s,initialRender:l}).then(()=>{n?window.requestAnimationFrame(()=>ur.restoreScrollRegions(p)):ur.reset(),this.pendingDeferredProps&&this.pendingDeferredProps.component===e.component&&this.pendingDeferredProps.url===e.url&&ws.fireInternalEvent("loadDeferredProps",this.pendingDeferredProps.deferredProps),this.pendingDeferredProps=null,t||Uf(e,{cached:o,visitId:a})})})})}setQuietly(e,{preserveState:t=!1}={}){return this.resolve(e.component,e).then(n=>(this.page=e,this.cleared=!1,xt.setCurrent(e),this.swap({component:n,page:e,preserveState:t,viewTransition:!1})))}clear(){this.cleared=!0}isCleared(){return this.cleared}get(){return this.page}getWithoutFlashData(){return{...this.page,flash:{}}}hasOnceProps(){return Object.keys(this.page.onceProps??{}).length>0}merge(e){this.page={...this.page,...e}}setPropsQuietly(e){return this.page={...this.page,props:e},this.resolve(this.page.component,this.page).then(t=>this.swap({component:t,page:this.page,preserveState:!0,viewTransition:!1}))}setFlash(e){var t;this.page={...this.page,flash:e},(t=this.onFlashCallback)==null||t.call(this,e)}setUrlHash(e){this.page.url.includes(e)||(this.page.url+=e)}remember(e){this.page.rememberedState=e}swap({component:e,page:t,preserveState:n,viewTransition:r,initialRender:s=!1}){const o=()=>this.swapComponent({component:e,page:t,preserveState:n,initialRender:s});if(!r||!(document!=null&&document.startViewTransition)||document.visibilityState==="hidden")return o();const l=typeof r=="boolean"?()=>null:r;return new Promise(a=>{const u=document.startViewTransition(()=>o().then(a));u.ready.catch(()=>{}),l(u)})}resolve(e,t){return Promise.resolve(this.resolveComponent(e,t))}nextOptimisticId(){return++this.optimisticCounter}setBaseline(e,t){e in this.optimisticBaseline||(this.optimisticBaseline[e]=t)}updateBaseline(e,t){e in this.optimisticBaseline&&(this.optimisticBaseline[e]=t)}hasBaseline(e){return e in this.optimisticBaseline}registerOptimistic(e,t){this.pendingOptimistics.push({id:e,callback:t})}unregisterOptimistic(e){this.pendingOptimistics=this.pendingOptimistics.filter(t=>t.id!==e)}replayOptimistics(){const e=Object.keys(this.optimisticBaseline);if(e.length===0)return{};const t=Tt(this.page.props);for(const r of e)t[r]=Tt(this.optimisticBaseline[r]);for(const{callback:r}of this.pendingOptimistics){const s=r(Tt(t));s&&Object.assign(t,s)}const n={};for(const r of e)n[r]=t[r];return n}pendingOptimisticCount(){return this.pendingOptimistics.length}clearOptimisticState(){this.optimisticBaseline={},this.pendingOptimistics=[]}isTheSame(e){return this.page.component===e.component}on(e,t){return this.listeners.push({event:e,callback:t}),()=>{this.listeners=this.listeners.filter(n=>n.event!==e&&n.callback!==t)}}fireEventsFor(e){this.listeners.filter(t=>t.event===e).forEach(t=>t.callback())}mergeOncePropsIntoResponse(e,{force:t=!1}={}){Object.entries(e.onceProps??{}).forEach(([n,r])=>{var o;const s=(o=this.page.onceProps)==null?void 0:o[n];s!==void 0&&(t||qt(e.props,r.prop)===void 0)&&(fr(e.props,r.prop,qt(this.page.props,s.prop)),e.onceProps[n].expiresAt=s.expiresAt)})}},Ve=new sD,Mm=class{constructor(){Re(this,"items",[]);Re(this,"processingPromise",null)}add(e){return this.items.push(e),this.process()}process(){return this.processingPromise??(this.processingPromise=this.processNext().finally(()=>{this.processingPromise=null})),this.processingPromise}processNext(){const e=this.items.shift();return e?Promise.resolve(e()).then(()=>this.processNext()):Promise.resolve()}},Mi=typeof window>"u",Qa=new Mm,Q_=!Mi&&/CriOS/.test(window.navigator.userAgent),oD=class{constructor(){Re(this,"rememberedState","rememberedState");Re(this,"scrollRegions","scrollRegions");Re(this,"preserveUrl",!1);Re(this,"current",{});Re(this,"initialState",null)}remember(e,t){var n;this.replaceState({...Ve.getWithoutFlashData(),rememberedState:{...((n=Ve.get())==null?void 0:n.rememberedState)??{},[t]:e}})}restore(e){var t,n,r,s;if(!Mi)return((t=this.current[this.rememberedState])==null?void 0:t[e])!==void 0?(n=this.current[this.rememberedState])==null?void 0:n[e]:(s=(r=this.initialState)==null?void 0:r[this.rememberedState])==null?void 0:s[e]}pushState(e,t=null){if(!Mi){if(this.preserveUrl){t&&t();return}this.current=e,Qa.add(()=>this.getPageData(e).then(n=>{const r=()=>this.doPushState({page:n},e.url).then(()=>t==null?void 0:t());return Q_?new Promise(s=>{setTimeout(()=>r().then(s))}):r()}))}}clonePageProps(e){try{return structuredClone(e.props),e}catch{return{...e,props:Tt(e.props)}}}getPageData(e){const t=this.clonePageProps(e);return new Promise(n=>e.encryptHistory?RM(t).then(n):n(t))}processQueue(){return Qa.process()}decrypt(e=null){var n;if(Mi)return Promise.resolve(e??Ve.get());const t=e??((n=window.history.state)==null?void 0:n.page);return this.decryptPageData(t).then(r=>{if(!r)throw new Error("Unable to decrypt history");return this.initialState===null?this.initialState=r??void 0:this.current=r??{},r})}decryptPageData(e){return e instanceof ArrayBuffer?NM(e):Promise.resolve(e)}saveScrollPositions(e){Qa.add(()=>Promise.resolve().then(()=>{var t;if((t=window.history.state)!=null&&t.page&&!ts(this.getScrollRegions(),e))return this.doReplaceState({page:window.history.state.page,scrollRegions:e})}))}saveDocumentScrollPosition(e){Qa.add(()=>Promise.resolve().then(()=>{var t;if((t=window.history.state)!=null&&t.page&&!ts(this.getDocumentScrollPosition(),e))return this.doReplaceState({page:window.history.state.page,documentScrollPosition:e})}))}getScrollRegions(){var e;return((e=window.history.state)==null?void 0:e.scrollRegions)||[]}getDocumentScrollPosition(){var e;return((e=window.history.state)==null?void 0:e.documentScrollPosition)||{top:0,left:0}}replaceState(e,t=null){if(ts(this.current,e)){t&&t();return}const{flash:n,...r}=e;if(Ve.merge(r),!Mi){if(this.preserveUrl){t&&t();return}this.current=e,Qa.add(()=>this.getPageData(e).then(s=>{const o=()=>this.doReplaceState({page:s},e.url).then(()=>t==null?void 0:t());return Q_?new Promise(l=>{setTimeout(()=>o().then(l))}):o()}))}}isHistoryThrottleError(e){return e instanceof Error&&e.name==="SecurityError"&&(e.message.includes("history.pushState")||e.message.includes("history.replaceState"))}isQuotaExceededError(e){return e instanceof Error&&e.name==="QuotaExceededError"}withThrottleProtection(e){return Promise.resolve().then(()=>{try{return e()}catch(t){if(!this.isHistoryThrottleError(t))throw t;console.error(t.message)}})}doReplaceState(e,t){return this.withThrottleProtection(()=>{var n,r;window.history.replaceState({...e,scrollRegions:e.scrollRegions??((n=window.history.state)==null?void 0:n.scrollRegions),documentScrollPosition:e.documentScrollPosition??((r=window.history.state)==null?void 0:r.documentScrollPosition)},"",t)})}doPushState(e,t){return this.withThrottleProtection(()=>{try{window.history.pushState(e,"",t)}catch(n){if(!this.isQuotaExceededError(n))throw n;ws.fireInternalEvent("historyQuotaExceeded",t)}})}getState(e,t){var n;return((n=this.current)==null?void 0:n[e])??t}deleteState(e){this.current[e]!==void 0&&(delete this.current[e],this.replaceState(this.current))}clearInitialState(e){this.initialState&&this.initialState[e]!==void 0&&delete this.initialState[e]}browserHasHistoryEntry(){var e;return!Mi&&!!((e=window.history.state)!=null&&e.page)}clear(){ar.remove(ua.key),ar.remove(ua.iv)}setCurrent(e){this.current=e}isValidState(e){return!!e.page}getAllState(){return this.current}};typeof window<"u"&&window.history.scrollRestoration&&(window.history.scrollRestoration="manual");var xt=new oD,lD=class{constructor(){Re(this,"internalListeners",[])}init(){typeof window<"u"&&(window.addEventListener("popstate",this.handlePopstateEvent.bind(this)),window.addEventListener("pageshow",this.handlePageshowEvent.bind(this)),window.addEventListener("scroll",lc(ur.onWindowScroll.bind(ur),100),!0)),typeof document<"u"&&document.addEventListener("scroll",lc(ur.onScroll.bind(ur),100),!0)}onGlobalEvent(e,t){const n=(r=>{const s=t(r);r.cancelable&&!r.defaultPrevented&&s===!1&&r.preventDefault()});return this.registerListener(`inertia:${e}`,n)}on(e,t){return this.internalListeners.push({event:e,listener:t}),()=>{this.internalListeners=this.internalListeners.filter(n=>n.listener!==t)}}onMissingHistoryItem(){Ve.clear(),this.fireInternalEvent("missingHistoryItem")}fireInternalEvent(e,...t){this.internalListeners.filter(n=>n.event===e).forEach(n=>n.listener(...t))}registerListener(e,t){return document.addEventListener(e,t),()=>document.removeEventListener(e,t)}handlePageshowEvent(e){e.persisted&&xt.decrypt().catch(()=>this.onMissingHistoryItem())}handlePopstateEvent(e){const t=e.state||null;if(t===null){const n=Lr(Ve.get().url);n.hash=window.location.hash,xt.replaceState({...Ve.getWithoutFlashData(),url:n.href}),ur.reset();return}if(!xt.isValidState(t))return this.onMissingHistoryItem();xt.decrypt(t.page).then(n=>{if(Ve.get().version!==n.version){this.onMissingHistoryItem();return}xe.cancelAll({prefetch:!1}),Ve.setQuietly(n,{preserveState:!1}).then(()=>{ur.restore(xt.getScrollRegions()),Uf(Ve.get());const r={},s=Ve.get().props;for(const[o,l]of Object.entries(n.initialDeferredProps??n.deferredProps??{})){const a=l.filter(u=>qt(s,u)===void 0);a.length>0&&(r[o]=a)}Object.keys(r).length>0&&this.fireInternalEvent("loadDeferredProps",r)})}).catch(()=>{this.onMissingHistoryItem()})}},ws=new lD,iD=class{constructor(){Re(this,"type");this.type=this.resolveType()}resolveType(){var t;if(typeof window>"u")return"navigate";const e=(t=window.performance)==null?void 0:t.getEntriesByType("navigation")[0];return(e==null?void 0:e.type)??"navigate"}get(){return this.type}isBackForward(){return this.type==="back_forward"}isReload(){return this.type==="reload"}},Dh=new iD;function nf(){const e=typeof window<"u"?window.crypto:void 0;if(e!=null&&e.randomUUID)return e.randomUUID();const t=()=>e!=null&&e.getRandomValues?e.getRandomValues(new Uint8Array(1))[0]:Math.floor(Math.random()*256);return"10000000-1000-4000-8000-100000000000".replace(/[018]/g,n=>(+n^t()&15>>+n/4).toString(16))}var aD=class{static handle(){this.clearRememberedStateOnReload(),[this.handleBackForward,this.handleLocation,this.handleDefault].find(t=>t.bind(this)())}static clearRememberedStateOnReload(){Dh.isReload()&&(xt.deleteState(xt.rememberedState),xt.clearInitialState(xt.rememberedState))}static handleBackForward(){if(!Dh.isBackForward()||!xt.browserHasHistoryEntry())return!1;const e=xt.getScrollRegions();return xt.decrypt().then(t=>{const n=nf();Ve.set(t,{preserveScroll:!0,preserveState:!0,visitId:n}).then(()=>{ur.restore(e),Uf(Ve.get(),{visitId:n})})}).catch(()=>{ws.onMissingHistoryItem()}),!0}static handleLocation(){if(!ar.exists(ar.locationVisitKey))return!1;const e=ar.get(ar.locationVisitKey)||{};return ar.remove(ar.locationVisitKey),typeof window<"u"&&Ve.setUrlHash(window.location.hash),xt.decrypt(Ve.get()).then(()=>{const t=nf(),n=xt.getState(xt.rememberedState,{}),r=xt.getScrollRegions();Ve.remember(n),Ve.set(Ve.get(),{preserveScroll:e.preserveScroll,preserveState:!0,initialRender:!0,visitId:t}).then(()=>{e.preserveScroll&&ur.restore(r),this.fireInitialEvents(t)})}).catch(()=>{ws.onMissingHistoryItem()}),!0}static handleDefault(){typeof window<"u"&&Ve.setUrlHash(window.location.hash);const e=nf();Ve.set(Ve.get(),{preserveScroll:!0,preserveState:!0,initialRender:!0,visitId:e}).then(()=>{Dh.isReload()?ur.restore(xt.getScrollRegions()):ur.scrollToAnchor(),this.fireInitialEvents(e)})}static fireInitialEvents(e){const t=Ve.get();Uf(t,{visitId:e}),Object.keys(t.flash).length>0&&queueMicrotask(()=>jf(t.flash))}},uD=class{constructor(e,t,n){Re(this,"intervalId",null);Re(this,"timeoutId",null);Re(this,"throttle",!1);Re(this,"keepAlive",!1);Re(this,"cb");Re(this,"interval");Re(this,"cbCount",0);Re(this,"mode");Re(this,"inFlight",!1);Re(this,"currentCancel",null);Re(this,"stopped",!0);Re(this,"instanceId",0);this.keepAlive=n.keepAlive??!1,this.mode=n.mode??"overlap",this.cb=t,this.interval=e,(n.autoStart??!0)&&this.start()}stop(){this.stopped=!0,this.instanceId++,this.inFlight=!1,this.currentCancel=null,this.intervalId&&(clearInterval(this.intervalId),this.intervalId=null),this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)}start(){if(!(typeof window>"u")){if(this.stop(),this.stopped=!1,this.mode==="rest"){this.scheduleNext();return}this.intervalId=window.setInterval(()=>this.tick(),this.interval)}}isInBackground(e){this.throttle=this.keepAlive?!1:e,this.throttle&&(this.cbCount=0)}scheduleNext(){this.stopped||(this.timeoutId=window.setTimeout(()=>{this.timeoutId=null,this.tick()},this.interval))}tick(){!this.throttle||this.cbCount%10===0?this.fire():this.mode==="rest"&&this.scheduleNext(),this.throttle&&this.cbCount++}fire(){var t;this.inFlight&&this.mode==="cancel"&&((t=this.currentCancel)==null||t.call(this));const e=this.instanceId;this.cb({onStart:n=>{e===this.instanceId&&(this.inFlight=!0,this.currentCancel=n)},onFinish:()=>{e===this.instanceId&&(this.inFlight=!1,this.currentCancel=null,this.mode==="rest"&&this.scheduleNext())}})}},cD=class{constructor(){Re(this,"polls",[]);this.setupVisibilityListener()}get count(){return this.polls.length}add(e,t,n){const r=new uD(e,t,n);return this.polls.push(r),{stop:()=>r.stop(),start:()=>r.start(),destroy:()=>{r.stop(),this.polls=this.polls.filter(s=>s!==r)}}}clear(){this.polls.forEach(e=>e.stop()),this.polls=[]}setupVisibilityListener(){typeof document>"u"||document.addEventListener("visibilitychange",()=>{this.polls.forEach(e=>e.isInBackground(document.hidden))},!1)}},Z_=new cD,dD=class{constructor(){Re(this,"requestHandlers",[]);Re(this,"responseHandlers",[]);Re(this,"errorHandlers",[])}onRequest(e){return this.requestHandlers.push(e),()=>{this.requestHandlers=this.requestHandlers.filter(t=>t!==e)}}onResponse(e){return this.responseHandlers.push(e),()=>{this.responseHandlers=this.responseHandlers.filter(t=>t!==e)}}onError(e){return this.errorHandlers.push(e),()=>{this.errorHandlers=this.errorHandlers.filter(t=>t!==e)}}async processRequest(e){let t=e;for(const n of this.requestHandlers)t=await n(t);return t}async processResponse(e){let t=e;for(const n of this.responseHandlers)t=await n(t);return t}async processError(e){for(const t of this.errorHandlers)await t(e)}},Jn=new dD,p1=class extends Error{constructor(n,r,s){super(s?`${n} (${s})`:n);Re(this,"code");Re(this,"url");this.name="HttpError",this.code=r,this.url=s}},ic=class extends p1{constructor(t,n,r){super(t,"ERR_HTTP_RESPONSE",r);Re(this,"response");this.name="HttpResponseError",this.response=n}},ac=class extends p1{constructor(e="Request was cancelled",t){super(e,"ERR_CANCELLED",t),this.name="HttpCancelledError"}},e0=class extends p1{constructor(t,n,r){super(t,"ERR_NETWORK",n);Re(this,"cause");this.name="HttpNetworkError",this.cause=r}};function fD(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null}function pD(e){const t={};return e.getAllResponseHeaders().split(`\r -`).forEach(n=>{const r=n.indexOf(":");r>0&&(t[n.slice(0,r).toLowerCase().trim()]=n.slice(r+1).trim())}),t}function cC(e){return typeof FormData<"u"&&e instanceof FormData}function mD(e){return typeof e=="string"||cC(e)||typeof Blob<"u"&&e instanceof Blob||typeof ArrayBuffer<"u"&&e instanceof ArrayBuffer||typeof ArrayBuffer<"u"&&ArrayBuffer.isView(e)||typeof URLSearchParams<"u"&&e instanceof URLSearchParams}function hD(e,t){if(!t.headers)return;const n=cC(t.data);Object.entries(t.headers).forEach(([r,s])=>{(r.toLowerCase()!=="content-type"||!n)&&e.setRequestHeader(r,String(s))})}function gD(e,t){if(!t||Object.keys(t).length===0)return e;const[n]=Bc("get",e,t);return n}var dC=class{constructor(e={}){Re(this,"xsrfCookieName");Re(this,"xsrfHeaderName");this.xsrfCookieName=e.xsrfCookieName??"XSRF-TOKEN",this.xsrfHeaderName=e.xsrfHeaderName??"X-XSRF-TOKEN"}async request(e){const t=await Jn.processRequest(e);try{const n=await this.doRequest(t);return await Jn.processResponse(n)}catch(n){throw(n instanceof ic||n instanceof e0||n instanceof ac)&&await Jn.processError(n),n}}doRequest(e){return new Promise((t,n)=>{var u,c;const r=new XMLHttpRequest,s=gD(e.url,e.params);r.open(e.method.toUpperCase(),s,!0);const o=fD(this.xsrfCookieName);o&&r.setRequestHeader(this.xsrfHeaderName,o),Object.keys(e.headers??{}).some(d=>d.toLowerCase()==="x-requested-with")||r.setRequestHeader("X-Requested-With","XMLHttpRequest");let a=null;e.data!==null&&e.data!==void 0&&(mD(e.data)?a=e.data:typeof e.data=="object"?(a=JSON.stringify(e.data),!((u=e.headers)!=null&&u["Content-Type"])&&!((c=e.headers)!=null&&c["content-type"])&&r.setRequestHeader("Content-Type","application/json")):a=String(e.data)),hD(r,e),e.onUploadProgress&&(r.upload.onprogress=d=>{const f=d.lengthComputable?d.loaded/d.total:void 0;e.onUploadProgress({progress:f,percentage:f?Math.round(f*100):0,loaded:d.loaded,total:d.lengthComputable?d.total:void 0})}),e.signal&&e.signal.addEventListener("abort",()=>r.abort()),r.onabort=()=>n(new ac("Request was cancelled",e.url)),r.onerror=()=>n(new e0("Network error",e.url)),r.onload=()=>{const d={status:r.status,data:r.responseText,headers:pD(r)};r.status>=400?n(new ic(`Request failed with status ${r.status}`,d,e.url)):t(d)},r.send(a)})}},vD=new dC,Vh=vD;function yD(e){return!("request"in e)}var Dm={getClient(){return Vh},setClient(e){if(!yD(e)){Vh=e;return}Vh=new dC(e),e.xsrfCookieName&&Yn.withXsrfCookieName(e.xsrfCookieName),e.xsrfHeaderName&&Yn.withXsrfHeaderName(e.xsrfHeaderName)},onRequest:Jn.onRequest.bind(Jn),onResponse:Jn.onResponse.bind(Jn),onError:Jn.onError.bind(Jn),processRequest:Jn.processRequest.bind(Jn),processResponse:Jn.processResponse.bind(Jn),processError:Jn.processError.bind(Jn)},bD=class{constructor(){Re(this,"requestHandlers",[]);Re(this,"responseHandlers",[])}onVisitRequest(e){return this.requestHandlers.push(e),()=>{this.requestHandlers=this.requestHandlers.filter(t=>t!==e)}}onVisitResponse(e){return this.responseHandlers.push(e),()=>{this.responseHandlers=this.responseHandlers.filter(t=>t!==e)}}async processRequest(e,t){let n=t;for(const r of this.requestHandlers)n=await r(e,n);return n}async processResponse(e,t){let n=t;for(const r of this.responseHandlers)n=await r(e,n);return n}},m1=new bD;function _D(){typeof window>"u"||(window.__inertia_interceptors__=m1)}var rf=class sf{constructor(t){Re(this,"callbacks",[]);Re(this,"params");if(!t.prefetch)this.params=t;else{const n={onBefore:this.wrapCallback(t,"onBefore"),onBeforeUpdate:this.wrapCallback(t,"onBeforeUpdate"),onStart:this.wrapCallback(t,"onStart"),onProgress:this.wrapCallback(t,"onProgress"),onFinish:this.wrapCallback(t,"onFinish"),onCancel:this.wrapCallback(t,"onCancel"),onSuccess:this.wrapCallback(t,"onSuccess"),onError:this.wrapCallback(t,"onError"),onHttpException:this.wrapCallback(t,"onHttpException"),onNetworkError:this.wrapCallback(t,"onNetworkError"),onFlash:this.wrapCallback(t,"onFlash"),onCancelToken:this.wrapCallback(t,"onCancelToken"),onPrefetched:this.wrapCallback(t,"onPrefetched"),onPrefetching:this.wrapCallback(t,"onPrefetching")};this.params={...t,...n,onPrefetchResponse:t.onPrefetchResponse||(()=>{}),onPrefetchError:t.onPrefetchError||(()=>{})}}}static create(t){return new sf(t)}data(){return this.params.method==="get"?null:this.params.data}queryParams(){return this.params.method==="get"?this.params.data:{}}isPartial(){return this.params.only.length>0||this.params.except.length>0||this.params.reset.length>0}isPrefetch(){return this.params.prefetch===!0}isDeferredPropsRequest(){return this.params.deferredProps===!0}isPollRequest(){return this.params.poll===!0}onCancelToken(t){this.params.onCancelToken({cancel:t})}markAsFinished(){this.params.completed=!0,this.params.cancelled=!1,this.params.interrupted=!1}markAsCancelled({cancelled:t=!0,interrupted:n=!1}){this.params.onCancel(),this.params.completed=!1,this.params.cancelled=t,this.params.interrupted=n}wasCancelledAtAll(){return this.params.cancelled||this.params.interrupted}onFinish(){this.params.onFinish(this.params)}onStart(){this.params.onStart(this.params)}onPrefetching(){this.params.onPrefetching(this.params)}onPrefetchResponse(t){this.params.onPrefetchResponse&&this.params.onPrefetchResponse(t)}onPrefetchError(t){this.params.onPrefetchError&&this.params.onPrefetchError(t)}all(){return this.params}headers(){const t={...this.params.headers};this.isPartial()&&(t["X-Inertia-Partial-Component"]=Ve.get().component);const n=this.params.only.concat(this.params.reset);return n.length>0&&(t["X-Inertia-Partial-Data"]=n.join(",")),this.params.except.length>0&&(t["X-Inertia-Partial-Except"]=this.params.except.join(",")),this.params.reset.length>0&&(t["X-Inertia-Reset"]=this.params.reset.join(",")),this.params.errorBag&&this.params.errorBag.length>0&&(t["X-Inertia-Error-Bag"]=this.params.errorBag),t}setPreserveOptions(t){this.params.preserveScroll=sf.resolvePreserveOption(this.params.preserveScroll,t),this.params.preserveState=sf.resolvePreserveOption(this.params.preserveState,t)}runCallbacks(){this.callbacks.forEach(({name:t,args:n})=>{this.params[t](...n)})}merge(t){this.params={...this.params,...t}}wrapCallback(t,n){return(...r)=>{this.recordCallback(n,r),t[n](...r)}}recordCallback(t,n){this.callbacks.push({name:t,args:n})}static resolvePreserveOption(t,n){return typeof t=="function"?t(n):t==="errors"?Object.keys(n.props.errors||{}).length>0:t}},xD={createIframeAndPage(e){typeof e=="object"&&(e=`All Inertia requests must receive a valid Inertia response, however a plain JSON response was received.
${JSON.stringify(e)}`);const t=document.createElement("html");t.innerHTML=e,t.querySelectorAll("a").forEach(r=>r.setAttribute("target","_top"));const n=document.createElement("iframe");return n.style.backgroundColor="white",n.style.borderRadius="5px",n.style.width="100%",n.style.height="100%",n.setAttribute("sandbox","allow-scripts"),{iframe:n,page:t}},show(e){const{iframe:t,page:n}=this.createIframeAndPage(e);t.style.boxSizing="border-box",t.style.display="block";const r=document.createElement("dialog");r.id="inertia-error-dialog",Object.assign(r.style,{width:"calc(100vw - 100px)",height:"calc(100vh - 100px)",padding:"0",margin:"auto",border:"none",backgroundColor:"transparent"});const s=document.createElement("style");s.textContent=` - dialog#inertia-error-dialog::backdrop { - background-color: rgba(0, 0, 0, 0.6); - } - - dialog#inertia-error-dialog:focus { - outline: none; - } - `;const o=ei.get("nonce");o&&(s.nonce=o),document.head.appendChild(s),r.addEventListener("click",l=>{l.target===r&&r.close()}),r.addEventListener("close",()=>{s.remove(),r.remove()}),r.appendChild(t),document.body.prepend(r),r.showModal(),r.focus(),t.srcdoc=n.outerHTML}},t0=(e,t)=>e===t||e.startsWith(`${t}.`),fC=(e,t)=>{const{only:n,except:r}=e;return!(n.length===0&&r.length===0||n.length>0&&!n.some(s=>t0(t,s))||r.length>0&&r.some(s=>t0(t,s)))},kD=(e,t)=>t.some(n=>fC(e,n)),wD=new Mm,n0=class pC{constructor(t,n,r){Re(this,"requestParams");Re(this,"response");Re(this,"originatingPage");Re(this,"wasPrefetched",!1);Re(this,"processed",!1);this.requestParams=t,this.response=n,this.originatingPage=r}static create(t,n,r){return new pC(t,n,r)}isProcessed(){return this.processed}async handlePrefetch(){Kf(this.requestParams.all().url,window.location)&&this.handle()}async handle(){return wD.add(()=>this.process())}async process(){if(this.requestParams.all().prefetch)return this.wasPrefetched=!0,this.requestParams.all().prefetch=!1,this.requestParams.all().onPrefetched(this.response,this.requestParams.all()),DM(this.response,this.requestParams.all()),Promise.resolve();if(this.requestParams.runCallbacks(),this.processed=!0,!this.isInertiaResponse())return this.handleNonInertiaResponse();if(this.isHttpException()){const r={...this.response,data:this.getDataFromResponse(this.response.data)};if(this.requestParams.all().onHttpException(r)===!1||!W_(r))return}await xt.processQueue(),xt.preserveUrl=this.requestParams.all().preserveUrl,await this.setPage();const{flash:t}=Ve.get();Object.keys(t).length>0&&!this.requestParams.isDeferredPropsRequest()&&(jf(t),this.requestParams.all().onFlash(t));const n=Ve.get().props.errors||{};if(Object.keys(n).length>0){const r=this.getScopedErrors(n);return CM(r,{page:Ve.get(),visitId:this.requestParams.all().id}),this.requestParams.all().onError(r)}xe.flushByCacheTags(this.requestParams.all().invalidateCacheTags||[]),this.wasPrefetched||xe.flush(Ve.get().url),MM(Ve.get(),{visitId:this.requestParams.all().id}),await this.requestParams.all().onSuccess(Ve.get()),xt.preserveUrl=!1}mergeParams(t){this.requestParams.merge(t)}getPageResponse(){const t=this.getDataFromResponse(this.response.data);return typeof t=="object"?this.response.data={...t,flash:t.flash??{},rescuedProps:t.rescuedProps??[]}:this.response.data=t}async handleNonInertiaResponse(){if(this.isInertiaRedirect()){xe.visit(this.getHeader("x-inertia-redirect"),{...this.requestParams.all(),method:"get",data:{}});return}if(this.isLocationVisit()){const n=Lr(this.getHeader("x-inertia-location"));return X_(this.requestParams.all().url,n),this.locationVisit(n)}const t={...this.response,data:this.getDataFromResponse(this.response.data)};if(this.requestParams.all().onHttpException(t)!==!1&&W_(t))return xD.show(t.data)}isInertiaResponse(){return this.hasHeader("x-inertia")}isHttpException(){return this.response.status>=400}hasStatus(t){return this.response.status===t}getHeader(t){return this.response.headers[t]}hasHeader(t){return this.getHeader(t)!==void 0}isInertiaRedirect(){return this.hasStatus(409)&&this.hasHeader("x-inertia-redirect")}isLocationVisit(){return this.hasStatus(409)&&this.hasHeader("x-inertia-location")}locationVisit(t){try{if(typeof window>"u")return;const n=this.getHeader("x-inertia-version"),r=!!n&&n!==Ve.get().version;if(!IM(t,r)||r&&this.requestParams.all().async)return;ar.set(ar.locationVisitKey,{preserveScroll:this.requestParams.all().preserveScroll===!0}),Kf(window.location,t)?window.location.reload():window.location.href=t.href}catch{return!1}}async setPage(){const t=this.getPageResponse();return this.shouldSetPage(t)?(this.response=await m1.processResponse(this.requestParams.all(),this.response),this.mergeProps(t),Ve.mergeOncePropsIntoResponse(t),this.preserveOptimisticProps(t),this.preserveEqualProps(t),await this.setRememberedState(t),this.requestParams.setPreserveOptions(t),t.url=xt.preserveUrl?Ve.get().url:this.pageUrl(t),this.requestParams.all().onBeforeUpdate(t),OM(t),Ve.set(t,{replace:this.requestParams.all().replace,preserveScroll:this.requestParams.all().preserveScroll,preserveState:this.requestParams.all().preserveState,viewTransition:this.requestParams.all().viewTransition,cached:this.requestParams.all().cached,visitId:this.requestParams.all().id})):Promise.resolve()}getDataFromResponse(t){if(typeof t!="string")return t;try{return JSON.parse(t)}catch{return t}}shouldSetPage(t){if(!this.requestParams.all().async||this.originatingPage.component!==t.component)return!0;if(this.originatingPage.component!==Ve.get().component)return!1;const n=Lr(this.originatingPage.url),r=Lr(Ve.get().url);return n.origin===r.origin&&n.pathname===r.pathname}pageUrl(t){const n=Lr(t.url);return t.preserveFragment?n.hash=this.requestParams.all().url.hash:X_(this.requestParams.all().url,n),n.pathname+n.search+n.hash}preserveOptimisticProps(t){if(xe.hasPendingOptimistic())for(const n of Object.keys(t.props))Ve.hasBaseline(n)&&(Ve.updateBaseline(n,t.props[n]),t.props[n]=Ve.get().props[n])}preserveEqualProps(t){if(t.component!==Ve.get().component)return;const n=Ve.get().props;Object.entries(t.props).forEach(([r,s])=>{ts(s,n[r])&&(t.props[r]=n[r])})}mergeProps(t){if(!this.requestParams.isPartial()||t.component!==Ve.get().component)return;const n=t.mergeProps||[],r=t.prependProps||[],s=t.deepMergeProps||[],o=t.matchPropsOn||[],l=(c,d)=>{const f=qt(Ve.get().props,c),p=qt(t.props,c);if(Array.isArray(p)){const h=this.mergeOrMatchItems(f||[],p,c,o,d);fr(t.props,c,h)}else if(typeof p=="object"&&p!==null){const h={...f||{},...p};fr(t.props,c,h)}};n.forEach(c=>l(c,!0)),r.forEach(c=>l(c,!1)),s.forEach(c=>{const d=qt(Ve.get().props,c),f=qt(t.props,c),p=(h,b,x)=>Array.isArray(b)?this.mergeOrMatchItems(h,b,x,o):typeof b=="object"&&b!==null?Object.keys(b).reduce((w,k)=>(w[k]=p(h?h[k]:void 0,b[k],`${x}.${k}`),w),{...h}):b;fr(t.props,c,p(d,f,c))});const a=new Set([...this.requestParams.all().only,...this.requestParams.all().except].filter(c=>c.includes(".")).map(c=>c.split(".")[0]));for(const c of a){const d=Ve.get().props[c];this.isObject(d)&&this.isObject(t.props[c])&&(t.props[c]=this.deepMergeObjects(d,t.props[c]))}t.props={...Ve.get().props,...t.props},this.shouldPreserveErrors(t)&&(t.props.errors=Ve.get().props.errors),Ve.get().scrollProps&&(t.scrollProps={...Ve.get().scrollProps||{},...t.scrollProps||{}}),Ve.hasOnceProps()&&(t.onceProps={...Ve.get().onceProps||{},...t.onceProps||{}}),this.requestParams.isDeferredPropsRequest()&&(t.flash={...Ve.get().flash});const u=Ve.get().initialDeferredProps;u&&Object.keys(u).length>0&&(t.initialDeferredProps=u),t.rescuedProps=this.mergeRescuedProps(t)}mergeRescuedProps(t){const n=Ve.get().rescuedProps??[],r=t.rescuedProps??[],s=new Set(n.filter(o=>!fC(this.requestParams.all(),o)));return r.forEach(o=>s.add(o)),Array.from(s)}shouldPreserveErrors(t){if(!this.requestParams.all().preserveErrors)return!1;const n=Ve.get().props.errors;if(!n||Object.keys(n).length===0)return!1;const r=t.props.errors;return!(r&&Object.keys(r).length>0)}isObject(t){return t&&typeof t=="object"&&!Array.isArray(t)}deepMergeObjects(t,n){const r={...t};for(const s of Object.keys(n)){const o=t[s],l=n[s];this.isObject(o)&&this.isObject(l)?r[s]=this.deepMergeObjects(o,l):r[s]=l}return r}mergeOrMatchItems(t,n,r,s,o=!0){const l=Array.isArray(t)?t:[],a=s.find(d=>d.split(".").slice(0,-1).join(".")===r);if(!a)return o?[...l,...n]:[...n,...l];const u=a.split(".").pop()||"",c=new Map;return n.forEach(d=>{this.hasUniqueProperty(d,u)&&c.set(d[u],d)}),o?this.appendWithMatching(l,n,c,u):this.prependWithMatching(l,n,c,u)}appendWithMatching(t,n,r,s){const o=t.map(a=>this.hasUniqueProperty(a,s)&&r.has(a[s])?r.get(a[s]):a),l=n.filter(a=>this.hasUniqueProperty(a,s)?!t.some(u=>this.hasUniqueProperty(u,s)&&u[s]===a[s]):!0);return[...o,...l]}prependWithMatching(t,n,r,s){const o=t.filter(l=>this.hasUniqueProperty(l,s)?!r.has(l[s]):!0);return[...n,...o]}hasUniqueProperty(t,n){return t&&typeof t=="object"&&n in t}async setRememberedState(t){const n=await xt.getState(xt.rememberedState,{});this.requestParams.all().preserveState&&n&&t.component===Ve.get().component&&(t.rememberedState=n)}getScopedErrors(t){return this.requestParams.all().errorBag?t[this.requestParams.all().errorBag||""]||{}:t}},r0=class mC{constructor(t,n,{optimistic:r=!1}={}){Re(this,"page");Re(this,"response");Re(this,"cancelToken");Re(this,"requestParams");Re(this,"requestHasFinished",!1);Re(this,"optimistic");this.page=n,this.requestParams=rf.create(t),this.cancelToken=new AbortController,this.optimistic=r}static create(t,n,r){return new mC(t,n,r)}isPrefetch(){return this.requestParams.isPrefetch()}getUrl(){return this.requestParams.all().url}isOptimistic(){return this.optimistic}isPendingOptimistic(){return this.isOptimistic()&&(!this.response||!this.response.isProcessed())}async send(){this.requestParams.onCancelToken(()=>{this.response||this.cancel({cancelled:!0})}),PM(this.requestParams.all()),this.requestParams.onStart(),this.requestParams.all().prefetch&&(this.requestParams.onPrefetching(),VM(this.requestParams.all()));const t=this.requestParams.all().prefetch,n={method:this.requestParams.all().method,url:zf(this.requestParams.all().url).href,data:this.requestParams.data(),signal:this.cancelToken.signal,headers:this.getHeaders(),onUploadProgress:this.onProgress.bind(this)},r=await m1.processRequest(this.requestParams.all(),n);return Dm.getClient().request(r).then(s=>(this.response=n0.create(this.requestParams,s,this.page),this.response.handle())).catch(s=>s instanceof ic?(this.response=n0.create(this.requestParams,s.response,this.page),this.response.handle()):Promise.reject(s)).catch(s=>{if(!(s instanceof ac)&&this.requestParams.all().onNetworkError(s)!==!1&&SM(s))return t&&this.requestParams.onPrefetchError(s),Promise.reject(s)}).finally(()=>{this.finish(),t&&this.response&&this.requestParams.onPrefetchResponse(this.response)})}finish(){this.requestParams.wasCancelledAtAll()||(this.requestParams.markAsFinished(),this.fireFinishEvents())}fireFinishEvents(){this.requestHasFinished||(this.requestHasFinished=!0,EM(this.requestParams.all()),this.requestParams.onFinish())}cancel({cancelled:t=!1,interrupted:n=!1}){this.requestHasFinished||(this.cancelToken.abort(),this.requestParams.markAsCancelled({cancelled:t,interrupted:n}),this.fireFinishEvents())}onProgress(t){this.requestParams.data()instanceof FormData&&(AM(t),this.requestParams.all().onProgress(t))}getHeaders(){const t={...this.requestParams.headers(),Accept:"text/html, application/xhtml+xml","X-Requested-With":"XMLHttpRequest","X-Inertia":!0},n=Ve.get();n.version&&(t["X-Inertia-Version"]=n.version);const r=Object.entries(n.onceProps||{}).filter(([,s])=>qt(n.props,s.prop)===void 0?!1:!s.expiresAt||s.expiresAt>Date.now()).map(([s])=>s);return r.length>0&&(t["X-Inertia-Except-Once-Props"]=r.join(",")),t}},s0=class{constructor({maxConcurrent:e,interruptible:t}){Re(this,"requests",[]);Re(this,"maxConcurrent");Re(this,"interruptible");this.maxConcurrent=e,this.interruptible=t}send(e){this.requests.push(e),e.send().finally(()=>{this.requests=this.requests.filter(t=>t!==e)})}interruptInFlight(){this.cancel({interrupted:!0},!1)}cancelInFlight(e={}){const t=typeof e=="function"?e:n=>{const{prefetch:r=!0,optimistic:s=!0}=e;return(r||!n.isPrefetch())&&(s||!n.isOptimistic())};this.requests.filter(t).forEach(n=>n.cancel({cancelled:!0}))}cancel({cancelled:e=!1,interrupted:t=!1}={},n=!1){if(!n&&!this.shouldCancel())return;const r=this.requests.shift();r==null||r.cancel({cancelled:e,interrupted:t})}shouldCancel(){return this.interruptible&&this.requests.length>=this.maxConcurrent}hasPendingOptimistic(){return this.requests.some(e=>e.isPendingOptimistic())}},sr=()=>{},$D=class{constructor(){Re(this,"syncRequestStream",new s0({maxConcurrent:1,interruptible:!0}));Re(this,"asyncRequestStream",new s0({maxConcurrent:1/0,interruptible:!1}));Re(this,"clientVisitQueue",new Mm);Re(this,"pendingOptimisticCallback")}init({initialPage:e,resolveComponent:t,swapComponent:n,onFlash:r}){Ve.init({initialPage:e,resolveComponent:t,swapComponent:n,onFlash:r}),aD.handle(),ws.init(),ws.on("missingHistoryItem",()=>{typeof window<"u"&&this.visit(window.location.href,{preserveState:!0,preserveScroll:!0,replace:!0})}),ws.on("loadDeferredProps",s=>{this.loadDeferredProps(s)}),ws.on("historyQuotaExceeded",s=>{window.location.href=s})}optimistic(e){return this.pendingOptimisticCallback=e,this}get(e,t={},n={}){return this.visit(e,{...n,method:"get",data:t})}post(e,t={},n={}){return this.visit(e,{preserveState:!0,...n,method:"post",data:t})}put(e,t={},n={}){return this.visit(e,{preserveState:!0,...n,method:"put",data:t})}patch(e,t={},n={}){return this.visit(e,{preserveState:!0,...n,method:"patch",data:t})}delete(e,t={}){return this.visit(e,{preserveState:!0,...t,method:"delete"})}reload(e={}){return this.doReload(e)}doReload(e={}){if(!(typeof window>"u"))return this.visit(window.location.href,{...e,preserveScroll:!0,preserveState:!0,async:!0,headers:{...e.headers||{},"Cache-Control":"no-cache"}})}remember(e,t="default"){xt.remember(e,t)}restore(e="default"){return xt.restore(e)}on(e,t){return typeof window>"u"?()=>{}:ws.onGlobalEvent(e,t)}once(e,t){if(typeof window>"u")return()=>{};const n=this.on(e,r=>(n(),t(r)));return n}hasPendingOptimistic(){return this.asyncRequestStream.hasPendingOptimistic()}get activePolls(){return Z_.count}cancelAll({async:e=!0,prefetch:t=!0,sync:n=!0}={}){e&&this.asyncRequestStream.cancelInFlight({prefetch:t}),n&&this.syncRequestStream.cancelInFlight()}poll(e,t={},n={}){return Z_.add(e,({onStart:r,onFinish:s})=>{const o=typeof t=="function"?t():t;this.doReload({poll:!0,preserveErrors:!0,...o,onCancelToken:l=>{var a;r(l.cancel),(a=o.onCancelToken)==null||a.call(o,l)},onFinish:l=>{var a;s(),(a=o.onFinish)==null||a.call(o,l)}})},{autoStart:n.autoStart??!0,keepAlive:n.keepAlive??!1,mode:n.mode})}visit(e,t={}){t.optimistic=t.optimistic??this.pendingOptimisticCallback,this.pendingOptimisticCallback=void 0,t.optimistic&&(t.async=t.async??!0);const n=this.getPendingVisit(e,{...t,showProgress:t.showProgress??(!t.async||!!t.optimistic)}),r=this.getVisitEvents(t);if(r.onBefore(n)===!1||!K_(n))return;const s=Lr(Ve.get().url);(n.only.length>0||n.except.length>0||n.reset.length>0?mv(n.url,s):Kf(n.url,s))||this.asyncRequestStream.cancelInFlight(c=>!c.isPrefetch()&&!c.isOptimistic()&&mv(c.getUrl(),s)),n.async||this.syncRequestStream.interruptInFlight(),t.optimistic&&this.applyOptimisticUpdate(t.optimistic,r),!Ve.isCleared()&&!n.preserveUrl&&ur.save();const a={...n,...r},u=()=>{const c=hs.get(a);c?(xr.reveal(c.inFlight),hs.use(c,a)):(xr.reveal(!0),(n.async?this.asyncRequestStream:this.syncRequestStream).send(r0.create(a,Ve.get(),{optimistic:!!t.optimistic})))};Array.isArray(n.component)&&(console.error(`The "component" prop received an array of components (${n.component.join(", ")}), but only a single component string is supported for instant visits. Pass an explicit component name instead.`),n.component=null),n.component?xt.processQueue().then(()=>{this.performInstantSwap(n).then(()=>{a.preserveScroll=!0,a.preserveState=!0,a.replace=!0,a.viewTransition=!1,u()})}):u()}getCached(e,t={}){return hs.findCached(this.getPrefetchParams(e,t))}flush(e,t={}){hs.remove(this.getPrefetchParams(e,t))}flushAll(){hs.removeAll()}flushByCacheTags(e){hs.removeByTags(Array.isArray(e)?e:[e])}getPrefetching(e,t={}){return hs.findInFlight(this.getPrefetchParams(e,t))}prefetch(e,t={},n={}){if((t.method??(Qr(e)?e.method:"get"))!=="get")throw new Error("Prefetch requests must use the GET method");const s=this.getPendingVisit(e,{...t,async:!0,showProgress:!1,prefetch:!0,viewTransition:!1}),o=s.url.origin+s.url.pathname+s.url.search,l=window.location.origin+window.location.pathname+window.location.search;if(o===l)return;const a=this.getVisitEvents(t);if(a.onBefore(s)===!1||!K_(s))return;xr.hide(),this.asyncRequestStream.interruptInFlight();const u={...s,...a};new Promise(d=>{const f=()=>{Ve.get()?d():setTimeout(f,50)};f()}).then(()=>{hs.add(u,d=>{this.asyncRequestStream.send(r0.create(d,Ve.get()))},{cacheFor:ei.get("prefetch.cacheFor"),cacheTags:[],...n})})}clearHistory(){xt.clear()}decryptHistory(){return xt.decrypt()}resolveComponent(e,t){return Ve.resolve(e,t)}replace(e){this.clientVisit(e,{replace:!0})}replaceProp(e,t,n){this.replace({preserveScroll:!0,preserveState:!0,props(r){const s=typeof t=="function"?t(qt(r,e),r):t;return HM(r,e,s)},...n||{}})}appendToProp(e,t,n){this.replaceProp(e,(r,s)=>{const o=typeof t=="function"?t(r,s):t;return Array.isArray(r)||(r=r!==void 0?[r]:[]),[...r,o]},n)}prependToProp(e,t,n){this.replaceProp(e,(r,s)=>{const o=typeof t=="function"?t(r,s):t;return Array.isArray(r)||(r=r!==void 0?[r]:[]),[o,...r]},n)}push(e){this.clientVisit(e)}flash(e,t){const n=Ve.get().flash;let r;if(typeof e=="function")r=e(n);else if(typeof e=="string")r={...n,[e]:t};else if(e&&Object.keys(e).length)r={...n,...e};else return;Ve.setFlash(r),Object.keys(r).length&&jf(r)}clientVisit(e,{replace:t=!1}={}){this.clientVisitQueue.add(()=>this.performClientVisit(e,{replace:t}))}performClientVisit(e,{replace:t=!1}={}){const n=Ve.get(),r=typeof e.props=="function"?Object.fromEntries(Object.values(n.onceProps??{}).map(w=>[w.prop,qt(n.props,w.prop)])):{},s=typeof e.props=="function"?e.props(n.props,r):e.props??n.props,o=typeof e.flash=="function"?e.flash(n.flash):e.flash,{viewTransition:l,onError:a,onFinish:u,onFlash:c,onSuccess:d,...f}=e,p={...n,...f,flash:o??{},props:s},h=rf.resolvePreserveOption(e.preserveScroll??!1,p),b=rf.resolvePreserveOption(e.preserveState??!1,p),x=this.createVisitId();return Ve.set(p,{replace:t,preserveScroll:h,preserveState:b,viewTransition:l,visitId:x}).then(()=>{TM(Ve.get(),{replace:t,visitId:x});const w=Ve.get().flash;Object.keys(w).length>0&&(jf(w),c==null||c(w));const k=Ve.get().props.errors||{};if(Object.keys(k).length===0){d==null||d(Ve.get());return}const C=e.errorBag?k[e.errorBag||""]||{}:k;a==null||a(C)}).finally(()=>u==null?void 0:u(e))}performInstantSwap(e){const t=Ve.get(),n=Object.fromEntries((t.sharedProps??[]).filter(a=>a in t.props).map(a=>[a,t.props[a]])),r=typeof e.pageProps=="function"?e.pageProps(Tt(t.props),Tt(n)):e.pageProps,s=r!==null?{...r}:{...n},o=this.preserveOncePropsOnInstantVisit(t,s),l={component:e.component,url:e.url.pathname+e.url.search+e.url.hash,version:t.version,props:{...s,errors:{}},flash:{},rescuedProps:[],clearHistory:!1,encryptHistory:t.encryptHistory,sharedProps:t.sharedProps,onceProps:o,rememberedState:{}};return Ve.set(l,{replace:e.replace,preserveScroll:rf.resolvePreserveOption(e.preserveScroll,l),preserveState:!1,viewTransition:e.viewTransition,visitId:e.id})}preserveOncePropsOnInstantVisit(e,t){const n={};return Object.entries(e.onceProps??{}).forEach(([r,s])=>{if(qt(t,s.prop)!==void 0)return;const o=qt(e.props,s.prop);o!==void 0&&(fr(t,s.prop,o),n[r]=s)}),n}getPrefetchParams(e,t){return{...this.getPendingVisit(e,{...t,async:!0,showProgress:!1,prefetch:!0,viewTransition:!1}),...this.getVisitEvents(t)}}createVisitId(){return nf()}getPendingVisit(e,t){if(Qr(e)){const u=e;e=u.url,t.method=t.method??u.method}const n=ei.get("visitOptions"),r=n?n(e.toString(),Tt(t))||{}:{},s={method:"get",data:{},replace:!1,preserveScroll:!1,preserveState:!1,only:[],except:[],headers:{},errorBag:"",forceFormData:!1,queryStringArrayFormat:"brackets",async:!1,showProgress:!0,fresh:!1,reset:[],preserveUrl:!1,preserveErrors:!1,prefetch:!1,invalidateCacheTags:[],viewTransition:!1,component:null,pageProps:null,cached:!1,...G_(t),...G_(r)},[o,l]=nD(e,s.data,s.method,s.forceFormData,s.queryStringArrayFormat),a={id:this.createVisitId(),cancelled:!1,completed:!1,interrupted:!1,...s,url:o,data:l};return a.prefetch&&(a.headers.Purpose="prefetch"),a}getVisitEvents(e){return{onCancelToken:e.onCancelToken||sr,onBefore:e.onBefore||sr,onBeforeUpdate:e.onBeforeUpdate||sr,onStart:e.onStart||sr,onProgress:e.onProgress||sr,onFinish:e.onFinish||sr,onCancel:e.onCancel||sr,onSuccess:e.onSuccess||sr,onError:e.onError||sr,onHttpException:e.onHttpException||sr,onNetworkError:e.onNetworkError||sr,onFlash:e.onFlash||sr,onPrefetched:e.onPrefetched||sr,onPrefetching:e.onPrefetching||sr}}applyOptimisticUpdate(e,t){const n=Ve.get().props,r=e(Tt(n));if(!r)return;const s=[];for(const d of Object.keys(r))ts(n[d],r[d])||s.push(d);if(s.length===0)return;const o=Ve.nextOptimisticId(),l=Ve.get().component;for(const d of s)Ve.setBaseline(d,Tt(n[d]));Ve.registerOptimistic(o,e),Ve.setPropsQuietly({...n,...r});let a=!0;const u=t.onSuccess;t.onSuccess=d=>(a=!1,u(d));const c=t.onFinish;t.onFinish=d=>{if(Ve.unregisterOptimistic(o),a&&Ve.get().component===l){const f=Ve.replayOptimistics();Object.keys(f).length>0&&Ve.setPropsQuietly({...Ve.get().props,...f})}return Ve.pendingOptimisticCount()===0&&Ve.clearOptimisticState(),c(d)}}loadDeferredProps(e){e&&Object.values(e).forEach(t=>{this.doReload({only:t,deferredProps:!0,preserveErrors:!0})})}},ca=class{static createWayfinderCallback(...e){return()=>e.length===1?Qr(e[0])?e[0]:e[0]():{method:typeof e[0]=="function"?e[0]():e[0],url:typeof e[1]=="function"?e[1]():e[1]}}static parseUseFormArguments(...e){return e.length===0?{rememberKey:null,data:{},precognitionEndpoint:null}:e.length===1?{rememberKey:null,data:e[0],precognitionEndpoint:null}:e.length===2?typeof e[0]=="string"?{rememberKey:e[0],data:e[1],precognitionEndpoint:null}:{rememberKey:null,data:e[1],precognitionEndpoint:this.createWayfinderCallback(e[0])}:{rememberKey:null,data:e[2],precognitionEndpoint:this.createWayfinderCallback(e[0],e[1])}}static parseSubmitArguments(e,t){return e.length===3||e.length===2&&typeof e[0]=="string"?{method:e[0],url:e[1],options:e[2]??{}}:Qr(e[0])?{...e[0],options:e[1]??{}}:{...t(),options:e[0]??{}}}static mergeHeadersForValidation(e,t,n){const r=s=>(s.headers={...n??{},...s.headers??{}},s);return e&&typeof e=="object"&&!("target"in e)?e=r(e):t&&typeof t=="object"?t=r(t):typeof e=="string"?t=r(t??{}):e=r(e??{}),[e,t]}};function CD(e){if(!e.includes("."))return e;const t=n=>n.startsWith("[")&&n.endsWith("]")?n:n.split(".").reduce((r,s,o)=>o===0?s:`${r}[${s}]`);return e.replace(/\\\./g,"__ESCAPED_DOT__").split(/(\[[^\]]*\])/).filter(Boolean).map(t).join("").replace(/__ESCAPED_DOT__/g,".")}function SD(e){const t=[],n=/([^\[\]]+)|\[(\d*)\]/g;let r;for(;(r=n.exec(e))!==null;)r[1]!==void 0?t.push(r[1]):r[2]!==void 0&&t.push(r[2]===""?"":Number(r[2]));return t}function ED(e,t,n){let r=e;for(let s=0;s/^\d+$/.test(r)).map(Number).sort((r,s)=>r-s);return t.length===n.length&&n.length>0&&n[0]===0&&n.every((r,s)=>r===s)}function of(e){if(Array.isArray(e))return e.map(of);if(typeof e!="object"||e===null||d1(e))return e;if(OD(e)){const n=[];for(let r=0;r/^\d+$/.test(u)).map(Number).sort((u,c)=>u-c);fr(t,o,a.length>0?[...a.map(u=>l[u]),r]:[r])}else fr(t,o,[r]);continue}ED(t,s.map(String),r)}return of(t)}var Za="server";function TD(e,t){return e.match(/\sdata-inertia(=|\s|>)/)?e:e.replace(/^<([a-zA-Z][^\s/>]*)/,`<$1 data-inertia="server-head-${t}"`)}function l0(e,t){if(!t)return[];const n=typeof t=="function"?t(e):e.props[t===!0?"head":t];return Array.isArray(n)?n.map(r=>typeof r=="string"?r.trim():r).filter(r=>typeof r=="string"&&r.length>0).map(TD):[]}var AD={buildDOMElement(e){const t=document.createElement("template");t.innerHTML=e;const n=t.content.firstChild;if(!e.startsWith(" + + diff --git a/packages/bundles/resources/js/components/BundleCard.vue b/packages/bundles/resources/js/components/BundleCard.vue new file mode 100644 index 0000000000..c0e12ae830 --- /dev/null +++ b/packages/bundles/resources/js/components/BundleCard.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/bundles/resources/js/components/BundleEditor.vue b/packages/bundles/resources/js/components/BundleEditor.vue new file mode 100644 index 0000000000..a54e65e24e --- /dev/null +++ b/packages/bundles/resources/js/components/BundleEditor.vue @@ -0,0 +1,375 @@ + + + diff --git a/packages/bundles/resources/js/components/ComponentList.vue b/packages/bundles/resources/js/components/ComponentList.vue new file mode 100644 index 0000000000..5a1c2f6fdb --- /dev/null +++ b/packages/bundles/resources/js/components/ComponentList.vue @@ -0,0 +1,99 @@ + + + diff --git a/packages/bundles/resources/js/components/GroupsEditor.vue b/packages/bundles/resources/js/components/GroupsEditor.vue new file mode 100644 index 0000000000..0b1f07a280 --- /dev/null +++ b/packages/bundles/resources/js/components/GroupsEditor.vue @@ -0,0 +1,161 @@ + + + diff --git a/packages/bundles/resources/js/components/IncludedInBundlesCard.vue b/packages/bundles/resources/js/components/IncludedInBundlesCard.vue new file mode 100644 index 0000000000..fc8cc8dd15 --- /dev/null +++ b/packages/bundles/resources/js/components/IncludedInBundlesCard.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/bundles/resources/js/components/VariantBundleList.vue b/packages/bundles/resources/js/components/VariantBundleList.vue new file mode 100644 index 0000000000..620a2c7551 --- /dev/null +++ b/packages/bundles/resources/js/components/VariantBundleList.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/bundles/resources/js/components/VariantThumb.vue b/packages/bundles/resources/js/components/VariantThumb.vue new file mode 100644 index 0000000000..d90b15df80 --- /dev/null +++ b/packages/bundles/resources/js/components/VariantThumb.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/bundles/resources/js/types.ts b/packages/bundles/resources/js/types.ts new file mode 100644 index 0000000000..c24f659ab6 --- /dev/null +++ b/packages/bundles/resources/js/types.ts @@ -0,0 +1,82 @@ +export interface VariantRow { + id: number; + public_id: string; + sku: string | null; + name: string; + option: string | null; + thumbnail: string | null; + edit_url: string; +} + +export interface BundleComponentRow { + id: number; + public_id: string; + group_id: number | null; + quantity: number; + default: boolean; + position: number; + variant: VariantRow; +} + +export interface BundleGroupRow { + id: number; + public_id: string; + name: Record; + label: string; + min_selections: number; + max_selections: number; + position: number; +} + +export interface DerivedPrice { + currency: string; + price: string | null; + list_price: string | null; + missing: string[]; +} + +export interface BundleDefinition { + id: number; + pricing: 'fixed' | 'components'; + discount_percentage: number | null; + configurable: boolean; + components: BundleComponentRow[]; + groups: BundleGroupRow[]; + availability: { available: number; unlimited: boolean }; + prices: DerivedPrice[]; +} + +export interface BundleSummary { + variant: VariantRow; + defined: boolean; + bundle: BundleDefinition | null; + default_language: string; + urls: { + show: string; + define: string; + destroy: string; + components: string; + groups: string; + search: string; + }; +} + +/** What the sync endpoints accept; every call sends the complete list. */ +export interface ComponentPayload { + variant_id: number; + quantity: number; + group_id: number | null; + default: boolean; + position: number; +} + +export interface GroupPayload { + id: number | null; + name: Record; + min_selections: number; + max_selections: number; + position: number; +} + +export const panelPath = (props: Record): string => + (props.panel as { path?: string } | undefined)?.path ?? 'panel'; diff --git a/packages/bundles/resources/lang/ar/bundles.php b/packages/bundles/resources/lang/ar/bundles.php index a204eb0e5d..ddba2103ca 100644 --- a/packages/bundles/resources/lang/ar/bundles.php +++ b/packages/bundles/resources/lang/ar/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'المكوّن ":identifier" ليس له سعر بعملة :currency، لذلك لا يمكن تسعير الحزمة بهذه العملة.', ], + 'panel' => [ + 'column_bundle' => 'حزمة', + 'badge_bundle' => 'حزمة', + 'filter_label' => 'الحزم', + 'filter_only' => 'الحزم فقط', + 'not_defined' => 'هذا المتغير لا يُباع كحزمة.', + 'card_title' => 'حزمة', + 'card_description' => 'بيع هذا المتغير كمجموعة من متغيرات أخرى. يُشتق المخزون من المكوّنات، ويُجهَّز كل مكوّن ويُنفَّذ من الطلب.', + 'sell_as_bundle' => 'البيع كحزمة', + 'confirm_remove_title' => 'إيقاف البيع كحزمة؟', + 'confirm_remove' => 'ستُحذف قائمة المكوّنات ومجموعات الخيارات. يحتفظ المتغير بأسعاره ويعود متغيرًا عاديًا.', + 'pricing' => 'التسعير', + 'pricing_fixed' => 'سعر ثابت', + 'pricing_components' => 'مجموع المكوّنات', + 'discount_percentage' => 'الخصم', + 'discount_hint' => 'يُخصم من إجمالي المكوّنات.', + 'derived_note' => 'في هذا الوضع تُشتق الأسعار من المكوّنات وتُستبدل عند كل إعادة تسعير للحزمة. بدّل إلى السعر الثابت لتحديدها يدويًا.', + 'components' => 'المكوّنات', + 'components_description' => 'مضمّنة دائمًا، بهذا الترتيب.', + 'components_empty' => 'لا توجد مكوّنات بعد. أضف مكوّنًا واحدًا على الأقل ليصبح بيع الحزمة ممكنًا.', + 'add_component' => 'إضافة مكوّن', + 'quantity' => 'الكمية', + 'move_up' => 'نقل لأعلى', + 'move_down' => 'نقل لأسفل', + 'remove' => 'إزالة', + 'availability' => 'التوفر', + 'availability_count' => 'يمكن بيع {count} من مخزون المكوّنات.', + 'availability_unlimited' => 'غير مقيّد بمخزون المكوّنات.', + 'availability_none' => 'غير متوفر: أحد المكوّنات غير متاح.', + 'derived_prices' => 'الأسعار المشتقة', + 'price' => 'السعر', + 'list_price' => 'السعر السابق', + 'missing_price' => 'لا يوجد سعر لـ: {components}', + 'groups' => 'مجموعات الخيارات', + 'groups_description' => 'دع المتسوق يختار من مجموعة من المكوّنات. الحزمة بلا مجموعات تكون ثابتة.', + 'add_group' => 'إضافة مجموعة', + 'group_name' => 'اسم المجموعة', + 'group_min' => 'الحد الأدنى', + 'group_max' => 'الحد الأقصى', + 'group_default' => 'افتراضي', + 'group_empty' => 'لا توجد خيارات في هذه المجموعة بعد.', + 'add_option' => 'إضافة خيار', + 'remove_group' => 'إزالة المجموعة', + 'confirm_remove_group' => 'إزالة هذه المجموعة وخياراتها؟', + 'new_group_name' => 'مجموعة جديدة', + 'saved' => 'تم حفظ الحزمة.', + 'removed' => 'تمت إزالة الحزمة.', + 'load_failed' => 'تعذّر تحميل الحزمة.', + 'variant_summary_description' => 'تُعرَّف الحزم لكل متغير على حدة. افتح متغيرًا لتعديل مكوّناته.', + 'variant_component_count' => '{count} مكوّن | {count} مكوّنات', + 'variant_not_bundle' => 'ليست حزمة', + 'edit_variant' => 'تعديل المتغير', + 'included_in_title' => 'مضمّن في الحزم', + 'included_in_description' => 'حذف متغير يزيله من هذه الحزم.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'تمت إعادة تسعير :count حزمة.', ], diff --git a/packages/bundles/resources/lang/bg/bundles.php b/packages/bundles/resources/lang/bg/bundles.php index c18aa9a218..df05bd9221 100644 --- a/packages/bundles/resources/lang/bg/bundles.php +++ b/packages/bundles/resources/lang/bg/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Компонентът ":identifier" няма цена в :currency, затова комплектът не може да бъде оценен в тази валута.', ], + 'panel' => [ + 'column_bundle' => 'Комплект', + 'badge_bundle' => 'Комплект', + 'filter_label' => 'Комплекти', + 'filter_only' => 'Само комплекти', + 'not_defined' => 'Този вариант не се продава като комплект.', + 'card_title' => 'Комплект', + 'card_description' => 'Продавайте този вариант като набор от други варианти. Наличността се извежда от компонентите и всеки компонент се събира и изпълнява от поръчката.', + 'sell_as_bundle' => 'Продавай като комплект', + 'confirm_remove_title' => 'Да се спре продажбата като комплект?', + 'confirm_remove' => 'Списъкът с компоненти и групите с опции ще бъдат премахнати. Вариантът запазва цените си и отново става обикновен вариант.', + 'pricing' => 'Ценообразуване', + 'pricing_fixed' => 'Фиксирана цена', + 'pricing_components' => 'Сума на компонентите', + 'discount_percentage' => 'Отстъпка', + 'discount_hint' => 'Приспада се от сумата на компонентите.', + 'derived_note' => 'В този режим цените се извеждат от компонентите и се презаписват при всяко преизчисляване на комплекта. Превключете на фиксирана цена, за да ги зададете ръчно.', + 'components' => 'Компоненти', + 'components_description' => 'Винаги включени, в този ред.', + 'components_empty' => 'Все още няма компоненти. Добавете поне един, за да може комплектът да се продава.', + 'add_component' => 'Добави компонент', + 'quantity' => 'Кол.', + 'move_up' => 'Премести нагоре', + 'move_down' => 'Премести надолу', + 'remove' => 'Премахни', + 'availability' => 'Наличност', + 'availability_count' => 'От наличността на компонентите могат да се продадат {count}.', + 'availability_unlimited' => 'Не е ограничено от наличността на компонентите.', + 'availability_none' => 'Изчерпано: един компонент не е наличен.', + 'derived_prices' => 'Изведени цени', + 'price' => 'Цена', + 'list_price' => 'Преди', + 'missing_price' => 'Няма цена за: {components}', + 'groups' => 'Групи с опции', + 'groups_description' => 'Позволете на клиента да избира от набор компоненти. Комплект без групи е фиксиран.', + 'add_group' => 'Добави група', + 'group_name' => 'Име на групата', + 'group_min' => 'Мин.', + 'group_max' => 'Макс.', + 'group_default' => 'По подразбиране', + 'group_empty' => 'Все още няма опции в тази група.', + 'add_option' => 'Добави опция', + 'remove_group' => 'Премахни групата', + 'confirm_remove_group' => 'Да се премахне тази група и опциите ѝ?', + 'new_group_name' => 'Нова група', + 'saved' => 'Комплектът е запазен.', + 'removed' => 'Комплектът е премахнат.', + 'load_failed' => 'Комплектът не можа да бъде зареден.', + 'variant_summary_description' => 'Комплектите се дефинират за всеки вариант. Отворете вариант, за да редактирате компонентите му.', + 'variant_component_count' => '{count} компонент | {count} компонента', + 'variant_not_bundle' => 'Не е комплект', + 'edit_variant' => 'Редактирай варианта', + 'included_in_title' => 'Включен в комплекти', + 'included_in_description' => 'Изтриването на вариант го премахва от тези комплекти.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Преизчислени са цените на :count комплекта.', ], diff --git a/packages/bundles/resources/lang/de/bundles.php b/packages/bundles/resources/lang/de/bundles.php index 8c188ecb9c..cc22984184 100644 --- a/packages/bundles/resources/lang/de/bundles.php +++ b/packages/bundles/resources/lang/de/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Komponente ":identifier" hat keinen Preis in :currency, daher kann das Bundle in dieser Währung nicht bepreist werden.', ], + 'panel' => [ + 'column_bundle' => 'Bundle', + 'badge_bundle' => 'Bundle', + 'filter_label' => 'Bundles', + 'filter_only' => 'Nur Bundles', + 'not_defined' => 'Diese Variante wird nicht als Bundle verkauft.', + 'card_title' => 'Bundle', + 'card_description' => 'Diese Variante als Zusammenstellung anderer Varianten verkaufen. Der Bestand wird aus den Komponenten abgeleitet und jede Komponente wird aus der Bestellung kommissioniert und erfüllt.', + 'sell_as_bundle' => 'Als Bundle verkaufen', + 'confirm_remove_title' => 'Nicht mehr als Bundle verkaufen?', + 'confirm_remove' => 'Die Komponentenliste und die Optionsgruppen werden entfernt. Die Variante behält ihre Preise und wird wieder eine gewöhnliche Variante.', + 'pricing' => 'Preisgestaltung', + 'pricing_fixed' => 'Festpreis', + 'pricing_components' => 'Summe der Komponenten', + 'discount_percentage' => 'Rabatt', + 'discount_hint' => 'Wird von der Komponentensumme abgezogen.', + 'derived_note' => 'Preise in diesem Modus werden aus den Komponenten abgeleitet und bei jeder Neuberechnung überschrieben. Wechseln Sie zum Festpreis, um sie manuell festzulegen.', + 'components' => 'Komponenten', + 'components_description' => 'Immer enthalten, in dieser Reihenfolge.', + 'components_empty' => 'Noch keine Komponenten. Fügen Sie mindestens eine hinzu, damit das Bundle verkauft werden kann.', + 'add_component' => 'Komponente hinzufügen', + 'quantity' => 'Menge', + 'move_up' => 'Nach oben', + 'move_down' => 'Nach unten', + 'remove' => 'Entfernen', + 'availability' => 'Verfügbarkeit', + 'availability_count' => '{count} können aus dem Komponentenbestand verkauft werden.', + 'availability_unlimited' => 'Nicht durch den Komponentenbestand begrenzt.', + 'availability_none' => 'Nicht auf Lager: eine Komponente ist nicht verfügbar.', + 'derived_prices' => 'Abgeleitete Preise', + 'price' => 'Preis', + 'list_price' => 'Vorher', + 'missing_price' => 'Kein Preis für: {components}', + 'groups' => 'Optionsgruppen', + 'groups_description' => 'Lassen Sie Kunden aus einer Auswahl von Komponenten wählen. Ein Bundle ohne Gruppen ist fest.', + 'add_group' => 'Gruppe hinzufügen', + 'group_name' => 'Gruppenname', + 'group_min' => 'Min.', + 'group_max' => 'Max.', + 'group_default' => 'Standard', + 'group_empty' => 'Noch keine Optionen in dieser Gruppe.', + 'add_option' => 'Option hinzufügen', + 'remove_group' => 'Gruppe entfernen', + 'confirm_remove_group' => 'Diese Gruppe und ihre Optionen entfernen?', + 'new_group_name' => 'Neue Gruppe', + 'saved' => 'Bundle gespeichert.', + 'removed' => 'Bundle entfernt.', + 'load_failed' => 'Das Bundle konnte nicht geladen werden.', + 'variant_summary_description' => 'Bundles werden pro Variante definiert. Öffnen Sie eine Variante, um ihre Komponenten zu bearbeiten.', + 'variant_component_count' => '{count} Komponente | {count} Komponenten', + 'variant_not_bundle' => 'Kein Bundle', + 'edit_variant' => 'Variante bearbeiten', + 'included_in_title' => 'Enthalten in Bundles', + 'included_in_description' => 'Beim Löschen einer Variante wird sie aus diesen Bundles entfernt.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count Bundles neu bepreist.', ], diff --git a/packages/bundles/resources/lang/en/bundles.php b/packages/bundles/resources/lang/en/bundles.php index 9d76b44e91..d7596091e5 100644 --- a/packages/bundles/resources/lang/en/bundles.php +++ b/packages/bundles/resources/lang/en/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Component ":identifier" has no price in :currency, so the bundle cannot be priced in that currency.', ], + 'panel' => [ + 'column_bundle' => 'Bundle', + 'badge_bundle' => 'Bundle', + 'filter_label' => 'Bundles', + 'filter_only' => 'Bundles only', + 'not_defined' => 'This variant is not sold as a bundle.', + 'card_title' => 'Bundle', + 'card_description' => 'Sell this variant as a set of other variants. Stock is derived from the components and each component is picked and fulfilled from the order.', + 'sell_as_bundle' => 'Sell as a bundle', + 'confirm_remove_title' => 'Stop selling as a bundle?', + 'confirm_remove' => 'The component list and option groups are removed. The variant keeps its prices and becomes an ordinary variant.', + 'pricing' => 'Pricing', + 'pricing_fixed' => 'Fixed price', + 'pricing_components' => 'Sum of components', + 'discount_percentage' => 'Discount', + 'discount_hint' => 'Taken off the component total.', + 'derived_note' => 'Prices in this mode are derived from the components and overwritten whenever the bundle is repriced. Switch to a fixed price to set them by hand.', + 'components' => 'Components', + 'components_description' => 'Always included, in this order.', + 'components_empty' => 'No components yet. Add at least one to make the bundle sellable.', + 'add_component' => 'Add component', + 'quantity' => 'Qty', + 'move_up' => 'Move up', + 'move_down' => 'Move down', + 'remove' => 'Remove', + 'availability' => 'Availability', + 'availability_count' => '{count} can be sold from component stock.', + 'availability_unlimited' => 'Not limited by component stock.', + 'availability_none' => 'Out of stock: a component is unavailable.', + 'derived_prices' => 'Derived prices', + 'price' => 'Price', + 'list_price' => 'Was', + 'missing_price' => 'No price for: {components}', + 'groups' => 'Option groups', + 'groups_description' => 'Let the shopper choose from a set of components. A bundle with no groups is fixed.', + 'add_group' => 'Add group', + 'group_name' => 'Group name', + 'group_min' => 'Min', + 'group_max' => 'Max', + 'group_default' => 'Default', + 'group_empty' => 'No options in this group yet.', + 'add_option' => 'Add option', + 'remove_group' => 'Remove group', + 'confirm_remove_group' => 'Remove this group and its options?', + 'new_group_name' => 'New group', + 'saved' => 'Bundle saved.', + 'removed' => 'Bundle removed.', + 'load_failed' => 'The bundle could not be loaded.', + 'variant_summary_description' => 'Bundles are defined per variant. Open a variant to edit its components.', + 'variant_component_count' => '{count} component | {count} components', + 'variant_not_bundle' => 'Not a bundle', + 'edit_variant' => 'Edit variant', + 'included_in_title' => 'Included in bundles', + 'included_in_description' => 'Deleting a variant removes it from these bundles.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Repriced :count bundles.', ], diff --git a/packages/bundles/resources/lang/es/bundles.php b/packages/bundles/resources/lang/es/bundles.php index 4b1db7e761..a813478554 100644 --- a/packages/bundles/resources/lang/es/bundles.php +++ b/packages/bundles/resources/lang/es/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'El componente ":identifier" no tiene precio en :currency, por lo que el paquete no se puede valorar en esa moneda.', ], + 'panel' => [ + 'column_bundle' => 'Paquete', + 'badge_bundle' => 'Paquete', + 'filter_label' => 'Paquetes', + 'filter_only' => 'Solo paquetes', + 'not_defined' => 'Esta variante no se vende como paquete.', + 'card_title' => 'Paquete', + 'card_description' => 'Vende esta variante como un conjunto de otras variantes. El stock se deriva de los componentes y cada componente se prepara y se entrega desde el pedido.', + 'sell_as_bundle' => 'Vender como paquete', + 'confirm_remove_title' => '¿Dejar de vender como paquete?', + 'confirm_remove' => 'Se eliminan la lista de componentes y los grupos de opciones. La variante conserva sus precios y vuelve a ser una variante normal.', + 'pricing' => 'Precio', + 'pricing_fixed' => 'Precio fijo', + 'pricing_components' => 'Suma de los componentes', + 'discount_percentage' => 'Descuento', + 'discount_hint' => 'Se resta del total de los componentes.', + 'derived_note' => 'En este modo los precios se derivan de los componentes y se sobrescriben cada vez que se recalcula el paquete. Cambia a precio fijo para definirlos a mano.', + 'components' => 'Componentes', + 'components_description' => 'Siempre incluidos, en este orden.', + 'components_empty' => 'Todavía no hay componentes. Añade al menos uno para que el paquete se pueda vender.', + 'add_component' => 'Añadir componente', + 'quantity' => 'Cant.', + 'move_up' => 'Subir', + 'move_down' => 'Bajar', + 'remove' => 'Quitar', + 'availability' => 'Disponibilidad', + 'availability_count' => 'Se pueden vender {count} con el stock de los componentes.', + 'availability_unlimited' => 'No está limitado por el stock de los componentes.', + 'availability_none' => 'Agotado: un componente no está disponible.', + 'derived_prices' => 'Precios derivados', + 'price' => 'Precio', + 'list_price' => 'Antes', + 'missing_price' => 'Sin precio para: {components}', + 'groups' => 'Grupos de opciones', + 'groups_description' => 'Deja que el cliente elija entre un conjunto de componentes. Un paquete sin grupos es fijo.', + 'add_group' => 'Añadir grupo', + 'group_name' => 'Nombre del grupo', + 'group_min' => 'Mín.', + 'group_max' => 'Máx.', + 'group_default' => 'Predeterminado', + 'group_empty' => 'Todavía no hay opciones en este grupo.', + 'add_option' => 'Añadir opción', + 'remove_group' => 'Quitar grupo', + 'confirm_remove_group' => '¿Quitar este grupo y sus opciones?', + 'new_group_name' => 'Nuevo grupo', + 'saved' => 'Paquete guardado.', + 'removed' => 'Paquete eliminado.', + 'load_failed' => 'No se pudo cargar el paquete.', + 'variant_summary_description' => 'Los paquetes se definen por variante. Abre una variante para editar sus componentes.', + 'variant_component_count' => '{count} componente | {count} componentes', + 'variant_not_bundle' => 'No es un paquete', + 'edit_variant' => 'Editar variante', + 'included_in_title' => 'Incluido en paquetes', + 'included_in_description' => 'Al eliminar una variante se quita de estos paquetes.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Se han recalculado los precios de :count paquetes.', ], diff --git a/packages/bundles/resources/lang/fa/bundles.php b/packages/bundles/resources/lang/fa/bundles.php index b110b2b36c..4976fc0bf5 100644 --- a/packages/bundles/resources/lang/fa/bundles.php +++ b/packages/bundles/resources/lang/fa/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'جزء ":identifier" قیمتی به :currency ندارد، بنابراین بسته با این ارز قابل قیمت‌گذاری نیست.', ], + 'panel' => [ + 'column_bundle' => 'بسته', + 'badge_bundle' => 'بسته', + 'filter_label' => 'بسته‌ها', + 'filter_only' => 'فقط بسته‌ها', + 'not_defined' => 'این گونه به‌صورت بسته فروخته نمی‌شود.', + 'card_title' => 'بسته', + 'card_description' => 'این گونه را به‌صورت مجموعه‌ای از گونه‌های دیگر بفروشید. موجودی از اجزا محاسبه می‌شود و هر جزء از سفارش آماده و تحویل می‌شود.', + 'sell_as_bundle' => 'فروش به‌صورت بسته', + 'confirm_remove_title' => 'فروش به‌صورت بسته متوقف شود؟', + 'confirm_remove' => 'فهرست اجزا و گروه‌های گزینه حذف می‌شوند. گونه قیمت‌های خود را نگه می‌دارد و دوباره یک گونه عادی می‌شود.', + 'pricing' => 'قیمت‌گذاری', + 'pricing_fixed' => 'قیمت ثابت', + 'pricing_components' => 'مجموع اجزا', + 'discount_percentage' => 'تخفیف', + 'discount_hint' => 'از مجموع اجزا کسر می‌شود.', + 'derived_note' => 'در این حالت قیمت‌ها از اجزا محاسبه می‌شوند و با هر بار قیمت‌گذاری مجدد بسته بازنویسی می‌شوند. برای تنظیم دستی به قیمت ثابت تغییر دهید.', + 'components' => 'اجزا', + 'components_description' => 'همیشه شامل می‌شوند، به این ترتیب.', + 'components_empty' => 'هنوز جزئی وجود ندارد. حداقل یک جزء اضافه کنید تا بسته قابل فروش شود.', + 'add_component' => 'افزودن جزء', + 'quantity' => 'تعداد', + 'move_up' => 'انتقال به بالا', + 'move_down' => 'انتقال به پایین', + 'remove' => 'حذف', + 'availability' => 'موجودی', + 'availability_count' => '{count} عدد از موجودی اجزا قابل فروش است.', + 'availability_unlimited' => 'محدود به موجودی اجزا نیست.', + 'availability_none' => 'ناموجود: یکی از اجزا در دسترس نیست.', + 'derived_prices' => 'قیمت‌های محاسبه‌شده', + 'price' => 'قیمت', + 'list_price' => 'قیمت قبلی', + 'missing_price' => 'قیمتی برای این موارد وجود ندارد: {components}', + 'groups' => 'گروه‌های گزینه', + 'groups_description' => 'به خریدار اجازه دهید از میان مجموعه‌ای از اجزا انتخاب کند. بسته بدون گروه، ثابت است.', + 'add_group' => 'افزودن گروه', + 'group_name' => 'نام گروه', + 'group_min' => 'حداقل', + 'group_max' => 'حداکثر', + 'group_default' => 'پیش‌فرض', + 'group_empty' => 'هنوز گزینه‌ای در این گروه وجود ندارد.', + 'add_option' => 'افزودن گزینه', + 'remove_group' => 'حذف گروه', + 'confirm_remove_group' => 'این گروه و گزینه‌هایش حذف شوند؟', + 'new_group_name' => 'گروه جدید', + 'saved' => 'بسته ذخیره شد.', + 'removed' => 'بسته حذف شد.', + 'load_failed' => 'بارگذاری بسته ممکن نشد.', + 'variant_summary_description' => 'بسته‌ها برای هر گونه جداگانه تعریف می‌شوند. برای ویرایش اجزا، یک گونه را باز کنید.', + 'variant_component_count' => '{count} جزء | {count} جزء', + 'variant_not_bundle' => 'بسته نیست', + 'edit_variant' => 'ویرایش گونه', + 'included_in_title' => 'موجود در بسته‌ها', + 'included_in_description' => 'حذف یک گونه آن را از این بسته‌ها حذف می‌کند.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'قیمت :count بسته دوباره محاسبه شد.', ], diff --git a/packages/bundles/resources/lang/fr/bundles.php b/packages/bundles/resources/lang/fr/bundles.php index 15fb83b6f5..2f93826403 100644 --- a/packages/bundles/resources/lang/fr/bundles.php +++ b/packages/bundles/resources/lang/fr/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Le composant ":identifier" n\'a pas de prix en :currency, le lot ne peut donc pas être tarifé dans cette devise.', ], + 'panel' => [ + 'column_bundle' => 'Lot', + 'badge_bundle' => 'Lot', + 'filter_label' => 'Lots', + 'filter_only' => 'Lots uniquement', + 'not_defined' => 'Cette variante n\'est pas vendue comme un lot.', + 'card_title' => 'Lot', + 'card_description' => 'Vendre cette variante comme un ensemble d\'autres variantes. Le stock est dérivé des composants et chaque composant est préparé et expédié depuis la commande.', + 'sell_as_bundle' => 'Vendre comme un lot', + 'confirm_remove_title' => 'Ne plus vendre comme un lot ?', + 'confirm_remove' => 'La liste des composants et les groupes d\'options sont supprimés. La variante conserve ses prix et redevient une variante ordinaire.', + 'pricing' => 'Tarification', + 'pricing_fixed' => 'Prix fixe', + 'pricing_components' => 'Somme des composants', + 'discount_percentage' => 'Remise', + 'discount_hint' => 'Déduite du total des composants.', + 'derived_note' => 'Dans ce mode, les prix sont dérivés des composants et écrasés à chaque recalcul. Passez au prix fixe pour les définir à la main.', + 'components' => 'Composants', + 'components_description' => 'Toujours inclus, dans cet ordre.', + 'components_empty' => 'Aucun composant pour l\'instant. Ajoutez-en au moins un pour que le lot soit vendable.', + 'add_component' => 'Ajouter un composant', + 'quantity' => 'Qté', + 'move_up' => 'Monter', + 'move_down' => 'Descendre', + 'remove' => 'Retirer', + 'availability' => 'Disponibilité', + 'availability_count' => '{count} peuvent être vendus à partir du stock des composants.', + 'availability_unlimited' => 'Non limité par le stock des composants.', + 'availability_none' => 'En rupture : un composant est indisponible.', + 'derived_prices' => 'Prix dérivés', + 'price' => 'Prix', + 'list_price' => 'Avant', + 'missing_price' => 'Aucun prix pour : {components}', + 'groups' => 'Groupes d\'options', + 'groups_description' => 'Laissez le client choisir parmi un ensemble de composants. Un lot sans groupe est fixe.', + 'add_group' => 'Ajouter un groupe', + 'group_name' => 'Nom du groupe', + 'group_min' => 'Min', + 'group_max' => 'Max', + 'group_default' => 'Par défaut', + 'group_empty' => 'Aucune option dans ce groupe pour l\'instant.', + 'add_option' => 'Ajouter une option', + 'remove_group' => 'Retirer le groupe', + 'confirm_remove_group' => 'Retirer ce groupe et ses options ?', + 'new_group_name' => 'Nouveau groupe', + 'saved' => 'Lot enregistré.', + 'removed' => 'Lot supprimé.', + 'load_failed' => 'Le lot n\'a pas pu être chargé.', + 'variant_summary_description' => 'Les lots se définissent par variante. Ouvrez une variante pour modifier ses composants.', + 'variant_component_count' => '{count} composant | {count} composants', + 'variant_not_bundle' => 'Pas un lot', + 'edit_variant' => 'Modifier la variante', + 'included_in_title' => 'Inclus dans des lots', + 'included_in_description' => 'Supprimer une variante la retire de ces lots.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count lots retarifés.', ], diff --git a/packages/bundles/resources/lang/hr/bundles.php b/packages/bundles/resources/lang/hr/bundles.php index 59dcd01781..e652ae4bd5 100644 --- a/packages/bundles/resources/lang/hr/bundles.php +++ b/packages/bundles/resources/lang/hr/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Komponenta ":identifier" nema cijenu u :currency, pa se paket ne može odrediti u toj valuti.', ], + 'panel' => [ + 'column_bundle' => 'Paket', + 'badge_bundle' => 'Paket', + 'filter_label' => 'Paketi', + 'filter_only' => 'Samo paketi', + 'not_defined' => 'Ova varijanta se ne prodaje kao paket.', + 'card_title' => 'Paket', + 'card_description' => 'Prodajte ovu varijantu kao skup drugih varijanti. Zaliha se izvodi iz komponenti, a svaka komponenta se priprema i isporučuje iz narudžbe.', + 'sell_as_bundle' => 'Prodaj kao paket', + 'confirm_remove_title' => 'Prestati prodavati kao paket?', + 'confirm_remove' => 'Popis komponenti i grupe opcija bit će uklonjeni. Varijanta zadržava svoje cijene i ponovno postaje obična varijanta.', + 'pricing' => 'Cijene', + 'pricing_fixed' => 'Fiksna cijena', + 'pricing_components' => 'Zbroj komponenti', + 'discount_percentage' => 'Popust', + 'discount_hint' => 'Oduzima se od ukupnog iznosa komponenti.', + 'derived_note' => 'U ovom načinu cijene se izvode iz komponenti i prepisuju pri svakom ponovnom izračunu paketa. Prijeđite na fiksnu cijenu da biste ih postavili ručno.', + 'components' => 'Komponente', + 'components_description' => 'Uvijek uključene, ovim redoslijedom.', + 'components_empty' => 'Još nema komponenti. Dodajte barem jednu kako bi se paket mogao prodavati.', + 'add_component' => 'Dodaj komponentu', + 'quantity' => 'Kol.', + 'move_up' => 'Pomakni gore', + 'move_down' => 'Pomakni dolje', + 'remove' => 'Ukloni', + 'availability' => 'Dostupnost', + 'availability_count' => 'Iz zalihe komponenti može se prodati {count}.', + 'availability_unlimited' => 'Nije ograničeno zalihom komponenti.', + 'availability_none' => 'Nema na zalihi: jedna komponenta nije dostupna.', + 'derived_prices' => 'Izvedene cijene', + 'price' => 'Cijena', + 'list_price' => 'Prije', + 'missing_price' => 'Nema cijene za: {components}', + 'groups' => 'Grupe opcija', + 'groups_description' => 'Dopustite kupcu da bira iz skupa komponenti. Paket bez grupa je fiksan.', + 'add_group' => 'Dodaj grupu', + 'group_name' => 'Naziv grupe', + 'group_min' => 'Min.', + 'group_max' => 'Maks.', + 'group_default' => 'Zadano', + 'group_empty' => 'U ovoj grupi još nema opcija.', + 'add_option' => 'Dodaj opciju', + 'remove_group' => 'Ukloni grupu', + 'confirm_remove_group' => 'Ukloniti ovu grupu i njezine opcije?', + 'new_group_name' => 'Nova grupa', + 'saved' => 'Paket je spremljen.', + 'removed' => 'Paket je uklonjen.', + 'load_failed' => 'Paket nije moguće učitati.', + 'variant_summary_description' => 'Paketi se definiraju po varijanti. Otvorite varijantu da biste uredili njezine komponente.', + 'variant_component_count' => '{count} komponenta | {count} komponente | {count} komponenti', + 'variant_not_bundle' => 'Nije paket', + 'edit_variant' => 'Uredi varijantu', + 'included_in_title' => 'Uključeno u pakete', + 'included_in_description' => 'Brisanje varijante uklanja je iz ovih paketa.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Ponovno izračunate cijene za :count paketa.', ], diff --git a/packages/bundles/resources/lang/hu/bundles.php b/packages/bundles/resources/lang/hu/bundles.php index 3fa712ea43..3ef0386428 100644 --- a/packages/bundles/resources/lang/hu/bundles.php +++ b/packages/bundles/resources/lang/hu/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'A(z) ":identifier" összetevőnek nincs ára :currency pénznemben, így a csomag nem árazható be ebben a pénznemben.', ], + 'panel' => [ + 'column_bundle' => 'Csomag', + 'badge_bundle' => 'Csomag', + 'filter_label' => 'Csomagok', + 'filter_only' => 'Csak csomagok', + 'not_defined' => 'Ez a változat nem csomagként kerül értékesítésre.', + 'card_title' => 'Csomag', + 'card_description' => 'Értékesítse ezt a változatot más változatok együtteseként. A készlet az összetevőkből származik, és minden összetevőt a rendelésből szednek össze és teljesítenek.', + 'sell_as_bundle' => 'Értékesítés csomagként', + 'confirm_remove_title' => 'Ne legyen többé csomagként értékesítve?', + 'confirm_remove' => 'Az összetevők listája és az opciócsoportok törlődnek. A változat megtartja az árait, és ismét hagyományos változattá válik.', + 'pricing' => 'Árazás', + 'pricing_fixed' => 'Fix ár', + 'pricing_components' => 'Összetevők összege', + 'discount_percentage' => 'Kedvezmény', + 'discount_hint' => 'Az összetevők összegéből kerül levonásra.', + 'derived_note' => 'Ebben a módban az árak az összetevőkből származnak, és a csomag minden újraárazásakor felülíródnak. Váltson fix árra, ha kézzel szeretné beállítani őket.', + 'components' => 'Összetevők', + 'components_description' => 'Mindig benne vannak, ebben a sorrendben.', + 'components_empty' => 'Még nincsenek összetevők. Adjon hozzá legalább egyet, hogy a csomag értékesíthető legyen.', + 'add_component' => 'Összetevő hozzáadása', + 'quantity' => 'Menny.', + 'move_up' => 'Mozgatás felfelé', + 'move_down' => 'Mozgatás lefelé', + 'remove' => 'Eltávolítás', + 'availability' => 'Elérhetőség', + 'availability_count' => 'Az összetevők készletéből {count} adható el.', + 'availability_unlimited' => 'Az összetevők készlete nem korlátozza.', + 'availability_none' => 'Nincs készleten: egy összetevő nem elérhető.', + 'derived_prices' => 'Származtatott árak', + 'price' => 'Ár', + 'list_price' => 'Korábban', + 'missing_price' => 'Nincs ár ehhez: {components}', + 'groups' => 'Opciócsoportok', + 'groups_description' => 'Engedje, hogy a vásárló összetevők közül válasszon. A csoportok nélküli csomag rögzített.', + 'add_group' => 'Csoport hozzáadása', + 'group_name' => 'Csoport neve', + 'group_min' => 'Min.', + 'group_max' => 'Max.', + 'group_default' => 'Alapértelmezett', + 'group_empty' => 'Ebben a csoportban még nincsenek opciók.', + 'add_option' => 'Opció hozzáadása', + 'remove_group' => 'Csoport eltávolítása', + 'confirm_remove_group' => 'Eltávolítja ezt a csoportot és az opcióit?', + 'new_group_name' => 'Új csoport', + 'saved' => 'Csomag mentve.', + 'removed' => 'Csomag eltávolítva.', + 'load_failed' => 'A csomagot nem sikerült betölteni.', + 'variant_summary_description' => 'A csomagok változatonként vannak meghatározva. Nyisson meg egy változatot az összetevői szerkesztéséhez.', + 'variant_component_count' => '{count} összetevő | {count} összetevő', + 'variant_not_bundle' => 'Nem csomag', + 'edit_variant' => 'Változat szerkesztése', + 'included_in_title' => 'Csomagok része', + 'included_in_description' => 'Egy változat törlése eltávolítja azt ezekből a csomagokból.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count csomag újraárazva.', ], diff --git a/packages/bundles/resources/lang/mn/bundles.php b/packages/bundles/resources/lang/mn/bundles.php index c52f5c4b43..a2c397c0c9 100644 --- a/packages/bundles/resources/lang/mn/bundles.php +++ b/packages/bundles/resources/lang/mn/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => '":identifier" бүрэлдэхүүн :currency валютаар үнэгүй тул багцыг энэ валютаар үнэлэх боломжгүй.', ], + 'panel' => [ + 'column_bundle' => 'Багц', + 'badge_bundle' => 'Багц', + 'filter_label' => 'Багцууд', + 'filter_only' => 'Зөвхөн багцууд', + 'not_defined' => 'Энэ хувилбар багц хэлбэрээр зарагддаггүй.', + 'card_title' => 'Багц', + 'card_description' => 'Энэ хувилбарыг бусад хувилбаруудын иж бүрдэл болгон зарна. Нөөц нь бүрэлдэхүүнүүдээс тооцоологдож, бүрэлдэхүүн бүр захиалгаас бэлтгэгдэн гүйцэтгэгдэнэ.', + 'sell_as_bundle' => 'Багц болгон зарах', + 'confirm_remove_title' => 'Багц болгон зарахаа зогсоох уу?', + 'confirm_remove' => 'Бүрэлдэхүүний жагсаалт болон сонголтын бүлгүүд устгагдана. Хувилбар үнээ хадгалж, энгийн хувилбар болно.', + 'pricing' => 'Үнэ тогтоолт', + 'pricing_fixed' => 'Тогтмол үнэ', + 'pricing_components' => 'Бүрэлдэхүүнүүдийн нийлбэр', + 'discount_percentage' => 'Хөнгөлөлт', + 'discount_hint' => 'Бүрэлдэхүүний нийт дүнгээс хасагдана.', + 'derived_note' => 'Энэ горимд үнэ нь бүрэлдэхүүнүүдээс тооцоологдож, багцын үнийг дахин тооцох бүрт дарж бичигдэнэ. Гараар тохируулахын тулд тогтмол үнэ рүү шилжинэ үү.', + 'components' => 'Бүрэлдэхүүнүүд', + 'components_description' => 'Энэ дарааллаар үргэлж багтана.', + 'components_empty' => 'Бүрэлдэхүүн байхгүй байна. Багцыг зарах боломжтой болгохын тулд дор хаяж нэгийг нэмнэ үү.', + 'add_component' => 'Бүрэлдэхүүн нэмэх', + 'quantity' => 'Тоо', + 'move_up' => 'Дээш зөөх', + 'move_down' => 'Доош зөөх', + 'remove' => 'Хасах', + 'availability' => 'Боломжит нөөц', + 'availability_count' => 'Бүрэлдэхүүний нөөцөөс {count} ширхэг зарах боломжтой.', + 'availability_unlimited' => 'Бүрэлдэхүүний нөөцөөр хязгаарлагдахгүй.', + 'availability_none' => 'Нөөц дууссан: нэг бүрэлдэхүүн байхгүй байна.', + 'derived_prices' => 'Тооцоолсон үнэ', + 'price' => 'Үнэ', + 'list_price' => 'Өмнөх үнэ', + 'missing_price' => 'Үнэ байхгүй: {components}', + 'groups' => 'Сонголтын бүлгүүд', + 'groups_description' => 'Худалдан авагчид бүрэлдэхүүнүүдээс сонгох боломж олгоно. Бүлэггүй багц тогтмол байна.', + 'add_group' => 'Бүлэг нэмэх', + 'group_name' => 'Бүлгийн нэр', + 'group_min' => 'Хамгийн бага', + 'group_max' => 'Хамгийн их', + 'group_default' => 'Үндсэн', + 'group_empty' => 'Энэ бүлэгт сонголт байхгүй байна.', + 'add_option' => 'Сонголт нэмэх', + 'remove_group' => 'Бүлэг хасах', + 'confirm_remove_group' => 'Энэ бүлэг болон сонголтуудыг нь хасах уу?', + 'new_group_name' => 'Шинэ бүлэг', + 'saved' => 'Багц хадгалагдлаа.', + 'removed' => 'Багц хасагдлаа.', + 'load_failed' => 'Багцыг ачаалж чадсангүй.', + 'variant_summary_description' => 'Багцыг хувилбар бүрээр тодорхойлно. Бүрэлдэхүүнийг засахын тулд хувилбарыг нээнэ үү.', + 'variant_component_count' => '{count} бүрэлдэхүүн | {count} бүрэлдэхүүн', + 'variant_not_bundle' => 'Багц биш', + 'edit_variant' => 'Хувилбар засах', + 'included_in_title' => 'Багцад багтсан', + 'included_in_description' => 'Хувилбарыг устгавал эдгээр багцаас хасагдана.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count багцын үнийг дахин тооцооллоо.', ], diff --git a/packages/bundles/resources/lang/nl/bundles.php b/packages/bundles/resources/lang/nl/bundles.php index 723bb32be1..4bc2057a95 100644 --- a/packages/bundles/resources/lang/nl/bundles.php +++ b/packages/bundles/resources/lang/nl/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Onderdeel ":identifier" heeft geen prijs in :currency, dus de bundel kan in die valuta niet geprijsd worden.', ], + 'panel' => [ + 'column_bundle' => 'Bundel', + 'badge_bundle' => 'Bundel', + 'filter_label' => 'Bundels', + 'filter_only' => 'Alleen bundels', + 'not_defined' => 'Deze variant wordt niet als bundel verkocht.', + 'card_title' => 'Bundel', + 'card_description' => 'Verkoop deze variant als een set van andere varianten. De voorraad wordt afgeleid van de onderdelen en elk onderdeel wordt vanuit de bestelling gepickt en afgehandeld.', + 'sell_as_bundle' => 'Als bundel verkopen', + 'confirm_remove_title' => 'Niet langer als bundel verkopen?', + 'confirm_remove' => 'De onderdelenlijst en optiegroepen worden verwijderd. De variant behoudt zijn prijzen en wordt weer een gewone variant.', + 'pricing' => 'Prijsbepaling', + 'pricing_fixed' => 'Vaste prijs', + 'pricing_components' => 'Som van de onderdelen', + 'discount_percentage' => 'Korting', + 'discount_hint' => 'Wordt van het onderdelentotaal afgetrokken.', + 'derived_note' => 'Prijzen in deze modus worden afgeleid van de onderdelen en overschreven bij elke herberekening. Schakel over naar een vaste prijs om ze handmatig in te stellen.', + 'components' => 'Onderdelen', + 'components_description' => 'Altijd inbegrepen, in deze volgorde.', + 'components_empty' => 'Nog geen onderdelen. Voeg er minstens één toe om de bundel verkoopbaar te maken.', + 'add_component' => 'Onderdeel toevoegen', + 'quantity' => 'Aantal', + 'move_up' => 'Omhoog', + 'move_down' => 'Omlaag', + 'remove' => 'Verwijderen', + 'availability' => 'Beschikbaarheid', + 'availability_count' => '{count} kunnen worden verkocht uit de onderdelenvoorraad.', + 'availability_unlimited' => 'Niet beperkt door de onderdelenvoorraad.', + 'availability_none' => 'Niet op voorraad: een onderdeel is niet beschikbaar.', + 'derived_prices' => 'Afgeleide prijzen', + 'price' => 'Prijs', + 'list_price' => 'Was', + 'missing_price' => 'Geen prijs voor: {components}', + 'groups' => 'Optiegroepen', + 'groups_description' => 'Laat de klant kiezen uit een set onderdelen. Een bundel zonder groepen is vast.', + 'add_group' => 'Groep toevoegen', + 'group_name' => 'Groepsnaam', + 'group_min' => 'Min.', + 'group_max' => 'Max.', + 'group_default' => 'Standaard', + 'group_empty' => 'Nog geen opties in deze groep.', + 'add_option' => 'Optie toevoegen', + 'remove_group' => 'Groep verwijderen', + 'confirm_remove_group' => 'Deze groep en zijn opties verwijderen?', + 'new_group_name' => 'Nieuwe groep', + 'saved' => 'Bundel opgeslagen.', + 'removed' => 'Bundel verwijderd.', + 'load_failed' => 'De bundel kon niet worden geladen.', + 'variant_summary_description' => 'Bundels worden per variant gedefinieerd. Open een variant om de onderdelen te bewerken.', + 'variant_component_count' => '{count} onderdeel | {count} onderdelen', + 'variant_not_bundle' => 'Geen bundel', + 'edit_variant' => 'Variant bewerken', + 'included_in_title' => 'Opgenomen in bundels', + 'included_in_description' => 'Als je een variant verwijdert, wordt deze uit deze bundels gehaald.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count bundels opnieuw geprijsd.', ], diff --git a/packages/bundles/resources/lang/pl/bundles.php b/packages/bundles/resources/lang/pl/bundles.php index 1193aa8a75..5c5f119c7a 100644 --- a/packages/bundles/resources/lang/pl/bundles.php +++ b/packages/bundles/resources/lang/pl/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Składnik ":identifier" nie ma ceny w :currency, więc zestaw nie może zostać wyceniony w tej walucie.', ], + 'panel' => [ + 'column_bundle' => 'Zestaw', + 'badge_bundle' => 'Zestaw', + 'filter_label' => 'Zestawy', + 'filter_only' => 'Tylko zestawy', + 'not_defined' => 'Ten wariant nie jest sprzedawany jako zestaw.', + 'card_title' => 'Zestaw', + 'card_description' => 'Sprzedawaj ten wariant jako komplet innych wariantów. Stan magazynowy wynika ze składników, a każdy składnik jest kompletowany i realizowany z zamówienia.', + 'sell_as_bundle' => 'Sprzedawaj jako zestaw', + 'confirm_remove_title' => 'Przestać sprzedawać jako zestaw?', + 'confirm_remove' => 'Lista składników i grupy opcji zostaną usunięte. Wariant zachowa swoje ceny i znów będzie zwykłym wariantem.', + 'pricing' => 'Wycena', + 'pricing_fixed' => 'Cena stała', + 'pricing_components' => 'Suma składników', + 'discount_percentage' => 'Rabat', + 'discount_hint' => 'Odejmowany od sumy składników.', + 'derived_note' => 'W tym trybie ceny wynikają ze składników i są nadpisywane przy każdym przeliczeniu zestawu. Przełącz na cenę stałą, aby ustawić je ręcznie.', + 'components' => 'Składniki', + 'components_description' => 'Zawsze w zestawie, w tej kolejności.', + 'components_empty' => 'Brak składników. Dodaj co najmniej jeden, aby zestaw można było sprzedawać.', + 'add_component' => 'Dodaj składnik', + 'quantity' => 'Ilość', + 'move_up' => 'Przenieś wyżej', + 'move_down' => 'Przenieś niżej', + 'remove' => 'Usuń', + 'availability' => 'Dostępność', + 'availability_count' => 'Ze stanu składników można sprzedać {count}.', + 'availability_unlimited' => 'Bez ograniczenia stanem składników.', + 'availability_none' => 'Brak na stanie: jeden ze składników jest niedostępny.', + 'derived_prices' => 'Ceny wynikowe', + 'price' => 'Cena', + 'list_price' => 'Poprzednio', + 'missing_price' => 'Brak ceny dla: {components}', + 'groups' => 'Grupy opcji', + 'groups_description' => 'Pozwól klientowi wybrać spośród zestawu składników. Zestaw bez grup jest stały.', + 'add_group' => 'Dodaj grupę', + 'group_name' => 'Nazwa grupy', + 'group_min' => 'Min.', + 'group_max' => 'Maks.', + 'group_default' => 'Domyślny', + 'group_empty' => 'W tej grupie nie ma jeszcze opcji.', + 'add_option' => 'Dodaj opcję', + 'remove_group' => 'Usuń grupę', + 'confirm_remove_group' => 'Usunąć tę grupę i jej opcje?', + 'new_group_name' => 'Nowa grupa', + 'saved' => 'Zestaw zapisany.', + 'removed' => 'Zestaw usunięty.', + 'load_failed' => 'Nie udało się wczytać zestawu.', + 'variant_summary_description' => 'Zestawy definiuje się dla wariantu. Otwórz wariant, aby edytować jego składniki.', + 'variant_component_count' => '{count} składnik | {count} składniki | {count} składników', + 'variant_not_bundle' => 'To nie zestaw', + 'edit_variant' => 'Edytuj wariant', + 'included_in_title' => 'Wchodzi w skład zestawów', + 'included_in_description' => 'Usunięcie wariantu usuwa go z tych zestawów.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Przeliczono ceny :count zestawów.', ], diff --git a/packages/bundles/resources/lang/pt_BR/bundles.php b/packages/bundles/resources/lang/pt_BR/bundles.php index 1ecaabbe8e..65c0b3f97c 100644 --- a/packages/bundles/resources/lang/pt_BR/bundles.php +++ b/packages/bundles/resources/lang/pt_BR/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'O componente ":identifier" não tem preço em :currency, portanto o pacote não pode ser precificado nessa moeda.', ], + 'panel' => [ + 'column_bundle' => 'Pacote', + 'badge_bundle' => 'Pacote', + 'filter_label' => 'Pacotes', + 'filter_only' => 'Somente pacotes', + 'not_defined' => 'Esta variante não é vendida como pacote.', + 'card_title' => 'Pacote', + 'card_description' => 'Venda esta variante como um conjunto de outras variantes. O estoque é derivado dos componentes e cada componente é separado e atendido a partir do pedido.', + 'sell_as_bundle' => 'Vender como pacote', + 'confirm_remove_title' => 'Parar de vender como pacote?', + 'confirm_remove' => 'A lista de componentes e os grupos de opções são removidos. A variante mantém seus preços e volta a ser uma variante comum.', + 'pricing' => 'Precificação', + 'pricing_fixed' => 'Preço fixo', + 'pricing_components' => 'Soma dos componentes', + 'discount_percentage' => 'Desconto', + 'discount_hint' => 'Descontado do total dos componentes.', + 'derived_note' => 'Neste modo os preços são derivados dos componentes e sobrescritos sempre que o pacote é reprecificado. Mude para preço fixo para defini-los manualmente.', + 'components' => 'Componentes', + 'components_description' => 'Sempre incluídos, nesta ordem.', + 'components_empty' => 'Nenhum componente ainda. Adicione pelo menos um para que o pacote possa ser vendido.', + 'add_component' => 'Adicionar componente', + 'quantity' => 'Qtd.', + 'move_up' => 'Mover para cima', + 'move_down' => 'Mover para baixo', + 'remove' => 'Remover', + 'availability' => 'Disponibilidade', + 'availability_count' => '{count} podem ser vendidos com o estoque dos componentes.', + 'availability_unlimited' => 'Não limitado pelo estoque dos componentes.', + 'availability_none' => 'Esgotado: um componente está indisponível.', + 'derived_prices' => 'Preços derivados', + 'price' => 'Preço', + 'list_price' => 'De', + 'missing_price' => 'Sem preço para: {components}', + 'groups' => 'Grupos de opções', + 'groups_description' => 'Deixe o cliente escolher entre um conjunto de componentes. Um pacote sem grupos é fixo.', + 'add_group' => 'Adicionar grupo', + 'group_name' => 'Nome do grupo', + 'group_min' => 'Mín.', + 'group_max' => 'Máx.', + 'group_default' => 'Padrão', + 'group_empty' => 'Nenhuma opção neste grupo ainda.', + 'add_option' => 'Adicionar opção', + 'remove_group' => 'Remover grupo', + 'confirm_remove_group' => 'Remover este grupo e suas opções?', + 'new_group_name' => 'Novo grupo', + 'saved' => 'Pacote salvo.', + 'removed' => 'Pacote removido.', + 'load_failed' => 'Não foi possível carregar o pacote.', + 'variant_summary_description' => 'Pacotes são definidos por variante. Abra uma variante para editar seus componentes.', + 'variant_component_count' => '{count} componente | {count} componentes', + 'variant_not_bundle' => 'Não é um pacote', + 'edit_variant' => 'Editar variante', + 'included_in_title' => 'Incluído em pacotes', + 'included_in_description' => 'Excluir uma variante a remove destes pacotes.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count pacotes reprecificados.', ], diff --git a/packages/bundles/resources/lang/ro/bundles.php b/packages/bundles/resources/lang/ro/bundles.php index 078bfb4c0f..65630fbe31 100644 --- a/packages/bundles/resources/lang/ro/bundles.php +++ b/packages/bundles/resources/lang/ro/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Componenta ":identifier" nu are preț în :currency, așa că pachetul nu poate fi evaluat în această monedă.', ], + 'panel' => [ + 'column_bundle' => 'Pachet', + 'badge_bundle' => 'Pachet', + 'filter_label' => 'Pachete', + 'filter_only' => 'Doar pachete', + 'not_defined' => 'Această variantă nu este vândută ca pachet.', + 'card_title' => 'Pachet', + 'card_description' => 'Vinde această variantă ca un set de alte variante. Stocul este derivat din componente, iar fiecare componentă este pregătită și livrată din comandă.', + 'sell_as_bundle' => 'Vinde ca pachet', + 'confirm_remove_title' => 'Nu mai vinzi ca pachet?', + 'confirm_remove' => 'Lista de componente și grupurile de opțiuni sunt eliminate. Varianta își păstrează prețurile și redevine o variantă obișnuită.', + 'pricing' => 'Prețuri', + 'pricing_fixed' => 'Preț fix', + 'pricing_components' => 'Suma componentelor', + 'discount_percentage' => 'Reducere', + 'discount_hint' => 'Scăzută din totalul componentelor.', + 'derived_note' => 'În acest mod prețurile sunt derivate din componente și suprascrise la fiecare recalculare a pachetului. Treci la preț fix pentru a le seta manual.', + 'components' => 'Componente', + 'components_description' => 'Întotdeauna incluse, în această ordine.', + 'components_empty' => 'Nicio componentă încă. Adaugă cel puțin una pentru ca pachetul să poată fi vândut.', + 'add_component' => 'Adaugă componentă', + 'quantity' => 'Cant.', + 'move_up' => 'Mută în sus', + 'move_down' => 'Mută în jos', + 'remove' => 'Elimină', + 'availability' => 'Disponibilitate', + 'availability_count' => '{count} pot fi vândute din stocul componentelor.', + 'availability_unlimited' => 'Nelimitat de stocul componentelor.', + 'availability_none' => 'Stoc epuizat: o componentă nu este disponibilă.', + 'derived_prices' => 'Prețuri derivate', + 'price' => 'Preț', + 'list_price' => 'Înainte', + 'missing_price' => 'Fără preț pentru: {components}', + 'groups' => 'Grupuri de opțiuni', + 'groups_description' => 'Lasă clientul să aleagă dintr-un set de componente. Un pachet fără grupuri este fix.', + 'add_group' => 'Adaugă grup', + 'group_name' => 'Numele grupului', + 'group_min' => 'Min.', + 'group_max' => 'Max.', + 'group_default' => 'Implicit', + 'group_empty' => 'Nicio opțiune în acest grup încă.', + 'add_option' => 'Adaugă opțiune', + 'remove_group' => 'Elimină grupul', + 'confirm_remove_group' => 'Elimini acest grup și opțiunile sale?', + 'new_group_name' => 'Grup nou', + 'saved' => 'Pachet salvat.', + 'removed' => 'Pachet eliminat.', + 'load_failed' => 'Pachetul nu a putut fi încărcat.', + 'variant_summary_description' => 'Pachetele se definesc per variantă. Deschide o variantă pentru a-i edita componentele.', + 'variant_component_count' => '{count} componentă | {count} componente', + 'variant_not_bundle' => 'Nu este pachet', + 'edit_variant' => 'Editează varianta', + 'included_in_title' => 'Inclus în pachete', + 'included_in_description' => 'Ștergerea unei variante o elimină din aceste pachete.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Prețurile pentru :count pachete au fost recalculate.', ], diff --git a/packages/bundles/resources/lang/tr/bundles.php b/packages/bundles/resources/lang/tr/bundles.php index bcefa7bad4..edb93df043 100644 --- a/packages/bundles/resources/lang/tr/bundles.php +++ b/packages/bundles/resources/lang/tr/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => '":identifier" bileşeninin :currency cinsinden fiyatı yok, bu nedenle paket bu para biriminde fiyatlandırılamaz.', ], + 'panel' => [ + 'column_bundle' => 'Paket', + 'badge_bundle' => 'Paket', + 'filter_label' => 'Paketler', + 'filter_only' => 'Yalnızca paketler', + 'not_defined' => 'Bu varyant paket olarak satılmıyor.', + 'card_title' => 'Paket', + 'card_description' => 'Bu varyantı başka varyantlardan oluşan bir set olarak satın. Stok bileşenlerden türetilir ve her bileşen siparişten toplanıp teslim edilir.', + 'sell_as_bundle' => 'Paket olarak sat', + 'confirm_remove_title' => 'Paket olarak satış durdurulsun mu?', + 'confirm_remove' => 'Bileşen listesi ve seçenek grupları kaldırılır. Varyant fiyatlarını korur ve yeniden sıradan bir varyant olur.', + 'pricing' => 'Fiyatlandırma', + 'pricing_fixed' => 'Sabit fiyat', + 'pricing_components' => 'Bileşenlerin toplamı', + 'discount_percentage' => 'İndirim', + 'discount_hint' => 'Bileşen toplamından düşülür.', + 'derived_note' => 'Bu modda fiyatlar bileşenlerden türetilir ve paket her yeniden fiyatlandırıldığında üzerine yazılır. Elle belirlemek için sabit fiyata geçin.', + 'components' => 'Bileşenler', + 'components_description' => 'Her zaman dahildir, bu sırayla.', + 'components_empty' => 'Henüz bileşen yok. Paketin satılabilmesi için en az bir tane ekleyin.', + 'add_component' => 'Bileşen ekle', + 'quantity' => 'Adet', + 'move_up' => 'Yukarı taşı', + 'move_down' => 'Aşağı taşı', + 'remove' => 'Kaldır', + 'availability' => 'Stok durumu', + 'availability_count' => 'Bileşen stoğundan {count} adet satılabilir.', + 'availability_unlimited' => 'Bileşen stoğuyla sınırlı değil.', + 'availability_none' => 'Stokta yok: bir bileşen mevcut değil.', + 'derived_prices' => 'Türetilen fiyatlar', + 'price' => 'Fiyat', + 'list_price' => 'Önceki', + 'missing_price' => 'Fiyatı olmayanlar: {components}', + 'groups' => 'Seçenek grupları', + 'groups_description' => 'Müşterinin bir bileşen kümesinden seçim yapmasına izin verin. Grubu olmayan bir paket sabittir.', + 'add_group' => 'Grup ekle', + 'group_name' => 'Grup adı', + 'group_min' => 'Min.', + 'group_max' => 'Maks.', + 'group_default' => 'Varsayılan', + 'group_empty' => 'Bu grupta henüz seçenek yok.', + 'add_option' => 'Seçenek ekle', + 'remove_group' => 'Grubu kaldır', + 'confirm_remove_group' => 'Bu grup ve seçenekleri kaldırılsın mı?', + 'new_group_name' => 'Yeni grup', + 'saved' => 'Paket kaydedildi.', + 'removed' => 'Paket kaldırıldı.', + 'load_failed' => 'Paket yüklenemedi.', + 'variant_summary_description' => 'Paketler varyant başına tanımlanır. Bileşenlerini düzenlemek için bir varyantı açın.', + 'variant_component_count' => '{count} bileşen | {count} bileşen', + 'variant_not_bundle' => 'Paket değil', + 'edit_variant' => 'Varyantı düzenle', + 'included_in_title' => 'Dahil olduğu paketler', + 'included_in_description' => 'Bir varyantı silmek onu bu paketlerden çıkarır.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => ':count paketin fiyatı yeniden hesaplandı.', ], diff --git a/packages/bundles/resources/lang/vi/bundles.php b/packages/bundles/resources/lang/vi/bundles.php index 3f45e3df4b..3580b6af9a 100644 --- a/packages/bundles/resources/lang/vi/bundles.php +++ b/packages/bundles/resources/lang/vi/bundles.php @@ -23,6 +23,62 @@ 'pricing' => [ 'missing_component_price' => 'Thành phần ":identifier" không có giá bằng :currency, vì vậy gói không thể được định giá bằng loại tiền đó.', ], + 'panel' => [ + 'column_bundle' => 'Gói', + 'badge_bundle' => 'Gói', + 'filter_label' => 'Gói sản phẩm', + 'filter_only' => 'Chỉ gói sản phẩm', + 'not_defined' => 'Biến thể này không được bán dưới dạng gói.', + 'card_title' => 'Gói sản phẩm', + 'card_description' => 'Bán biến thể này như một bộ gồm các biến thể khác. Tồn kho được tính từ các thành phần và mỗi thành phần được soạn và giao từ đơn hàng.', + 'sell_as_bundle' => 'Bán dưới dạng gói', + 'confirm_remove_title' => 'Ngừng bán dưới dạng gói?', + 'confirm_remove' => 'Danh sách thành phần và các nhóm tùy chọn sẽ bị xóa. Biến thể giữ nguyên giá và trở lại thành biến thể thông thường.', + 'pricing' => 'Định giá', + 'pricing_fixed' => 'Giá cố định', + 'pricing_components' => 'Tổng các thành phần', + 'discount_percentage' => 'Giảm giá', + 'discount_hint' => 'Được trừ vào tổng giá các thành phần.', + 'derived_note' => 'Ở chế độ này, giá được tính từ các thành phần và bị ghi đè mỗi khi gói được tính lại giá. Chuyển sang giá cố định để đặt giá thủ công.', + 'components' => 'Thành phần', + 'components_description' => 'Luôn được bao gồm, theo thứ tự này.', + 'components_empty' => 'Chưa có thành phần nào. Thêm ít nhất một thành phần để gói có thể bán được.', + 'add_component' => 'Thêm thành phần', + 'quantity' => 'SL', + 'move_up' => 'Chuyển lên', + 'move_down' => 'Chuyển xuống', + 'remove' => 'Xóa', + 'availability' => 'Tình trạng còn hàng', + 'availability_count' => 'Có thể bán {count} từ tồn kho của các thành phần.', + 'availability_unlimited' => 'Không bị giới hạn bởi tồn kho của các thành phần.', + 'availability_none' => 'Hết hàng: một thành phần không có sẵn.', + 'derived_prices' => 'Giá được tính', + 'price' => 'Giá', + 'list_price' => 'Giá cũ', + 'missing_price' => 'Chưa có giá cho: {components}', + 'groups' => 'Nhóm tùy chọn', + 'groups_description' => 'Cho phép khách hàng chọn từ một tập hợp thành phần. Gói không có nhóm là gói cố định.', + 'add_group' => 'Thêm nhóm', + 'group_name' => 'Tên nhóm', + 'group_min' => 'Tối thiểu', + 'group_max' => 'Tối đa', + 'group_default' => 'Mặc định', + 'group_empty' => 'Nhóm này chưa có tùy chọn nào.', + 'add_option' => 'Thêm tùy chọn', + 'remove_group' => 'Xóa nhóm', + 'confirm_remove_group' => 'Xóa nhóm này và các tùy chọn của nó?', + 'new_group_name' => 'Nhóm mới', + 'saved' => 'Đã lưu gói.', + 'removed' => 'Đã xóa gói.', + 'load_failed' => 'Không thể tải gói.', + 'variant_summary_description' => 'Gói được định nghĩa cho từng biến thể. Mở một biến thể để chỉnh sửa các thành phần của nó.', + 'variant_component_count' => '{count} thành phần | {count} thành phần', + 'variant_not_bundle' => 'Không phải gói', + 'edit_variant' => 'Chỉnh sửa biến thể', + 'included_in_title' => 'Có trong các gói', + 'included_in_description' => 'Xóa một biến thể sẽ loại nó khỏi các gói này.', + 'included_quantity' => 'x{quantity}', + ], 'console' => [ 'repriced' => 'Đã tính lại giá cho :count gói.', ], diff --git a/packages/bundles/src/BundlesServiceProvider.php b/packages/bundles/src/BundlesServiceProvider.php index f6476a4929..5f838f805a 100644 --- a/packages/bundles/src/BundlesServiceProvider.php +++ b/packages/bundles/src/BundlesServiceProvider.php @@ -25,6 +25,7 @@ use Lunar\Bundles\Models\Bundle; use Lunar\Bundles\Models\BundleComponent; use Lunar\Bundles\Models\BundleGroup; +use Lunar\Bundles\Panel\BundlesSectionExtension; use Lunar\Bundles\Pipelines\CartLine\PriceBundleSelection; use Lunar\Bundles\Pipelines\Order\Creation\CreateBundleComponentLines; use Lunar\Bundles\Validation\CartLine\BundleSelection; @@ -34,6 +35,8 @@ use Lunar\Core\Models\Price; use Lunar\Core\Models\Product; use Lunar\Core\Models\ProductVariant; +use Lunar\Panel\Facades\Panel; +use Lunar\Panel\PanelManager; class BundlesServiceProvider extends ServiceProvider { @@ -103,6 +106,7 @@ public function boot(): void $this->registerCartHooks(); $this->registerListeners(); $this->registerConsole(); + $this->registerPanel(); } /** @@ -144,4 +148,21 @@ protected function registerConsole(): void RepriceBundlesCommand::class, ]); } + + /** The panel extension only exists when lunarphp/panel is installed and booted. */ + protected function registerPanel(): void + { + if (! class_exists(PanelManager::class) || ! $this->app->bound(PanelManager::class)) { + return; + } + + Panel::extendSection(new BundlesSectionExtension); + + $this->app->make(PanelManager::class)->vite('bundles', [ + 'input' => 'resources/js/addon.ts', + 'hotFile' => null, + 'buildDirectory' => 'vendor/lunar-panel/bundles', + '__buildSourcePath' => "{$this->root}/build", + ]); + } } diff --git a/packages/bundles/src/Panel/BundleSummary.php b/packages/bundles/src/Panel/BundleSummary.php new file mode 100644 index 0000000000..75bdeb9f14 --- /dev/null +++ b/packages/bundles/src/Panel/BundleSummary.php @@ -0,0 +1,179 @@ + + */ + public function for(ProductVariant $variant): array + { + $variant->loadMissing(['product.thumbnail', 'images', 'values.option', 'bundle']); + + /** @var ?Bundle $bundle */ + $bundle = $variant->bundle; + + return [ + 'variant' => $this->variantRow($variant), + 'defined' => $bundle !== null, + 'bundle' => $bundle ? $this->bundle($bundle, $variant) : null, + 'default_language' => Language::query()->where('default', true)->value('code') ?? config('app.locale'), + 'urls' => [ + 'show' => route('panel.bundles.variants.show', $variant), + 'define' => route('panel.bundles.variants.update', $variant), + 'destroy' => route('panel.bundles.variants.destroy', $variant), + 'components' => route('panel.bundles.variants.components', $variant), + 'groups' => route('panel.bundles.variants.groups', $variant), + 'search' => route('panel.bundles.search-variants', ['exclude' => $variant->getKey()]), + ], + ]; + } + + /** + * The picker row and the component row share this shape so a variant + * reads the same before and after it is added. + * + * @return array{id: int, public_id: string, sku: ?string, name: string, option: ?string, thumbnail: ?string, edit_url: string} + */ + public function variantRow(ProductVariant $variant): array + { + $variant->loadMissing(['product.thumbnail', 'images', 'values.option']); + + return [ + 'id' => (int) $variant->getKey(), + 'public_id' => (string) $variant->public_id, + 'sku' => $variant->sku, + 'name' => (string) $variant->product?->translate('name'), + 'option' => $this->optionLabel($variant), + 'thumbnail' => $variant->getThumbnailImage() ?: null, + 'edit_url' => route('panel.products.variants.edit', [$variant->product_id, $variant->getKey()]), + ]; + } + + /** + * @return array + */ + protected function bundle(Bundle $bundle, ProductVariant $variant): array + { + $bundle->loadMissing(['groups', 'components.variant.product.thumbnail', 'components.variant.images', 'components.variant.values.option', 'components.variant.prices']); + + $inventory = $this->inventory->execute($variant); + + return [ + 'id' => (int) $bundle->getKey(), + 'pricing' => $bundle->pricing->value, + 'discount_percentage' => $bundle->discount_percentage, + 'configurable' => $bundle->groups->isNotEmpty(), + 'components' => $bundle->components->map(fn (BundleComponent $component) => [ + 'id' => (int) $component->getKey(), + 'public_id' => (string) $component->public_id, + 'group_id' => $component->bundle_group_id ? (int) $component->bundle_group_id : null, + 'quantity' => (int) $component->quantity, + 'default' => (bool) $component->default, + 'position' => (int) $component->position, + 'variant' => $this->variantRow($component->variant), + ])->values()->all(), + 'groups' => $bundle->groups->map(fn (BundleGroup $group) => [ + 'id' => (int) $group->getKey(), + 'public_id' => (string) $group->public_id, + 'name' => $group->name?->getArrayCopy() ?: (object) [], + 'label' => (string) $group->translate('name'), + 'min_selections' => (int) $group->min_selections, + 'max_selections' => (int) $group->max_selections, + 'position' => (int) $group->position, + ])->values()->all(), + 'availability' => [ + 'available' => $inventory->available, + 'unlimited' => $inventory->unlimited, + ], + 'prices' => $bundle->pricing === BundlePricing::Components ? $this->derivedPrices($bundle, $variant) : [], + ]; + } + + /** + * Per currency: the materialised base price when every default-selection + * component has a base price, otherwise the components that lack one. + * + * @return array}> + */ + protected function derivedPrices(Bundle $bundle, ProductVariant $variant): array + { + $variant->loadMissing('prices'); + + $components = $bundle->components + ->filter(fn (BundleComponent $component) => $component->isFixed() || $component->default) + ->values(); + + return Currency::query()->orderByDesc('default')->orderBy('code')->get() + ->map(function (Currency $currency) use ($components, $variant) { + $missing = $components + ->reject(fn (BundleComponent $component) => $this->basePrice($component->variant->prices, $currency) !== null) + ->map(fn (BundleComponent $component) => $this->componentLabel($component->variant)) + ->values() + ->all(); + + $price = $missing === [] ? $this->basePrice($variant->prices, $currency) : null; + + return [ + 'currency' => $currency->code, + 'price' => $price ? (new PriceValue((int) $price->price, $currency))->format() : null, + 'list_price' => $price?->list_price ? (new PriceValue((int) $price->list_price, $currency))->format() : null, + 'missing' => $missing, + ]; + }) + ->values() + ->all(); + } + + /** + * @param Collection $prices + */ + protected function basePrice(Collection $prices, Currency $currency): ?Price + { + return $prices->first( + fn (Price $price) => (int) $price->currency_id === (int) $currency->getKey() + && (int) $price->min_quantity === 1 + && $price->customer_group_id === null + ); + } + + protected function optionLabel(ProductVariant $variant): ?string + { + $label = $variant->values + ->map(fn (ProductOptionValue $value) => $value->translate('name')) + ->filter() + ->implode(' / '); + + return $label !== '' ? $label : null; + } + + protected function componentLabel(ProductVariant $variant): string + { + $name = (string) $variant->product?->translate('name'); + $option = $this->optionLabel($variant); + + return $option ? "{$name} ({$option})" : ($name !== '' ? $name : (string) $variant->sku); + } +} diff --git a/packages/bundles/src/Panel/BundlesSectionExtension.php b/packages/bundles/src/Panel/BundlesSectionExtension.php new file mode 100644 index 0000000000..fc3e57423a --- /dev/null +++ b/packages/bundles/src/Panel/BundlesSectionExtension.php @@ -0,0 +1,83 @@ +name('panel.bundles.') + ->middleware('can:'.CatalogSection::PRODUCTS_PERMISSION) + ->group(function (): void { + Route::get('/search-variants', [VariantSearchController::class, 'search'])->name('search-variants'); + + Route::get('/variants/{variant}', [BundleController::class, 'show'])->name('variants.show'); + Route::put('/variants/{variant}', [BundleController::class, 'update'])->name('variants.update'); + Route::delete('/variants/{variant}', [BundleController::class, 'destroy'])->name('variants.destroy'); + Route::put('/variants/{variant}/components', [BundleComponentsController::class, 'update'])->name('variants.components'); + Route::put('/variants/{variant}/groups', [BundleGroupsController::class, 'update'])->name('variants.groups'); + + Route::get('/products/{product}', [ProductBundlesController::class, 'show'])->name('products.show'); + Route::get('/products/{product}/included-in', [ProductBundlesController::class, 'includedIn'])->name('products.included-in'); + }); + }; + } + + public function slots(SlotRegistry $registry): void + { + $registry->add(new Slot( + zone: 'products.edit:variants:after', + component: 'bundles::BundleCard', + permission: CatalogSection::PRODUCTS_PERMISSION, + )); + + $registry->add(new Slot( + zone: 'products.variants.edit:main:after', + component: 'bundles::BundleCard', + permission: CatalogSection::PRODUCTS_PERMISSION, + )); + + $registry->add(new Slot( + zone: 'products.edit:sidebar:after', + component: 'bundles::IncludedInBundlesCard', + permission: CatalogSection::PRODUCTS_PERMISSION, + )); + } + + /** @return array */ + public function tableExtensions(): array + { + return ['products.index' => ProductsTableExtension::class]; + } + + /** @return array */ + public function langNamespaces(): array + { + return ['bundles']; + } +} diff --git a/packages/bundles/src/Panel/Http/Controllers/BundleComponentsController.php b/packages/bundles/src/Panel/Http/Controllers/BundleComponentsController.php new file mode 100644 index 0000000000..f5da407c19 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Controllers/BundleComponentsController.php @@ -0,0 +1,36 @@ +bundle($variant); + + $components = collect($request->validated('components', [])) + ->values() + ->map(fn (array $component, int $index) => [ + 'variant' => (int) $component['variant_id'], + 'quantity' => (int) ($component['quantity'] ?? 1), + 'group' => isset($component['group_id']) ? (int) $component['group_id'] : null, + 'default' => (bool) ($component['default'] ?? false), + 'position' => (int) ($component['position'] ?? $index), + ]) + ->all(); + + $this->guarded('components', fn () => $bundle->syncComponents($components)); + + $variant->unsetRelation('bundle'); + + return response()->json($summary->for($variant)); + } +} diff --git a/packages/bundles/src/Panel/Http/Controllers/BundleController.php b/packages/bundles/src/Panel/Http/Controllers/BundleController.php new file mode 100644 index 0000000000..6b347d528e --- /dev/null +++ b/packages/bundles/src/Panel/Http/Controllers/BundleController.php @@ -0,0 +1,49 @@ +json($summary->for($variant)); + } + + public function update(DefineBundleRequest $request, ProductVariant $variant, DefinesBundle $define, BundleSummary $summary): JsonResponse + { + $discount = $request->input('discount_percentage'); + + $this->guarded('bundle', fn () => $define->execute( + $variant, + BundlePricing::from($request->string('pricing')->value()), + $discount === null || $discount === '' ? null : (float) $discount, + )); + + $variant->unsetRelation('bundle'); + + return response()->json($summary->for($variant)); + } + + public function destroy(ProductVariant $variant, DeletesBundle $delete, BundleSummary $summary): JsonResponse + { + $variant->loadMissing('bundle'); + + if ($variant->bundle) { + $delete->execute($variant->bundle); + $variant->unsetRelation('bundle'); + } + + return response()->json($summary->for($variant)); + } +} diff --git a/packages/bundles/src/Panel/Http/Controllers/BundleGroupsController.php b/packages/bundles/src/Panel/Http/Controllers/BundleGroupsController.php new file mode 100644 index 0000000000..df9200fa29 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Controllers/BundleGroupsController.php @@ -0,0 +1,36 @@ +bundle($variant); + + $groups = collect($request->validated('groups', [])) + ->values() + ->map(fn (array $group, int $index) => array_filter([ + 'id' => isset($group['id']) ? (int) $group['id'] : null, + 'name' => array_filter($group['name'], fn ($value) => $value !== null && $value !== ''), + 'min_selections' => (int) $group['min_selections'], + 'max_selections' => (int) $group['max_selections'], + 'position' => (int) ($group['position'] ?? $index), + ], fn ($value) => $value !== null)) + ->all(); + + $this->guarded('groups', fn () => $bundle->syncGroups($groups)); + + $variant->unsetRelation('bundle'); + + return response()->json($summary->for($variant)); + } +} diff --git a/packages/bundles/src/Panel/Http/Controllers/Concerns/TranslatesBundleExceptions.php b/packages/bundles/src/Panel/Http/Controllers/Concerns/TranslatesBundleExceptions.php new file mode 100644 index 0000000000..37ecbe5667 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Controllers/Concerns/TranslatesBundleExceptions.php @@ -0,0 +1,30 @@ + [$exception->getMessage()]]); + } + } +} diff --git a/packages/bundles/src/Panel/Http/Controllers/ProductBundlesController.php b/packages/bundles/src/Panel/Http/Controllers/ProductBundlesController.php new file mode 100644 index 0000000000..38c1d25faf --- /dev/null +++ b/packages/bundles/src/Panel/Http/Controllers/ProductBundlesController.php @@ -0,0 +1,63 @@ +variants() + ->with(['product.thumbnail', 'images', 'values.option', 'bundle']) + ->orderBy('id') + ->get() + ->map(fn (ProductVariant $variant) => $summary->for($variant)) + ->values(); + + return response()->json(['variants' => $variants]); + } + + /** + * Bundles that include any of this product's variants, each linking to + * its own product: the merchant's warning before deleting a component. + */ + public function includedIn(Product $product, BundleSummary $summary): JsonResponse + { + $variantIds = $product->variants()->pluck('id'); + + $bundles = Bundle::query() + ->whereHas('components', fn ($query) => $query->whereIn('product_variant_id', $variantIds)) + ->with([ + 'variant.product.thumbnail', + 'variant.images', + 'variant.values.option', + 'components' => fn ($query) => $query + ->whereIn('product_variant_id', $variantIds) + ->with(['variant.product.thumbnail', 'variant.images', 'variant.values.option']), + ]) + ->get() + ->map(fn (Bundle $bundle) => [ + 'id' => (int) $bundle->getKey(), + 'variant' => $summary->variantRow($bundle->variant), + 'product_url' => route('panel.products.edit', $bundle->variant->product_id), + 'components' => $bundle->components->map(fn (BundleComponent $component) => [ + 'quantity' => (int) $component->quantity, + 'variant' => $summary->variantRow($component->variant), + ])->values()->all(), + ]) + ->values(); + + return response()->json(['data' => $bundles]); + } +} diff --git a/packages/bundles/src/Panel/Http/Controllers/VariantSearchController.php b/packages/bundles/src/Panel/Http/Controllers/VariantSearchController.php new file mode 100644 index 0000000000..ab08e858f0 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Controllers/VariantSearchController.php @@ -0,0 +1,54 @@ +string('q')->value(); + + $variants = ProductVariant::query() + ->with(['product.thumbnail', 'images', 'values.option']) + ->whereNotIn('id', Bundle::query()->select('product_variant_id')) + ->when($request->filled('exclude'), fn ($query) => $query->whereKeyNot($request->integer('exclude'))) + ->when($term !== '', function ($query) use ($term) { + $like = "%{$term}%"; + + $query->where(function ($query) use ($like) { + // The dedicated name column holds a {locale: text} map. + $query->where('sku', 'like', $like) + ->orWhereHas('product', fn ($query) => $query->where('name', 'like', $like)); + }); + }) + ->orderBy('id') + ->limit(20) + ->get() + ->map(function (ProductVariant $variant) use ($summary) { + $row = $summary->variantRow($variant); + + return [ + ...$row, + 'kind' => 'variants', + 'label' => $row['option'] ? "{$row['name']} ({$row['option']})" : $row['name'], + 'hint' => $row['sku'], + 'is_bundle' => false, + ]; + }) + ->values(); + + return response()->json(['data' => $variants]); + } +} diff --git a/packages/bundles/src/Panel/Http/Requests/Concerns/RequiresBundle.php b/packages/bundles/src/Panel/Http/Requests/Concerns/RequiresBundle.php new file mode 100644 index 0000000000..7ee2a9d488 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Requests/Concerns/RequiresBundle.php @@ -0,0 +1,30 @@ +loadMissing('bundle'); + + /** @var ?Bundle $bundle */ + $bundle = $variant->bundle; + + if (! $bundle) { + throw ValidationException::withMessages([ + 'bundle' => [__('bundles::bundles.panel.not_defined')], + ]); + } + + return $bundle; + } +} diff --git a/packages/bundles/src/Panel/Http/Requests/DefineBundleRequest.php b/packages/bundles/src/Panel/Http/Requests/DefineBundleRequest.php new file mode 100644 index 0000000000..0ec856de15 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Requests/DefineBundleRequest.php @@ -0,0 +1,19 @@ +> */ + public function rules(): array + { + return [ + 'pricing' => ['required', Rule::enum(BundlePricing::class)], + 'discount_percentage' => ['nullable', 'numeric', 'min:0', 'max:100'], + ]; + } +} diff --git a/packages/bundles/src/Panel/Http/Requests/SyncBundleComponentsRequest.php b/packages/bundles/src/Panel/Http/Requests/SyncBundleComponentsRequest.php new file mode 100644 index 0000000000..7c02e35fa4 --- /dev/null +++ b/packages/bundles/src/Panel/Http/Requests/SyncBundleComponentsRequest.php @@ -0,0 +1,26 @@ +> */ + public function rules(): array + { + return [ + 'components' => ['present', 'array'], + 'components.*.variant_id' => ['required', 'integer', 'exists:'.(new ProductVariant)->getTable().',id'], + 'components.*.quantity' => ['required', 'integer', 'min:1'], + 'components.*.group_id' => ['nullable', 'integer', 'exists:'.(new BundleGroup)->getTable().',id'], + 'components.*.default' => ['sometimes', 'boolean'], + 'components.*.position' => ['sometimes', 'integer', 'min:0'], + ]; + } +} diff --git a/packages/bundles/src/Panel/Http/Requests/SyncBundleGroupsRequest.php b/packages/bundles/src/Panel/Http/Requests/SyncBundleGroupsRequest.php new file mode 100644 index 0000000000..674a7e15ac --- /dev/null +++ b/packages/bundles/src/Panel/Http/Requests/SyncBundleGroupsRequest.php @@ -0,0 +1,26 @@ +> */ + public function rules(): array + { + return [ + 'groups' => ['present', 'array'], + 'groups.*.id' => ['nullable', 'integer', 'exists:'.(new BundleGroup)->getTable().',id'], + 'groups.*.name' => ['required', 'array'], + 'groups.*.name.*' => ['nullable', 'string', 'max:255'], + 'groups.*.min_selections' => ['required', 'integer', 'min:0'], + 'groups.*.max_selections' => ['required', 'integer', 'min:1'], + 'groups.*.position' => ['sometimes', 'integer', 'min:0'], + ]; + } +} diff --git a/packages/bundles/src/Panel/Tables/BundleColumn.php b/packages/bundles/src/Panel/Tables/BundleColumn.php new file mode 100644 index 0000000000..26face8302 --- /dev/null +++ b/packages/bundles/src/Panel/Tables/BundleColumn.php @@ -0,0 +1,34 @@ +withExists('bundles as is_bundle'); + } +} diff --git a/packages/bundles/src/Panel/Tables/BundlesOnlyFilter.php b/packages/bundles/src/Panel/Tables/BundlesOnlyFilter.php new file mode 100644 index 0000000000..148d046316 --- /dev/null +++ b/packages/bundles/src/Panel/Tables/BundlesOnlyFilter.php @@ -0,0 +1,34 @@ + */ + public function options(): array + { + return [ + 'only' => __('bundles::bundles.panel.filter_only'), + ]; + } + + public function query(Builder $query, mixed $value): void + { + if ($value === 'only') { + $query->whereHas('bundles'); + } + } +} diff --git a/packages/bundles/src/Panel/Tables/ProductsTableExtension.php b/packages/bundles/src/Panel/Tables/ProductsTableExtension.php new file mode 100644 index 0000000000..a31082fea4 --- /dev/null +++ b/packages/bundles/src/Panel/Tables/ProductsTableExtension.php @@ -0,0 +1,20 @@ + */ + public function columns(): array + { + return [BundleColumn::class]; + } + + /** @return array */ + public function filters(): array + { + return [BundlesOnlyFilter::class]; + } +} diff --git a/packages/bundles/vite.config.js b/packages/bundles/vite.config.js new file mode 100644 index 0000000000..3962d47a78 --- /dev/null +++ b/packages/bundles/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import lunarPanelPlugin from '@lunarphp/panel-vite-plugin'; + +// Compiles resources/js/addon.ts to one IIFE bundle sharing the panel's Vue instance. +export default defineConfig({ + plugins: [ + vue(), + lunarPanelPlugin({ name: 'LunarBundlesAddon' }), + ], + build: { + outDir: 'build', + rollupOptions: { + input: 'resources/js/addon.ts', + }, + }, +}); diff --git a/tests/bundles/Feature/Panel/BundleEndpointsTest.php b/tests/bundles/Feature/Panel/BundleEndpointsTest.php new file mode 100644 index 0000000000..b23f15871d --- /dev/null +++ b/tests/bundles/Feature/Panel/BundleEndpointsTest.php @@ -0,0 +1,337 @@ +currency = Catalogue::store(); + $this->variant = ProductVariant::factory()->create(['sku' => 'KIT-1']); +}); + +function actingAsAdmin(): Staff +{ + $staff = Staff::factory()->create(['admin' => true]); + test()->actingAs($staff, 'staff'); + + return $staff; +} + +it('redirects guests and forbids staff without the products permission', function () { + $this->getJson(route('panel.bundles.variants.show', $this->variant))->assertUnauthorized(); + $this->get(route('panel.bundles.variants.show', $this->variant))->assertRedirect(route('panel.login')); + + $this->actingAs(Staff::factory()->create(['admin' => false]), 'staff'); + + $this->getJson(route('panel.bundles.variants.show', $this->variant))->assertForbidden(); + $this->getJson(route('panel.bundles.search-variants'))->assertForbidden(); + $this->putJson(route('panel.bundles.variants.update', $this->variant), ['pricing' => 'fixed'])->assertForbidden(); + $this->deleteJson(route('panel.bundles.variants.destroy', $this->variant))->assertForbidden(); + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => []])->assertForbidden(); + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => []])->assertForbidden(); + $this->getJson(route('panel.bundles.products.show', $this->variant->product))->assertForbidden(); + $this->getJson(route('panel.bundles.products.included-in', $this->variant->product))->assertForbidden(); +}); + +it('summarises an ordinary variant as undefined', function () { + actingAsAdmin(); + + $this->getJson(route('panel.bundles.variants.show', $this->variant)) + ->assertOk() + ->assertJsonPath('defined', false) + ->assertJsonPath('bundle', null) + ->assertJsonPath('variant.sku', 'KIT-1') + ->assertJsonPath('default_language', 'en') + ->assertJsonPath('urls.define', route('panel.bundles.variants.update', $this->variant)); +}); + +it('defines, redefines and removes a bundle', function () { + actingAsAdmin(); + + $this->putJson(route('panel.bundles.variants.update', $this->variant), ['pricing' => 'fixed']) + ->assertOk() + ->assertJsonPath('defined', true) + ->assertJsonPath('bundle.pricing', 'fixed') + ->assertJsonPath('bundle.components', []) + ->assertJsonPath('bundle.availability.available', 0); + + $this->putJson(route('panel.bundles.variants.update', $this->variant), ['pricing' => 'components', 'discount_percentage' => 12.5]) + ->assertOk() + ->assertJsonPath('bundle.pricing', 'components') + ->assertJsonPath('bundle.discount_percentage', 12.5); + + expect(Bundle::query()->where('product_variant_id', $this->variant->id)->count())->toBe(1); + + $this->deleteJson(route('panel.bundles.variants.destroy', $this->variant)) + ->assertOk() + ->assertJsonPath('defined', false); + + expect(Bundle::query()->count())->toBe(0); +}); + +it('validates the definition payload', function () { + actingAsAdmin(); + + $this->putJson(route('panel.bundles.variants.update', $this->variant), ['pricing' => 'magic']) + ->assertUnprocessable() + ->assertJsonValidationErrors(['pricing']); + + $this->putJson(route('panel.bundles.variants.update', $this->variant), ['pricing' => 'components', 'discount_percentage' => 120]) + ->assertUnprocessable() + ->assertJsonValidationErrors(['discount_percentage']); +}); + +it('refuses to define a component variant as a bundle with a translated 422', function () { + actingAsAdmin(); + + $other = Bundle::factory()->create(); + BundleComponent::factory()->create(['bundle_id' => $other->id, 'product_variant_id' => $this->variant->id]); + + $this->putJson(route('panel.bundles.variants.update', $this->variant), ['pricing' => 'fixed']) + ->assertUnprocessable() + ->assertJsonPath('errors.bundle.0', __('bundles::bundles.nesting.is_component')); +}); + +it('syncs components and reports derived availability and prices', function () { + actingAsAdmin(); + + $body = Catalogue::variant($this->currency, 10000, 10, ['sku' => 'BODY']); + $bag = Catalogue::variant($this->currency, 2500, 4, ['sku' => 'BAG']); + + Bundle::factory()->componentPriced(10)->create(['product_variant_id' => $this->variant->id]); + + $response = $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => $body->id, 'quantity' => 1], + ['variant_id' => $bag->id, 'quantity' => 2], + ]]) + ->assertOk() + ->assertJsonCount(2, 'bundle.components') + ->assertJsonPath('bundle.components.0.variant.sku', 'BODY') + ->assertJsonPath('bundle.components.1.variant.sku', 'BAG') + ->assertJsonPath('bundle.components.1.quantity', 2) + ->assertJsonPath('bundle.availability.available', 2) + ->assertJsonPath('bundle.prices.0.currency', 'GBP') + ->assertJsonPath('bundle.prices.0.missing', []); + + // 10000 + 2 x 2500 = 15000, less 10% = 13500. + expect(Price::query()->where('priceable_id', $this->variant->id)->value('price'))->toBe(13500) + ->and($response->json('bundle.prices.0.price'))->toContain('135'); + + // Reordering and removal go through the same complete-list sync. + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => $bag->id, 'quantity' => 1, 'position' => 0], + ]]) + ->assertOk() + ->assertJsonCount(1, 'bundle.components') + ->assertJsonPath('bundle.components.0.variant.sku', 'BAG'); +}); + +it('names the component missing a price in a currency', function () { + actingAsAdmin(); + + $unpriced = ProductVariant::factory()->create(['sku' => 'NOPRICE']); + Bundle::factory()->componentPriced()->create(['product_variant_id' => $this->variant->id]); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => $unpriced->id, 'quantity' => 1], + ]]) + ->assertOk() + ->assertJsonPath('bundle.prices.0.price', null) + ->assertJsonCount(1, 'bundle.prices.0.missing'); +}); + +it('rejects invalid component syncs with a 422', function () { + actingAsAdmin(); + + Bundle::factory()->create(['product_variant_id' => $this->variant->id]); + $other = Bundle::factory()->create(); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => []]) + ->assertUnprocessable() + ->assertJsonPath('errors.components.0', __('bundles::bundles.definition.empty')); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => $other->product_variant_id, 'quantity' => 1], + ]]) + ->assertUnprocessable() + ->assertJsonPath('errors.components.0', __('bundles::bundles.nesting.is_bundle')); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => $this->variant->id, 'quantity' => 1], + ]]) + ->assertUnprocessable() + ->assertJsonPath('errors.components.0', __('bundles::bundles.nesting.self')); + + config()->set('lunar.bundles.max_components', 1); + $parts = ProductVariant::factory()->count(2)->create(); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => $parts + ->map(fn (ProductVariant $part) => ['variant_id' => $part->id, 'quantity' => 1]) + ->all()]) + ->assertUnprocessable() + ->assertJsonPath('errors.components.0', __('bundles::bundles.definition.too_many_components', ['max' => 1])); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => 999999, 'quantity' => 0], + ]]) + ->assertUnprocessable() + ->assertJsonValidationErrors(['components.0.variant_id', 'components.0.quantity']); +}); + +it('refuses component and group syncs on an undefined variant', function () { + actingAsAdmin(); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => []]) + ->assertUnprocessable() + ->assertJsonPath('errors.bundle.0', __('bundles::bundles.panel.not_defined')); + + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => []]) + ->assertUnprocessable() + ->assertJsonPath('errors.bundle.0', __('bundles::bundles.panel.not_defined')); +}); + +it('syncs groups with their options and defaults', function () { + actingAsAdmin(); + + $bundle = Bundle::factory()->create(['product_variant_id' => $this->variant->id]); + $body = Catalogue::variant($this->currency, 10000, 10, ['sku' => 'BODY']); + $lensA = Catalogue::variant($this->currency, 3000, 5, ['sku' => 'LENS-A']); + $lensB = Catalogue::variant($this->currency, 4000, 1, ['sku' => 'LENS-B']); + + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => [ + ['name' => ['en' => 'Lens'], 'min_selections' => 1, 'max_selections' => 1], + ]]) + ->assertOk() + ->assertJsonCount(1, 'bundle.groups') + ->assertJsonPath('bundle.groups.0.label', 'Lens') + ->assertJsonPath('bundle.configurable', true); + + $group = BundleGroup::query()->where('bundle_id', $bundle->id)->firstOrFail(); + + $this->putJson(route('panel.bundles.variants.components', $this->variant), ['components' => [ + ['variant_id' => $body->id, 'quantity' => 1], + ['variant_id' => $lensA->id, 'quantity' => 1, 'group_id' => $group->id, 'default' => true], + ['variant_id' => $lensB->id, 'quantity' => 1, 'group_id' => $group->id], + ]]) + ->assertOk() + ->assertJsonCount(3, 'bundle.components') + ->assertJsonPath('bundle.components.1.group_id', $group->id) + ->assertJsonPath('bundle.components.1.default', true) + // Body x10, best-stocked lens x5: five complete bundles. + ->assertJsonPath('bundle.availability.available', 5); + + // Rename and widen the group in place, keyed by id. + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => [ + ['id' => $group->id, 'name' => ['en' => 'Lenses'], 'min_selections' => 1, 'max_selections' => 2], + ]]) + ->assertOk() + ->assertJsonPath('bundle.groups.0.id', $group->id) + ->assertJsonPath('bundle.groups.0.label', 'Lenses') + ->assertJsonPath('bundle.groups.0.max_selections', 2); + + // Dropping the group drops its options; the fixed component stays. + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => []]) + ->assertOk() + ->assertJsonCount(0, 'bundle.groups') + ->assertJsonCount(1, 'bundle.components') + ->assertJsonPath('bundle.configurable', false); +}); + +it('rejects group limits the bundle cannot satisfy', function () { + actingAsAdmin(); + + Bundle::factory()->create(['product_variant_id' => $this->variant->id]); + + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => [ + ['name' => ['en' => 'Lens'], 'min_selections' => 3, 'max_selections' => 1], + ]]) + ->assertUnprocessable() + ->assertJsonPath('errors.groups.0', __('bundles::bundles.definition.group_selections', ['group' => 'Lens'])); + + $this->putJson(route('panel.bundles.variants.groups', $this->variant), ['groups' => [ + ['name' => [], 'min_selections' => -1, 'max_selections' => 0], + ]]) + ->assertUnprocessable() + ->assertJsonValidationErrors(['groups.0.name', 'groups.0.min_selections', 'groups.0.max_selections']); +}); + +it('searches variants by product name and sku, excluding bundle variants and the bundle itself', function () { + actingAsAdmin(); + + $camera = Product::factory()->create(['name' => collect(['en' => 'Camera body'])]); + $body = ProductVariant::factory()->create(['product_id' => $camera->id, 'sku' => 'BODY-1']); + $bag = ProductVariant::factory()->create(['sku' => 'BAG-9']); + $other = Bundle::factory()->create(['product_variant_id' => ProductVariant::factory()->create(['sku' => 'OTHER-KIT'])->id]); + + $this->getJson(route('panel.bundles.search-variants', ['q' => 'camera'])) + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $body->id) + ->assertJsonPath('data.0.kind', 'variants') + ->assertJsonPath('data.0.label', 'Camera body') + ->assertJsonPath('data.0.hint', 'BODY-1') + ->assertJsonPath('data.0.is_bundle', false); + + $this->getJson(route('panel.bundles.search-variants', ['q' => 'BAG'])) + ->assertOk() + ->assertJsonPath('data.0.id', $bag->id); + + $ids = $this->getJson(route('panel.bundles.search-variants', ['q' => '', 'exclude' => $this->variant->id, 'bucket' => 'components', 'kinds' => ['variants']])) + ->assertOk() + ->json('data.*.id'); + + expect($ids)->not->toContain($other->product_variant_id) + ->not->toContain($this->variant->id) + ->toContain($body->id, $bag->id); +}); + +it('summarises every variant of a product for the product page card', function () { + actingAsAdmin(); + + $product = $this->variant->product; + $sibling = ProductVariant::factory()->create(['product_id' => $product->id, 'sku' => 'KIT-2']); + Bundle::factory()->create(['product_variant_id' => $sibling->id]); + + $this->getJson(route('panel.bundles.products.show', $product)) + ->assertOk() + ->assertJsonCount(2, 'variants') + ->assertJsonPath('variants.0.variant.id', $this->variant->id) + ->assertJsonPath('variants.0.defined', false) + ->assertJsonPath('variants.1.variant.id', $sibling->id) + ->assertJsonPath('variants.1.defined', true) + ->assertJsonPath('variants.1.variant.edit_url', route('panel.products.variants.edit', [$product, $sibling])); +}); + +it('lists the bundles a product is included in', function () { + actingAsAdmin(); + + $part = ProductVariant::factory()->create(['sku' => 'PART']); + $bundle = Bundle::factory()->create(['product_variant_id' => $this->variant->id]); + BundleComponent::factory()->create(['bundle_id' => $bundle->id, 'product_variant_id' => $part->id, 'quantity' => 3]); + + $this->getJson(route('panel.bundles.products.included-in', $part->product)) + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $bundle->id) + ->assertJsonPath('data.0.variant.sku', 'KIT-1') + ->assertJsonPath('data.0.product_url', route('panel.products.edit', $this->variant->product_id)) + ->assertJsonPath('data.0.components.0.quantity', 3) + ->assertJsonPath('data.0.components.0.variant.sku', 'PART'); + + $this->getJson(route('panel.bundles.products.included-in', $this->variant->product)) + ->assertOk() + ->assertJsonCount(0, 'data'); +}); + +it('keeps the bundle pricing enum in step with the panel select', function () { + expect(array_map(fn (BundlePricing $case) => $case->value, BundlePricing::cases()))->toBe(['fixed', 'components']); +}); diff --git a/tests/bundles/Feature/Panel/BundlesExtensionTest.php b/tests/bundles/Feature/Panel/BundlesExtensionTest.php new file mode 100644 index 0000000000..935d1ed318 --- /dev/null +++ b/tests/bundles/Feature/Panel/BundlesExtensionTest.php @@ -0,0 +1,91 @@ +create(['default' => true, 'code' => 'en']); +}); + +it('registers the bundle cards in the product page zones', function () { + $this->actingAs(Staff::factory()->create(['admin' => true]), 'staff'); + + $product = Product::factory()->create(); + $variant = ProductVariant::factory()->create(['product_id' => $product->id]); + + $this->get(route('panel.products.edit', $product)) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->where('slots', function ($slots) { + $variants = collect($slots->get('products.edit:variants:after', []))->pluck('component'); + $sidebar = collect($slots->get('products.edit:sidebar:after', []))->pluck('component'); + + return $variants->contains('bundles::BundleCard') + && $sidebar->contains('bundles::IncludedInBundlesCard'); + }) + ); + + $this->get(route('panel.products.variants.edit', [$product, $variant])) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->where('slots', fn ($slots) => collect($slots->get('products.variants.edit:main:after', [])) + ->pluck('component') + ->contains('bundles::BundleCard')) + ); +}); + +it('hides the bundle slots from staff without the products permission', function () { + $this->actingAs(Staff::factory()->create(['admin' => false]), 'staff'); + + $this->get(route('panel.dashboard')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->where('slots', fn ($slots) => collect($slots->all()) + ->flatten(1) + ->pluck('component') + ->filter(fn ($component) => str_starts_with($component, 'bundles::')) + ->isEmpty()) + ); +}); + +it('serves the bundles lang namespace to the panel frontend', function () { + $this->actingAs(Staff::factory()->create(['admin' => true]), 'staff'); + + $this->get(route('panel.translations', ['locale' => 'en'])) + ->assertOk() + ->assertJsonPath('messages.bundles::bundles.panel.card_title', 'Bundle'); +}); + +it('adds a bundle badge column and a bundles-only filter to the products table', function () { + $this->actingAs(Staff::factory()->create(['admin' => true]), 'staff'); + + $bundle = Bundle::factory()->create(); + $bundleProduct = $bundle->variant->product; + $plain = Product::factory()->create(); + ProductVariant::factory()->create(['product_id' => $plain->id]); + + $this->get(route('panel.products.index')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->where('columns', fn ($columns) => collect($columns)->firstWhere('key', 'is_bundle')['component'] === 'bundles::BundleBadge') + ->where('tableFilters.0.key', 'bundles') + ->has('products.data', 2) + ->where('products.data', fn ($rows) => collect($rows) + ->mapWithKeys(fn ($row) => [$row['id'] => (bool) $row['is_bundle']]) + ->all() === [$bundleProduct->id => true, $plain->id => false]) + ); + + $this->get(route('panel.products.index', ['filter' => ['bundles' => 'only']])) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->has('products.data', 1) + ->where('products.data.0.id', $bundleProduct->id) + ); +}); diff --git a/tests/bundles/PanelTestCase.php b/tests/bundles/PanelTestCase.php new file mode 100644 index 0000000000..2ae71faf48 --- /dev/null +++ b/tests/bundles/PanelTestCase.php @@ -0,0 +1,26 @@ + Date: Tue, 15 Sep 2026 09:36:12 +0100 Subject: [PATCH 8/9] =?UTF-8?q?fix(bundles):=20review=20follow-ups=20?= =?UTF-8?q?=E2=80=94=20per-unit=20selection=20pricing,=20empty=20required?= =?UTF-8?q?=20groups,=20pricing-switch=20confirmation,=20listener=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- TODO.md | 2 +- .../bundles/build/assets/addon-Bd9bHYW_.js | 1 + .../bundles/build/assets/addon-DV2m4Dh6.js | 1 - packages/bundles/build/manifest.json | 2 +- .../resources/js/components/BundleEditor.vue | 30 +- .../bundles/resources/lang/ar/bundles.php | 2 + .../bundles/resources/lang/bg/bundles.php | 2 + .../bundles/resources/lang/de/bundles.php | 2 + .../bundles/resources/lang/en/bundles.php | 2 + .../bundles/resources/lang/es/bundles.php | 2 + .../bundles/resources/lang/fa/bundles.php | 2 + .../bundles/resources/lang/fr/bundles.php | 2 + .../bundles/resources/lang/hr/bundles.php | 2 + .../bundles/resources/lang/hu/bundles.php | 2 + .../bundles/resources/lang/mn/bundles.php | 2 + .../bundles/resources/lang/nl/bundles.php | 2 + .../bundles/resources/lang/pl/bundles.php | 2 + .../bundles/resources/lang/pt_BR/bundles.php | 2 + .../bundles/resources/lang/ro/bundles.php | 2 + .../bundles/resources/lang/tr/bundles.php | 2 + .../bundles/resources/lang/vi/bundles.php | 2 + packages/bundles/src/Actions/DefineBundle.php | 2 +- .../src/Actions/ResolveBundleInventory.php | 5 + .../CartLine/PriceBundleSelection.php | 3 +- specs/0082-selling-policy-rework.md | 225 +++++++++++ specs/0083-cart-line-fulfilment-method.md | 370 ++++++++++++++++++ specs/0085-product-bundles.md | 24 +- specs/README.md | 2 +- .../Feature/BundleInvalidationTest.php | 70 ++++ tests/bundles/Feature/BundleInventoryTest.php | 14 + 30 files changed, 761 insertions(+), 22 deletions(-) create mode 100644 packages/bundles/build/assets/addon-Bd9bHYW_.js delete mode 100644 packages/bundles/build/assets/addon-DV2m4Dh6.js create mode 100644 specs/0082-selling-policy-rework.md create mode 100644 specs/0083-cart-line-fulfilment-method.md create mode 100644 tests/bundles/Feature/BundleInvalidationTest.php diff --git a/TODO.md b/TODO.md index adaa023754..5d45d80d8f 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,6 @@ Items tagged _(judgement)_ are genuine line-calls worth revisiting. - Bulk order operations — goal-oriented bulk actions on the orders table (spec 0026) - Order print templates — Print dropdown of selectable PDF templates, ships an Advice Note (spec 0027) - Cart totals caching in the database — additive performance optimisation -- Product bundles — `lunarphp/bundles`: sell a set of variants as one product with derived stock and component order lines (spec 0085) - Add Boost guidelines to packages ## Ideas @@ -35,6 +34,7 @@ Items tagged _(judgement)_ are genuine line-calls worth revisiting. ## Done +- Product bundles — `lunarphp/bundles`: a variant with components, derived stock, component order lines, panel editor (spec 0085) - Attribute field-type configuration schema — field types declare their settings once in core; the panel and Filament bridge both render from the descriptors (spec 0054) - Filament admin & bridge hardening — standalone bridge, dead hooks and config, locale nav groups, guard and asset id (spec 0076) - First staff account creation in core — `lunar:create-admin` moves out of the Filament admin; panel install offers it (spec 0075) diff --git a/packages/bundles/build/assets/addon-Bd9bHYW_.js b/packages/bundles/build/assets/addon-Bd9bHYW_.js new file mode 100644 index 0000000000..74d8d29274 --- /dev/null +++ b/packages/bundles/build/assets/addon-Bd9bHYW_.js @@ -0,0 +1 @@ +(function(e,D,B,i){"use strict";const I=a=>{var p;return((p=a.panel)==null?void 0:p.path)??"panel"},R={class:"flex items-center gap-2.5 min-w-0"},j={class:"w-8 h-8 shrink-0 rounded-md border border-line bg-surface-2 overflow-hidden flex items-center justify-center text-ink-400"},O=["src"],A={class:"min-w-0"},Q={class:"text-[12.5px] font-medium text-ink-900 truncate"},H={class:"text-[11px] text-ink-500 truncate"},J={key:0,class:"font-mono"},K={key:1},W={key:2},q=e.defineComponent({__name:"VariantThumb",props:{variant:{}},setup(a){return(p,o)=>(e.openBlock(),e.createElementBlock("div",R,[e.createElementVNode("div",j,[a.variant.thumbnail?(e.openBlock(),e.createElementBlock("img",{key:0,src:a.variant.thumbnail,alt:"",class:"w-full h-full object-cover"},null,8,O)):(e.openBlock(),e.createBlock(e.unref(i.Icon),{key:1,name:"image",cls:"sm"}))]),e.createElementVNode("div",A,[e.createElementVNode("div",Q,e.toDisplayString(a.variant.name),1),e.createElementVNode("div",H,[a.variant.sku?(e.openBlock(),e.createElementBlock("span",J,e.toDisplayString(a.variant.sku),1)):e.createCommentVNode("",!0),a.variant.sku&&a.variant.option?(e.openBlock(),e.createElementBlock("span",K," / ")):e.createCommentVNode("",!0),a.variant.option?(e.openBlock(),e.createElementBlock("span",W,e.toDisplayString(a.variant.option),1)):e.createCommentVNode("",!0)])])]))}}),X={key:0,class:"px-3 py-3 text-[12px] text-ink-500"},Y={key:1,class:"divide-y divide-line"},Z={key:0,class:"flex items-center gap-1.5 text-[11px] text-ink-500 shrink-0"},v={class:"w-[84px] shrink-0"},ee={class:"flex items-center gap-0.5 shrink-0"},F=e.defineComponent({__name:"ComponentList",props:{components:{},withDefault:{type:Boolean},disabled:{type:Boolean},emptyText:{}},emits:["quantity","move","remove","default"],setup(a,{emit:p}){const o=p,{t:s}=B.useI18n(),d=(r,g)=>{const m=Math.max(1,Math.floor(Number(g.target.value))||1);m!==r.quantity&&o("quantity",r,m)};return(r,g)=>a.components.length?(e.openBlock(),e.createElementBlock("ul",Y,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(a.components,(m,f)=>(e.openBlock(),e.createElementBlock("li",{key:m.id,class:"flex items-center gap-3 px-3 py-2"},[e.createVNode(q,{variant:m.variant,class:"flex-1"},null,8,["variant"]),a.withDefault?(e.openBlock(),e.createElementBlock("label",Z,[e.createVNode(e.unref(i.Checkbox),{"model-value":m.default,"aria-label":e.unref(s)("bundles::bundles.panel.group_default"),"onUpdate:modelValue":x=>o("default",m,x)},null,8,["model-value","aria-label","onUpdate:modelValue"]),e.createTextVNode(" "+e.toDisplayString(e.unref(s)("bundles::bundles.panel.group_default")),1)])):e.createCommentVNode("",!0),e.createElementVNode("div",v,[e.createVNode(e.unref(i.TextInput),{type:"number",min:"1",step:"1","model-value":m.quantity,disabled:a.disabled,"aria-label":e.unref(s)("bundles::bundles.panel.quantity"),onChange:x=>d(m,x)},{prefix:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(s)("bundles::bundles.panel.quantity")),1)]),_:1},8,["model-value","disabled","aria-label","onChange"])]),e.createElementVNode("div",ee,[e.createVNode(e.unref(i.Button),{variant:"ghost",size:"sm",icon:"chevDown","icon-cls":"sm rotate-180",disabled:a.disabled||f===0,"aria-label":e.unref(s)("bundles::bundles.panel.move_up"),title:e.unref(s)("bundles::bundles.panel.move_up"),onClick:x=>o("move",m,-1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(i.Button),{variant:"ghost",size:"sm",icon:"chevDown",disabled:a.disabled||f===a.components.length-1,"aria-label":e.unref(s)("bundles::bundles.panel.move_down"),title:e.unref(s)("bundles::bundles.panel.move_down"),onClick:x=>o("move",m,1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(i.Button),{variant:"ghost",size:"sm",icon:"trash",disabled:a.disabled,"aria-label":e.unref(s)("bundles::bundles.panel.remove"),title:e.unref(s)("bundles::bundles.panel.remove"),onClick:x=>o("remove",m)},null,8,["disabled","aria-label","title","onClick"])])]))),128))])):(e.openBlock(),e.createElementBlock("div",X,e.toDisplayString(a.emptyText),1))}}),te={class:"flex flex-col gap-3"},ne={class:"flex flex-wrap items-end gap-3 px-3 py-2.5 bg-surface-2 border-b border-line"},le={class:"flex-1 min-w-[180px]"},oe={class:"w-[72px]"},ae={class:"w-[72px]"},ie={class:"flex items-center gap-0.5 pb-0.5"},se={class:"px-3 py-2 border-t border-line"},de=e.defineComponent({__name:"GroupsEditor",props:{groups:{},components:{},defaultLanguage:{},disabled:{type:Boolean}},emits:["add","update","move","remove","add-option","component-quantity","component-move","component-remove","component-default"],setup(a,{emit:p}){const o=a,s=p,{t:d}=B.useI18n(),r=_=>o.components.filter(u=>u.group_id===_.id),g=_=>_.name[o.defaultLanguage]??_.label??"",m=(_,u)=>{const c=u.target.value.trim();c&&c!==g(_)&&s("update",_,{name:{..._.name,[o.defaultLanguage]:c}})},f=(_,u,c)=>{const y=Math.max(u==="min_selections"?0:1,Math.floor(Number(c.target.value))||0);y!==_[u]&&s("update",_,{[u]:y})},x=e.ref(null);return(_,u)=>(e.openBlock(),e.createElementBlock("div",te,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(a.groups,(c,y)=>(e.openBlock(),e.createElementBlock("div",{key:c.id,class:"border border-line rounded-lg bg-surface overflow-hidden"},[e.createElementVNode("div",ne,[e.createElementVNode("div",le,[e.createVNode(e.unref(i.FieldLabel),{for:`bundle-group-${c.id}-name`,hint:a.defaultLanguage.toUpperCase()},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(d)("bundles::bundles.panel.group_name")),1)]),_:1},8,["for","hint"]),e.createVNode(e.unref(i.TextInput),{id:`bundle-group-${c.id}-name`,"model-value":g(c),disabled:a.disabled,onChange:b=>m(c,b)},null,8,["id","model-value","disabled","onChange"])]),e.createElementVNode("div",oe,[e.createVNode(e.unref(i.FieldLabel),{for:`bundle-group-${c.id}-min`},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(d)("bundles::bundles.panel.group_min")),1)]),_:1},8,["for"]),e.createVNode(e.unref(i.TextInput),{id:`bundle-group-${c.id}-min`,type:"number",min:"0",step:"1","model-value":c.min_selections,disabled:a.disabled,onChange:b=>f(c,"min_selections",b)},null,8,["id","model-value","disabled","onChange"])]),e.createElementVNode("div",ae,[e.createVNode(e.unref(i.FieldLabel),{for:`bundle-group-${c.id}-max`},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(d)("bundles::bundles.panel.group_max")),1)]),_:1},8,["for"]),e.createVNode(e.unref(i.TextInput),{id:`bundle-group-${c.id}-max`,type:"number",min:"1",step:"1","model-value":c.max_selections,disabled:a.disabled,onChange:b=>f(c,"max_selections",b)},null,8,["id","model-value","disabled","onChange"])]),e.createElementVNode("div",ie,[e.createVNode(e.unref(i.Button),{variant:"ghost",size:"sm",icon:"chevDown","icon-cls":"sm rotate-180",disabled:a.disabled||y===0,"aria-label":e.unref(d)("bundles::bundles.panel.move_up"),title:e.unref(d)("bundles::bundles.panel.move_up"),onClick:b=>s("move",c,-1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(i.Button),{variant:"ghost",size:"sm",icon:"chevDown",disabled:a.disabled||y===a.groups.length-1,"aria-label":e.unref(d)("bundles::bundles.panel.move_down"),title:e.unref(d)("bundles::bundles.panel.move_down"),onClick:b=>s("move",c,1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(i.Button),{variant:"ghost",size:"sm",icon:"trash",disabled:a.disabled,"aria-label":e.unref(d)("bundles::bundles.panel.remove_group"),title:e.unref(d)("bundles::bundles.panel.remove_group"),onClick:b=>x.value=c},null,8,["disabled","aria-label","title","onClick"])])]),e.createVNode(F,{components:r(c),disabled:a.disabled,"empty-text":e.unref(d)("bundles::bundles.panel.group_empty"),"with-default":"",onQuantity:u[0]||(u[0]=(b,V)=>s("component-quantity",b,V)),onMove:u[1]||(u[1]=(b,V)=>s("component-move",b,V)),onRemove:u[2]||(u[2]=b=>s("component-remove",b)),onDefault:u[3]||(u[3]=(b,V)=>s("component-default",b,V))},null,8,["components","disabled","empty-text"]),e.createElementVNode("div",se,[e.createVNode(e.unref(i.Button),{size:"sm",icon:"plus",disabled:a.disabled,onClick:b=>s("add-option",c)},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(d)("bundles::bundles.panel.add_option")),1)]),_:1},8,["disabled","onClick"])])]))),128)),e.createElementVNode("div",null,[e.createVNode(e.unref(i.Button),{icon:"plus",disabled:a.disabled,onClick:u[4]||(u[4]=c=>s("add"))},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(d)("bundles::bundles.panel.add_group")),1)]),_:1},8,["disabled"])]),e.createVNode(e.unref(i.ConfirmDialog),{open:x.value!==null,title:e.unref(d)("bundles::bundles.panel.remove_group"),description:e.unref(d)("bundles::bundles.panel.confirm_remove_group"),tone:"danger","onUpdate:open":u[5]||(u[5]=c=>{c||(x.value=null)}),onConfirm:u[6]||(u[6]=()=>{x.value&&s("remove",x.value)})},null,8,["open","title","description"])]))}}),re={class:"flex flex-col gap-5"},ce={class:"flex items-center justify-between gap-4"},me={class:"flex items-center gap-2.5 text-[13px] text-ink-900 cursor-pointer"},pe={key:0,class:"text-[12px] text-ink-500","data-testid":"bundle-availability"},ue={key:0,class:"rounded-md border border-danger-border bg-danger-soft px-3 py-2 text-[12px] text-danger"},fe={class:"grid gap-3 sm:grid-cols-2"},be={value:"fixed"},ge={value:"components"},_e={key:0},ke={key:0,class:"text-[12px] text-ink-500 leading-normal"},xe={class:"flex items-center justify-between gap-3 mb-2"},ye={class:"text-[12.5px] font-semibold text-ink-900"},he={class:"text-[11.5px] text-ink-500"},Be={class:"border border-line rounded-lg bg-surface overflow-hidden"},Ve={key:1},Ne={class:"text-[12.5px] font-semibold text-ink-900 mb-2"},Ce={class:"w-full text-[12.5px] border border-line rounded-lg overflow-hidden"},Ee={class:"divide-y divide-line"},$e={class:"px-3 py-1.5 font-mono text-ink-700 w-[80px]"},De={key:0,class:"px-3 py-1.5 text-warn-ink"},we={key:1,class:"px-3 py-1.5 text-ink-900 [font-variant-numeric:tabular-nums]"},Se={key:0,class:"ml-2 text-ink-500 line-through"},Te={class:"mb-2"},Le={class:"text-[12.5px] font-semibold text-ink-900"},Ie={class:"text-[11.5px] text-ink-500"},qe=e.defineComponent({__name:"BundleEditor",props:{summary:{}},setup(a){const p=a,{t:o}=B.useI18n(),s=i.useToasts(),d=e.ref(p.summary);e.watch(()=>p.summary,n=>{d.value=n});const r=e.computed(()=>d.value.bundle),g=e.ref(!1),m=e.ref([]),f=async(n,l=o("bundles::bundles.panel.saved"))=>{g.value=!0,m.value=[];try{d.value=await n(),s.success(l)}catch(t){t instanceof i.ValidationError?m.value=Object.values(t.errors).flat():s.error(o("bundles::bundles.panel.load_failed"))}finally{g.value=!1}},x=e.ref(!1),_=(n,l)=>i.http.put(d.value.urls.define,{pricing:n,discount_percentage:l}),u=()=>{if(d.value.defined){x.value=!0;return}f(()=>_("fixed",null))},c=()=>{f(()=>i.http.delete(d.value.urls.destroy),o("bundles::bundles.panel.removed"))},y=e.ref(!1),b=n=>{if(!(!r.value||n===r.value.pricing)){if(n==="components"){y.value=!0;return}f(()=>_("fixed",null))}},V=()=>{f(()=>{var n;return _("components",((n=r.value)==null?void 0:n.discount_percentage)??null)})},We=n=>{const l=n.target.value.trim(),t=l===""?null:Number(l);r.value&&t!==r.value.discount_percentage&&f(()=>_(r.value.pricing,t))},M=n=>{var l;return(((l=r.value)==null?void 0:l.components)??[]).filter(t=>t.group_id===n)},Xe=e.computed(()=>M(null)),Ye=n=>{const l=new Map;return n.map(t=>{const k=l.get(t.group_id)??0;return l.set(t.group_id,k+1),{variant_id:t.variant.id,quantity:t.quantity,group_id:t.group_id,default:t.default,position:k}})},N=n=>{f(()=>i.http.put(d.value.urls.components,{components:Ye(n)}))},C=()=>{var n;return[...((n=r.value)==null?void 0:n.components)??[]]},P=(n,l)=>{N(C().map(t=>t.id===n.id?{...t,quantity:l}:t))},Ze=(n,l)=>{N(C().map(t=>t.id===n.id?{...t,default:l}:t))},U=n=>{N(C().filter(l=>l.id!==n.id))},z=(n,l)=>{const t=M(n.group_id),k=t.findIndex(L=>L.id===n.id),h=t[k+l];if(!h)return;const $=[...t];$[k]=h,$[k+l]=n,N([...C().filter(L=>L.group_id!==n.group_id),...$])},S=e.ref(!1),T=e.ref(null),G=n=>{T.value=n,S.value=!0},ve=n=>{const l=C(),t=n.filter(k=>!l.some(h=>h.group_id===T.value&&h.variant.id===k.id)).map((k,h)=>({id:-(h+1),public_id:"",group_id:T.value,quantity:1,default:!1,position:0,variant:{...k,id:k.id}}));t.length&&N([...l,...t])},et=n=>n.map((l,t)=>({id:l.id>0?l.id:null,name:l.name,min_selections:l.min_selections,max_selections:l.max_selections,position:t})),w=n=>{f(()=>i.http.put(d.value.urls.groups,{groups:et(n)}))},E=()=>{var n;return[...((n=r.value)==null?void 0:n.groups)??[]]},tt=()=>{w([...E(),{id:0,public_id:"",name:{[d.value.default_language]:o("bundles::bundles.panel.new_group_name")},label:"",min_selections:1,max_selections:1,position:E().length}])},nt=(n,l)=>{w(E().map(t=>t.id===n.id?{...t,...l}:t))},lt=n=>{w(E().filter(l=>l.id!==n.id))},ot=(n,l)=>{const t=E(),k=t.findIndex($=>$.id===n.id),h=t[k+l];h&&(t[k]=h,t[k+l]=n,w(t))},at=e.computed(()=>{var l;const n=(l=r.value)==null?void 0:l.availability;return n?n.unlimited?o("bundles::bundles.panel.availability_unlimited"):n.available>0?o("bundles::bundles.panel.availability_count",{count:n.available}):o("bundles::bundles.panel.availability_none"):""});return(n,l)=>(e.openBlock(),e.createElementBlock("div",re,[e.createElementVNode("div",ce,[e.createElementVNode("label",me,[e.createVNode(e.unref(i.Toggle),{on:d.value.defined,disabled:g.value,onToggle:u},null,8,["on","disabled"]),e.createTextVNode(" "+e.toDisplayString(e.unref(o)("bundles::bundles.panel.sell_as_bundle")),1)]),r.value?(e.openBlock(),e.createElementBlock("span",pe,e.toDisplayString(at.value),1)):e.createCommentVNode("",!0)]),m.value.length?(e.openBlock(),e.createElementBlock("div",ue,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(m.value,(t,k)=>(e.openBlock(),e.createElementBlock("div",{key:k},e.toDisplayString(t),1))),128))])):e.createCommentVNode("",!0),r.value?(e.openBlock(),e.createElementBlock(e.Fragment,{key:1},[e.createElementVNode("div",fe,[e.createElementVNode("div",null,[e.createVNode(e.unref(i.FieldLabel),{for:"bundle-pricing"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(o)("bundles::bundles.panel.pricing")),1)]),_:1}),(e.openBlock(),e.createBlock(e.unref(i.Select),{id:"bundle-pricing",key:r.value.pricing+String(y.value),"model-value":r.value.pricing,"onUpdate:modelValue":b},{default:e.withCtx(()=>[e.createElementVNode("option",be,e.toDisplayString(e.unref(o)("bundles::bundles.panel.pricing_fixed")),1),e.createElementVNode("option",ge,e.toDisplayString(e.unref(o)("bundles::bundles.panel.pricing_components")),1)]),_:1},8,["model-value"]))]),r.value.pricing==="components"?(e.openBlock(),e.createElementBlock("div",_e,[e.createVNode(e.unref(i.FieldLabel),{for:"bundle-discount",hint:e.unref(o)("bundles::bundles.panel.discount_hint")},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(o)("bundles::bundles.panel.discount_percentage")),1)]),_:1},8,["hint"]),e.createVNode(e.unref(i.TextInput),{id:"bundle-discount",type:"number",min:"0",max:"100",step:"0.01","model-value":r.value.discount_percentage??"",disabled:g.value,onChange:We},{suffix:e.withCtx(()=>[...l[5]||(l[5]=[e.createTextVNode("%",-1)])]),_:1},8,["model-value","disabled"])])):e.createCommentVNode("",!0)]),r.value.pricing==="components"?(e.openBlock(),e.createElementBlock("p",ke,e.toDisplayString(e.unref(o)("bundles::bundles.panel.derived_note")),1)):e.createCommentVNode("",!0),e.createElementVNode("div",null,[e.createElementVNode("div",xe,[e.createElementVNode("div",null,[e.createElementVNode("div",ye,e.toDisplayString(e.unref(o)("bundles::bundles.panel.components")),1),e.createElementVNode("div",he,e.toDisplayString(e.unref(o)("bundles::bundles.panel.components_description")),1)]),e.createVNode(e.unref(i.Button),{size:"sm",icon:"plus",disabled:g.value,onClick:l[0]||(l[0]=t=>G(null))},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(o)("bundles::bundles.panel.add_component")),1)]),_:1},8,["disabled"])]),e.createElementVNode("div",Be,[e.createVNode(F,{components:Xe.value,disabled:g.value,"empty-text":e.unref(o)("bundles::bundles.panel.components_empty"),onQuantity:P,onMove:z,onRemove:U},null,8,["components","disabled","empty-text"])])]),r.value.pricing==="components"&&r.value.prices.length?(e.openBlock(),e.createElementBlock("div",Ve,[e.createElementVNode("div",Ne,e.toDisplayString(e.unref(o)("bundles::bundles.panel.derived_prices")),1),e.createElementVNode("table",Ce,[e.createElementVNode("tbody",Ee,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(r.value.prices,t=>(e.openBlock(),e.createElementBlock("tr",{key:t.currency},[e.createElementVNode("td",$e,e.toDisplayString(t.currency),1),t.missing.length?(e.openBlock(),e.createElementBlock("td",De,e.toDisplayString(e.unref(o)("bundles::bundles.panel.missing_price",{components:t.missing.join(", ")})),1)):(e.openBlock(),e.createElementBlock("td",we,[e.createTextVNode(e.toDisplayString(t.price)+" ",1),t.list_price?(e.openBlock(),e.createElementBlock("span",Se,e.toDisplayString(t.list_price),1)):e.createCommentVNode("",!0)]))]))),128))])])])):e.createCommentVNode("",!0),e.createElementVNode("div",null,[e.createElementVNode("div",Te,[e.createElementVNode("div",Le,e.toDisplayString(e.unref(o)("bundles::bundles.panel.groups")),1),e.createElementVNode("div",Ie,e.toDisplayString(e.unref(o)("bundles::bundles.panel.groups_description")),1)]),e.createVNode(de,{groups:r.value.groups,components:r.value.components,"default-language":d.value.default_language,disabled:g.value,onAdd:tt,onUpdate:nt,onMove:ot,onRemove:lt,onAddOption:l[1]||(l[1]=t=>G(t.id)),onComponentQuantity:P,onComponentMove:z,onComponentRemove:U,onComponentDefault:Ze},null,8,["groups","components","default-language","disabled"])])],64)):e.createCommentVNode("",!0),e.createVNode(e.unref(i.ConfirmDialog),{open:y.value,"onUpdate:open":l[2]||(l[2]=t=>y.value=t),title:e.unref(o)("bundles::bundles.panel.confirm_components_title"),description:e.unref(o)("bundles::bundles.panel.confirm_components"),onConfirm:V},null,8,["open","title","description"]),e.createVNode(e.unref(i.ConfirmDialog),{open:x.value,"onUpdate:open":l[3]||(l[3]=t=>x.value=t),title:e.unref(o)("bundles::bundles.panel.confirm_remove_title"),description:e.unref(o)("bundles::bundles.panel.confirm_remove"),tone:"danger",onConfirm:c},null,8,["open","title","description"]),e.createVNode(e.unref(i.TargetPickerDialog),{open:S.value,"onUpdate:open":l[4]||(l[4]=t=>S.value=t),"search-url":d.value.urls.search,bucket:"components",kinds:["variants"],onAdd:ve},null,8,["open","search-url"])]))}}),Fe={class:"text-[12px] text-ink-500 mb-3"},Me={class:"border border-line rounded-lg bg-surface overflow-hidden divide-y divide-line"},Pe={key:1,class:"text-[11.5px] text-ink-500"},Ue=e.defineComponent({__name:"VariantBundleList",props:{variants:{}},setup(a){const{t:p}=B.useI18n();return(o,s)=>(e.openBlock(),e.createElementBlock("div",null,[e.createElementVNode("p",Fe,e.toDisplayString(e.unref(p)("bundles::bundles.panel.variant_summary_description")),1),e.createElementVNode("ul",Me,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(a.variants,d=>(e.openBlock(),e.createElementBlock("li",{key:d.variant.id,class:"flex items-center gap-3 px-3 py-2"},[e.createVNode(q,{variant:d.variant,class:"flex-1"},null,8,["variant"]),d.bundle?(e.openBlock(),e.createBlock(e.unref(i.StatusBadge),{key:0,tone:"sage",size:"sm"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(p)("bundles::bundles.panel.variant_component_count",d.bundle.components.length)),1)]),_:2},1024)):(e.openBlock(),e.createElementBlock("span",Pe,e.toDisplayString(e.unref(p)("bundles::bundles.panel.variant_not_bundle")),1)),e.createVNode(e.unref(D.Link),{href:d.variant.edit_url,class:"inline-flex items-center gap-1 text-[12px] text-ink-700 underline underline-offset-2 hover:text-ink-900"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(p)("bundles::bundles.panel.edit_variant"))+" ",1),e.createVNode(e.unref(i.Icon),{name:"chevRight",cls:"sm"})]),_:1},8,["href"])]))),128))])]))}}),ze={key:0,class:"text-[12px] text-danger"},Ge=e.defineComponent({__name:"BundleCard",props:{product:{},variant:{}},setup(a){const p=a,{t:o}=B.useI18n(),s=e.ref(null),d=e.ref(!1),r=I(D.usePage().props);return e.onMounted(async()=>{try{if(p.variant)s.value=[await i.http.get(`/${r}/bundles/variants/${p.variant.id}`)];else if(p.product){const g=await i.http.get(`/${r}/bundles/products/${p.product.id}`);s.value=g.variants}}catch{d.value=!0}}),(g,m)=>s.value||d.value?(e.openBlock(),e.createBlock(e.unref(i.Section),{key:0,title:e.unref(o)("bundles::bundles.panel.card_title")},{desc:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(o)("bundles::bundles.panel.card_description")),1)]),default:e.withCtx(()=>[d.value?(e.openBlock(),e.createElementBlock("p",ze,e.toDisplayString(e.unref(o)("bundles::bundles.panel.load_failed")),1)):s.value&&s.value.length===1?(e.openBlock(),e.createBlock(qe,{key:1,summary:s.value[0]},null,8,["summary"])):s.value?(e.openBlock(),e.createBlock(Ue,{key:2,variants:s.value},null,8,["variants"])):e.createCommentVNode("",!0)]),_:1},8,["title"])):e.createCommentVNode("",!0)}}),Re=e.defineComponent({__name:"BundleBadge",props:{row:{},value:{}},setup(a){const{t:p}=B.useI18n();return(o,s)=>a.value?(e.openBlock(),e.createBlock(e.unref(i.StatusBadge),{key:0,tone:"sage",size:"sm"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(p)("bundles::bundles.panel.badge_bundle")),1)]),_:1})):e.createCommentVNode("",!0)}}),je={class:"flex flex-col gap-2.5"},Oe={key:0,class:"text-ink-500 font-normal"},Ae={class:"mt-0.5 text-[11.5px] text-ink-500"},Qe={key:0,class:"font-mono"},He={key:1},Je={class:"mt-3 text-[11px] text-ink-500"},Ke=e.defineComponent({__name:"IncludedInBundlesCard",props:{product:{}},setup(a){const p=a,{t:o}=B.useI18n(),s=e.ref([]),d=I(D.usePage().props);return e.onMounted(async()=>{if(p.product)try{const r=await i.http.get(`/${d}/bundles/products/${p.product.id}/included-in`);s.value=r.data}catch{s.value=[]}}),(r,g)=>s.value.length?(e.openBlock(),e.createBlock(e.unref(i.SideCard),{key:0,title:e.unref(o)("bundles::bundles.panel.included_in_title")},{default:e.withCtx(()=>[e.createElementVNode("ul",je,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(s.value,m=>(e.openBlock(),e.createElementBlock("li",{key:m.id},[e.createVNode(e.unref(D.Link),{href:m.product_url,class:"block text-[12.5px] font-medium text-ink-900 hover:underline underline-offset-2 truncate"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(m.variant.name),1),m.variant.option?(e.openBlock(),e.createElementBlock("span",Oe," ("+e.toDisplayString(m.variant.option)+")",1)):e.createCommentVNode("",!0)]),_:2},1032,["href"]),e.createElementVNode("ul",Ae,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(m.components,f=>(e.openBlock(),e.createElementBlock("li",{key:f.variant.id,class:"truncate"},[e.createTextVNode(e.toDisplayString(e.unref(o)("bundles::bundles.panel.included_quantity",{quantity:f.quantity}))+" ",1),f.variant.sku?(e.openBlock(),e.createElementBlock("span",Qe,e.toDisplayString(f.variant.sku),1)):e.createCommentVNode("",!0),f.variant.option?(e.openBlock(),e.createElementBlock("span",He,e.toDisplayString(f.variant.option),1)):e.createCommentVNode("",!0)]))),128))])]))),128))]),e.createElementVNode("p",Je,e.toDisplayString(e.unref(o)("bundles::bundles.panel.included_in_description")),1)]),_:1},8,["title"])):e.createCommentVNode("",!0)}});window.LunarPanel.registerComponents("bundles",{BundleCard:Ge,BundleBadge:Re,IncludedInBundlesCard:Ke})})(Vue,InertiaVue3,VueI18n,LunarPanelUI); diff --git a/packages/bundles/build/assets/addon-DV2m4Dh6.js b/packages/bundles/build/assets/addon-DV2m4Dh6.js deleted file mode 100644 index 3b23dc3fe5..0000000000 --- a/packages/bundles/build/assets/addon-DV2m4Dh6.js +++ /dev/null @@ -1 +0,0 @@ -(function(e,D,V,d){"use strict";const I=o=>{var p;return((p=o.panel)==null?void 0:p.path)??"panel"},G={class:"flex items-center gap-2.5 min-w-0"},R={class:"w-8 h-8 shrink-0 rounded-md border border-line bg-surface-2 overflow-hidden flex items-center justify-center text-ink-400"},j=["src"],O={class:"min-w-0"},A={class:"text-[12.5px] font-medium text-ink-900 truncate"},Q={class:"text-[11px] text-ink-500 truncate"},H={key:0,class:"font-mono"},J={key:1},K={key:2},q=e.defineComponent({__name:"VariantThumb",props:{variant:{}},setup(o){return(p,a)=>(e.openBlock(),e.createElementBlock("div",G,[e.createElementVNode("div",R,[o.variant.thumbnail?(e.openBlock(),e.createElementBlock("img",{key:0,src:o.variant.thumbnail,alt:"",class:"w-full h-full object-cover"},null,8,j)):(e.openBlock(),e.createBlock(e.unref(d.Icon),{key:1,name:"image",cls:"sm"}))]),e.createElementVNode("div",O,[e.createElementVNode("div",A,e.toDisplayString(o.variant.name),1),e.createElementVNode("div",Q,[o.variant.sku?(e.openBlock(),e.createElementBlock("span",H,e.toDisplayString(o.variant.sku),1)):e.createCommentVNode("",!0),o.variant.sku&&o.variant.option?(e.openBlock(),e.createElementBlock("span",J," / ")):e.createCommentVNode("",!0),o.variant.option?(e.openBlock(),e.createElementBlock("span",K,e.toDisplayString(o.variant.option),1)):e.createCommentVNode("",!0)])])]))}}),W={key:0,class:"px-3 py-3 text-[12px] text-ink-500"},X={key:1,class:"divide-y divide-line"},Y={key:0,class:"flex items-center gap-1.5 text-[11px] text-ink-500 shrink-0"},Z={class:"w-[84px] shrink-0"},v={class:"flex items-center gap-0.5 shrink-0"},F=e.defineComponent({__name:"ComponentList",props:{components:{},withDefault:{type:Boolean},disabled:{type:Boolean},emptyText:{}},emits:["quantity","move","remove","default"],setup(o,{emit:p}){const a=p,{t:i}=V.useI18n(),s=(r,g)=>{const m=Math.max(1,Math.floor(Number(g.target.value))||1);m!==r.quantity&&a("quantity",r,m)};return(r,g)=>o.components.length?(e.openBlock(),e.createElementBlock("ul",X,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.components,(m,b)=>(e.openBlock(),e.createElementBlock("li",{key:m.id,class:"flex items-center gap-3 px-3 py-2"},[e.createVNode(q,{variant:m.variant,class:"flex-1"},null,8,["variant"]),o.withDefault?(e.openBlock(),e.createElementBlock("label",Y,[e.createVNode(e.unref(d.Checkbox),{"model-value":m.default,"aria-label":e.unref(i)("bundles::bundles.panel.group_default"),"onUpdate:modelValue":k=>a("default",m,k)},null,8,["model-value","aria-label","onUpdate:modelValue"]),e.createTextVNode(" "+e.toDisplayString(e.unref(i)("bundles::bundles.panel.group_default")),1)])):e.createCommentVNode("",!0),e.createElementVNode("div",Z,[e.createVNode(e.unref(d.TextInput),{type:"number",min:"1",step:"1","model-value":m.quantity,disabled:o.disabled,"aria-label":e.unref(i)("bundles::bundles.panel.quantity"),onChange:k=>s(m,k)},{prefix:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(i)("bundles::bundles.panel.quantity")),1)]),_:1},8,["model-value","disabled","aria-label","onChange"])]),e.createElementVNode("div",v,[e.createVNode(e.unref(d.Button),{variant:"ghost",size:"sm",icon:"chevDown","icon-cls":"sm rotate-180",disabled:o.disabled||b===0,"aria-label":e.unref(i)("bundles::bundles.panel.move_up"),title:e.unref(i)("bundles::bundles.panel.move_up"),onClick:k=>a("move",m,-1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(d.Button),{variant:"ghost",size:"sm",icon:"chevDown",disabled:o.disabled||b===o.components.length-1,"aria-label":e.unref(i)("bundles::bundles.panel.move_down"),title:e.unref(i)("bundles::bundles.panel.move_down"),onClick:k=>a("move",m,1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(d.Button),{variant:"ghost",size:"sm",icon:"trash",disabled:o.disabled,"aria-label":e.unref(i)("bundles::bundles.panel.remove"),title:e.unref(i)("bundles::bundles.panel.remove"),onClick:k=>a("remove",m)},null,8,["disabled","aria-label","title","onClick"])])]))),128))])):(e.openBlock(),e.createElementBlock("div",W,e.toDisplayString(o.emptyText),1))}}),ee={class:"flex flex-col gap-3"},te={class:"flex flex-wrap items-end gap-3 px-3 py-2.5 bg-surface-2 border-b border-line"},ne={class:"flex-1 min-w-[180px]"},le={class:"w-[72px]"},oe={class:"w-[72px]"},ae={class:"flex items-center gap-0.5 pb-0.5"},ie={class:"px-3 py-2 border-t border-line"},se=e.defineComponent({__name:"GroupsEditor",props:{groups:{},components:{},defaultLanguage:{},disabled:{type:Boolean}},emits:["add","update","move","remove","add-option","component-quantity","component-move","component-remove","component-default"],setup(o,{emit:p}){const a=o,i=p,{t:s}=V.useI18n(),r=x=>a.components.filter(u=>u.group_id===x.id),g=x=>x.name[a.defaultLanguage]??x.label??"",m=(x,u)=>{const c=u.target.value.trim();c&&c!==g(x)&&i("update",x,{name:{...x.name,[a.defaultLanguage]:c}})},b=(x,u,c)=>{const B=Math.max(u==="min_selections"?0:1,Math.floor(Number(c.target.value))||0);B!==x[u]&&i("update",x,{[u]:B})},k=e.ref(null);return(x,u)=>(e.openBlock(),e.createElementBlock("div",ee,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.groups,(c,B)=>(e.openBlock(),e.createElementBlock("div",{key:c.id,class:"border border-line rounded-lg bg-surface overflow-hidden"},[e.createElementVNode("div",te,[e.createElementVNode("div",ne,[e.createVNode(e.unref(d.FieldLabel),{for:`bundle-group-${c.id}-name`,hint:o.defaultLanguage.toUpperCase()},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(s)("bundles::bundles.panel.group_name")),1)]),_:1},8,["for","hint"]),e.createVNode(e.unref(d.TextInput),{id:`bundle-group-${c.id}-name`,"model-value":g(c),disabled:o.disabled,onChange:f=>m(c,f)},null,8,["id","model-value","disabled","onChange"])]),e.createElementVNode("div",le,[e.createVNode(e.unref(d.FieldLabel),{for:`bundle-group-${c.id}-min`},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(s)("bundles::bundles.panel.group_min")),1)]),_:1},8,["for"]),e.createVNode(e.unref(d.TextInput),{id:`bundle-group-${c.id}-min`,type:"number",min:"0",step:"1","model-value":c.min_selections,disabled:o.disabled,onChange:f=>b(c,"min_selections",f)},null,8,["id","model-value","disabled","onChange"])]),e.createElementVNode("div",oe,[e.createVNode(e.unref(d.FieldLabel),{for:`bundle-group-${c.id}-max`},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(s)("bundles::bundles.panel.group_max")),1)]),_:1},8,["for"]),e.createVNode(e.unref(d.TextInput),{id:`bundle-group-${c.id}-max`,type:"number",min:"1",step:"1","model-value":c.max_selections,disabled:o.disabled,onChange:f=>b(c,"max_selections",f)},null,8,["id","model-value","disabled","onChange"])]),e.createElementVNode("div",ae,[e.createVNode(e.unref(d.Button),{variant:"ghost",size:"sm",icon:"chevDown","icon-cls":"sm rotate-180",disabled:o.disabled||B===0,"aria-label":e.unref(s)("bundles::bundles.panel.move_up"),title:e.unref(s)("bundles::bundles.panel.move_up"),onClick:f=>i("move",c,-1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(d.Button),{variant:"ghost",size:"sm",icon:"chevDown",disabled:o.disabled||B===o.groups.length-1,"aria-label":e.unref(s)("bundles::bundles.panel.move_down"),title:e.unref(s)("bundles::bundles.panel.move_down"),onClick:f=>i("move",c,1)},null,8,["disabled","aria-label","title","onClick"]),e.createVNode(e.unref(d.Button),{variant:"ghost",size:"sm",icon:"trash",disabled:o.disabled,"aria-label":e.unref(s)("bundles::bundles.panel.remove_group"),title:e.unref(s)("bundles::bundles.panel.remove_group"),onClick:f=>k.value=c},null,8,["disabled","aria-label","title","onClick"])])]),e.createVNode(F,{components:r(c),disabled:o.disabled,"empty-text":e.unref(s)("bundles::bundles.panel.group_empty"),"with-default":"",onQuantity:u[0]||(u[0]=(f,h)=>i("component-quantity",f,h)),onMove:u[1]||(u[1]=(f,h)=>i("component-move",f,h)),onRemove:u[2]||(u[2]=f=>i("component-remove",f)),onDefault:u[3]||(u[3]=(f,h)=>i("component-default",f,h))},null,8,["components","disabled","empty-text"]),e.createElementVNode("div",ie,[e.createVNode(e.unref(d.Button),{size:"sm",icon:"plus",disabled:o.disabled,onClick:f=>i("add-option",c)},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(s)("bundles::bundles.panel.add_option")),1)]),_:1},8,["disabled","onClick"])])]))),128)),e.createElementVNode("div",null,[e.createVNode(e.unref(d.Button),{icon:"plus",disabled:o.disabled,onClick:u[4]||(u[4]=c=>i("add"))},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(s)("bundles::bundles.panel.add_group")),1)]),_:1},8,["disabled"])]),e.createVNode(e.unref(d.ConfirmDialog),{open:k.value!==null,title:e.unref(s)("bundles::bundles.panel.remove_group"),description:e.unref(s)("bundles::bundles.panel.confirm_remove_group"),tone:"danger","onUpdate:open":u[5]||(u[5]=c=>{c||(k.value=null)}),onConfirm:u[6]||(u[6]=()=>{k.value&&i("remove",k.value)})},null,8,["open","title","description"])]))}}),de={class:"flex flex-col gap-5"},re={class:"flex items-center justify-between gap-4"},ce={class:"flex items-center gap-2.5 text-[13px] text-ink-900 cursor-pointer"},me={key:0,class:"text-[12px] text-ink-500","data-testid":"bundle-availability"},pe={key:0,class:"rounded-md border border-danger-border bg-danger-soft px-3 py-2 text-[12px] text-danger"},ue={class:"grid gap-3 sm:grid-cols-2"},be={value:"fixed"},fe={value:"components"},ge={key:0},_e={key:0,class:"text-[12px] text-ink-500 leading-normal"},ke={class:"flex items-center justify-between gap-3 mb-2"},xe={class:"text-[12.5px] font-semibold text-ink-900"},ye={class:"text-[11.5px] text-ink-500"},he={class:"border border-line rounded-lg bg-surface overflow-hidden"},Ve={key:1},Be={class:"text-[12.5px] font-semibold text-ink-900 mb-2"},Ne={class:"w-full text-[12.5px] border border-line rounded-lg overflow-hidden"},Ce={class:"divide-y divide-line"},Ee={class:"px-3 py-1.5 font-mono text-ink-700 w-[80px]"},$e={key:0,class:"px-3 py-1.5 text-warn-ink"},De={key:1,class:"px-3 py-1.5 text-ink-900 [font-variant-numeric:tabular-nums]"},we={key:0,class:"ml-2 text-ink-500 line-through"},Se={class:"mb-2"},Te={class:"text-[12.5px] font-semibold text-ink-900"},Le={class:"text-[11.5px] text-ink-500"},Ie=e.defineComponent({__name:"BundleEditor",props:{summary:{}},setup(o){const p=o,{t:a}=V.useI18n(),i=d.useToasts(),s=e.ref(p.summary);e.watch(()=>p.summary,n=>{s.value=n});const r=e.computed(()=>s.value.bundle),g=e.ref(!1),m=e.ref([]),b=async(n,l=a("bundles::bundles.panel.saved"))=>{g.value=!0,m.value=[];try{s.value=await n(),i.success(l)}catch(t){t instanceof d.ValidationError?m.value=Object.values(t.errors).flat():i.error(a("bundles::bundles.panel.load_failed"))}finally{g.value=!1}},k=e.ref(!1),x=(n,l)=>d.http.put(s.value.urls.define,{pricing:n,discount_percentage:l}),u=()=>{if(s.value.defined){k.value=!0;return}b(()=>x("fixed",null))},c=()=>{b(()=>d.http.delete(s.value.urls.destroy),a("bundles::bundles.panel.removed"))},B=n=>{r.value&&n!==r.value.pricing&&b(()=>{var l;return x(n,((l=r.value)==null?void 0:l.discount_percentage)??null)})},f=n=>{const l=n.target.value.trim(),t=l===""?null:Number(l);r.value&&t!==r.value.discount_percentage&&b(()=>x(r.value.pricing,t))},h=n=>{var l;return(((l=r.value)==null?void 0:l.components)??[]).filter(t=>t.group_id===n)},Ke=e.computed(()=>h(null)),We=n=>{const l=new Map;return n.map(t=>{const _=l.get(t.group_id)??0;return l.set(t.group_id,_+1),{variant_id:t.variant.id,quantity:t.quantity,group_id:t.group_id,default:t.default,position:_}})},N=n=>{b(()=>d.http.put(s.value.urls.components,{components:We(n)}))},C=()=>{var n;return[...((n=r.value)==null?void 0:n.components)??[]]},M=(n,l)=>{N(C().map(t=>t.id===n.id?{...t,quantity:l}:t))},Xe=(n,l)=>{N(C().map(t=>t.id===n.id?{...t,default:l}:t))},P=n=>{N(C().filter(l=>l.id!==n.id))},z=(n,l)=>{const t=h(n.group_id),_=t.findIndex(L=>L.id===n.id),y=t[_+l];if(!y)return;const $=[...t];$[_]=y,$[_+l]=n,N([...C().filter(L=>L.group_id!==n.group_id),...$])},S=e.ref(!1),T=e.ref(null),U=n=>{T.value=n,S.value=!0},Ye=n=>{const l=C(),t=n.filter(_=>!l.some(y=>y.group_id===T.value&&y.variant.id===_.id)).map((_,y)=>({id:-(y+1),public_id:"",group_id:T.value,quantity:1,default:!1,position:0,variant:{..._,id:_.id}}));t.length&&N([...l,...t])},Ze=n=>n.map((l,t)=>({id:l.id>0?l.id:null,name:l.name,min_selections:l.min_selections,max_selections:l.max_selections,position:t})),w=n=>{b(()=>d.http.put(s.value.urls.groups,{groups:Ze(n)}))},E=()=>{var n;return[...((n=r.value)==null?void 0:n.groups)??[]]},ve=()=>{w([...E(),{id:0,public_id:"",name:{[s.value.default_language]:a("bundles::bundles.panel.new_group_name")},label:"",min_selections:1,max_selections:1,position:E().length}])},et=(n,l)=>{w(E().map(t=>t.id===n.id?{...t,...l}:t))},tt=n=>{w(E().filter(l=>l.id!==n.id))},nt=(n,l)=>{const t=E(),_=t.findIndex($=>$.id===n.id),y=t[_+l];y&&(t[_]=y,t[_+l]=n,w(t))},lt=e.computed(()=>{var l;const n=(l=r.value)==null?void 0:l.availability;return n?n.unlimited?a("bundles::bundles.panel.availability_unlimited"):n.available>0?a("bundles::bundles.panel.availability_count",{count:n.available}):a("bundles::bundles.panel.availability_none"):""});return(n,l)=>(e.openBlock(),e.createElementBlock("div",de,[e.createElementVNode("div",re,[e.createElementVNode("label",ce,[e.createVNode(e.unref(d.Toggle),{on:s.value.defined,disabled:g.value,onToggle:u},null,8,["on","disabled"]),e.createTextVNode(" "+e.toDisplayString(e.unref(a)("bundles::bundles.panel.sell_as_bundle")),1)]),r.value?(e.openBlock(),e.createElementBlock("span",me,e.toDisplayString(lt.value),1)):e.createCommentVNode("",!0)]),m.value.length?(e.openBlock(),e.createElementBlock("div",pe,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(m.value,(t,_)=>(e.openBlock(),e.createElementBlock("div",{key:_},e.toDisplayString(t),1))),128))])):e.createCommentVNode("",!0),r.value?(e.openBlock(),e.createElementBlock(e.Fragment,{key:1},[e.createElementVNode("div",ue,[e.createElementVNode("div",null,[e.createVNode(e.unref(d.FieldLabel),{for:"bundle-pricing"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(a)("bundles::bundles.panel.pricing")),1)]),_:1}),e.createVNode(e.unref(d.Select),{id:"bundle-pricing","model-value":r.value.pricing,"onUpdate:modelValue":B},{default:e.withCtx(()=>[e.createElementVNode("option",be,e.toDisplayString(e.unref(a)("bundles::bundles.panel.pricing_fixed")),1),e.createElementVNode("option",fe,e.toDisplayString(e.unref(a)("bundles::bundles.panel.pricing_components")),1)]),_:1},8,["model-value"])]),r.value.pricing==="components"?(e.openBlock(),e.createElementBlock("div",ge,[e.createVNode(e.unref(d.FieldLabel),{for:"bundle-discount",hint:e.unref(a)("bundles::bundles.panel.discount_hint")},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(a)("bundles::bundles.panel.discount_percentage")),1)]),_:1},8,["hint"]),e.createVNode(e.unref(d.TextInput),{id:"bundle-discount",type:"number",min:"0",max:"100",step:"0.01","model-value":r.value.discount_percentage??"",disabled:g.value,onChange:f},{suffix:e.withCtx(()=>[...l[4]||(l[4]=[e.createTextVNode("%",-1)])]),_:1},8,["model-value","disabled"])])):e.createCommentVNode("",!0)]),r.value.pricing==="components"?(e.openBlock(),e.createElementBlock("p",_e,e.toDisplayString(e.unref(a)("bundles::bundles.panel.derived_note")),1)):e.createCommentVNode("",!0),e.createElementVNode("div",null,[e.createElementVNode("div",ke,[e.createElementVNode("div",null,[e.createElementVNode("div",xe,e.toDisplayString(e.unref(a)("bundles::bundles.panel.components")),1),e.createElementVNode("div",ye,e.toDisplayString(e.unref(a)("bundles::bundles.panel.components_description")),1)]),e.createVNode(e.unref(d.Button),{size:"sm",icon:"plus",disabled:g.value,onClick:l[0]||(l[0]=t=>U(null))},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(a)("bundles::bundles.panel.add_component")),1)]),_:1},8,["disabled"])]),e.createElementVNode("div",he,[e.createVNode(F,{components:Ke.value,disabled:g.value,"empty-text":e.unref(a)("bundles::bundles.panel.components_empty"),onQuantity:M,onMove:z,onRemove:P},null,8,["components","disabled","empty-text"])])]),r.value.pricing==="components"&&r.value.prices.length?(e.openBlock(),e.createElementBlock("div",Ve,[e.createElementVNode("div",Be,e.toDisplayString(e.unref(a)("bundles::bundles.panel.derived_prices")),1),e.createElementVNode("table",Ne,[e.createElementVNode("tbody",Ce,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(r.value.prices,t=>(e.openBlock(),e.createElementBlock("tr",{key:t.currency},[e.createElementVNode("td",Ee,e.toDisplayString(t.currency),1),t.missing.length?(e.openBlock(),e.createElementBlock("td",$e,e.toDisplayString(e.unref(a)("bundles::bundles.panel.missing_price",{components:t.missing.join(", ")})),1)):(e.openBlock(),e.createElementBlock("td",De,[e.createTextVNode(e.toDisplayString(t.price)+" ",1),t.list_price?(e.openBlock(),e.createElementBlock("span",we,e.toDisplayString(t.list_price),1)):e.createCommentVNode("",!0)]))]))),128))])])])):e.createCommentVNode("",!0),e.createElementVNode("div",null,[e.createElementVNode("div",Se,[e.createElementVNode("div",Te,e.toDisplayString(e.unref(a)("bundles::bundles.panel.groups")),1),e.createElementVNode("div",Le,e.toDisplayString(e.unref(a)("bundles::bundles.panel.groups_description")),1)]),e.createVNode(se,{groups:r.value.groups,components:r.value.components,"default-language":s.value.default_language,disabled:g.value,onAdd:ve,onUpdate:et,onMove:nt,onRemove:tt,onAddOption:l[1]||(l[1]=t=>U(t.id)),onComponentQuantity:M,onComponentMove:z,onComponentRemove:P,onComponentDefault:Xe},null,8,["groups","components","default-language","disabled"])])],64)):e.createCommentVNode("",!0),e.createVNode(e.unref(d.ConfirmDialog),{open:k.value,"onUpdate:open":l[2]||(l[2]=t=>k.value=t),title:e.unref(a)("bundles::bundles.panel.confirm_remove_title"),description:e.unref(a)("bundles::bundles.panel.confirm_remove"),tone:"danger",onConfirm:c},null,8,["open","title","description"]),e.createVNode(e.unref(d.TargetPickerDialog),{open:S.value,"onUpdate:open":l[3]||(l[3]=t=>S.value=t),"search-url":s.value.urls.search,bucket:"components",kinds:["variants"],onAdd:Ye},null,8,["open","search-url"])]))}}),qe={class:"text-[12px] text-ink-500 mb-3"},Fe={class:"border border-line rounded-lg bg-surface overflow-hidden divide-y divide-line"},Me={key:1,class:"text-[11.5px] text-ink-500"},Pe=e.defineComponent({__name:"VariantBundleList",props:{variants:{}},setup(o){const{t:p}=V.useI18n();return(a,i)=>(e.openBlock(),e.createElementBlock("div",null,[e.createElementVNode("p",qe,e.toDisplayString(e.unref(p)("bundles::bundles.panel.variant_summary_description")),1),e.createElementVNode("ul",Fe,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(o.variants,s=>(e.openBlock(),e.createElementBlock("li",{key:s.variant.id,class:"flex items-center gap-3 px-3 py-2"},[e.createVNode(q,{variant:s.variant,class:"flex-1"},null,8,["variant"]),s.bundle?(e.openBlock(),e.createBlock(e.unref(d.StatusBadge),{key:0,tone:"sage",size:"sm"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(p)("bundles::bundles.panel.variant_component_count",s.bundle.components.length)),1)]),_:2},1024)):(e.openBlock(),e.createElementBlock("span",Me,e.toDisplayString(e.unref(p)("bundles::bundles.panel.variant_not_bundle")),1)),e.createVNode(e.unref(D.Link),{href:s.variant.edit_url,class:"inline-flex items-center gap-1 text-[12px] text-ink-700 underline underline-offset-2 hover:text-ink-900"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(p)("bundles::bundles.panel.edit_variant"))+" ",1),e.createVNode(e.unref(d.Icon),{name:"chevRight",cls:"sm"})]),_:1},8,["href"])]))),128))])]))}}),ze={key:0,class:"text-[12px] text-danger"},Ue=e.defineComponent({__name:"BundleCard",props:{product:{},variant:{}},setup(o){const p=o,{t:a}=V.useI18n(),i=e.ref(null),s=e.ref(!1),r=I(D.usePage().props);return e.onMounted(async()=>{try{if(p.variant)i.value=[await d.http.get(`/${r}/bundles/variants/${p.variant.id}`)];else if(p.product){const g=await d.http.get(`/${r}/bundles/products/${p.product.id}`);i.value=g.variants}}catch{s.value=!0}}),(g,m)=>i.value||s.value?(e.openBlock(),e.createBlock(e.unref(d.Section),{key:0,title:e.unref(a)("bundles::bundles.panel.card_title")},{desc:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(a)("bundles::bundles.panel.card_description")),1)]),default:e.withCtx(()=>[s.value?(e.openBlock(),e.createElementBlock("p",ze,e.toDisplayString(e.unref(a)("bundles::bundles.panel.load_failed")),1)):i.value&&i.value.length===1?(e.openBlock(),e.createBlock(Ie,{key:1,summary:i.value[0]},null,8,["summary"])):i.value?(e.openBlock(),e.createBlock(Pe,{key:2,variants:i.value},null,8,["variants"])):e.createCommentVNode("",!0)]),_:1},8,["title"])):e.createCommentVNode("",!0)}}),Ge=e.defineComponent({__name:"BundleBadge",props:{row:{},value:{}},setup(o){const{t:p}=V.useI18n();return(a,i)=>o.value?(e.openBlock(),e.createBlock(e.unref(d.StatusBadge),{key:0,tone:"sage",size:"sm"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(e.unref(p)("bundles::bundles.panel.badge_bundle")),1)]),_:1})):e.createCommentVNode("",!0)}}),Re={class:"flex flex-col gap-2.5"},je={key:0,class:"text-ink-500 font-normal"},Oe={class:"mt-0.5 text-[11.5px] text-ink-500"},Ae={key:0,class:"font-mono"},Qe={key:1},He={class:"mt-3 text-[11px] text-ink-500"},Je=e.defineComponent({__name:"IncludedInBundlesCard",props:{product:{}},setup(o){const p=o,{t:a}=V.useI18n(),i=e.ref([]),s=I(D.usePage().props);return e.onMounted(async()=>{if(p.product)try{const r=await d.http.get(`/${s}/bundles/products/${p.product.id}/included-in`);i.value=r.data}catch{i.value=[]}}),(r,g)=>i.value.length?(e.openBlock(),e.createBlock(e.unref(d.SideCard),{key:0,title:e.unref(a)("bundles::bundles.panel.included_in_title")},{default:e.withCtx(()=>[e.createElementVNode("ul",Re,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(i.value,m=>(e.openBlock(),e.createElementBlock("li",{key:m.id},[e.createVNode(e.unref(D.Link),{href:m.product_url,class:"block text-[12.5px] font-medium text-ink-900 hover:underline underline-offset-2 truncate"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(m.variant.name),1),m.variant.option?(e.openBlock(),e.createElementBlock("span",je," ("+e.toDisplayString(m.variant.option)+")",1)):e.createCommentVNode("",!0)]),_:2},1032,["href"]),e.createElementVNode("ul",Oe,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(m.components,b=>(e.openBlock(),e.createElementBlock("li",{key:b.variant.id,class:"truncate"},[e.createTextVNode(e.toDisplayString(e.unref(a)("bundles::bundles.panel.included_quantity",{quantity:b.quantity}))+" ",1),b.variant.sku?(e.openBlock(),e.createElementBlock("span",Ae,e.toDisplayString(b.variant.sku),1)):e.createCommentVNode("",!0),b.variant.option?(e.openBlock(),e.createElementBlock("span",Qe,e.toDisplayString(b.variant.option),1)):e.createCommentVNode("",!0)]))),128))])]))),128))]),e.createElementVNode("p",He,e.toDisplayString(e.unref(a)("bundles::bundles.panel.included_in_description")),1)]),_:1},8,["title"])):e.createCommentVNode("",!0)}});window.LunarPanel.registerComponents("bundles",{BundleCard:Ue,BundleBadge:Ge,IncludedInBundlesCard:Je})})(Vue,InertiaVue3,VueI18n,LunarPanelUI); diff --git a/packages/bundles/build/manifest.json b/packages/bundles/build/manifest.json index dde80b7ce9..ee4bf9d16a 100644 --- a/packages/bundles/build/manifest.json +++ b/packages/bundles/build/manifest.json @@ -1,6 +1,6 @@ { "resources/js/addon.ts": { - "file": "assets/addon-DV2m4Dh6.js", + "file": "assets/addon-Bd9bHYW_.js", "name": "addon", "src": "resources/js/addon.ts", "isEntry": true diff --git a/packages/bundles/resources/js/components/BundleEditor.vue b/packages/bundles/resources/js/components/BundleEditor.vue index a54e65e24e..df3004e84f 100644 --- a/packages/bundles/resources/js/components/BundleEditor.vue +++ b/packages/bundles/resources/js/components/BundleEditor.vue @@ -71,11 +71,26 @@ const remove = (): void => { void run(() => http.delete(summary.value.urls.destroy), t('bundles::bundles.panel.removed')); }; -// Pricing. +// Pricing. Switching to component pricing hands the variant's base prices +// over to the package, which rewrites them on every reprice, so confirm first. +const confirmingComponents = ref(false); + const onPricing = (value: string | number | null): void => { - if (bundle.value && value !== bundle.value.pricing) { - void run(() => define(value as 'fixed' | 'components', bundle.value?.discount_percentage ?? null)); + if (!bundle.value || value === bundle.value.pricing) { + return; } + + if (value === 'components') { + confirmingComponents.value = true; + + return; + } + + void run(() => define('fixed', null)); +}; + +const switchToComponents = (): void => { + void run(() => define('components', bundle.value?.discount_percentage ?? null)); }; const onDiscount = (event: Event): void => { @@ -265,7 +280,7 @@ const availabilityText = computed(() => {
{{ t('bundles::bundles.panel.pricing') }} - @@ -356,6 +371,13 @@ const availabilityText = computed(() => {
+ + 'البيع كحزمة', 'confirm_remove_title' => 'إيقاف البيع كحزمة؟', 'confirm_remove' => 'ستُحذف قائمة المكوّنات ومجموعات الخيارات. يحتفظ المتغير بأسعاره ويعود متغيرًا عاديًا.', + 'confirm_components_title' => 'التسعير من المكونات؟', + 'confirm_components' => 'تُستبدل الأسعار الأساسية الخاصة بالمتغير بمجموع مكوناته وتُعاد كتابتها كلما تغير سعر أحد المكونات. عد إلى السعر الثابت لتحديدها يدويًا مرة أخرى.', 'pricing' => 'التسعير', 'pricing_fixed' => 'سعر ثابت', 'pricing_components' => 'مجموع المكوّنات', diff --git a/packages/bundles/resources/lang/bg/bundles.php b/packages/bundles/resources/lang/bg/bundles.php index df05bd9221..2e35fcf860 100644 --- a/packages/bundles/resources/lang/bg/bundles.php +++ b/packages/bundles/resources/lang/bg/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Продавай като комплект', 'confirm_remove_title' => 'Да се спре продажбата като комплект?', 'confirm_remove' => 'Списъкът с компоненти и групите с опции ще бъдат премахнати. Вариантът запазва цените си и отново става обикновен вариант.', + 'confirm_components_title' => 'Цена от компонентите?', + 'confirm_components' => 'Собствените базови цени на варианта се заменят със сумата от компонентите му и се презаписват при всяка промяна на цената на компонент. Върнете се към фиксирана цена, за да ги задавате ръчно отново.', 'pricing' => 'Ценообразуване', 'pricing_fixed' => 'Фиксирана цена', 'pricing_components' => 'Сума на компонентите', diff --git a/packages/bundles/resources/lang/de/bundles.php b/packages/bundles/resources/lang/de/bundles.php index cc22984184..803c455ffb 100644 --- a/packages/bundles/resources/lang/de/bundles.php +++ b/packages/bundles/resources/lang/de/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Als Bundle verkaufen', 'confirm_remove_title' => 'Nicht mehr als Bundle verkaufen?', 'confirm_remove' => 'Die Komponentenliste und die Optionsgruppen werden entfernt. Die Variante behält ihre Preise und wird wieder eine gewöhnliche Variante.', + 'confirm_components_title' => 'Preis aus den Komponenten bilden?', + 'confirm_components' => 'Die eigenen Grundpreise der Variante werden durch die Summe ihrer Komponenten ersetzt und bei jeder Preisänderung einer Komponente neu geschrieben. Wechseln Sie zurück zum Festpreis, um sie wieder von Hand zu setzen.', 'pricing' => 'Preisgestaltung', 'pricing_fixed' => 'Festpreis', 'pricing_components' => 'Summe der Komponenten', diff --git a/packages/bundles/resources/lang/en/bundles.php b/packages/bundles/resources/lang/en/bundles.php index d7596091e5..e8ee23592b 100644 --- a/packages/bundles/resources/lang/en/bundles.php +++ b/packages/bundles/resources/lang/en/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Sell as a bundle', 'confirm_remove_title' => 'Stop selling as a bundle?', 'confirm_remove' => 'The component list and option groups are removed. The variant keeps its prices and becomes an ordinary variant.', + 'confirm_components_title' => 'Price from the components?', + 'confirm_components' => 'The variant\'s own base prices are replaced by the sum of its components and rewritten whenever a component\'s price changes. Switch back to a fixed price to set them by hand again.', 'pricing' => 'Pricing', 'pricing_fixed' => 'Fixed price', 'pricing_components' => 'Sum of components', diff --git a/packages/bundles/resources/lang/es/bundles.php b/packages/bundles/resources/lang/es/bundles.php index a813478554..307925a3e2 100644 --- a/packages/bundles/resources/lang/es/bundles.php +++ b/packages/bundles/resources/lang/es/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Vender como paquete', 'confirm_remove_title' => '¿Dejar de vender como paquete?', 'confirm_remove' => 'Se eliminan la lista de componentes y los grupos de opciones. La variante conserva sus precios y vuelve a ser una variante normal.', + 'confirm_components_title' => '¿Calcular el precio a partir de los componentes?', + 'confirm_components' => 'Los precios base propios de la variante se sustituyen por la suma de sus componentes y se reescriben cada vez que cambia el precio de un componente. Vuelva al precio fijo para definirlos a mano de nuevo.', 'pricing' => 'Precio', 'pricing_fixed' => 'Precio fijo', 'pricing_components' => 'Suma de los componentes', diff --git a/packages/bundles/resources/lang/fa/bundles.php b/packages/bundles/resources/lang/fa/bundles.php index 4976fc0bf5..f2b5fd234b 100644 --- a/packages/bundles/resources/lang/fa/bundles.php +++ b/packages/bundles/resources/lang/fa/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'فروش به‌صورت بسته', 'confirm_remove_title' => 'فروش به‌صورت بسته متوقف شود؟', 'confirm_remove' => 'فهرست اجزا و گروه‌های گزینه حذف می‌شوند. گونه قیمت‌های خود را نگه می‌دارد و دوباره یک گونه عادی می‌شود.', + 'confirm_components_title' => 'قیمت از اجزا محاسبه شود؟', + 'confirm_components' => 'قیمت‌های پایه خود گونه با مجموع اجزای آن جایگزین می‌شود و هر بار که قیمت یکی از اجزا تغییر کند دوباره نوشته می‌شود. برای تعیین دستی دوباره، به قیمت ثابت برگردید.', 'pricing' => 'قیمت‌گذاری', 'pricing_fixed' => 'قیمت ثابت', 'pricing_components' => 'مجموع اجزا', diff --git a/packages/bundles/resources/lang/fr/bundles.php b/packages/bundles/resources/lang/fr/bundles.php index 2f93826403..09498395b4 100644 --- a/packages/bundles/resources/lang/fr/bundles.php +++ b/packages/bundles/resources/lang/fr/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Vendre comme un lot', 'confirm_remove_title' => 'Ne plus vendre comme un lot ?', 'confirm_remove' => 'La liste des composants et les groupes d\'options sont supprimés. La variante conserve ses prix et redevient une variante ordinaire.', + 'confirm_components_title' => 'Calculer le prix a partir des composants ?', + 'confirm_components' => 'Les prix de base propres a la variante sont remplaces par la somme de ses composants et reecrits a chaque changement de prix d\'un composant. Revenez au prix fixe pour les definir a nouveau a la main.', 'pricing' => 'Tarification', 'pricing_fixed' => 'Prix fixe', 'pricing_components' => 'Somme des composants', diff --git a/packages/bundles/resources/lang/hr/bundles.php b/packages/bundles/resources/lang/hr/bundles.php index e652ae4bd5..92ef01eb22 100644 --- a/packages/bundles/resources/lang/hr/bundles.php +++ b/packages/bundles/resources/lang/hr/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Prodaj kao paket', 'confirm_remove_title' => 'Prestati prodavati kao paket?', 'confirm_remove' => 'Popis komponenti i grupe opcija bit će uklonjeni. Varijanta zadržava svoje cijene i ponovno postaje obična varijanta.', + 'confirm_components_title' => 'Cijena iz komponenata?', + 'confirm_components' => 'Vlastite osnovne cijene varijante zamjenjuju se zbrojem njezinih komponenata i ponovno se zapisuju kad god se promijeni cijena komponente. Vratite se na fiksnu cijenu da biste ih ponovno postavili ručno.', 'pricing' => 'Cijene', 'pricing_fixed' => 'Fiksna cijena', 'pricing_components' => 'Zbroj komponenti', diff --git a/packages/bundles/resources/lang/hu/bundles.php b/packages/bundles/resources/lang/hu/bundles.php index 3ef0386428..e9565aac48 100644 --- a/packages/bundles/resources/lang/hu/bundles.php +++ b/packages/bundles/resources/lang/hu/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Értékesítés csomagként', 'confirm_remove_title' => 'Ne legyen többé csomagként értékesítve?', 'confirm_remove' => 'Az összetevők listája és az opciócsoportok törlődnek. A változat megtartja az árait, és ismét hagyományos változattá válik.', + 'confirm_components_title' => 'Ár az összetevőkből?', + 'confirm_components' => 'A változat saját alapárait az összetevők összege váltja fel, és minden összetevő-árváltozáskor újraíródnak. Váltson vissza rögzített árra, hogy újra kézzel adja meg őket.', 'pricing' => 'Árazás', 'pricing_fixed' => 'Fix ár', 'pricing_components' => 'Összetevők összege', diff --git a/packages/bundles/resources/lang/mn/bundles.php b/packages/bundles/resources/lang/mn/bundles.php index a2c397c0c9..8553685db0 100644 --- a/packages/bundles/resources/lang/mn/bundles.php +++ b/packages/bundles/resources/lang/mn/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Багц болгон зарах', 'confirm_remove_title' => 'Багц болгон зарахаа зогсоох уу?', 'confirm_remove' => 'Бүрэлдэхүүний жагсаалт болон сонголтын бүлгүүд устгагдана. Хувилбар үнээ хадгалж, энгийн хувилбар болно.', + 'confirm_components_title' => 'Бүрэлдэхүүнээс үнэ тооцох уу?', + 'confirm_components' => 'Хувилбарын өөрийн үндсэн үнийг бүрэлдэхүүн хэсгүүдийн нийлбэрээр солино, бүрэлдэхүүний үнэ өөрчлөгдөх бүрт дахин бичнэ. Гараар тохируулахын тулд тогтмол үнэ рүү буцна уу.', 'pricing' => 'Үнэ тогтоолт', 'pricing_fixed' => 'Тогтмол үнэ', 'pricing_components' => 'Бүрэлдэхүүнүүдийн нийлбэр', diff --git a/packages/bundles/resources/lang/nl/bundles.php b/packages/bundles/resources/lang/nl/bundles.php index 4bc2057a95..7befcea261 100644 --- a/packages/bundles/resources/lang/nl/bundles.php +++ b/packages/bundles/resources/lang/nl/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Als bundel verkopen', 'confirm_remove_title' => 'Niet langer als bundel verkopen?', 'confirm_remove' => 'De onderdelenlijst en optiegroepen worden verwijderd. De variant behoudt zijn prijzen en wordt weer een gewone variant.', + 'confirm_components_title' => 'Prijs uit de onderdelen berekenen?', + 'confirm_components' => 'De eigen basisprijzen van de variant worden vervangen door de som van de onderdelen en opnieuw geschreven zodra de prijs van een onderdeel wijzigt. Schakel terug naar een vaste prijs om ze weer handmatig in te stellen.', 'pricing' => 'Prijsbepaling', 'pricing_fixed' => 'Vaste prijs', 'pricing_components' => 'Som van de onderdelen', diff --git a/packages/bundles/resources/lang/pl/bundles.php b/packages/bundles/resources/lang/pl/bundles.php index 5c5f119c7a..8943d49a79 100644 --- a/packages/bundles/resources/lang/pl/bundles.php +++ b/packages/bundles/resources/lang/pl/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Sprzedawaj jako zestaw', 'confirm_remove_title' => 'Przestać sprzedawać jako zestaw?', 'confirm_remove' => 'Lista składników i grupy opcji zostaną usunięte. Wariant zachowa swoje ceny i znów będzie zwykłym wariantem.', + 'confirm_components_title' => 'Wyliczać cenę ze składników?', + 'confirm_components' => 'Własne ceny bazowe wariantu zostaną zastąpione sumą jego składników i nadpisane przy każdej zmianie ceny składnika. Wróć do ceny stałej, aby znów ustawiać je ręcznie.', 'pricing' => 'Wycena', 'pricing_fixed' => 'Cena stała', 'pricing_components' => 'Suma składników', diff --git a/packages/bundles/resources/lang/pt_BR/bundles.php b/packages/bundles/resources/lang/pt_BR/bundles.php index 65c0b3f97c..9b48aee3c7 100644 --- a/packages/bundles/resources/lang/pt_BR/bundles.php +++ b/packages/bundles/resources/lang/pt_BR/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Vender como pacote', 'confirm_remove_title' => 'Parar de vender como pacote?', 'confirm_remove' => 'A lista de componentes e os grupos de opções são removidos. A variante mantém seus preços e volta a ser uma variante comum.', + 'confirm_components_title' => 'Calcular o preço a partir dos componentes?', + 'confirm_components' => 'Os preços base da própria variante são substituídos pela soma dos componentes e reescritos sempre que o preço de um componente mudar. Volte para preço fixo para defini-los manualmente de novo.', 'pricing' => 'Precificação', 'pricing_fixed' => 'Preço fixo', 'pricing_components' => 'Soma dos componentes', diff --git a/packages/bundles/resources/lang/ro/bundles.php b/packages/bundles/resources/lang/ro/bundles.php index 65630fbe31..1e776d8f11 100644 --- a/packages/bundles/resources/lang/ro/bundles.php +++ b/packages/bundles/resources/lang/ro/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Vinde ca pachet', 'confirm_remove_title' => 'Nu mai vinzi ca pachet?', 'confirm_remove' => 'Lista de componente și grupurile de opțiuni sunt eliminate. Varianta își păstrează prețurile și redevine o variantă obișnuită.', + 'confirm_components_title' => 'Preț calculat din componente?', + 'confirm_components' => 'Prețurile de bază proprii ale variantei sunt înlocuite cu suma componentelor și rescrise ori de câte ori se schimbă prețul unei componente. Reveniți la preț fix pentru a le seta din nou manual.', 'pricing' => 'Prețuri', 'pricing_fixed' => 'Preț fix', 'pricing_components' => 'Suma componentelor', diff --git a/packages/bundles/resources/lang/tr/bundles.php b/packages/bundles/resources/lang/tr/bundles.php index edb93df043..085956b908 100644 --- a/packages/bundles/resources/lang/tr/bundles.php +++ b/packages/bundles/resources/lang/tr/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Paket olarak sat', 'confirm_remove_title' => 'Paket olarak satış durdurulsun mu?', 'confirm_remove' => 'Bileşen listesi ve seçenek grupları kaldırılır. Varyant fiyatlarını korur ve yeniden sıradan bir varyant olur.', + 'confirm_components_title' => 'Fiyat bileşenlerden hesaplansın mı?', + 'confirm_components' => 'Varyantın kendi taban fiyatları bileşenlerinin toplamıyla değiştirilir ve bir bileşenin fiyatı her değiştiğinde yeniden yazılır. Yeniden elle belirlemek için sabit fiyata dönün.', 'pricing' => 'Fiyatlandırma', 'pricing_fixed' => 'Sabit fiyat', 'pricing_components' => 'Bileşenlerin toplamı', diff --git a/packages/bundles/resources/lang/vi/bundles.php b/packages/bundles/resources/lang/vi/bundles.php index 3580b6af9a..2aad83dee9 100644 --- a/packages/bundles/resources/lang/vi/bundles.php +++ b/packages/bundles/resources/lang/vi/bundles.php @@ -34,6 +34,8 @@ 'sell_as_bundle' => 'Bán dưới dạng gói', 'confirm_remove_title' => 'Ngừng bán dưới dạng gói?', 'confirm_remove' => 'Danh sách thành phần và các nhóm tùy chọn sẽ bị xóa. Biến thể giữ nguyên giá và trở lại thành biến thể thông thường.', + 'confirm_components_title' => 'Tính giá từ các thành phần?', + 'confirm_components' => 'Giá cơ bản riêng của biến thể sẽ được thay bằng tổng các thành phần và được ghi lại mỗi khi giá một thành phần thay đổi. Chuyển về giá cố định để đặt lại thủ công.', 'pricing' => 'Định giá', 'pricing_fixed' => 'Giá cố định', 'pricing_components' => 'Tổng các thành phần', diff --git a/packages/bundles/src/Actions/DefineBundle.php b/packages/bundles/src/Actions/DefineBundle.php index 057abc4841..b02c6f9cc7 100644 --- a/packages/bundles/src/Actions/DefineBundle.php +++ b/packages/bundles/src/Actions/DefineBundle.php @@ -26,7 +26,7 @@ public function execute(ProductVariant $variant, BundlePricing $pricing, ?float $bundle->fill([ 'pricing' => $pricing, - 'discount_percentage' => $discountPercentage, + 'discount_percentage' => $pricing === BundlePricing::Components ? $discountPercentage : null, ])->save(); $bundle->setRelation('variant', $variant); diff --git a/packages/bundles/src/Actions/ResolveBundleInventory.php b/packages/bundles/src/Actions/ResolveBundleInventory.php index a17f995306..6f53a4204a 100644 --- a/packages/bundles/src/Actions/ResolveBundleInventory.php +++ b/packages/bundles/src/Actions/ResolveBundleInventory.php @@ -68,6 +68,11 @@ protected function candidates(Bundle $bundle, Collection $inventories): Collecti : $this->completeUnits($component, $inventories)) ->first(); + // A required group with nothing to choose from cannot be sold at all. + if (! $best && $group->min_selections > 0) { + return collect(); + } + if ($best) { $candidates->push($best); } diff --git a/packages/bundles/src/Pipelines/CartLine/PriceBundleSelection.php b/packages/bundles/src/Pipelines/CartLine/PriceBundleSelection.php index 9d044b9207..1d19ea6ca0 100644 --- a/packages/bundles/src/Pipelines/CartLine/PriceBundleSelection.php +++ b/packages/bundles/src/Pipelines/CartLine/PriceBundleSelection.php @@ -20,6 +20,7 @@ * Cart-time pricing for a configurable `components` bundle whose selection * differs from the default. Runs after `GetUnitPrice` and overwrites the unit * price it set; the materialised rows already cover the default selection. + * Components are priced per bundle unit, so their quantity breaks never apply. */ class PriceBundleSelection { @@ -65,7 +66,7 @@ public function handle(CartLine $cartLine, Closure $next): mixed /** @var SelectedComponent $component */ $matched = $this->pricing ->currency($currency) - ->qty($cartLine->quantity) + ->qty(1) ->customerGroups($customerGroups) ->for($component->variant) ->get() diff --git a/specs/0082-selling-policy-rework.md b/specs/0082-selling-policy-rework.md new file mode 100644 index 0000000000..36eb734305 --- /dev/null +++ b/specs/0082-selling-policy-rework.md @@ -0,0 +1,225 @@ +# 0082 — Selling-policy rework + +- Status: draft +- Author: Glenn Jacobs (with Claude) +- Created: 2026-09-09 +- TODO item: Selling-policy rework — declarative model: deny-oversell, sell-against-incoming, continue-selling boolean (follow-on to 0038) + +## Problem + +`product_variants.selling_policy` is a closed three-value enum (`always` / `in_stock` / +`in_stock_or_on_backorder`, from [[0048-rename-purchasable-to-selling-policy]]) plus a manual +`backorder` integer that only the third mode consults. Three things creak: + +- **Two disconnected forward-stock numbers.** [[0038-inventory-fundamentals]] made `incoming` + a real per-location bucket (`StockLevel.incoming`, rolled up to `stock_incoming`) but + deliberately left the backorder policy alone: "Selling against it is a backorder policy + (`backorder` column + `purchasable` mode), unchanged by this spec." So the variant now + carries a merchant-maintained allowance *and* a tracked incoming figure, and the selling + policy consults the stale-prone manual one. Worse, `incoming` has no first-party write + path — `RecordStockMovement` is "the single write seam for the `on_hand` ledger", the + panel's `InventoryCard` and the Filament bridge's `ProductVariantInventory` both show + incoming read-only — so the tracked figure sits at 0 in every store without an add-on. +- **No way to hard-stop overselling.** 0038 records that oversell at placement is allowed by + design: `CartLineStock` validates at add-to-cart/update time, nothing re-checks at + placement, so concurrent carts can jointly drive `available` negative. The "configurable + deny-oversell mode" was tracked as a later option. A merchant selling strictly limited + stock has no enforcement, only advice. +- **The enum is closed.** The three values encode two orthogonal decisions (ignore stock + entirely; consult incoming as well as available). Each new combination needs a new enum + case and every consumer updated. The panel also still labels the field "Purchasable" — the + pre-0048 name. + +This is the last Ideas item that reshapes a baseline column. Pre-release it is a fold-in +edit; post-release it becomes a schema migration, an enum deprecation, a Rector rule and a +breaking-change cycle. Decide now or live with the enum. + +## Proposal + +Replace the enum with the two decisions it encodes, give `incoming` a first-party manual +write path, and make the stock check enforced at placement. + +### Two booleans replace the enum + +`product_variants` drops `selling_policy` and `backorder`, gains: + +``` +continue_selling boolean, default true sell regardless of stock +sell_against_incoming boolean, default false sellable quantity includes stock_incoming +``` + +- `continue_selling = true` is the out-of-the-box default, matching today's `always` default — + a fresh store sells without setting up stock. Turning it off makes stock binding. +- The `SellingPolicy` enum is deleted. Value mapping: `always` → `(true, false)`, + `in_stock` → `(false, false)`, `in_stock_or_on_backorder` → `(false, true)`. +- `ProductVariant` logic collapses to: + +```php +public function getTotalInventory(): int +{ + return $this->stock_available + + ($this->sell_against_incoming ? $this->stock_incoming : 0); +} + +public function canBeFulfilledAtQuantity(int $quantity): bool +{ + return $this->continue_selling || $quantity <= $this->getTotalInventory(); +} +``` + +The `Purchasable` contract is untouched — `canBeFulfilledAtQuantity()` keeps its signature, +so `CartLineStock`, `ShippingOption` and custom purchasables are unaffected at the seam. + +The backorder allowance is gone: selling beyond available stock now means selling against +the tracked `stock_incoming` figure, one source of truth for forward stock. + +### `incoming` becomes hand-editable + +Without a write path, `sell_against_incoming` would be a dead toggle in stock-core Lunar +(incoming permanently 0 — a capability regression from the editable `backorder` field). So +core gains a minimal manual editor, per 0038's minimal-controls stance: a field, not a +purchasing system. + +- **New action + contract**: `Actions/Products/SetIncomingStock` implementing + `Contracts\Actions\Products\SetsIncomingStock`, registered in `ActionServiceProvider`. + `execute(ProductVariant $variant, Location $location, int $quantity, ?string $note = null): StockLevel` + — sets the level's `incoming` **absolutely** (it is a statement of expectation, not a + ledger delta), inside a transaction with the same `firstOrCreate` + `lockForUpdate` + discipline as `RecordStockMovement`, then recomputes the rollup via + `RecomputesStockRollup`. No `StockMovement` row — incoming is not part of the `on_hand` + ledger. When goods arrive the merchant records the arrival with the existing adjust-stock + action and lowers `incoming` accordingly; automating that pairing is add-on territory. +- **Panel**: the `InventoryCard` per-location incoming cell becomes editable (inline, next to + the adjust-stock affordance), writing through the new action. +- **Filament bridge**: a matching control in `ProductVariantInventory`. + +**Single-writer seam.** A purchasing add-on that derives `incoming` from purchase orders +must be able to switch the manual entry off — one bucket, one writer — while keeping core's +other inventory controls (a purchasing-only add-on is not a full inventory takeover). The +incoming editor is therefore individually disableable in both admins, alongside the existing +whole-surface takeover from 0038: + +- Filament: a toggle on the bridge configuration (naming per the existing + `withoutInventoryControls()` pattern, e.g. `withoutIncomingStockEntry()`). +- Panel: the equivalent extension point on the panel's configuration surface. + +Core UI and add-ons share the `SetsIncomingStock` seam either way — disabling entry is +purely a UI concern, never a behavioural fork. + +### Deny-oversell: the stock check is re-run at placement + +There is no advisory/deny toggle. `continue_selling` already expresses the merchant's +intent per variant: `true` means oversell freely, `false` means stock is binding — so when +it is false, placement enforces it. + +- Order creation re-validates every stock-tracked line (`canBeFulfilledAtQuantity()` against + fresh data) as the first validation step of the create-order pipeline, before any order + rows are written. A failing line aborts placement with a typed exception carrying the + offending line(s), which the storefront surfaces the same way it surfaces any other + placement failure. +- This shrinks the oversell window from "add-to-cart → placement" to the placement + transaction itself. Full closure (concurrent placements racing the same last unit) is the + checkout-reservations follow-on — the `TracksStock::reserveStock()` seam and + `StockReservation` model already exist for it; this spec does not implement first-party + reservation-at-checkout. + +### Admin form changes + +Both variant forms replace the three-option select + backorder quantity with two toggles: + +- "Continue selling when out of stock" (`continue_selling`). +- "Sell against incoming stock" (`sell_against_incoming`), shown/enabled only when + continue-selling is off (when stock is ignored entirely, incoming is irrelevant) — + replacing the current `disabled unless in_stock_or_on_backorder` dance on the backorder + field. + +This also retires the panel's stale "Purchasable" field label. + +### Factories + +`ProductVariantFactory` defaults to `(continue_selling: true, sell_against_incoming: false)`; +states `inStock()` and `sellsAgainstIncoming()` cover the other combinations so tests never +hand-set the booleans. + +## Alternatives considered + +- **Keep the enum, add cases.** The two decisions produce four combinations today and more + with every future dimension; a closed set forces a new case plus every consumer updated + per combination. Rejected — the enum is the wrong shape, not merely incomplete. +- **Keep a manual backorder allowance alongside `incoming`.** Preserves a hand-set cap but + keeps two forward-stock numbers that drift apart — the exact problem. Rejected. +- **A cap on incoming-selling** (sell only *n* of the inbound quantity). Real need for some + merchants, but it reintroduces a second number; a purchasing add-on that owns `incoming` + can model caps properly (per-PO, per-window). Rejected for core. +- **Deny-oversell as store-level config or a third per-variant boolean.** A config switch + makes enforcement a value (`config` is for values, but this is behaviour that belongs to + the variant's own declaration); a third boolean conflates merchandising intent with + enforcement. `continue_selling = false` already *is* the merchant saying "stock is + binding". Rejected. +- **Do nothing.** Ships v2.0 with a closed enum, a dead `incoming` bucket, a stale-prone + allowance and no enforcement — and converts this spec's baseline edit into a post-release + breaking change. Rejected. + +## Migration impact + +- **Database**: baseline edit to `create_product_variants_table` — drop `selling_policy` + (and its index) and `backorder`, add the two booleans. No new tables. +- **Breaking changes**: `Enums\SellingPolicy` is removed; `ProductVariant` property surface + changes (`$selling_policy`/`$backorder` → the booleans). `Purchasable` and `TracksStock` + contracts unchanged. Pre-release, so baseline + Rector, no deprecation cycle. +- **Upgrade path (v1.x, `packages/upgrade`)**: the existing + `rename_purchasable_to_selling_policy` step reworks into purchasable-mode → booleans + (mapping above, rogue values → `(true, false)` matching its current `always` fallback). + Where the v1 mode consulted the allowance (`in_stock_or_on_backorder`) and `backorder > 0`, + seed the default location's `StockLevel.incoming` from it (one-time courtesy so + backorder-selling keeps working on day one), recompute rollups, then drop `backorder`. + Rector: property/enum rewrites added to `LunarSetList`. +- **Translations**: retire the `selling_policy.*` / `backorder.*` keys and add the two + toggle labels + tooltips and the incoming-editor strings in `panel`, `filament` and + `admin` — English first, translated across all 16 locales. +- **Filament / admin impact**: `ProductVariantInventory` and `ProductVariantForm` (bridge), + `ManageProductInventory` (admin resources), `VariantFields` + `InventoryCard` + + `ProductVariantController` (panel), plus the two new disable seams. + +## Open questions + +1. **Where exactly the placement re-check sits relative to payment authorization.** Payment + drivers authorize before placing the order; a deny after auth means the storefront must + void/release. Proposed resolution: the re-check runs in the create-order pipeline's + validation phase, and drivers already surface order-creation failure to the storefront — + verify the Stripe driver's failure path handles it cleanly and document the expectation + on `PaymentType` implementers. Owner: implementation slice 3. +2. **Seeding `incoming` from v1 `backorder`.** Proposed above as a one-time courtesy; + alternative is dropping the number and release-noting that backorder-selling needs + `incoming` populated. Decide at review. +3. **Exact names of the two disable seams.** Proposed `withoutIncomingStockEntry()` on the + Filament bridge and the panel equivalent; settle naming against the existing extension + surfaces during implementation review. + +## References + +- [[0038-inventory-fundamentals]] — the stock buckets, minimal-controls stance, the + oversell-by-design note and the add-on takeover toggle this spec builds on. +- [[0048-rename-purchasable-to-selling-policy]] — the rename that scoped this rework out as + "a separate post-alpha item"; this spec is that item, pulled pre-release. +- `Contracts/TracksStock.php`, `Actions/Products/ReserveStock.php` — the reservation seam + the deny-oversell stance defers to. +- `Actions/Products/RecordStockMovement.php` — the locking discipline `SetIncomingStock` + mirrors. +- TODO Ideas: checkout stock reservations, location-scoped availability — unchanged + follow-ons; both read the same buckets this spec keys selling off. + +## Implementation plan + +- [ ] Slice 1 — core schema + model: booleans on the baseline, enum removal, + `getTotalInventory()` / `canBeFulfilledAtQuantity()` rework, factory states, tests. +- [ ] Slice 2 — `SetIncomingStock` action + contract, `ActionServiceProvider` registration, + tests. +- [ ] Slice 3 — placement-time re-validation in the create-order pipeline + typed failure, + tests (resolves open question 1). +- [ ] Slice 4 — panel: toggles, inline incoming editor, disable seam, lang keys across 16 + locales. +- [ ] Slice 5 — Filament bridge + admin resources: same surface, disable seam, lang keys + across 16 locales. +- [ ] Slice 6 — upgrade package: mode → booleans data step, incoming seed, `backorder` + drop, Rector rules. diff --git a/specs/0083-cart-line-fulfilment-method.md b/specs/0083-cart-line-fulfilment-method.md new file mode 100644 index 0000000000..c960fe9118 --- /dev/null +++ b/specs/0083-cart-line-fulfilment-method.md @@ -0,0 +1,370 @@ +# 0083 — Cart-line fulfilment method: choosing how a line is fulfilled at cart time + +- Status: draft +- Author: Glenn Jacobs (with Claude) +- Created: 2026-09-10 +- TODO item: Cart-line fulfilment method — cart default + per-line override, keyed on registered fulfilment methods + +## Problem + +[[0031-fulfilment-methods]] made fulfilment *flow* pluggable — `shipping`, `collection`, +`digital`, or anything a consumer registers against `FulfilmentMethodManifest`. But the +decision of **which** method a line lands in is made after the order is placed, by +`EnsureInitialFulfilment` walking the registered methods in priority order and letting each +`claim()` the lines it recognises. The only inputs to that decision are: + +- two capability flags snapshotted onto the order line from the purchasable — + `requires_shipping` (`isShippable()`) and `requires_fulfilment` (`requiresFulfilment()`). + They say what a product *can* be, not how the customer wants it; +- one cart-wide signal — the chosen shipping option's `collect` flag, copied into the + shipping order line's `meta` by `CreateShippingLine` and read back out by + `Drivers\FulfilmentMethods\Collection::claim()`. + +Nothing on the cart or the cart line says how a line is to be fulfilled. Consequences: + +- **No per-line choice.** A cart cannot express "deliver these, collect that one". The + `collect` flag is all-or-nothing, so a trade customer who wants the bulky item delivered + and the small one held at the counter has to place two orders. +- **Intent is smuggled through shipping.** Fulfilment intent only exists as a side effect + of picking a shipping *rate*. A digital-only cart has no shipping option and therefore no + way to express intent at all; a consumer-registered method (a prescription flow, a 3PL + hand-off) cannot be *chosen* by the customer — it can only infer from the order after the + fact. +- **Nothing to read.** A storefront cannot tell the customer, before the order exists, how + a line will be fulfilled. `$cartLine` has no accessor for it; the answer is implicit in + `isShippable()` plus whichever rate is selected. +- **The common case is implicit.** "Fulfil the whole cart one way" — what most storefronts + want — already works, but only by picking a shipping option. There is no verb that says + it directly. + +Two existing mechanics constrain the fix. Cart-line identity is *purchasable plus a `meta` +diff* (`GetExistingCartLine`, `CreateOrderLines`, `MergeCart`), so a per-line preference +must join that identity or lines with different preferences merge. And order lines are +truthful snapshots by design ([[0030-fulfillable-order-lines]]), so the resolved preference +must be stamped at order creation, not re-derived from a mutable cart. + +Pre-release this is three nullable baseline columns and two additive contract methods. +Post-release it is a schema migration, a contract break on `FulfilmentMethod` and on the +add-to-cart action seam, and a Rector cycle. Decide now. + +## Proposal + +Add a first-class **requested fulfilment method** at two levels — a cart-wide default and +a per-line override — keyed on the registered `FulfilmentMethod` keys, resolved through +one precedence chain, and snapshotted onto the order line so the initial-fulfilment pass +routes explicitly-keyed lines straight to their method. Lines with no explicit method keep +today's claim-based inference, so existing storefronts behave exactly as they do now. + +### A. Schema + +Three nullable, indexed string columns holding a `FulfilmentMethod` key (the same shape as +`fulfilments.method`): + +| Table | Column | Means | +|---|---|---| +| `carts` | `fulfilment_method` (after `coupon_code`) | the cart-wide default, set explicitly by the storefront | +| `cart_lines` | `fulfilment_method` (after `quantity`) | the per-line override; null inherits the cart default | +| `order_lines` | `fulfilment_method` (after `requires_fulfilment`) | the *resolved* method at order creation; null means "inferred at fulfilment time" | + +All three are nullable with no default. Null is meaningful: "nothing requested". Models gain +the matching `@property ?string $fulfilment_method`; `CartLineFactory` and +`OrderLineFactory` default it to null so existing tests are untouched. + +### B. `FulfilmentMethod` contract additions + +Two methods, both additive with obvious core implementations: + +```php +interface FulfilmentMethod +{ + // ...existing surface unchanged... + + /** + * Whether this method can fulfil the given purchasable. Consulted when a + * method is requested for a cart line (validation) and when the cart-wide + * default is applied to a line (a default that cannot fulfil the line is + * skipped, not forced). + */ + public function supports(Purchasable $purchasable): bool; + + /** + * Whether a fulfilment of this method is delivered to the customer — it + * needs a shipping option and a shipping address at checkout, and its lines + * count towards shipping-rate calculation. + */ + public function requiresDelivery(): bool; +} +``` + +Core mapping: + +| Method | `supports()` | `requiresDelivery()` | +|---|---|---| +| `shipping` | `$purchasable->isShippable()` | `true` | +| `collection` | `$purchasable->isShippable()` | `false` | +| `digital` | `$purchasable->requiresFulfilment() && ! $purchasable->isShippable()` | `false` | + +`supports()` mirrors each method's `claim()` predicate over a single purchasable rather +than an order line — the same rule, asked earlier. The existing `claim()` stays as the +inference fallback (section E). + +`FulfilmentMethodManifest` gains one query helper, for storefront pickers and the validator: + +```php +/** @return Collection keyed by method key, in priority order */ +public function supporting(Purchasable $purchasable): Collection; +``` + +### C. Resolution — one precedence chain + +A single internal action, `Actions\Fulfilment\ResolveCartLineFulfilmentMethod` +(contract `ResolvesCartLineFulfilmentMethod`, registered in `ActionServiceProvider`), +answers "which method has this line asked for". It is orchestration-only — no model verb — +and the model accessors delegate to it: + +```php +Cart::fulfilmentMethod(): ?FulfilmentMethod + // 1. the explicit carts.fulfilment_method, resolved via the manifest + // 2. else derived from the chosen shipping option: collect ? 'collection' : 'shipping' + // 3. else null + +CartLine::fulfilmentMethod(): ?FulfilmentMethod + // 1. the explicit cart_lines.fulfilment_method + // 2. else the cart default (above), but only if it supports($line->purchasable) + // 3. else null — "inferred at fulfilment time" +``` + +Step 2 of the line chain is what keeps digital goods correct with no storefront change: a +cart whose default is `shipping` (explicit or derived) has a licence key in it; `shipping` +does not support it, so the line resolves to null and `Digital::claim()` picks it up at +order time exactly as today. A default is a preference, never a coercion. + +An explicit key that no longer resolves (the method was unregistered after the cart was +built) resolves to null at that step and falls through. It is not an error: the cart is +still orderable, and the claim pass handles the line. + +### D. Cart surface + +**Adding a line.** `Cart::add()` and `addLines()` gain an optional named argument, appended +so positional callers are unaffected; the action contract gains it too: + +```php +$cart->add($variant, quantity: 2, fulfilmentMethod: 'collection'); +$cart->addLines([['purchasable' => $variant, 'quantity' => 2, 'fulfilment_method' => 'collection']]); + +interface AddsOrUpdatesPurchasable +{ + public function execute(Cart $cart, Purchasable $purchasable, int $quantity = 1, array $meta = [], ?string $fulfilmentMethod = null): void; +} +``` + +**Changing it.** Two new model verbs, each one-line-delegating to an action contract +([[0029-entry-point-conventions]]); `updateLine()` / `UpdatesCartLine` are untouched: + +```php +$cart->setFulfilmentMethod(?string $key, bool $refresh = true): Cart; // SetsCartFulfilmentMethod +$cartLine->setFulfilmentMethod(?string $key): CartLine; // SetsCartLineFulfilmentMethod +``` + +Passing null clears the value (back to "inherit" / "nothing requested"). Both run a +validator chain first, mirroring `setShippingOption()`: + +- `config('lunar.cart.validators.set_fulfilment_method')` — default + `Validation\Cart\CartFulfilmentMethod`: the key is null or registered. +- `config('lunar.cart.validators.set_line_fulfilment_method')` — default + `Validation\CartLine\CartLineFulfilmentMethod`: the key is null or registered, **and** + `supports($purchasable)`. +- `add_to_cart` gains `CartLineFulfilmentMethod` after `CartLineAvailability`, receiving the + `fulfilmentMethod` parameter alongside `purchasable`. + +Failures throw a `CartException` with two new translated messages: +`carts.fulfilment_method_unknown` and `carts.fulfilment_method_unsupported`. + +**Identity and fingerprint.** The method joins the line identity everywhere the `meta` diff +is used today: `GetExistingCartLine`, `CreateOrderLines`' existing-line match, and +`MergeCart`. The same variant requested for delivery and for collection is two lines. +`GenerateFingerprint` includes each line's `fulfilment_method` and the cart's, so a changed +preference invalidates a checkout fingerprint like a changed quantity does. + +**Delivery lines.** The lines that actually need delivering: + +```php +Cart::deliveryLines(): Collection +``` + +A line is a delivery line when its *explicit* method (line override, else the explicit cart +default where supported) `requiresDelivery()`, or when it has no explicit method and +`isShippable()`. The shipping-option-derived step of the chain is deliberately **not** +consulted here — shipping-rate calculation reads `deliveryLines()`, and the rate being +calculated cannot be an input to which lines it covers. `Cart::isShippable()` keeps its +name and re-keys to `deliveryLines()->isNotEmpty()`; behaviour only changes when an +override is present. + +### E. Order side + +**Stamp.** `CreateOrderLines` sets `'fulfilment_method' => $cartLine->fulfilmentMethod()?->getKey()`. +The full chain applies here (the shipping option is fixed by now), so a collect-option cart +stamps `collection` on its physical lines and the order line is self-describing — the +merchant, the panel and any consumer reading the line see the requested method before a +fulfilment exists. + +**Assignment.** `EnsureInitialFulfilment` gains a first pass ahead of the claim loop: + +1. **Explicit pass.** Group the fulfillable lines carrying a `fulfilment_method` by key. For + each key that resolves to a registered method, create one fulfilment in that method's + `defaultState()` covering those lines at full quantity, and remove them from the pool. A + key that no longer resolves leaves its lines in the pool. +2. **Claim pass.** Today's priority-ordered `claim()` loop over whatever remains. + +`Collection::claim()` keeps reading the shipping line's `collect` flag as the fallback for +lines with no stamp (orders created by paths other than the cart pipeline, v1-upgraded +orders). Nothing about `claim()` implementations changes; a consumer method becomes +selectable at cart time with no code beyond `supports()` / `requiresDelivery()`. + +The existing `FulfilmentQuantity` guard (`requires_fulfilment` must be true) still applies — +the explicit pass draws from `fulfillableLines()`, so a consumer method that `supports()` a +non-fulfillable service line never gets a fulfilment for it. + +### F. Checkout validation + +`ValidateCartForOrderCreation` re-keys its shipping rules: + +- a shipping option is required when `$cart->isShippable()` (unchanged in name, now + delivery-line based); +- a shipping address is required when there are delivery lines **and** the chosen option is + not a collection — today's rule, expressed over `deliveryLines()`. + +A cart whose every physical line is overridden to `collection` therefore needs neither a +shipping option nor a shipping address, which is the case the current model cannot express. + +### G. Shipping-rate calculation over delivery lines + +The table-rate drivers and resolver sum weight, subtotal, stock and exclusions over +`$cart->lines`. They switch to `$cart->deliveryLines()`: + +- `Drivers\ShippingMethods\{FlatRate,FreeShipping,ShipBy}` — subtotal thresholds and + product exclusions; +- `Resolvers\ShippingRateResolver` — `cartWeightKg` and `allCartItemsAreInStock`; +- `Models\ShippingRate` — the per-line loop. + +`Drivers\ShippingMethods\Collection` keeps `$cart->lines`: its exclusion check asks whether +anything in the cart cannot be collected, which is a whole-cart question. A line collected +from the counter no longer adds weight to the parcel rate, and a free-shipping threshold is +met by what is actually shipped. + +### H. Admin + +None required in core. The panel order view is fulfilment-centric (spec 0067) and every +fulfilment card already badges its method; the requested method on the order line only +differs from the fulfilment's method after a merchant moves lines by hand. The Filament +bridge and the panel may surface `order_lines.fulfilment_method` as a line subline later — +it is a read of an existing column, not part of this spec. + +## Alternatives considered + +- **A `meta['fulfilment_method']` convention, no schema.** Costs nothing up front but is + untyped, unvalidated, invisible to `EnsureInitialFulfilment`, and `meta` is the consumer's + bag — core would be squatting a key in it. Rejected; it is what a consumer can already do + today with a custom `claim()`, and that it is possible is exactly why it is undiscoverable. +- **Per-line shipping options (commercetools "multiple" shipping mode).** Each line targets + an address and a rate; carts become sets of shipments. Far larger, and the wrong + granularity: the customer-facing choice is *how* a line is fulfilled, and the rate remains + a cart-wide price. Shopify's delivery groups reach the same conclusion. Rejected; a later + multi-address spec can build on the per-line key this one introduces. +- **Cart default only, no line override.** Covers the common case but is trivially + derivable from the shipping option today; the whole value is in the line override. + Rejected. +- **Make the shipping option own the intent** (`ShippingOption::fulfilmentMethod`, stamped + onto the cart by `SetShippingOption`). Two writers for one value — the option and the + explicit setter — drifting apart, the pattern [[0082-selling-policy-rework]] removes + elsewhere. Rejected in favour of a derived read (section C, step 2). +- **Make the cart default coerce every line.** Simpler chain, but a `shipping` default would + stamp `shipping` on a licence key and the order would need a parcel for it. Rejected: a + default is a preference applied where it can be, never a coercion (section C). +- **Do nothing.** Ships v2.0 with fulfilment intent expressible only through a shipping + rate, no per-line choice, and turns three nullable baseline columns into a post-release + migration plus two contract breaks. Rejected. + +## Migration impact + +- **Database** (baseline edits, v2 pre-release): `carts.fulfilment_method`, + `cart_lines.fulfilment_method`, `order_lines.fulfilment_method` — nullable string, + indexed. No new tables, no backfill. +- **Breaking changes to the public contract surface:** + - `Contracts\FulfilmentMethod` gains `supports()` and `requiresDelivery()`. Consumers + implementing the contract must add them; Rector note in `LunarSetList`. + - `Contracts\Actions\Carts\AddsOrUpdatesPurchasable::execute()` gains a trailing + `?string $fulfilmentMethod = null`. Consumers who bound their own implementation must + add the parameter; Rector note. + - `Cart::isShippable()` re-keys to delivery lines — behavioural only when a line override + is present. + - Cart-line identity now includes `fulfilment_method` — a consumer relying on two lines + with the same purchasable and `meta` always merging sees them stay separate only when + they carry different methods. + - New surface (additive): `Cart::fulfilmentMethod()`, `Cart::setFulfilmentMethod()`, + `Cart::deliveryLines()`, `CartLine::fulfilmentMethod()`, + `CartLine::setFulfilmentMethod()`, `FulfilmentMethodManifest::supporting()`, + `Actions\Carts\{SetCartFulfilmentMethod,SetCartLineFulfilmentMethod}`, + `Actions\Fulfilment\ResolveCartLineFulfilmentMethod` and their contracts, + `Validation\Cart\CartFulfilmentMethod`, `Validation\CartLine\CartLineFulfilmentMethod`, + the two validator config keys, and the `fulfilmentMethod` argument on `add()` / + `addLines()`. +- **Upgrade path (`packages/upgrade`)**: no data step — the columns are nullable and v1 + carries no equivalent; v1 orders keep inferring via `claim()`. Rector rules for the two + contract changes above. +- **Translations (16 locales)**: `exceptions.carts.fulfilment_method_unknown` and + `exceptions.carts.fulfilment_method_unsupported` in `core` — English first, translated + into the other 15. +- **Filament / admin impact**: none (section H). + +## Open questions + +1. **Collection location per line.** A line overridden to `collection` collects from the + order's resolved location, as 0031 left it. Choosing *which* counter per line is deferred + to the location-scoped availability and stock routing item (TODO Ideas), which owns + per-location selection at sell time. Confirm at review that this spec should not reserve + a column for it. +2. **A collect rate with a line overridden to `shipping`.** The override wins for that line + (it becomes a delivery line, an address is required, the collect rate still prices the + cart). Consistent but odd; the storefront is expected to filter rates against + `deliveryLines()`. Decide whether the order-creation validator should reject the + combination instead. Owner: slice 4. +3. **Exposing the likely inferred method.** When a line resolves to null, a storefront may + want to show what `claim()` will probably do. `FulfilmentMethodManifest::supporting()` + gives the candidates in priority order; whether the first candidate should be surfaced + as a "probable method" accessor is left to the storefront API spec. + +## References + +- [[0031-fulfilment-methods]] — the method seam, `claim()`, the `collect` flag on the + shipping line, and the open question on collection location this spec touches. +- [[0030-fulfillable-order-lines]] — the snapshot reasoning the `order_lines.fulfilment_method` + stamp follows. +- [[0045-optional-purchasables-and-shipping-de-morph]] — the self-describing shipping line + and the `collect` meta the fallback claim reads. +- [[0029-entry-point-conventions]] — model verbs delegating to action contracts. +- [[0016-service-layer-di]] — actions bound in `ActionServiceProvider`. +- [[0082-selling-policy-rework]] — the single-writer stance the derived cart default + follows. +- `Actions/Carts/GetExistingCartLine.php`, `Actions/Carts/MergeCart.php`, + `Pipelines/Order/Creation/CreateOrderLines.php` — the three sites where line identity is + a purchasable plus a `meta` diff. +- Prior art: Shopify cart delivery groups and `FulfillmentOrder.deliveryMethod`; + commercetools `Cart.shippingMode = Multiple` with per-line `ItemShippingDetails`; + Saleor `Checkout.deliveryMethod` (cart-wide only). + +## Implementation plan + +- [ ] Slice 1 — `FulfilmentMethod::supports()` / `requiresDelivery()` on the contract and + the three core drivers, `FulfilmentMethodManifest::supporting()`, tests. +- [ ] Slice 2 — cart side: the three baseline columns, model properties and factories, the + resolution action + accessors, `add()` / `addLines()` argument, the two setter verbs and + actions, validators and config keys, identity in `GetExistingCartLine` / `MergeCart`, + fingerprint, tests. +- [ ] Slice 3 — order side: `CreateOrderLines` stamp and identity match, the explicit pass + in `EnsureInitialFulfilment`, tests. +- [ ] Slice 4 — delivery lines: `Cart::deliveryLines()`, `isShippable()` re-key, + `ValidateCartForOrderCreation`, table-rate drivers and resolver over delivery lines, + tests in the `core` and `shipping` suites (resolves open question 2). +- [ ] Slice 5 — the two exception keys across 16 locales, Rector rules in the upgrade + package, docs. diff --git a/specs/0085-product-bundles.md b/specs/0085-product-bundles.md index 340e39654d..61f89d76fe 100644 --- a/specs/0085-product-bundles.md +++ b/specs/0085-product-bundles.md @@ -1,6 +1,6 @@ # 0085 — Product bundles (`lunarphp/bundles`) -- Status: accepted +- Status: implemented - Author: Glenn Jacobs - Created: 2026-09-15 - TODO item: Product bundles — sell a set of variants as one product with derived stock and component fulfilment @@ -359,7 +359,7 @@ Slots, all lazily loading their data through the extension's JSON routes so ordi | Zone | Component | Purpose | |---|---|---| -| `products.edit:variants:after` | `bundles::BundleCard` | Single-variant product: the full bundle editor inline. Multi-variant product: a summary per variant linking to the variant page. | +| `products.edit:variants:after` | `bundles::BundleCard` | Single-variant product: the full bundle editor inline. Multi-variant product: a summary per variant linking to the variant page (served by `GET bundles/products/{product}`). | | `products.variants.edit:main:after` | `bundles::BundleCard` | The bundle editor for that variant. | | `products.edit:sidebar:after` | `bundles::IncludedInBundlesCard` | Bundles that include any of this product's variants, each linking to its product. Hidden when empty. | @@ -379,7 +379,7 @@ PUT bundles/variants/{variant}/groups sync GET bundles/products/{product}/included-in JSON for the sidebar card ``` -Table extension on `products.index`: a `Bundle` badge column (hidden by default) and a "Bundles only" filter. +Table extension on `products.index`: a `Bundle` badge column and a "Bundles only" filter. (`TableColumn` has no hidden-by-default option, so the column always shows.) Order view: covered by the first-party changes in 1.1. The extension adds nothing. @@ -467,12 +467,12 @@ Cache: editing a bundle's components or groups invalidates the bundle's product ## Implementation plan -- [ ] Slice 1 — core: `order_lines.parent_line_id`, `OrderLine` relations and `topLevel` scope, creation stages skip component lines, parent-only `refundableQuantity()`, tests -- [ ] Slice 2 — core: `ResolvesInventory` contract, `VariantInventory`, `ResolveInventory` action, `ProductVariant` delegation, `ActionServiceProvider` binding, tests -- [ ] Slice 3 — `bundles`: skeleton and monorepo wiring, migrations, models, factories, `ProductVariant::bundle` relation, define/sync/delete actions and verbs, invariants, cache invalidation, headless tests -- [ ] Slice 4 — `bundles`: `ResolveBundleInventory` decorator, `ResolveBundleSelection`, `BundleSelection` validator, `CreateBundleComponentLines`, end-to-end cart → order → fulfilment → stock tests including mixed physical and digital -- [ ] Slice 5 — `bundles`: `RepriceBundle`, materialised price rows, listeners, `PriceBundleSelection` stage, `lunar:bundles:reprice`, tests -- [ ] Slice 6 — panel (first party): component-line nesting in the order view and refund composer, `Section` exported from `ui.ts` -- [ ] Slice 7 — `bundles` panel: section extension, routes, `BundleCard` for fixed bundles, variant search endpoint, `IncludedInBundlesCard`, products table badge and filter, 16 locales, panel tests, JS build and CI -- [ ] Slice 8 — `bundles` panel: configurable groups editor -- [ ] Slice 9 — demo data: two bundles in the catalogue generator; docs PR (`lunarphp/docs`) +- [x] Slice 1 — core: `order_lines.parent_line_id`, `OrderLine` relations and `topLevel` scope, creation stages skip component lines, parent-only `refundableQuantity()`, tests +- [x] Slice 2 — core: `ResolvesInventory` contract, `VariantInventory`, `ResolveInventory` action, `ProductVariant` delegation, `ActionServiceProvider` binding, tests +- [x] Slice 3 — `bundles`: skeleton and monorepo wiring, migrations, models, factories, `ProductVariant::bundle` relation, define/sync/delete actions and verbs, invariants, cache invalidation, headless tests +- [x] Slice 4 — `bundles`: `ResolveBundleInventory` decorator, `ResolveBundleSelection`, `BundleSelection` validator, `CreateBundleComponentLines`, end-to-end cart → order → fulfilment → stock tests including mixed physical and digital +- [x] Slice 5 — `bundles`: `RepriceBundle`, materialised price rows, listeners, `PriceBundleSelection` stage, `lunar:bundles:reprice`, tests +- [x] Slice 6 — panel (first party): component-line nesting in the order view and refund composer, `Section` exported from `ui.ts` +- [x] Slice 7 — `bundles` panel: section extension, routes, `BundleCard` for fixed bundles, variant search endpoint, `IncludedInBundlesCard`, products table badge and filter, 16 locales, panel tests, JS build and CI +- [x] Slice 8 — `bundles` panel: configurable groups editor +- [x] Slice 9 — demo data: two bundles in the catalogue generator; docs PR (`lunarphp/docs`) diff --git a/specs/README.md b/specs/README.md index d532f54a68..19d7e9366b 100644 --- a/specs/README.md +++ b/specs/README.md @@ -93,4 +93,4 @@ Each spec carries a `Status:` line in its frontmatter / header: | 0072 | Panel Discounts section | accepted | | 0073 | Split `AmountOff` into `PercentageOff` and `FixedAmountOff` | implemented | | 0074 | Panel global search (command palette) | implemented | -| 0085 | Product bundles (`lunarphp/bundles`) | accepted | +| 0085 | Product bundles (`lunarphp/bundles`) | implemented | diff --git a/tests/bundles/Feature/BundleInvalidationTest.php b/tests/bundles/Feature/BundleInvalidationTest.php new file mode 100644 index 0000000000..28ae6d85dc --- /dev/null +++ b/tests/bundles/Feature/BundleInvalidationTest.php @@ -0,0 +1,70 @@ +currency = Catalogue::store(); +}); + +test('a component product invalidation reprices every components bundle that includes it', function () { + $body = Catalogue::variant($this->currency, 3000); + $bag = Catalogue::variant($this->currency, 500); + + $bundle = app(DefinesBundle::class)->execute(ProductVariant::factory()->create(), BundlePricing::Components); + $bundle->syncComponents([ + ['variant' => $body, 'quantity' => 1], + ['variant' => $bag, 'quantity' => 1], + ]); + + // A price change the price observer cannot see: written without model events. + Price::withoutEvents(fn () => Price::query() + ->where('priceable_type', $body->getMorphClass()) + ->where('priceable_id', $body->id) + ->update(['price' => 2000])); + + event(new ProductInvalidated($body->product, CacheInvalidationReason::Updated)); + + expect(Price::query() + ->where('priceable_type', ProductVariant::morphName()) + ->where('priceable_id', $bundle->product_variant_id) + ->value('price'))->toBe(2500); +}); + +test('a component product invalidation fans out to the bundles that include it', function () { + $body = Catalogue::variant($this->currency, 3000); + $other = Catalogue::variant($this->currency, 100); + + $bundle = app(DefinesBundle::class)->execute(ProductVariant::factory()->create(), BundlePricing::Fixed); + $bundle->syncComponents([['variant' => $body, 'quantity' => 1]]); + + $recorded = []; + + $this->mock(CacheInvalidator::class, function ($mock) use (&$recorded) { + $mock->shouldReceive('record')->andReturnUsing(function ($model, $reason) use (&$recorded) { + $recorded[] = [$model::class, $model->getKey(), $reason]; + }); + $mock->shouldReceive('flush')->andReturnNull(); + }); + + event(new ProductInvalidated($body->product, CacheInvalidationReason::Updated)); + event(new ProductInvalidated($other->product, CacheInvalidationReason::Updated)); + + $bundles = array_values(array_filter($recorded, fn (array $row) => $row[0] === Bundle::class)); + + expect($bundles)->toHaveCount(1) + ->and($bundles[0][1])->toBe($bundle->id) + ->and($bundles[0][2])->toBe(CacheInvalidationReason::RelatedChanged); +}); diff --git a/tests/bundles/Feature/BundleInventoryTest.php b/tests/bundles/Feature/BundleInventoryTest.php index 79f86d645f..5f7147363e 100644 --- a/tests/bundles/Feature/BundleInventoryTest.php +++ b/tests/bundles/Feature/BundleInventoryTest.php @@ -1,6 +1,8 @@ using(cart: $cart, purchasable: $variant, quantity: 3, meta: [])->validate(); })->throws(CartException::class); + +test('a required group with no options makes the bundle unsellable', function () { + $body = Catalogue::variant($this->currency, 3000, stock: 10); + + $bundle = app(DefinesBundle::class)->execute(ProductVariant::factory()->create(), BundlePricing::Fixed); + $bundle->syncComponents([['variant' => $body, 'quantity' => 1]]); + $bundle->syncGroups([ + ['name' => ['en' => 'Extra'], 'min_selections' => 1, 'max_selections' => 1], + ]); + + expect($bundle->variant->refresh()->getTotalInventory())->toBe(0); +}); From ac54bbed3edf1d037b7be9d048f38e1f54fea19a Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Tue, 15 Sep 2026 12:41:20 +0100 Subject: [PATCH 9/9] fix(demo-data): build the demo bundles from existing stocked products on a re-run Co-Authored-By: Claude Fable 5.1 --- .../demo-data/src/Generators/BundlesGenerator.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/demo-data/src/Generators/BundlesGenerator.php b/packages/demo-data/src/Generators/BundlesGenerator.php index 487c7ea716..600a464a53 100644 --- a/packages/demo-data/src/Generators/BundlesGenerator.php +++ b/packages/demo-data/src/Generators/BundlesGenerator.php @@ -28,11 +28,22 @@ public function generate(DemoContext $context): void $context->reseed(); + // On a re-run the catalogue generator creates nothing, so fall back to + // the products already in the store (never a bundle's own product). /** @var Collection $products */ - $products = $context->get('products', collect()) + $products = collect($context->get('products', collect())) + ->whenEmpty(fn () => Product::query() + ->whereHas('variants', fn ($query) => $query + ->whereNotIn('sku', [self::KIT_SKU, self::SET_SKU]) + ->where('stock_available', '>', 0)) + ->orderBy('id') + ->limit(3) + ->get()) ->filter(fn (Product $product) => $product->variants()->count() > 0) ->values(); + $context->set('products', $products); + if ($products->count() < 3) { return; }